// Copyright 2017-2026 @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 { 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).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 ( {link && ( {link} )} {thisEvents.map(({ key, record }) => )} {weight && ( <> <>{formatNumber(weight)}
{weightPercentage.toFixed(2)}%
)} {value.isSigned ? ( <>
{t('index')} {formatNumber(value.nonce)}
) : timestamp ? timestamp.toLocaleString() : null }
); } 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);