Display asset balances (#5128)
This commit is contained in:
parent
7a889aae8b
commit
b3ba9a4b6a
44
packages/page-assets/src/Balances/Balance.tsx
Normal file
44
packages/page-assets/src/Balances/Balance.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright 2017-2021 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetBalance } from '@polkadot/types/interfaces';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressSmall } from '@polkadot/react-components';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
accountId: string;
|
||||
balance: AssetBalance;
|
||||
className?: string;
|
||||
siFormat: [number, string];
|
||||
}
|
||||
|
||||
function Balance ({ accountId, balance: { balance, isFrozen, isSufficient }, className, siFormat }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<td className='address'>
|
||||
<AddressSmall value={accountId} />
|
||||
</td>
|
||||
<td className='start'>
|
||||
{isFrozen.isTrue ? t<string>('Yes') : t<string>('No')}
|
||||
</td>
|
||||
<td className='start'>
|
||||
{isSufficient.isTrue ? t<string>('Yes') : t<string>('No')}
|
||||
</td>
|
||||
<td className='number all'>
|
||||
<FormatBalance
|
||||
format={siFormat}
|
||||
value={balance}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Balance);
|
||||
94
packages/page-assets/src/Balances/index.tsx
Normal file
94
packages/page-assets/src/Balances/index.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
// Copyright 2017-2021 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetInfo, AssetInfoComplete } from '../types';
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Dropdown, Table } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import Balance from './Balance';
|
||||
import useBalances from './useBalances';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
infos?: AssetInfo[];
|
||||
}
|
||||
|
||||
function Balances ({ className, infos = [] }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [infoIndex, setInfoIndex] = useState(0);
|
||||
const [info, setInfo] = useState<AssetInfoComplete | null>(null);
|
||||
const balances = useBalances(info);
|
||||
|
||||
const headerRef = useRef([
|
||||
[t('accounts'), 'start'],
|
||||
[t('frozen'), 'start'],
|
||||
[t('sufficient'), 'start'],
|
||||
[]
|
||||
]);
|
||||
|
||||
const completeInfos = useMemo(
|
||||
() => infos
|
||||
.filter((i): i is AssetInfoComplete => !!(i.details && i.metadata) && !i.details.supply.isZero())
|
||||
.sort((a, b) => a.id.cmp(b.id)),
|
||||
[infos]
|
||||
);
|
||||
|
||||
const assetOptions = useMemo(
|
||||
() => completeInfos.map(({ id, metadata }, index) => ({
|
||||
text: `${metadata.name.toUtf8()} (${formatNumber(id)})`,
|
||||
value: index
|
||||
})),
|
||||
[completeInfos]
|
||||
);
|
||||
|
||||
const siFormat = useMemo(
|
||||
(): [number, string] => info
|
||||
? [info.metadata.decimals.toNumber(), info.metadata.symbol.toUtf8().toUpperCase()]
|
||||
: [0, 'NONE'],
|
||||
[info]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
setInfo(() =>
|
||||
infoIndex >= 0 && infoIndex < completeInfos.length
|
||||
? completeInfos[infoIndex]
|
||||
: null
|
||||
);
|
||||
}, [completeInfos, infoIndex]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Table
|
||||
empty={info && balances && t<string>('No accounts with balances found for the asset')}
|
||||
filter={assetOptions.length
|
||||
? (
|
||||
<Dropdown
|
||||
isFull
|
||||
label={t<string>('the asset to query for balances')}
|
||||
onChange={setInfoIndex}
|
||||
options={assetOptions}
|
||||
value={infoIndex}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{info && balances?.map(({ accountId, balance }) => (
|
||||
<Balance
|
||||
accountId={accountId}
|
||||
balance={balance}
|
||||
key={accountId}
|
||||
siFormat={siFormat}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Balances);
|
||||
51
packages/page-assets/src/Balances/useBalances.ts
Normal file
51
packages/page-assets/src/Balances/useBalances.ts
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2017-2021 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetBalance, AssetId } from '@polkadot/types/interfaces';
|
||||
import type { AssetInfoComplete } from '../types';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useAccounts, useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks';
|
||||
|
||||
interface Result {
|
||||
accountId: string;
|
||||
balance: AssetBalance;
|
||||
}
|
||||
|
||||
const queryOptions = {
|
||||
transform: ([[params], results]: [[[AssetId, string][]], AssetBalance[]]): Result[] =>
|
||||
results
|
||||
.filter(({ balance }) => !balance.isZero())
|
||||
.map((balance, index) => ({
|
||||
accountId: params[index][1],
|
||||
balance
|
||||
})),
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
export default function useBalances (info?: AssetInfoComplete | null): Result[] | null {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const [state, setState] = useState<Result[] | null>(null);
|
||||
const query = useCall(info && api.query.assets.account.multi, info && [allAccounts.map((a) => [info.id, a])], queryOptions);
|
||||
|
||||
useEffect((): void => {
|
||||
if (mountedRef.current) {
|
||||
setState(null);
|
||||
|
||||
if (info) {
|
||||
// dummy
|
||||
}
|
||||
}
|
||||
}, [info, mountedRef]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (mountedRef.current && query) {
|
||||
setState(query);
|
||||
}
|
||||
}, [query, mountedRef]);
|
||||
|
||||
return state;
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2021 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetInfo } from './types';
|
||||
import type { AssetInfo } from '../types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2021 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetId } from '@polkadot/types/interfaces';
|
||||
import type { AssetInfo } from '../types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
@ -9,16 +9,14 @@ import { Table } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import Asset from './Asset';
|
||||
import useAssetInfos from './useAssetInfos';
|
||||
|
||||
interface Props {
|
||||
assetIds?: AssetId[];
|
||||
className?: string;
|
||||
infos?: AssetInfo[];
|
||||
}
|
||||
|
||||
function Assets ({ assetIds, className }: Props): React.ReactElement<Props> {
|
||||
function Assets ({ className, infos }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const assetInfos = useAssetInfos(assetIds);
|
||||
|
||||
const headerRef = useRef([
|
||||
[t('assets'), 'start', 2],
|
||||
@ -33,10 +31,10 @@ function Assets ({ assetIds, className }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={assetInfos && t<string>('No assets found')}
|
||||
empty={infos && t<string>('No assets found')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{assetInfos?.map((info) => (
|
||||
{infos?.map((info) => (
|
||||
<Asset
|
||||
key={info.key}
|
||||
value={info}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
// Copyright 2017-2021 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetId } from '@polkadot/types/interfaces';
|
||||
import type { AssetInfo } from '../types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@polkadot/react-components';
|
||||
@ -8,22 +11,21 @@ import { Button } from '@polkadot/react-components';
|
||||
import Assets from './Assets';
|
||||
import Create from './Create';
|
||||
import Summary from './Summary';
|
||||
import useAssetIds from './useAssetIds';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
ids?: AssetId[];
|
||||
infos?: AssetInfo[];
|
||||
}
|
||||
|
||||
function Overview ({ className }: Props): React.ReactElement<Props> {
|
||||
const assetIds = useAssetIds();
|
||||
|
||||
function Overview ({ className, ids, infos }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary numAssets={assetIds?.length} />
|
||||
<Summary numAssets={ids?.length} />
|
||||
<Button.Group>
|
||||
<Create assetIds={assetIds} />
|
||||
<Create assetIds={ids} />
|
||||
</Button.Group>
|
||||
<Assets assetIds={assetIds} />
|
||||
<Assets infos={infos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
// Copyright 2017-2021 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { Route, Switch } from 'react-router';
|
||||
|
||||
import { Tabs } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
|
||||
import Balances from './Balances';
|
||||
import Overview from './Overview';
|
||||
import { useTranslation } from './translate';
|
||||
import useAssetIds from './useAssetIds';
|
||||
import useAssetInfos from './useAssetInfos';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
@ -16,24 +20,43 @@ interface Props {
|
||||
|
||||
function GiltApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const ids = useAssetIds();
|
||||
const infos = useAssetInfos(ids);
|
||||
|
||||
const tabsRef = useRef([
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t<string>('Overview')
|
||||
},
|
||||
{
|
||||
name: 'balances',
|
||||
text: t<string>('Balances')
|
||||
}
|
||||
]);
|
||||
|
||||
const hidden = useMemo(
|
||||
() => (hasAccounts && infos?.length) ? [] : ['balances'],
|
||||
[hasAccounts, infos]
|
||||
);
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
hidden={hidden}
|
||||
items={tabsRef.current}
|
||||
/>
|
||||
<Switch>
|
||||
<Route path={`${basePath}/balances`}>
|
||||
<Balances infos={infos} />
|
||||
</Route>
|
||||
<Route>
|
||||
<Overview />
|
||||
<Overview
|
||||
ids={ids}
|
||||
infos={infos}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</main>
|
||||
|
||||
@ -13,3 +13,8 @@ export interface AssetInfo {
|
||||
key: string;
|
||||
metadata: AssetMetadata | null;
|
||||
}
|
||||
|
||||
export interface AssetInfoComplete extends AssetInfo {
|
||||
details: AssetDetails;
|
||||
metadata: AssetMetadata;
|
||||
}
|
||||
@ -68,7 +68,7 @@ export default React.memo(styled(Table)`
|
||||
table {
|
||||
border-spacing: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
@ -99,7 +99,7 @@ function subscribe <T> (mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn
|
||||
// - returns a promise with an unsubscribe function
|
||||
// - has a callback to set the value
|
||||
// FIXME The typings here need some serious TLC
|
||||
export function useCall <T> (fn: TrackFn | undefined | null | false, params?: CallParams, options?: CallOptions<T>): T | undefined {
|
||||
export function useCall <T> (fn: TrackFn | undefined | null | false, params?: CallParams | null, options?: CallOptions<T>): T | undefined {
|
||||
const mountedRef = useIsMountedRef();
|
||||
const tracker = useRef<Tracker>({ isActive: false, serialized: null, subscriber: null });
|
||||
const [value, setValue] = useState<T | undefined>((options || {}).defaultValue);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user