diff --git a/packages/page-staking/src/Bags/Bag.tsx b/packages/page-staking/src/Bags/Bag.tsx new file mode 100644 index 000000000000..7d7843011090 --- /dev/null +++ b/packages/page-staking/src/Bags/Bag.tsx @@ -0,0 +1,40 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { u64 } from '@polkadot/types'; +import type { PalletBagsListListBag } from '@polkadot/types/lookup'; +import type { StashNode } from './types'; + +import React from 'react'; + +import { AddressMini } from '@polkadot/react-components'; +import { formatNumber } from '@polkadot/util'; + +// import useBagEntries from './useBagEntries'; + +interface Props { + id: u64; + info: PalletBagsListListBag; + stashNodes?: StashNode[]; +} + +export default function Bag ({ id, info, stashNodes = [] }: Props): React.ReactElement { + // const entries = useBagEntries(stashNodes.length ? info.head.unwrapOr(null) : null); + + return ( + + {formatNumber(id)} + {info.head.isSome && } + {info.tail.isSome && } + + {stashNodes?.map(({ stashId }) => ( + + ))} + + + ); +} diff --git a/packages/page-staking/src/Bags/Summary.tsx b/packages/page-staking/src/Bags/Summary.tsx new file mode 100644 index 000000000000..82471772a309 --- /dev/null +++ b/packages/page-staking/src/Bags/Summary.tsx @@ -0,0 +1,31 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { u64 } from '@polkadot/types'; + +import React from 'react'; + +import { CardSummary, Spinner, SummaryBox } from '@polkadot/react-components'; +import { formatNumber } from '@polkadot/util'; + +import { useTranslation } from '../translate'; + +interface Props { + className?: string; + ids?: u64[]; +} + +export default function Summary ({ className = '', ids }: Props): React.ReactElement { + const { t } = useTranslation(); + + return ( + + ('total bags')}> + {ids + ? formatNumber(ids.length) + : + } + + + ); +} diff --git a/packages/page-staking/src/Bags/index.tsx b/packages/page-staking/src/Bags/index.tsx new file mode 100644 index 000000000000..5ac43281d033 --- /dev/null +++ b/packages/page-staking/src/Bags/index.tsx @@ -0,0 +1,74 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { StakerState } from '@polkadot/react-hooks/types'; + +import React, { useMemo, useRef } from 'react'; + +import { Table } from '@polkadot/react-components'; + +import { useTranslation } from '../translate'; +import Bag from './Bag'; +import Summary from './Summary'; +import useBagsIds from './useBagsIds'; +import useBagsList from './useBagsList'; +import useBagsNodes from './useBagsNodes'; + +interface Props { + ownStashes?: StakerState[]; +} + +export default function Bags ({ ownStashes }: Props): React.ReactElement { + const { t } = useTranslation(); + const ids = useBagsIds(); + const list = useBagsList(ids); + const stashIds = useMemo( + () => ownStashes + ? ownStashes.map(({ stashId }) => stashId) + : [], + [ownStashes] + ); + const nodes = useBagsNodes(stashIds); + + const headerRef = useRef([ + [t('bags')], + [t('head'), 'address'], + [t('tail'), 'address'], + [t('mine'), 'address'] + ]); + + const sorted = useMemo( + () => list + ? [...list].sort((a, b) => + nodes[a[0]] + ? nodes[b[0]] + ? b[1].cmp(a[1]) + : -1 + : nodes[b[0]] + ? 1 + : b[1].cmp(a[1]) + ) + : null, + [list, nodes] + ); + + return ( + <> + + ('No available bags')} + emptySpinner={t('Retrieving all available bags, this will take some time')} + header={headerRef.current} + > + {sorted && sorted.map(([key, id, info]) => ( + + ))} +
+ + ); +} diff --git a/packages/page-staking/src/Bags/types.ts b/packages/page-staking/src/Bags/types.ts new file mode 100644 index 000000000000..9722fc75541f --- /dev/null +++ b/packages/page-staking/src/Bags/types.ts @@ -0,0 +1,9 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { PalletBagsListListNode } from '@polkadot/types/lookup'; + +export interface StashNode { + stashId: string; + node: PalletBagsListListNode; +} diff --git a/packages/page-staking/src/Bags/useBagEntries.tsx b/packages/page-staking/src/Bags/useBagEntries.tsx new file mode 100644 index 000000000000..be6cc00139f1 --- /dev/null +++ b/packages/page-staking/src/Bags/useBagEntries.tsx @@ -0,0 +1,43 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Option } from '@polkadot/types'; +import type { AccountId32 } from '@polkadot/types/interfaces'; +import type { PalletBagsListListNode } from '@polkadot/types/lookup'; + +import { useEffect, useState } from 'react'; + +import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; + +function useBagEntriesImpl (headId: AccountId32 | null): AccountId32[] { + const { api } = useApi(); + const [[currId, entries], setCurrent] = useState<[AccountId32 | null, AccountId32[]]>([null, []]); + const node = useCall>(!!currId && api.query.bagsList.listNodes, [currId]); + + useEffect( + (): void => { + setCurrent( + headId + ? [headId, [headId]] + : [null, []] + ); + }, + [headId] + ); + + useEffect((): void => { + if (node && node.isSome) { + const { next } = node.unwrap(); + + if (next.isSome) { + const currId = next.unwrap(); + + setCurrent(([, entries]) => [currId, [...entries, currId]]); + } + } + }, [node]); + + return entries; +} + +export default createNamedHook('useBagEntries', useBagEntriesImpl); diff --git a/packages/page-staking/src/Bags/useBagsIds.ts b/packages/page-staking/src/Bags/useBagsIds.ts new file mode 100644 index 000000000000..f76d05e359b7 --- /dev/null +++ b/packages/page-staking/src/Bags/useBagsIds.ts @@ -0,0 +1,19 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { StorageKey, u64 } from '@polkadot/types'; + +import { createNamedHook, useApi, useMapKeys } from '@polkadot/react-hooks'; + +const keyOptions = { + transform: (keys: StorageKey<[u64]>[]): u64[] => + keys.map(({ args: [id] }) => id) +}; + +function useBagsIdsImpl (): u64[] | undefined { + const { api } = useApi(); + + return useMapKeys(api.query.bagsList.listBags, keyOptions); +} + +export default createNamedHook('useBagsIds', useBagsIdsImpl); diff --git a/packages/page-staking/src/Bags/useBagsList.ts b/packages/page-staking/src/Bags/useBagsList.ts new file mode 100644 index 000000000000..6c8970120969 --- /dev/null +++ b/packages/page-staking/src/Bags/useBagsList.ts @@ -0,0 +1,26 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Option, u64 } from '@polkadot/types'; +import type { PalletBagsListListBag } from '@polkadot/types/lookup'; + +import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; + +type Result = [string, u64, PalletBagsListListBag]; + +const multiOptions = { + transform: ([[ids], opts]: [[u64[]], Option[]]): Result[] => + ids + .map((id, index): [u64, Option] => [id, opts[index]]) + .filter(([, opt]) => opt.isSome) + .map(([id, opt]): Result => [id.toString(), id, opt.unwrap()]), + withParamsTransform: true +}; + +function useBagsListImpl (ids?: u64[]): Result[] | undefined { + const { api } = useApi(); + + return useCall(ids && ids.length !== 0 && api.query.bagsList.listBags.multi, [ids], multiOptions); +} + +export default createNamedHook('useBagsList', useBagsListImpl); diff --git a/packages/page-staking/src/Bags/useBagsNodes.tsx b/packages/page-staking/src/Bags/useBagsNodes.tsx new file mode 100644 index 000000000000..09a5eac0401d --- /dev/null +++ b/packages/page-staking/src/Bags/useBagsNodes.tsx @@ -0,0 +1,37 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Option } from '@polkadot/types'; +import type { PalletBagsListListNode } from '@polkadot/types/lookup'; +import type { StashNode } from './types'; + +import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; + +type Result = Record; + +const multiOptions = { + defaultValue: {} as Result, + transform: (opts: Option[]): Result => + opts + .filter((o) => o.isSome) + .map((o): PalletBagsListListNode => o.unwrap()) + .reduce((all: Result, node): Result => { + const id = node.bagUpper.toString(); + + if (!all[id]) { + all[id] = []; + } + + all[id].push({ node, stashId: node.id.toString() }); + + return all; + }, {}) +}; + +function useBagsNodesImpl (stashIds: string[]): Result { + const { api } = useApi(); + + return useCall(stashIds && stashIds.length !== 0 && api.query.bagsList.listNodes.multi, [stashIds], multiOptions) as Result; +} + +export default createNamedHook('useBagsNodes', useBagsNodesImpl); diff --git a/packages/page-staking/src/index.tsx b/packages/page-staking/src/index.tsx index 6079ef462c57..6258fc116b2e 100644 --- a/packages/page-staking/src/index.tsx +++ b/packages/page-staking/src/index.tsx @@ -19,6 +19,7 @@ import basicMd from './md/basic.md'; import Summary from './Overview/Summary'; import Actions from './Actions'; import ActionsBanner from './ActionsBanner'; +import Bags from './Bags'; import { STORE_FAVS_BASE } from './constants'; import Overview from './Overview'; import Payouts from './Payouts'; @@ -129,6 +130,9 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement + + +