From 16826c92682ea665db0354fc9d9580cb769bb4de Mon Sep 17 00:00:00 2001 From: Arjun Porwal <65214523+ap211unitech@users.noreply.github.com> Date: Wed, 19 Feb 2025 18:46:59 +0530 Subject: [PATCH] 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 --- .../apps/public/locales/en/app-assets.json | 3 + .../apps/public/locales/en/translation.json | 3 + .../page-assets/src/foreignAssets/Asset.tsx | 71 ++++++++++++++++ .../page-assets/src/foreignAssets/Assets.tsx | 48 +++++++++++ .../page-assets/src/foreignAssets/Summary.tsx | 28 +++++++ .../page-assets/src/foreignAssets/index.tsx | 27 ++++++ packages/page-assets/src/index.tsx | 29 ++++++- .../page-assets/src/useForeignAssetInfos.ts | 82 +++++++++++++++++++ .../src/useForeignAssetLocations.ts | 24 ++++++ 9 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 packages/page-assets/src/foreignAssets/Asset.tsx create mode 100644 packages/page-assets/src/foreignAssets/Assets.tsx create mode 100644 packages/page-assets/src/foreignAssets/Summary.tsx create mode 100644 packages/page-assets/src/foreignAssets/index.tsx create mode 100644 packages/page-assets/src/useForeignAssetInfos.ts create mode 100644 packages/page-assets/src/useForeignAssetLocations.ts diff --git a/packages/apps/public/locales/en/app-assets.json b/packages/apps/public/locales/en/app-assets.json index 38eaabc74c..579751a5ba 100644 --- a/packages/apps/public/locales/en/app-assets.json +++ b/packages/apps/public/locales/en/app-assets.json @@ -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", diff --git a/packages/apps/public/locales/en/translation.json b/packages/apps/public/locales/en/translation.json index 4afa2738bb..99c061859d 100644 --- a/packages/apps/public/locales/en/translation.json +++ b/packages/apps/public/locales/en/translation.json @@ -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": "", diff --git a/packages/page-assets/src/foreignAssets/Asset.tsx b/packages/page-assets/src/foreignAssets/Asset.tsx new file mode 100644 index 0000000000..adca8db112 --- /dev/null +++ b/packages/page-assets/src/foreignAssets/Asset.tsx @@ -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 { + const format = useMemo( + (): [number, string] => metadata + ? [metadata.decimals.toNumber(), metadata.symbol.toUtf8()] + : [0, '---'], + [metadata] + ); + + return ( + + + +
+            {JSON.stringify(location?.toJSON(), null, 2)}
+          
+
+ +
+ {metadata?.name.toUtf8()} + {details && } + {details && } + {details && } + {details && } + {details && ( + + )} + {details?.accounts.toString() || '0'} + + ); +} + +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); diff --git a/packages/page-assets/src/foreignAssets/Assets.tsx b/packages/page-assets/src/foreignAssets/Assets.tsx new file mode 100644 index 0000000000..ec2b8434f6 --- /dev/null +++ b/packages/page-assets/src/foreignAssets/Assets.tsx @@ -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 { + 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 ( + + {infos?.map((info) => ( + + ))} +
+ ); +} + +export default React.memo(Assets); diff --git a/packages/page-assets/src/foreignAssets/Summary.tsx b/packages/page-assets/src/foreignAssets/Summary.tsx new file mode 100644 index 0000000000..c5f75978ef --- /dev/null +++ b/packages/page-assets/src/foreignAssets/Summary.tsx @@ -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 { + const { t } = useTranslation(); + + return ( + + + {formatNumber(numAssets)} + + + ); +} + +export default React.memo(Summary); diff --git a/packages/page-assets/src/foreignAssets/index.tsx b/packages/page-assets/src/foreignAssets/index.tsx new file mode 100644 index 0000000000..79f53c4bd9 --- /dev/null +++ b/packages/page-assets/src/foreignAssets/index.tsx @@ -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 { + return ( +
+ + +
+ ); +} + +export default React.memo(ForeignAssets); diff --git a/packages/page-assets/src/index.tsx b/packages/page-assets/src/index.tsx index bc8d15a021..0d15d5538f 100644 --- a/packages/page-assets/src/index.tsx +++ b/packages/page-assets/src/index.tsx @@ -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 { 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 { } path='balances' /> + + } + path='foreignAssets' + /> a === address); +} + +function extractInfo (allAccounts: string[], location: StagingXcmV3MultiLocation, optDetails: Option, 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[]]>(isReady && api.query.foreignAssets.asset.multi, [locations], QUERY_OPTS); + const [state, setState] = useState(); + + 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); diff --git a/packages/page-assets/src/useForeignAssetLocations.ts b/packages/page-assets/src/useForeignAssetLocations.ts new file mode 100644 index 0000000000..b6789d2620 --- /dev/null +++ b/packages/page-assets/src/useForeignAssetLocations.ts @@ -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);