polkadot-js/apps

View on GitHub
packages/page-explorer/src/BlockInfo/Extrinsic.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
// Copyright 2017-2024 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import type { BlockNumber, DispatchInfo, Extrinsic } from '@polkadot/types/interfaces';
import type { ICompact, INumber } from '@polkadot/types/types';

import React, { useMemo } from 'react';

import { AddressMini, LinkExternal, styled } from '@polkadot/react-components';
import { convertWeight } from '@polkadot/react-hooks/useWeight';
import { CallExpander } from '@polkadot/react-params';
import { BN, formatNumber } from '@polkadot/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);