Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/page-staking/src/Bags/Bag.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> {
// const entries = useBagEntries(stashNodes.length ? info.head.unwrapOr(null) : null);

return (
<tr>
<td className='number'>{formatNumber(id)}</td>
<td className='address'>{info.head.isSome && <AddressMini value={info.head} />}</td>
<td className='address'>{info.tail.isSome && <AddressMini value={info.tail} />}</td>
<td className='address'>
{stashNodes?.map(({ stashId }) => (
<AddressMini
key={stashId}
value={stashId}
withBonded
/>
))}
</td>
</tr>
);
}
31 changes: 31 additions & 0 deletions packages/page-staking/src/Bags/Summary.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> {
const { t } = useTranslation();

return (
<SummaryBox className={className}>
<CardSummary label={t<string>('total bags')}>
{ids
? formatNumber(ids.length)
: <Spinner />
}
</CardSummary>
</SummaryBox>
);
}
74 changes: 74 additions & 0 deletions packages/page-staking/src/Bags/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> {
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 (
<>
<Summary ids={ids} />
<Table
empty={list && list.length === 0 && t<string>('No available bags')}
emptySpinner={t<string>('Retrieving all available bags, this will take some time')}
header={headerRef.current}
>
{sorted && sorted.map(([key, id, info]) => (
<Bag
id={id}
info={info}
key={key}
stashNodes={nodes[key]}
/>
))}
</Table>
</>
);
}
9 changes: 9 additions & 0 deletions packages/page-staking/src/Bags/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
43 changes: 43 additions & 0 deletions packages/page-staking/src/Bags/useBagEntries.tsx
Original file line number Diff line number Diff line change
@@ -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<Option<PalletBagsListListNode>>(!!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);
19 changes: 19 additions & 0 deletions packages/page-staking/src/Bags/useBagsIds.ts
Original file line number Diff line number Diff line change
@@ -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);
26 changes: 26 additions & 0 deletions packages/page-staking/src/Bags/useBagsList.ts
Original file line number Diff line number Diff line change
@@ -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<PalletBagsListListBag>[]]): Result[] =>
ids
.map((id, index): [u64, Option<PalletBagsListListBag>] => [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);
37 changes: 37 additions & 0 deletions packages/page-staking/src/Bags/useBagsNodes.tsx
Original file line number Diff line number Diff line change
@@ -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<string, StashNode[]>;

const multiOptions = {
defaultValue: {} as Result,
transform: (opts: Option<PalletBagsListListNode>[]): 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);
4 changes: 4 additions & 0 deletions packages/page-staking/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -129,6 +130,9 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement<Pr
targets={targets}
/>
<Switch>
<Route path={`${basePath}/bags`}>
<Bags ownStashes={ownStashes} />
</Route>
<Route path={`${basePath}/payout`}>
<Payouts
isInElection={isInElection}
Expand Down