Show Multilocation for Foreign Assets <> Assets Page (#11306)
* feat: create hook for quering foreign assets locations * feat: add foreign assets tab * fix: useMapKeys used instead of useMapEntries * chore: rename hook * feat: add ForeignAssets component and Summary for displaying foreign asset counts * chore: rename folder * feat: implement useForeignAssetInfos hook for managing foreign asset information * feat: add Asset and Assets components for displaying foreign asset information * feat: enhance Asset component with CopyButton for location and adjust Assets table layout * chore: add translations * fix: CI error * fix: CI build errors * feat: added expander component for multilocation * chore: align position change
This commit is contained in:
parent
783206911a
commit
16826c9268
@ -1,6 +1,7 @@
|
||||
{
|
||||
"Balances": "Balances",
|
||||
"Create": "Create",
|
||||
"Foreign assets": "Foreign assets",
|
||||
"Mint": "Mint",
|
||||
"Next": "Next",
|
||||
"No": "No",
|
||||
@ -44,10 +45,12 @@
|
||||
"assets": "assets",
|
||||
"create asset {{step}}/{{steps}}": "create asset {{step}}/{{steps}}",
|
||||
"creator account": "creator account",
|
||||
"foreign assets": "foreign assets",
|
||||
"free balance": "free balance",
|
||||
"freezer": "freezer",
|
||||
"freezer account": "freezer account",
|
||||
"frozen": "frozen",
|
||||
"holders": "holders",
|
||||
"issuer": "issuer",
|
||||
"issuer account": "issuer account",
|
||||
"minimum balance": "minimum balance",
|
||||
|
||||
@ -293,6 +293,7 @@
|
||||
"Folder": "",
|
||||
"For approvals outstanding approvers will be shown, for hashes that should be cancelled the first approver is required.": "",
|
||||
"For fund security, your session key should not match your stash key.": "",
|
||||
"Foreign assets": "",
|
||||
"Forget": "",
|
||||
"Forget this account": "",
|
||||
"Forget this address": "",
|
||||
@ -1439,6 +1440,7 @@
|
||||
"first": "",
|
||||
"first - last": "",
|
||||
"first period": "",
|
||||
"foreign assets": "",
|
||||
"forks": "",
|
||||
"found signatories": "",
|
||||
"founder": "",
|
||||
@ -1466,6 +1468,7 @@
|
||||
"hex-encoded call": "",
|
||||
"hex-encoded storage key": "",
|
||||
"historic results": "",
|
||||
"holders": "",
|
||||
"https://example.com": "",
|
||||
"id": "",
|
||||
"ideal staked": "",
|
||||
|
||||
71
packages/page-assets/src/foreignAssets/Asset.tsx
Normal file
71
packages/page-assets/src/foreignAssets/Asset.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2017-2025 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ForeignAssetInfo } from '../useForeignAssetInfos.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, CopyButton, Expander, styled } from '@polkadot/react-components';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: ForeignAssetInfo;
|
||||
}
|
||||
|
||||
function Asset ({ className, value: { details, location, metadata } }: Props): React.ReactElement<Props> {
|
||||
const format = useMemo(
|
||||
(): [number, string] => metadata
|
||||
? [metadata.decimals.toNumber(), metadata.symbol.toUtf8()]
|
||||
: [0, '---'],
|
||||
[metadata]
|
||||
);
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<Location>
|
||||
<Expander
|
||||
isLeft
|
||||
summary={JSON.stringify(location?.toJSON()).substring(0, 40)}
|
||||
>
|
||||
<pre>
|
||||
{JSON.stringify(location?.toJSON(), null, 2)}
|
||||
</pre>
|
||||
</Expander>
|
||||
<CopyButton value={JSON.stringify(location?.toJSON(), null, 2)} />
|
||||
</Location>
|
||||
<td className='together'>{metadata?.name.toUtf8()}</td>
|
||||
<td className='address media--1000'>{details && <AddressSmall value={details.owner} />}</td>
|
||||
<td className='address media--1300'>{details && <AddressSmall value={details.admin} />}</td>
|
||||
<td className='address media--1600'>{details && <AddressSmall value={details.issuer} />}</td>
|
||||
<td className='address media--1900'>{details && <AddressSmall value={details.freezer} />}</td>
|
||||
<td className='number media--800'>{details && (
|
||||
<FormatBalance
|
||||
format={format}
|
||||
value={details.supply}
|
||||
/>
|
||||
)}</td>
|
||||
<td className='number all'>{details?.accounts.toString() || '0'}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const Location = styled.td`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
white-space: nowrap;
|
||||
font-style: italic;
|
||||
pre {
|
||||
overflow: visible;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.ui--Expander-summary svg {
|
||||
padding: 0.3rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--border-input);
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Asset);
|
||||
48
packages/page-assets/src/foreignAssets/Assets.tsx
Normal file
48
packages/page-assets/src/foreignAssets/Assets.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright 2017-2025 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ForeignAssetInfo } from '../useForeignAssetInfos.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Asset from './Asset.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
infos?: ForeignAssetInfo[];
|
||||
}
|
||||
|
||||
function Assets ({ className, infos }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('foreign assets'), 'start'],
|
||||
[],
|
||||
[t('owner'), 'address media--1000'],
|
||||
[t('admin'), 'address media--1300'],
|
||||
[t('issuer'), 'address media--1600'],
|
||||
[t('freezer'), 'address media--1900'],
|
||||
[t('supply'), 'media--800'],
|
||||
[t('holders')]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={infos && t('No assets found')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{infos?.map((info) => (
|
||||
<Asset
|
||||
key={info.key}
|
||||
value={info}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Assets);
|
||||
28
packages/page-assets/src/foreignAssets/Summary.tsx
Normal file
28
packages/page-assets/src/foreignAssets/Summary.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2017-2025 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
numAssets?: number;
|
||||
}
|
||||
|
||||
function Summary ({ className, numAssets }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SummaryBox className={className}>
|
||||
<CardSummary label={t('foreign assets')}>
|
||||
{formatNumber(numAssets)}
|
||||
</CardSummary>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
27
packages/page-assets/src/foreignAssets/index.tsx
Normal file
27
packages/page-assets/src/foreignAssets/index.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2025 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StagingXcmV3MultiLocation } from '@polkadot/types/lookup';
|
||||
import type { ForeignAssetInfo } from '../useForeignAssetInfos.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Assets from './Assets.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
foreignAssetInfos?: ForeignAssetInfo[]
|
||||
locations?: StagingXcmV3MultiLocation[];
|
||||
}
|
||||
|
||||
function ForeignAssets ({ className, foreignAssetInfos, locations }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary numAssets={locations?.length} />
|
||||
<Assets infos={foreignAssetInfos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ForeignAssets);
|
||||
@ -16,8 +16,11 @@ import { useAccounts, useApi, useAssetIds, useAssetInfos } from '@polkadot/react
|
||||
import { BN_ONE } from '@polkadot/util';
|
||||
|
||||
import Balances from './Balances/index.js';
|
||||
import ForeignAssets from './foreignAssets/index.js';
|
||||
import Overview from './Overview/index.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
import { useForeignAssetInfos } from './useForeignAssetInfos.js';
|
||||
import { useForeignAssetLocations } from './useForeignAssetLocations.js';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
@ -54,23 +57,32 @@ function AssetApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const ids = useAssetIds();
|
||||
const infos = useAssetInfos(ids);
|
||||
|
||||
const foreignAssetLocations = useForeignAssetLocations();
|
||||
const foreignAssetInfos = useForeignAssetInfos(foreignAssetLocations);
|
||||
|
||||
const tabsRef = useRef([
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Overview')
|
||||
},
|
||||
{
|
||||
name: 'foreignAssets',
|
||||
text: t('Foreign assets')
|
||||
},
|
||||
{
|
||||
name: 'balances',
|
||||
text: t('Balances')
|
||||
}
|
||||
]);
|
||||
|
||||
const showForeignAssetsTab = useMemo(() => !!foreignAssetLocations.length, [foreignAssetLocations.length]);
|
||||
const showBalancesTab = useMemo(() => hasAccounts && infos && infos.some(({ details, metadata }) => !!(details && metadata)), [hasAccounts, infos]);
|
||||
|
||||
const hidden = useMemo(
|
||||
() => (hasAccounts && infos && infos.some(({ details, metadata }) => !!(details && metadata)))
|
||||
? []
|
||||
: ['balances'],
|
||||
[hasAccounts, infos]
|
||||
() =>
|
||||
[!showForeignAssetsTab && 'foreignAssets', !showBalancesTab && 'balances'].filter((a) => typeof a === 'string'),
|
||||
[showBalancesTab, showForeignAssetsTab]
|
||||
);
|
||||
|
||||
const openId = useMemo(
|
||||
@ -93,6 +105,15 @@ function AssetApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
}
|
||||
path='balances'
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ForeignAssets
|
||||
foreignAssetInfos={foreignAssetInfos}
|
||||
locations={foreignAssetLocations}
|
||||
/>
|
||||
}
|
||||
path='foreignAssets'
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<Overview
|
||||
|
||||
82
packages/page-assets/src/useForeignAssetInfos.ts
Normal file
82
packages/page-assets/src/useForeignAssetInfos.ts
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2017-2025 @polkadot/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@polkadot/types';
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
import type { PalletAssetsAssetDetails, PalletAssetsAssetMetadata, StagingXcmV3MultiLocation } from '@polkadot/types/lookup';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
const EMPTY_FLAGS = {
|
||||
isAdminMe: false,
|
||||
isFreezerMe: false,
|
||||
isIssuerMe: false,
|
||||
isOwnerMe: false
|
||||
};
|
||||
|
||||
export interface ForeignAssetInfo {
|
||||
details: PalletAssetsAssetDetails | null;
|
||||
location: StagingXcmV3MultiLocation;
|
||||
isAdminMe: boolean;
|
||||
isIssuerMe: boolean;
|
||||
isFreezerMe: boolean;
|
||||
isOwnerMe: boolean;
|
||||
key: string;
|
||||
metadata: PalletAssetsAssetMetadata | null;
|
||||
}
|
||||
|
||||
const QUERY_OPTS = { withParams: true };
|
||||
|
||||
function isAccount (allAccounts: string[], accountId: AccountId): boolean {
|
||||
const address = accountId.toString();
|
||||
|
||||
return allAccounts.some((a) => a === address);
|
||||
}
|
||||
|
||||
function extractInfo (allAccounts: string[], location: StagingXcmV3MultiLocation, optDetails: Option<PalletAssetsAssetDetails>, metadata: PalletAssetsAssetMetadata): ForeignAssetInfo {
|
||||
const details = optDetails.unwrapOr(null);
|
||||
|
||||
return {
|
||||
...(details
|
||||
? {
|
||||
isAdminMe: isAccount(allAccounts, details.admin),
|
||||
isFreezerMe: isAccount(allAccounts, details.freezer),
|
||||
isIssuerMe: isAccount(allAccounts, details.issuer),
|
||||
isOwnerMe: isAccount(allAccounts, details.owner)
|
||||
}
|
||||
: EMPTY_FLAGS
|
||||
),
|
||||
details,
|
||||
key: String(location),
|
||||
location,
|
||||
metadata: metadata.isEmpty
|
||||
? null
|
||||
: metadata
|
||||
};
|
||||
}
|
||||
|
||||
function useForeignAssetInfosImpl (locations?: StagingXcmV3MultiLocation[]): ForeignAssetInfo[] | undefined {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const isReady = useMemo(() => !!locations?.length && !!api.tx.foreignAssets?.setMetadata && !!api.tx.foreignAssets?.transferKeepAlive, [api.tx.foreignAssets?.setMetadata, api.tx.foreignAssets?.transferKeepAlive, locations?.length]);
|
||||
|
||||
const metadata = useCall<[[StagingXcmV3MultiLocation[]], PalletAssetsAssetMetadata[]]>(isReady && api.query.foreignAssets.metadata.multi, [locations], QUERY_OPTS);
|
||||
const details = useCall<[[StagingXcmV3MultiLocation[]], Option<PalletAssetsAssetDetails>[]]>(isReady && api.query.foreignAssets.asset.multi, [locations], QUERY_OPTS);
|
||||
const [state, setState] = useState<ForeignAssetInfo[] | undefined>();
|
||||
|
||||
useEffect((): void => {
|
||||
details && metadata && (details[0][0].length === metadata[0][0].length) &&
|
||||
setState(
|
||||
details[0][0].map((location, index) =>
|
||||
extractInfo(allAccounts, location, details[1][index], metadata[1][index])
|
||||
)
|
||||
);
|
||||
}, [allAccounts, details, locations, metadata]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const useForeignAssetInfos = createNamedHook('useForeignAssetInfos', useForeignAssetInfosImpl);
|
||||
24
packages/page-assets/src/useForeignAssetLocations.ts
Normal file
24
packages/page-assets/src/useForeignAssetLocations.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2017-2025 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StorageKey } from '@polkadot/types';
|
||||
import type { StagingXcmV3MultiLocation } from '@polkadot/types/lookup';
|
||||
|
||||
import { createNamedHook, useApi, useMapKeys } from '@polkadot/react-hooks';
|
||||
|
||||
const EMPTY_PARAMS: unknown[] = [];
|
||||
|
||||
const OPT_KEY = {
|
||||
transform: (keys: StorageKey<[StagingXcmV3MultiLocation]>[]): StagingXcmV3MultiLocation[] =>
|
||||
keys.flatMap(({ args }) => args)
|
||||
};
|
||||
|
||||
function useForeignAssetLocationsImpl () {
|
||||
const { api, isApiReady } = useApi();
|
||||
|
||||
const values = useMapKeys(isApiReady && api.query.foreignAssets?.asset, EMPTY_PARAMS, OPT_KEY) || [];
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export const useForeignAssetLocations = createNamedHook('useForeignAssetLocations', useForeignAssetLocationsImpl);
|
||||
Loading…
Reference in New Issue
Block a user