mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-20 14:01:05 +00:00
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:
@@ -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);
|
||||
Reference in New Issue
Block a user