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}
);