Files
pezkuwi-extension/packages/extension-ui/src/Popup/Signing/Extrinsic.tsx
T

181 lines
5.3 KiB
TypeScript

// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Chain } from '@pezkuwi/extension-chains/types';
import type { Call, ExtrinsicEra, ExtrinsicPayload } from '@pezkuwi/types/interfaces';
import type { AnyJson, SignerPayloadJSON } from '@pezkuwi/types/types';
import type { BN } from '@pezkuwi/util';
import type { TFunction } from '../../hooks/useTranslation.js';
import { convertMultilocationToUrl } from '@paraspell/xcm-analyser';
import React, { useMemo, useRef } from 'react';
import { bnToBn, formatNumber } from '@pezkuwi/util';
import { Table } from '../../components/index.js';
import { useMetadata, useTranslation } from '../../hooks/index.js';
interface Decoded {
args: AnyJson | null;
method: Call | null;
}
interface Props {
className?: string;
payload: ExtrinsicPayload;
request: SignerPayloadJSON;
url: string;
}
function displayDecodeVersion (message: string, chain: Chain, specVersion: BN): string {
return `${message}: chain=${chain.name}, specVersion=${chain.specVersion.toString()} (request specVersion=${specVersion.toString()})`;
}
function decodeMethod (data: string, chain: Chain, specVersion: BN): Decoded {
let args: AnyJson | null = null;
let method: Call | null = null;
try {
if (specVersion.eqn(chain.specVersion)) {
method = chain.registry.createType('Call', data);
args = (method.toHuman() as { args: AnyJson }).args;
} else {
console.log(displayDecodeVersion('Outdated metadata to decode', chain, specVersion));
}
} catch (error) {
console.error(`${displayDecodeVersion('Error decoding method', chain, specVersion)}:: ${(error as Error).message}`);
args = null;
method = null;
}
return { args, method };
}
function renderMethod (data: string, { args, method }: Decoded, t: TFunction): React.ReactNode {
if (!args || !method) {
return (
<tr>
<td className='label'>{t('method data')}</td>
<td className='data'>{data}</td>
</tr>
);
}
return (
<>
<tr>
<td className='label'>{t('method')}</td>
<td className='data'>
<details>
<summary>{method.section}.{method.method}{
method.meta
? `(${method.meta.args.map(({ name }) => name).join(', ')})`
: ''
}</summary>
<pre>{JSON.stringify(args, null, 2)}</pre>
</details>
</td>
</tr>
{method.meta && (
<tr>
<td className='label'>{t('info')}</td>
<td className='data'>
<details>
<summary>{method.meta.docs.map((d) => d.toString().trim()).join(' ')}</summary>
</details>
</td>
</tr>
)}
</>
);
}
function mortalityAsString (era: ExtrinsicEra, hexBlockNumber: string, t: TFunction): string {
if (era.isImmortalEra) {
return t('immortal');
}
const blockNumber = bnToBn(hexBlockNumber);
const mortal = era.asMortalEra;
return t('mortal, valid from {{birth}} to {{death}}', {
replace: {
birth: formatNumber(mortal.birth(blockNumber)),
death: formatNumber(mortal.death(blockNumber))
}
});
}
function getHumanReadableAssetId (assetId: unknown): string | undefined {
try {
return convertMultilocationToUrl(assetId);
} catch (_) {
return undefined;
}
}
function Extrinsic ({ className, payload, request: { blockNumber, genesisHash, method, specVersion: hexSpec }, url }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const chain = useMetadata(genesisHash);
const specVersion = useRef(bnToBn(hexSpec)).current;
const decoded = useMemo(
() => chain && chain.hasMetadata
? decodeMethod(method, chain, specVersion)
: { args: null, method: null },
[method, chain, specVersion]
);
const humanReadablePayload = payload.toHuman() as Record<string, unknown>;
const assetId = humanReadablePayload['assetId'];
const humanReadableAssetId = getHumanReadableAssetId(assetId);
return (
<Table
className={className}
isFull
>
<tr>
<td className='label'>{t('from')}</td>
<td className='data'>{url}</td>
</tr>
<tr>
<td className='label'>{chain ? t('chain') : t('genesis')}</td>
<td className='data'>{chain ? chain.name : genesisHash}</td>
</tr>
<tr>
<td className='label'>{t('version')}</td>
<td className='data'>{specVersion.toNumber()}</td>
</tr>
<tr>
<td className='label'>{t('nonce')}</td>
<td className='data'>{formatNumber(payload.nonce)}</td>
</tr>
{!payload.tip.isEmpty && (
<tr>
<td className='label'>{t('tip')}</td>
<td className='data'>{formatNumber(payload.tip)}</td>
</tr>
)}
{!!assetId && (
<tr>
<td className='label'>{t('assetId')}</td>
<td className='data'>
<details>
<summary>{humanReadableAssetId || '{...}'}</summary>
<pre>{JSON.stringify(assetId, null, 2)}</pre>
</details>
</td>
</tr>
)}
{renderMethod(method, decoded, t)}
<tr>
<td className='label'>{t('lifetime')}</td>
<td className='data'>{mortalityAsString(payload.era, blockNumber, t)}</td>
</tr>
</Table>
);
}
export default React.memo(Extrinsic);