Display asset balances (#5128)

This commit is contained in:
Jaco 2021-04-23 14:46:51 +03:00 committed by GitHub
parent 7a889aae8b
commit b3ba9a4b6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 236 additions and 19 deletions

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

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

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

View File

@ -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';

View File

@ -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}

View File

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

View File

@ -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>

View File

@ -13,3 +13,8 @@ export interface AssetInfo {
key: string;
metadata: AssetMetadata | null;
}
export interface AssetInfoComplete extends AssetInfo {
details: AssetDetails;
metadata: AssetMetadata;
}

View File

@ -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;

View File

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