feat: initial Pezkuwi Apps rebrand from polkadot-apps

Rebranded terminology:
- Polkadot → Pezkuwi
- Kusama → Dicle
- Westend → Zagros
- Rococo → PezkuwiChain
- Substrate → Bizinikiwi
- parachain → teyrchain

Custom logos with Kurdistan brand colors (#e6007a → #86e62a):
- bizinikiwi-hexagon.svg
- sora-bizinikiwi.svg
- hezscanner.svg
- heztreasury.svg
- pezkuwiscan.svg
- pezkuwistats.svg
- pezkuwiassembly.svg
- pezkuwiholic.svg
This commit is contained in:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
@@ -0,0 +1,280 @@
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HeaderExtended } from '@pezkuwi/api-derive/types';
import type { KeyedEvent } from '@pezkuwi/react-hooks/ctx/types';
import type { V2Weight } from '@pezkuwi/react-hooks/useWeight';
import type { EventRecord, Hash, RuntimeVersionPartial, SignedBlock } from '@pezkuwi/types/interfaces';
import type { FrameSupportDispatchPerDispatchClassWeight } from '@pezkuwi/types/lookup';
import React, { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { AddressSmall, Columar, CopyButton, LinkExternal, MarkError, styled, Table } from '@pezkuwi/react-components';
import { useApi, useIsMountedRef } from '@pezkuwi/react-hooks';
import { convertWeight } from '@pezkuwi/react-hooks/useWeight';
import { formatNumber, isBn } from '@pezkuwi/util';
import Events from '../Events.js';
import { useTranslation } from '../translate.js';
import Extrinsics from './Extrinsics.js';
import Justifications from './Justifications.js';
import Logs from './Logs.js';
import Summary from './Summary.js';
interface Props {
className?: string;
error?: Error | null;
value?: string | null;
}
interface State {
events?: KeyedEvent[] | null;
blockWeight?: FrameSupportDispatchPerDispatchClassWeight | null;
getBlock?: SignedBlock;
getHeader?: HeaderExtended;
nextBlockHash?: Hash | null;
runtimeVersion?: RuntimeVersionPartial;
}
const EMPTY_HEADER: [React.ReactNode?, string?, number?][] = [['...', 'start', 6]];
function transformResult ([[runtimeVersion, events, blockWeight], getBlock, getHeader]: [[RuntimeVersionPartial, EventRecord[] | null, FrameSupportDispatchPerDispatchClassWeight|null], SignedBlock, HeaderExtended?]): State {
return {
blockWeight,
events: events?.map((record, index) => ({
indexes: [index],
key: `${Date.now()}-${index}-${record.hash.toHex()}`,
record
})),
getBlock,
getHeader,
runtimeVersion
};
}
function BlockByHash ({ className = '', error, value }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const mountedRef = useIsMountedRef();
const [{ blockWeight, events, getBlock, getHeader, nextBlockHash, runtimeVersion }, setState] = useState<State>({});
const [blkError, setBlkError] = useState<Error | null | undefined>(error);
const [evtError, setEvtError] = useState<Error | null | undefined>();
const [isVersionCurrent, maxBlockWeight] = useMemo(
() => [
!!runtimeVersion && api.runtimeVersion.specName.eq(runtimeVersion.specName) && api.runtimeVersion.specVersion.eq(runtimeVersion.specVersion),
api.consts.system.blockWeights && api.consts.system.blockWeights.maxBlock && convertWeight(api.consts.system.blockWeights.maxBlock).v2Weight
],
[api, runtimeVersion]
);
useEffect((): void => {
error && setBlkError(error);
}, [error]);
const systemEvents = useMemo(
() => events?.filter(({ record: { phase } }) => !phase.isApplyExtrinsic),
[events]
);
useEffect((): void => {
value && Promise
.all([
api
.at(value)
.then((apiAt) =>
Promise.all([
Promise.resolve(apiAt.runtimeVersion),
apiAt.query.system
.events()
.catch((error: Error) => {
mountedRef.current && setEvtError(error);
return null;
}),
apiAt.query.system
.blockWeight()
.catch(() => {
return null;
})
])
),
api.rpc.chain.getBlock(value),
api.derive.chain.getHeader(value)
])
.then((result): void => {
mountedRef.current && setState(transformResult(result));
})
.catch((error: Error): void => {
mountedRef.current && setBlkError(error);
});
}, [api, mountedRef, value]);
useEffect((): (() => void) | undefined => {
if (!mountedRef.current || !getHeader?.number) {
return;
}
const nextBlockNumber = getHeader.number.unwrap().addn(1);
let unsub: (() => void) | undefined;
api.rpc.chain.getBlockHash(nextBlockNumber)
.then((hash) => {
if (!hash.isEmpty) {
setState((prev) => ({
...prev,
nextBlockHash: hash
}));
} else {
// Subscribe to new block headers until the next block is found, then unsubscribes.
api.derive.chain.subscribeNewHeads((header: HeaderExtended): void => {
if (mountedRef.current && header.number.unwrap().eq(nextBlockNumber)) {
setState((prev) => ({
...prev,
nextBlockHash: header.hash
}));
unsub && unsub();
}
}).then((_unsub) => {
unsub = _unsub;
}).catch((error: Error) => {
mountedRef.current && setBlkError(error);
});
}
})
.catch((error: Error) => {
mountedRef.current && setBlkError(error);
});
return (): void => {
unsub && unsub();
};
}, [api, getHeader?.number, mountedRef]);
const header = useMemo<[React.ReactNode?, string?, number?][]>(
() => getHeader
? [
[formatNumber(getHeader.number.unwrap()), 'start --digits', 1],
[t('hash'), 'start'],
[t('parent'), 'start'],
[t('next'), 'start'],
[t('extrinsics'), 'start media--1300'],
[t('state'), 'start media--1200'],
[runtimeVersion ? `${runtimeVersion.specName.toString()}/${runtimeVersion.specVersion.toString()}` : undefined, 'media--1000']
]
: EMPTY_HEADER,
[getHeader, runtimeVersion, t]
);
const blockNumber = getHeader?.number.unwrap();
const parentHash = getHeader?.parentHash.toHex();
const hasParent = !getHeader?.parentHash.isEmpty;
return (
<div className={className}>
<Summary
blockWeight={blockWeight}
events={events}
maxBlockWeight={(maxBlockWeight as V2Weight).refTime.toBn()}
maxProofSize={isBn(maxBlockWeight.proofSize) ? maxBlockWeight.proofSize : (maxBlockWeight as V2Weight).proofSize.toBn()}
signedBlock={getBlock}
/>
<StyledTable header={header}>
{blkError
? (
<tr>
<td colSpan={6}>
<MarkError content={t('Unable to retrieve the specified block details. {{error}}', { replace: { error: blkError.message } }) } />
</td>
</tr>
)
: getBlock && getHeader && !getBlock.isEmpty && !getHeader.isEmpty && (
<tr>
<td className='address'>
{getHeader.author && (
<AddressSmall value={getHeader.author} />
)}
</td>
<td className='hash overflow'>{getHeader.hash.toHex()}</td>
<td className='hash overflow'>{
hasParent
? (
<span className='inline-hash-copy'>
<Link to={`/explorer/query/${parentHash || ''}`}>{parentHash}</Link>
<CopyButton value={parentHash} />
</span>
)
: parentHash
}</td>
<td className='hash overflow'>{
nextBlockHash
? (
<span className='inline-hash-copy'>
<Link to={`/explorer/query/${nextBlockHash.toHex()}`}>{nextBlockHash.toHex()}</Link>
<CopyButton value={nextBlockHash.toHex()} />
</span>
)
: t('Waiting for next block...')
}</td>
<td className='hash overflow media--1300'>{getHeader.extrinsicsRoot.toHex()}</td>
<td className='hash overflow media--1200'>{getHeader.stateRoot.toHex()}</td>
<td className='media--1000'>
{value && (
<LinkExternal
data={value}
type='block'
/>
)}
</td>
</tr>
)
}
</StyledTable>
{getBlock && getHeader && (
<>
<Extrinsics
blockNumber={blockNumber}
events={events}
maxBlockWeight={(maxBlockWeight as V2Weight).refTime.toBn()}
value={getBlock.block.extrinsics}
withLink={isVersionCurrent}
/>
<Columar>
<Columar.Column>
<Events
error={evtError}
eventClassName='explorer--BlockByHash-block'
events={systemEvents}
label={t('system events')}
/>
</Columar.Column>
<Columar.Column>
<Logs value={getHeader.digest.logs} />
<Justifications value={getBlock.justifications} />
</Columar.Column>
</Columar>
</>
)}
</div>
);
}
const StyledTable = styled(Table)`
.inline-hash-copy {
align-items: center;
display: inline-flex;
gap: 0.25em;
width: 100%;
a {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
`;
export default React.memo(BlockByHash);
@@ -0,0 +1,47 @@
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Hash } from '@pezkuwi/types/interfaces';
import React, { useEffect, useState } from 'react';
import { useApi, useIsMountedRef } from '@pezkuwi/react-hooks';
import BlockByHash from './ByHash.js';
interface Props {
value: string;
}
function BlockByNumber ({ value }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const [getBlockHash, setState] = useState<Hash | null>(null);
const mountedRef = useIsMountedRef();
const [error, setError] = useState<Error | null>(null);
useEffect((): void => {
api.rpc.chain
.getBlockHash(value)
.then((result): void => {
mountedRef.current && setState(result);
})
.catch((error: Error): void => {
console.error(1);
mountedRef.current && setError(error);
});
}, [api, mountedRef, value]);
return (
<BlockByHash
error={error}
value={
getBlockHash
? getBlockHash.toHex()
: null
}
/>
);
}
export default React.memo(BlockByNumber);
@@ -0,0 +1,210 @@
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyedEvent } from '@pezkuwi/react-hooks/ctx/types';
import type { BlockNumber, DispatchInfo, Extrinsic } from '@pezkuwi/types/interfaces';
import type { ICompact, INumber } from '@pezkuwi/types/types';
import React, { useMemo } from 'react';
import { AddressMini, LinkExternal, styled } from '@pezkuwi/react-components';
import { convertWeight } from '@pezkuwi/react-hooks/useWeight';
import { CallExpander } from '@pezkuwi/react-params';
import { BN, formatNumber } from '@pezkuwi/util';
import Event from '../Event.js';
import { useTranslation } from '../translate.js';
interface Props {
blockNumber?: BlockNumber;
className?: string;
events?: KeyedEvent[] | null;
index: number;
maxBlockWeight?: BN;
value: Extrinsic;
withLink: boolean;
}
const BN_TEN_THOUSAND = new BN(10_000);
function getEra ({ era }: Extrinsic, blockNumber?: BlockNumber): [number, number] | null {
if (blockNumber && era.isMortalEra) {
const mortalEra = era.asMortalEra;
return [mortalEra.birth(blockNumber.toNumber()), mortalEra.death(blockNumber.toNumber())];
}
return null;
}
function filterEvents (index: number, events?: KeyedEvent[] | null, maxBlockWeight?: BN): [DispatchInfo | undefined, BN | undefined, number, KeyedEvent[]] {
const filtered = events
? events.filter(({ record: { phase } }) =>
phase.isApplyExtrinsic &&
phase.asApplyExtrinsic.eq(index)
)
: [];
const infoRecord = filtered.find(({ record: { event: { method, section } } }) =>
section === 'system' &&
['ExtrinsicFailed', 'ExtrinsicSuccess'].includes(method)
);
const dispatchInfo = infoRecord
? infoRecord.record.event.method === 'ExtrinsicSuccess'
? infoRecord.record.event.data[0] as DispatchInfo
: infoRecord.record.event.data[1] as DispatchInfo
: undefined;
const weight = dispatchInfo && convertWeight(dispatchInfo.weight);
return [
dispatchInfo,
weight?.v1Weight,
weight && maxBlockWeight
? weight.v1Weight.mul(BN_TEN_THOUSAND).div(maxBlockWeight).toNumber() / 100
: 0,
filtered
];
}
function ExtrinsicDisplay ({ blockNumber, className = '', events, index, maxBlockWeight, value, withLink }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const link = useMemo(
() => withLink
? `#/extrinsics/decode/${value.toHex()}`
: null,
[value, withLink]
);
const { method, section } = useMemo(
() => value.registry.findMetaCall(value.callIndex),
[value]
);
const timestamp = useMemo(
() => section === 'timestamp' && method === 'set'
? new Date((value.args[0] as ICompact<INumber>).unwrap().toNumber())
: undefined,
[method, section, value]
);
const mortality = useMemo(
(): string | undefined => {
if (value.isSigned) {
const era = getEra(value, blockNumber);
return era
? t('mortal, valid from #{{startAt}} to #{{endsAt}}', {
replace: {
endsAt: formatNumber(era[1]),
startAt: formatNumber(era[0])
}
})
: t('immortal');
}
return undefined;
},
[blockNumber, t, value]
);
const [, weight, weightPercentage, thisEvents] = useMemo(
() => filterEvents(index, events, maxBlockWeight),
[index, events, maxBlockWeight]
);
return (
<StyledTr
className={className}
key={`extrinsic:${index}`}
>
<td
className='top'
colSpan={2}
>
<CallExpander
className='details'
mortality={mortality}
tip={value.tip?.toBn()}
value={value}
withHash
withSignature
/>
{link && (
<a
className='isDecoded'
href={link}
rel='noreferrer'
>{link}</a>
)}
</td>
<td
className='top media--1000'
colSpan={2}
>
{thisEvents.map(({ key, record }) =>
<Event
className='explorer--BlockByHash-event'
key={key}
value={record}
/>
)}
</td>
<td className='top number media--1400'>
{weight && (
<>
<>{formatNumber(weight)}</>
<div>{weightPercentage.toFixed(2)}%</div>
</>
)}
</td>
<td className='top media--1200'>
{value.isSigned
? (
<>
<AddressMini value={value.signer} />
<div className='explorer--BlockByHash-nonce'>
{t('index')} {formatNumber(value.nonce)}
</div>
<LinkExternal
data={value.hash.toHex()}
type='extrinsic'
/>
</>
)
: timestamp
? timestamp.toLocaleString()
: null
}
</td>
</StyledTr>
);
}
const StyledTr = styled.tr`
.explorer--BlockByHash-event+.explorer--BlockByHash-event {
margin-top: 0.75rem;
}
.explorer--BlockByHash-nonce {
font-size: var(--font-size-small);
margin-left: 2.25rem;
margin-top: -0.5rem;
opacity: var(--opacity-light);
text-align: left;
}
.explorer--BlockByHash-unsigned {
opacity: var(--opacity-light);
font-weight: var(--font-weight-normal);
}
a.isDecoded {
display: block;
margin-top: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
export default React.memo(ExtrinsicDisplay);
@@ -0,0 +1,109 @@
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyedEvent } from '@pezkuwi/react-hooks/ctx/types';
import type { GenericExtrinsic } from '@pezkuwi/types';
import type { BlockNumber, Extrinsic } from '@pezkuwi/types/interfaces';
import type { Vec } from '@pezkuwi/types-codec';
import type { AnyTuple } from '@pezkuwi/types-codec/types';
import type { BN } from '@pezkuwi/util';
import React, { useMemo } from 'react';
import { styled, Table, Toggle } from '@pezkuwi/react-components';
import { useAccounts, useToggle } from '@pezkuwi/react-hooks';
import { isEventFromMyAccounts } from '@pezkuwi/react-hooks/utils/isEventFromMyAccounts';
import { useTranslation } from '../translate.js';
import ExtrinsicDisplay from './Extrinsic.js';
interface Props {
blockNumber?: BlockNumber;
className?: string;
events?: KeyedEvent[] | null;
label?: React.ReactNode;
maxBlockWeight?: BN;
value?: Extrinsic[] | null;
withLink: boolean;
}
function Extrinsics ({ blockNumber, className = '', events, label, maxBlockWeight, value, withLink }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { allAccounts } = useAccounts();
const [showOnlyUserEvents, onToggleUserEvents] = useToggle();
const header = useMemo<[React.ReactNode?, string?, number?][]>(
() => [
[label || t('extrinsics'), 'start', 2],
[t('events'), 'start media--1000', 2],
[t('weight'), 'media--1400'],
[
<EventsToggle
key='eventsToggle'
onToggleUserEvents={onToggleUserEvents}
showOnlyUserEvents={showOnlyUserEvents}
/>,
'end media--1000'
]
],
[label, onToggleUserEvents, showOnlyUserEvents, t]
);
const filteredEvents = useMemo(() => {
if (!showOnlyUserEvents) {
return events;
}
return events?.filter((event) => isEventFromMyAccounts(event.record, value as Vec<GenericExtrinsic<AnyTuple>>, undefined, allAccounts));
}, [allAccounts, events, showOnlyUserEvents, value]);
return (
<Table
className={className}
empty={t('No extrinsics available')}
header={header}
isFixed
>
{value?.map((extrinsic, index): React.ReactNode =>
<ExtrinsicDisplay
blockNumber={blockNumber}
events={filteredEvents}
index={index}
key={`extrinsic:${index}`}
maxBlockWeight={maxBlockWeight}
value={extrinsic}
withLink={withLink}
/>
)}
</Table>
);
}
interface EventsToggleProps {
showOnlyUserEvents: boolean;
onToggleUserEvents: () => void;
}
const EventsToggle = ({ onToggleUserEvents, showOnlyUserEvents }: EventsToggleProps) => {
const { t } = useTranslation();
return (
<StyledDiv>
<Toggle
label={t('Show my events')}
onChange={onToggleUserEvents}
value={showOnlyUserEvents}
/>
</StyledDiv>
);
};
const StyledDiv = styled.div`
.ui--Toggle {
label{
font-size: var(--font-size-base);
}
}
`;
export default React.memo(Extrinsics);
@@ -0,0 +1,70 @@
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Option, Tuple } from '@pezkuwi/types';
import type { Justifications } from '@pezkuwi/types/interfaces';
import type { Codec, TypeDef } from '@pezkuwi/types/types';
import React, { useRef } from 'react';
import { Expander, Table } from '@pezkuwi/react-components';
import Params from '@pezkuwi/react-params';
import { getTypeDef } from '@pezkuwi/types/create';
import { useTranslation } from '../translate.js';
interface Props {
value: Option<Justifications>;
}
function formatTuple (tuple: Tuple): React.ReactNode {
const params = tuple.Types.map((type): { type: TypeDef } => ({
type: getTypeDef(type)
}));
const values = tuple.toArray().map((value): { isValid: boolean; value: Codec } => ({
isValid: true,
value
}));
return (
<Params
isDisabled
params={params}
values={values}
withExpander
/>
);
}
function JustificationList ({ value }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t('justifications'), 'start']
]);
const justifications = value.unwrapOr(null);
if (!justifications) {
return null;
}
return (
<Table
empty={t('No justifications available')}
header={headerRef.current}
>
{justifications?.map((justification, index) => (
<tr key={`justification:${index}`}>
<td className='overflow'>
<Expander summary={justification[0].toString()}>
{formatTuple(justification as unknown as Tuple)}
</Expander>
</td>
</tr>
))}
</Table>
);
}
export default React.memo(JustificationList);
@@ -0,0 +1,133 @@
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DigestItem } from '@pezkuwi/types/interfaces';
import type { Codec, TypeDef } from '@pezkuwi/types/types';
import React, { useRef } from 'react';
import { Expander, Table } from '@pezkuwi/react-components';
import Params from '@pezkuwi/react-params';
import { Raw, Struct, Tuple, Vec } from '@pezkuwi/types';
import { getTypeDef } from '@pezkuwi/types/create';
import { useTranslation } from '../translate.js';
interface Props {
value?: DigestItem[];
}
function formatU8a (value: Raw): React.ReactNode {
return (
<Params
isDisabled
params={[{ type: getTypeDef('Bytes') }]}
values={[{ isValid: true, value }]}
withExpander
/>
);
}
function formatStruct (struct: Struct): React.ReactNode {
const params = Object.entries(struct.Type).map(([name, value]): { name: string; type: TypeDef } => ({
name,
type: getTypeDef(value)
}));
const values = struct.toArray().map((value): { isValid: boolean; value: Codec } => ({
isValid: true,
value
}));
return (
<Params
isDisabled
params={params}
values={values}
withExpander
/>
);
}
function formatTuple (tuple: Tuple): React.ReactNode {
const params = tuple.Types.map((type): { type: TypeDef } => ({
type: getTypeDef(type)
}));
const values = tuple.toArray().map((value): { isValid: boolean; value: Codec } => ({
isValid: true,
value
}));
return (
<Params
isDisabled
params={params}
values={values}
withExpander
/>
);
}
function formatVector (vector: Vec<Codec>): React.ReactNode {
const type = getTypeDef(vector.Type);
const values = vector.toArray().map((value): { isValid: boolean; value: Codec } => ({
isValid: true,
value
}));
const params = values.map((_, index): { name: string; type: TypeDef } => ({
name: `${index}`,
type
}));
return (
<Params
isDisabled
params={params}
values={values}
withExpander
/>
);
}
function formatItem (item: DigestItem): React.ReactNode {
if (item.value instanceof Struct) {
return formatStruct(item.value as Struct);
} else if (item.value instanceof Tuple) {
return formatTuple(item.value);
} else if (item.value instanceof Vec) {
return formatVector(item.value as Vec<Codec>);
} else if (item.value instanceof Raw) {
return formatU8a(item.value);
}
return <div>{item.value.toString().split(',').join(', ')}</div>;
}
function Logs ({ value }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t('logs'), 'start']
]);
return (
<Table
empty={t('No logs available')}
header={headerRef.current}
>
{value?.map((log, index) => (
<tr key={`log:${index}`}>
<td className='overflow'>
<Expander
isLeft
summary={log.type.toString()}
>
{formatItem(log)}
</Expander>
</td>
</tr>
))}
</Table>
);
}
export default React.memo(Logs);
@@ -0,0 +1,154 @@
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyedEvent } from '@pezkuwi/react-hooks/ctx/types';
import type { V2Weight } from '@pezkuwi/react-hooks/useWeight';
import type { Balance, DispatchInfo, SignedBlock } from '@pezkuwi/types/interfaces';
import type { FrameSupportDispatchPerDispatchClassWeight } from '@pezkuwi/types/lookup';
import React, { useMemo } from 'react';
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
import { useApi } from '@pezkuwi/react-hooks';
import { convertWeight } from '@pezkuwi/react-hooks/useWeight';
import { FormatBalance } from '@pezkuwi/react-query';
import { BN, BN_ONE, BN_THREE, BN_TWO, formatNumber, isBn } from '@pezkuwi/util';
import { useTranslation } from '../translate.js';
interface Props {
events?: KeyedEvent[] | null;
blockWeight?: FrameSupportDispatchPerDispatchClassWeight | null;
maxBlockWeight?: BN;
maxProofSize?: BN;
signedBlock?: SignedBlock;
}
function accumulateWeights (
weight?: FrameSupportDispatchPerDispatchClassWeight | null
): { totalRefTime: BN; totalProofSize: BN } {
const totalRefTime = new BN(0);
const totalProofSize = new BN(0);
(['normal', 'operational', 'mandatory'] as const).forEach((cls) => {
totalRefTime.iadd(weight?.[cls].refTime.toBn() ?? new BN(0));
totalProofSize.iadd(weight?.[cls].proofSize.toBn() ?? new BN(0));
});
return { totalProofSize, totalRefTime };
}
function extractEventDetails (events?: KeyedEvent[] | null): [BN?, BN?, BN?, BN?] {
return events
? events.reduce(([deposits, transfers, weight, proofSize], { record: { event: { data, method, section } } }) => {
const size = (convertWeight(
((method === 'ExtrinsicSuccess' ? data[0] : data[1]) as DispatchInfo)?.weight
).v2Weight as V2Weight).proofSize;
return [
section === 'balances' && method === 'Deposit'
? deposits.iadd(data[1] as Balance)
: deposits,
section === 'balances' && method === 'Transfer'
? transfers.iadd(data[2] as Balance)
: transfers,
section === 'system' && ['ExtrinsicFailed', 'ExtrinsicSuccess'].includes(method)
? weight.iadd(convertWeight(
((method === 'ExtrinsicSuccess' ? data[0] : data[1]) as DispatchInfo)?.weight
).v1Weight)
: weight,
section === 'system' && ['ExtrinsicFailed', 'ExtrinsicSuccess'].includes(method)
? proofSize.iadd(isBn(size) ? size : size.toBn())
: proofSize
];
}, [new BN(0), new BN(0), new BN(0), new BN(0)])
: [];
}
function Summary ({ blockWeight, events, maxBlockWeight, maxProofSize, signedBlock }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const [deposits, transfers, weight, size] = useMemo(
() => {
const eventDetails = extractEventDetails(events);
const { totalProofSize, totalRefTime } = accumulateWeights(blockWeight);
// Block weight is the source of truth; using events data as fallback only
if (blockWeight) {
eventDetails[2] = totalRefTime;
eventDetails[3] = totalProofSize;
}
return eventDetails;
},
[blockWeight, events]
);
return (
<SummaryBox>
<section>
{api.query.balances && (
<>
<CardSummary label={t('deposits')}>
<FormatBalance
className={deposits ? '' : '--tmp'}
value={deposits || BN_ONE}
/>
</CardSummary>
<CardSummary
className='media--1000'
label={t('transfers')}
>
<FormatBalance
className={transfers ? '' : '--tmp'}
value={transfers || BN_ONE}
/>
</CardSummary>
</>
)}
</section>
<section>
<CardSummary
label={t('ref time')}
progress={{
hideValue: true,
isBlurred: !(maxBlockWeight && weight),
total: (maxBlockWeight && weight) ? maxBlockWeight : BN_THREE,
value: (maxBlockWeight && weight) ? weight : BN_TWO
}}
>
{weight
? formatNumber(weight)
: <span className='--tmp'>999,999,999</span>}
</CardSummary>
{maxProofSize && size &&
<CardSummary
label={t('proof size')}
progress={{
hideValue: true,
isBlurred: false,
total: maxProofSize,
value: size
}}
>
{formatNumber(size)}
</CardSummary>}
</section>
<section className='media--900'>
<CardSummary label={t('event count')}>
{events
? formatNumber(events.length)
: <span className='--tmp'>99</span>}
</CardSummary>
<CardSummary label={t('extrinsic count')}>
{signedBlock
? formatNumber(signedBlock.block.extrinsics.length)
: <span className='--tmp'>99</span>}
</CardSummary>
</section>
</SummaryBox>
);
}
export default React.memo(Summary);
@@ -0,0 +1,48 @@
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useBestNumber } from '@pezkuwi/react-hooks';
import { isHex } from '@pezkuwi/util';
import Query from '../Query.js';
import BlockByHash from './ByHash.js';
import BlockByNumber from './ByNumber.js';
function Entry (): React.ReactElement | null {
const bestNumber = useBestNumber();
const { value } = useParams<{ value: string }>();
const [stateValue, setStateValue] = useState<string | undefined>(value);
useEffect((): void => {
setStateValue((stateValue) =>
value && value !== stateValue
? value
: !stateValue && bestNumber
? bestNumber.toString()
: stateValue
);
}, [bestNumber, value]);
if (!stateValue) {
return null;
}
const Component = isHex(stateValue)
? BlockByHash
: BlockByNumber;
return (
<>
<Query />
<Component
key={stateValue}
value={stateValue}
/>
</>
);
}
export default React.memo(Entry);