diff --git a/packages/page-parachains/src/Disputes/index.tsx b/packages/page-parachains/src/Disputes/index.tsx new file mode 100644 index 000000000000..00e7c925b3da --- /dev/null +++ b/packages/page-parachains/src/Disputes/index.tsx @@ -0,0 +1,85 @@ +// Copyright 2017-2023 @polkadot/app-parachains authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DisputeRecord } from './types.js'; + +import React, { useMemo, useRef } from 'react'; + +import { AddressMini, Table } from '@polkadot/react-components'; + +import { useTranslation } from '../translate.js'; +import useSessionDisputes from './useSessionDisputes.js'; + +interface Props { + className?: string; +} + +function transposeDisputes (disputes: DisputeRecord): React.ReactNode[] { + let lastSession = ''; + let inclSession = true; + + return Object + .entries(disputes) + .reduce((flattened: [string, string, string[]][], [s, r]) => + Object + .entries(r) + .reduce((flattened, [k, vals]) => { + flattened.push([s, k, vals]); + + return flattened; + }, flattened), [] + ) + .map(([s, k, vals], index) => { + if (lastSession !== s) { + lastSession = s; + inclSession = true; + } else { + inclSession = false; + } + + return ( + + { + inclSession + ? + : + } + {k} + + {vals.map((v) => ( + + ))} + + + ); + }); +} + +function Disputes ({ className }: Props): React.ReactElement { + const { t } = useTranslation(); + const disputeInfo = useSessionDisputes(); + + const headerRef = useRef<[React.ReactNode?, string?, number?][]>([ + [t('disputes'), 'start', 3] + ]); + + const rows = useMemo( + () => disputeInfo?.disputes && transposeDisputes(disputeInfo.disputes), + [disputeInfo] + ); + + return ( + ('No ongoing disputes found')} + header={headerRef.current} + > + {rows} +
+ ); +} + +export default React.memo(Disputes); diff --git a/packages/page-parachains/src/Disputes/types.ts b/packages/page-parachains/src/Disputes/types.ts new file mode 100644 index 000000000000..5d23436f3b7c --- /dev/null +++ b/packages/page-parachains/src/Disputes/types.ts @@ -0,0 +1,19 @@ +// Copyright 2017-2023 @polkadot/app-parachains authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; +import type { HexString } from '@polkadot/util/types'; + +export interface SessionInfo { + paraValidators: string[]; + sessionCurrentIndex: BN; + sessionIndexes: BN[]; + sessionValidators: string[]; +} + +export type DisputeRecord = Record>; + +export interface DisputeInfo { + disputes?: DisputeRecord; + sessionInfo: SessionInfo; +} diff --git a/packages/page-parachains/src/Disputes/useSessionDisputes.ts b/packages/page-parachains/src/Disputes/useSessionDisputes.ts new file mode 100644 index 000000000000..7b97091d7aa8 --- /dev/null +++ b/packages/page-parachains/src/Disputes/useSessionDisputes.ts @@ -0,0 +1,77 @@ +// Copyright 2017-2023 @polkadot/app-parachains authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { Option, StorageKey, u32 } from '@polkadot/types'; +import type { Hash } from '@polkadot/types/interfaces'; +import type { PolkadotPrimitivesV4DisputeState } from '@polkadot/types/lookup'; +import type { BN } from '@polkadot/util'; +import type { HexString } from '@polkadot/util/types'; +import type { DisputeInfo, SessionInfo } from './types.js'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useApi } from '@polkadot/react-hooks'; +import { formatNumber } from '@polkadot/util'; + +import useSessionInfo from './useSessionInfo.js'; + +function queryDisputes (api: ApiPromise, sessionInfo: SessionInfo): Promise { + return Promise + .all( + // FIXME We would need to pull the list of validators alongside + // sessionInfo.sessionIndexes.map((index) => + [sessionInfo.sessionCurrentIndex].map((index) => + api.query.parasDisputes.disputes.entries(index) as unknown as Promise<[StorageKey<[u32, Hash]>, Option][]> + ) + ) + .then((entries) => + // TODO Here we wish to extract the actual sessionValidators as well + entries.map((list) => + list.map(([{ args: [session, id] }, optInfo]): [BN, Hash, boolean[]] => [session, id, optInfo.isSome ? optInfo.unwrap().validatorsAgainst.toBoolArray() : []]) + ) + ) + .then((entries) => ({ + disputes: entries.reduce>>((all, list) => + list.reduce((all, [sessionIndex, hash, flags]) => { + const s = formatNumber(sessionIndex); + + if (!all[s]) { + all[s] = {}; + } + + all[s][hash.toHex()] = flags.reduce((vals, flag, index) => { + if (flag) { + vals.push(sessionInfo.paraValidators[index]); + } + + return vals; + }, []); + + return all; + }, all), {}), + sessionInfo + })); +} + +function useSessionDisputesImpl (): DisputeInfo | undefined { + const { api } = useApi(); + const [state, setState] = useState(); + const sessionInfo = useSessionInfo(); + + useEffect((): void => { + if (sessionInfo) { + if (sessionInfo.sessionIndexes) { + queryDisputes(api, sessionInfo) + .then(setState) + .catch(console.error); + } else { + setState({ sessionInfo }); + } + } + }, [api, sessionInfo]); + + return state; +} + +export default createNamedHook('useSessionDisputes', useSessionDisputesImpl); diff --git a/packages/page-parachains/src/Disputes/useSessionInfo.ts b/packages/page-parachains/src/Disputes/useSessionInfo.ts new file mode 100644 index 000000000000..49b0ae023b18 --- /dev/null +++ b/packages/page-parachains/src/Disputes/useSessionInfo.ts @@ -0,0 +1,47 @@ +// Copyright 2017-2023 @polkadot/app-parachains authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Option, u32 } from '@polkadot/types'; +import type { AccountId } from '@polkadot/types/interfaces'; +import type { BN } from '@polkadot/util'; +import type { SessionInfo } from './types.js'; + +import { createNamedHook, useApi, useCallMulti } from '@polkadot/react-hooks'; +import { BN_ONE } from '@polkadot/util'; + +const OPT_MULTI = { + transform: ([sessionCurrentIndex, validators, optLastPruned, activeValidatorIndices]: [u32, AccountId[], Option, u32[]]): SessionInfo => { + const sessionValidators = validators.map((v) => v.toString()); + const sessionIndexes: BN[] = [sessionCurrentIndex]; + + if (optLastPruned.isSome) { + const lastPruned = optLastPruned.unwrap(); + const nextIndex = sessionCurrentIndex.sub(BN_ONE); + + while (nextIndex.gt(lastPruned)) { + sessionIndexes.push(nextIndex); + nextIndex.isub(BN_ONE); + } + } + + return { + paraValidators: activeValidatorIndices.map((i) => sessionValidators[i.toNumber()]), + sessionCurrentIndex, + sessionIndexes, + sessionValidators + }; + } +}; + +function useSessionInfoImpl (): SessionInfo | undefined { + const { api } = useApi(); + + return useCallMulti([ + api.query.session?.currentIndex, + api.query.session?.validators, + api.query.parasDisputes?.lastPrunedSession, + api.query.parasShared?.activeValidatorIndices + ], OPT_MULTI); +} + +export default createNamedHook('useSessionInfo', useSessionInfoImpl); diff --git a/packages/page-parachains/src/index.tsx b/packages/page-parachains/src/index.tsx index aef7186219be..c18b33a58bf4 100644 --- a/packages/page-parachains/src/index.tsx +++ b/packages/page-parachains/src/index.tsx @@ -14,6 +14,7 @@ import { useApi, useCall } from '@polkadot/react-hooks'; import Auctions from './Auctions/index.js'; import Crowdloan from './Crowdloan/index.js'; +import Disputes from './Disputes/index.js'; import Overview from './Overview/index.js'; import Parathreads from './Parathreads/index.js'; import Proposals from './Proposals/index.js'; @@ -100,6 +101,12 @@ function ParachainsApp ({ basePath, className }: Props): React.ReactElement + + } + path='disputes' + /> diff --git a/packages/react-components/src/Table/Column/Id.tsx b/packages/react-components/src/Table/Column/Id.tsx index a97d7dc7fa91..10d3372482f1 100644 --- a/packages/react-components/src/Table/Column/Id.tsx +++ b/packages/react-components/src/Table/Column/Id.tsx @@ -5,7 +5,7 @@ import type { BN } from '@polkadot/util'; import React from 'react'; -import { formatNumber } from '@polkadot/util'; +import { formatNumber, isString } from '@polkadot/util'; import { styled } from '../../styled.js'; @@ -14,7 +14,7 @@ export interface Props { className?: string; colSpan?: number; rowSpan?: number; - value: BN | number; + value: BN | number | string; } function Id ({ children, className = '', colSpan, rowSpan, value }: Props): React.ReactElement { @@ -24,7 +24,13 @@ function Id ({ children, className = '', colSpan, rowSpan, value }: Props): Reac colSpan={colSpan} rowSpan={rowSpan} > -

{formatNumber(value)}

+

+ { + isString(value) + ? value + : formatNumber(value) + } +

{children} );