polkadot-js/apps

View on GitHub
packages/page-staking-legacy/src/Slashes/index.tsx

Summary

Maintainability
F
1 wk
Test Coverage
// Copyright 2017-2024 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { StakerState } from '@polkadot/react-hooks/types';
import type { UnappliedSlash } from '@polkadot/types/interfaces';
import type { Slash, SlashEra } from './types.js';

import React, { useMemo, useRef, useState } from 'react';

import { getSlashProposalThreshold } from '@polkadot/apps-config';
import { Table, ToggleGroup } from '@polkadot/react-components';
import { useAccounts, useApi, useCollectiveMembers } from '@polkadot/react-hooks';
import { BN, BN_ONE, formatNumber } from '@polkadot/util';

import { useTranslation } from '../translate.js';
import Era from './Era.js';

interface Props {
  ownStashes?: StakerState[];
  slashes: [BN, UnappliedSlash[]][];
}

function calcSlashEras (slashes: [BN, UnappliedSlash[]][], ownStashes: StakerState[]): SlashEra[] {
  const slashEras: SlashEra[] = [];

  slashes
    .reduce((rows: Slash[], [era, slashes]): Slash[] => {
      return slashes.reduce((rows: Slash[], slash): Slash[] => {
        const totalOther = slash.others.reduce((total: BN, [, value]): BN => {
          return total.add(value);
        }, new BN(0));

        const isMine = ownStashes.some(({ stashId }): boolean => {
          return slash.validator.eq(stashId) || slash.others.some(([nominatorId]) => nominatorId.eq(stashId));
        });

        rows.push({ era, isMine, slash, total: slash.own.add(totalOther), totalOther });

        return rows;
      }, rows);
    }, [])
    .forEach((slash): void => {
      let slashEra = slashEras.find(({ era }) => era.eq(slash.era));

      if (!slashEra) {
        slashEra = {
          era: slash.era,
          nominators: [],
          payout: new BN(0),
          reporters: [],
          slashes: [],
          total: new BN(0),
          validators: []
        };
        slashEras.push(slashEra);
      }

      slashEra.payout.iadd(slash.slash.payout);
      slashEra.total.iadd(slash.total);
      slashEra.slashes.push(slash);

      const validatorId = slash.slash.validator.toString();

      if (!slashEra.validators.includes(validatorId)) {
        slashEra.validators.push(validatorId);
      }

      slash.slash.others.forEach(([accountId]): void => {
        const nominatorId = accountId.toString();

        if (slashEra && !slashEra.nominators.includes(nominatorId)) {
          slashEra.nominators.push(nominatorId);
        }
      });

      slash.slash.reporters.forEach((accountId): void => {
        const reporterId = accountId.toString();

        if (slashEra && !slashEra.reporters.includes(reporterId)) {
          slashEra.reporters.push(reporterId);
        }
      });
    });

  return slashEras.sort((a, b) => b.era.cmp(a.era));
}

function Slashes ({ ownStashes = [], slashes }: Props): React.ReactElement<Props> | null {
  const { t } = useTranslation();
  const { api } = useApi();
  const { allAccounts } = useAccounts();
  const { members } = useCollectiveMembers('council');
  const [selectedIndex, setSelectedIndex] = useState(0);

  const rows = useMemo(
    () => calcSlashEras(slashes, ownStashes),
    [ownStashes, slashes]
  );

  const eraOpts = useMemo(
    () => rows
      .map(({ era }) =>
        api.query.staking.earliestUnappliedSlash || !api.consts.staking.slashDeferDuration
          ? era
          : era.sub(api.consts.staking.slashDeferDuration).sub(BN_ONE)
      )
      .map((era) => ({
        text: t('era {{era}}', { replace: { era: formatNumber(era) } }),
        value: era.toString()
      })),
    [api, rows, t]
  );

  const councilId = useMemo(
    () => allAccounts.find((accountId) => members.includes(accountId)) || null,
    [allAccounts, members]
  );

  const emptyHeader = useRef<[React.ReactNode?, string?, number?][]>([
    [t('unapplied'), 'start']
  ]);

  if (!rows.length) {
    return (
      <Table
        empty={t('There are no unapplied/pending slashes')}
        header={emptyHeader.current}
      />
    );
  }

  const councilThreshold = Math.ceil((members.length || 0) * getSlashProposalThreshold(api));

  return (
    <Era
      buttons={
        <ToggleGroup
          onChange={setSelectedIndex}
          options={eraOpts}
          value={selectedIndex}
        />
      }
      councilId={councilId}
      councilThreshold={councilThreshold}
      key={rows[selectedIndex].era.toString()}
      slash={rows[selectedIndex]}
    />
  );
}

export default React.memo(Slashes);