From 3afc055b3b3f3ec5d0a8a4cf6f8038ffc43183e0 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 23 Mar 2021 23:03:32 +0100 Subject: [PATCH] Asset create & list display (#4911) * Asset create & list display * metadata WIP * ... inputs * Steps * min balance * with balance * Expand display --- packages/apps-routing/src/assets.ts | 5 +- packages/apps-routing/src/index.ts | 2 + packages/page-assets/src/Overview/Asset.tsx | 31 +++++ packages/page-assets/src/Overview/Assets.tsx | 49 +++++++ .../page-assets/src/Overview/Create/Info.tsx | 75 ++++++++++ .../src/Overview/Create/Metadata.tsx | 78 +++++++++++ .../page-assets/src/Overview/Create/Team.tsx | 68 +++++++++ .../page-assets/src/Overview/Create/index.tsx | 130 ++++++++++++++++++ .../page-assets/src/Overview/Create/types.ts | 19 +++ packages/page-assets/src/Overview/Summary.tsx | 28 ++++ packages/page-assets/src/Overview/index.tsx | 15 +- packages/page-assets/src/Overview/types.ts | 15 ++ .../page-assets/src/Overview/useAssetIds.ts | 28 ++++ .../page-assets/src/Overview/useAssetInfos.ts | 47 +++++++ packages/react-components/src/Table/Body.tsx | 4 + 15 files changed, 590 insertions(+), 4 deletions(-) create mode 100644 packages/page-assets/src/Overview/Asset.tsx create mode 100644 packages/page-assets/src/Overview/Assets.tsx create mode 100644 packages/page-assets/src/Overview/Create/Info.tsx create mode 100644 packages/page-assets/src/Overview/Create/Metadata.tsx create mode 100644 packages/page-assets/src/Overview/Create/Team.tsx create mode 100644 packages/page-assets/src/Overview/Create/index.tsx create mode 100644 packages/page-assets/src/Overview/Create/types.ts create mode 100644 packages/page-assets/src/Overview/Summary.tsx create mode 100644 packages/page-assets/src/Overview/types.ts create mode 100644 packages/page-assets/src/Overview/useAssetIds.ts create mode 100644 packages/page-assets/src/Overview/useAssetInfos.ts diff --git a/packages/apps-routing/src/assets.ts b/packages/apps-routing/src/assets.ts index c2d3d0bf26..c85868ca09 100644 --- a/packages/apps-routing/src/assets.ts +++ b/packages/apps-routing/src/assets.ts @@ -10,12 +10,11 @@ export default function create (t: TFunction): Route { return { Component, display: { - isHidden: true, needsApi: [ - 'tx.assets.create' + 'tx.assets.setMetadata' ] }, - group: 'accounts', + group: 'network', icon: 'shopping-basket', name: 'assets', text: t('nav.assets', 'Assets', { ns: 'apps-routing' }) diff --git a/packages/apps-routing/src/index.ts b/packages/apps-routing/src/index.ts index 4f3b0843ec..a76080dd98 100644 --- a/packages/apps-routing/src/index.ts +++ b/packages/apps-routing/src/index.ts @@ -6,6 +6,7 @@ import type { Routes } from './types'; import accounts from './accounts'; import addresses from './addresses'; +import assets from './assets'; import bounties from './bounties'; import calendar from './calendar'; import claims from './claims'; @@ -45,6 +46,7 @@ export default function create (t: TFunction): Routes { techcomm(t), parachains(t), gilt(t), + assets(t), society(t), calendar(t), contracts(t), diff --git a/packages/page-assets/src/Overview/Asset.tsx b/packages/page-assets/src/Overview/Asset.tsx new file mode 100644 index 0000000000..15dc2448e1 --- /dev/null +++ b/packages/page-assets/src/Overview/Asset.tsx @@ -0,0 +1,31 @@ +// Copyright 2017-2021 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AssetInfo } from './types'; + +import React from 'react'; + +import { AddressSmall } from '@polkadot/react-components'; +import { formatNumber } from '@polkadot/util'; + +interface Props { + className?: string; + value: AssetInfo; +} + +function Asset ({ className, value: { details, id, metadata } }: Props): React.ReactElement { + return ( + +

{formatNumber(id)}

+ {metadata?.name.toUtf8()} + {details && } + {details && } + {details && } + {formatNumber(metadata?.decimals)} + {metadata?.symbol.toUtf8()} + + + ); +} + +export default React.memo(Asset); diff --git a/packages/page-assets/src/Overview/Assets.tsx b/packages/page-assets/src/Overview/Assets.tsx new file mode 100644 index 0000000000..eba3748c87 --- /dev/null +++ b/packages/page-assets/src/Overview/Assets.tsx @@ -0,0 +1,49 @@ +// Copyright 2017-2021 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AssetId } from '@polkadot/types/interfaces'; + +import React, { useRef } from 'react'; + +import { Table } from '@polkadot/react-components'; + +import { useTranslation } from '../translate'; +import Asset from './Asset'; +import useAssetInfos from './useAssetInfos'; + +interface Props { + assetIds?: AssetId[]; + className?: string; +} + +function Assets ({ assetIds, className }: Props): React.ReactElement { + const { t } = useTranslation(); + const assetInfos = useAssetInfos(assetIds); + + const headerRef = useRef([ + [t('assets'), 'start', 2], + [t('admin'), 'start'], + [t('issuer'), 'start'], + [t('freezer'), 'start'], + [t('decimals')], + [t('symbol'), 'start'], + [] + ]); + + return ( + ('No assets found')} + header={headerRef.current} + > + {assetInfos?.map((info) => ( + + ))} +
+ ); +} + +export default React.memo(Assets); diff --git a/packages/page-assets/src/Overview/Create/Info.tsx b/packages/page-assets/src/Overview/Create/Info.tsx new file mode 100644 index 0000000000..f0a10c146e --- /dev/null +++ b/packages/page-assets/src/Overview/Create/Info.tsx @@ -0,0 +1,75 @@ +// Copyright 2017-2021 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type BN from 'bn.js'; +import type { AssetId } from '@polkadot/types/interfaces'; +import type { InfoState } from './types'; + +import React, { useEffect, useMemo, useState } from 'react'; + +import { InputAddress, InputNumber, Modal } from '@polkadot/react-components'; +import { useApi } from '@polkadot/react-hooks'; +import { BN_ZERO } from '@polkadot/util'; + +import { useTranslation } from '../../translate'; + +interface Props { + assetIds: AssetId[]; + className?: string; + onChange: (info: InfoState | null) => void; +} + +function Info ({ assetIds, className = '', onChange }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api } = useApi(); + const [accountId, setAccountId] = useState(null); + const [assetId, setAssetId] = useState(null); + const [minBalance, setMinBalance] = useState(null); + + const isIdValid = useMemo( + () => !!assetId && assetId.gt(BN_ZERO) && !assetIds.some((a) => a.eq(assetId)), + [assetId, assetIds] + ); + + useEffect((): void => { + onChange( + assetId && accountId && minBalance && isIdValid && !minBalance.isZero() + ? { + accountId, + assetId, + createTx: api.tx.assets.create(assetId, accountId, minBalance) + } + : null + ); + }, [api, accountId, assetId, assetIds, isIdValid, minBalance, onChange]); + + return ( + + ('The account that is to be used to create this asset and setup the initial metadata.')}> + ('creator account')} + onChange={setAccountId} + type='account' + /> + + ('The selected id for the asset. Should not match an already-existing id.')}> + ('asset id')} + onChange={setAssetId} + /> + + ('The minimum balance for the asset.')}> + ('minimum balance')} + onChange={setMinBalance} + /> + + + ); +} + +export default React.memo(Info); diff --git a/packages/page-assets/src/Overview/Create/Metadata.tsx b/packages/page-assets/src/Overview/Create/Metadata.tsx new file mode 100644 index 0000000000..fcaf1fda8c --- /dev/null +++ b/packages/page-assets/src/Overview/Create/Metadata.tsx @@ -0,0 +1,78 @@ +// Copyright 2017-2021 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type BN from 'bn.js'; +import type { MetadataState } from './types'; + +import React, { useEffect, useMemo, useState } from 'react'; + +import { Input, InputNumber, Modal } from '@polkadot/react-components'; +import { useApi } from '@polkadot/react-hooks'; + +import { useTranslation } from '../../translate'; + +interface Props { + assetId?: BN | null; + className?: string; + onChange: (info: MetadataState | null) => void; +} + +function Metadata ({ assetId, className = '', onChange }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api } = useApi(); + const [assetDecimals, setAssetDecimals] = useState(null); + const [assetName, setAssetName] = useState(null); + const [assetSymbol, setAssetSymbol] = useState(null); + + const isValidDecimals = useMemo( + () => !!assetDecimals && assetDecimals.lten(20), + [assetDecimals] + ); + + const isValidName = useMemo( + () => !!assetName && assetName.length >= 3 && assetName.length <= 32, + [assetName] + ); + + const isValidSymbol = useMemo( + () => !!assetSymbol && assetSymbol.length >= 3, + [assetSymbol] + ); + + useEffect((): void => { + onChange( + assetId && assetName && assetSymbol && assetDecimals && isValidName && isValidSymbol && isValidDecimals + ? { metadataTx: api.tx.assets.setMetadata(assetId, assetName, assetSymbol, assetDecimals) } + : null + ); + }, [api, assetDecimals, assetId, assetName, assetSymbol, isValidName, isValidSymbol, isValidDecimals, onChange]); + + return ( + + ('The descriptive name for this asset.')}> + ('asset name')} + onChange={setAssetName} + /> + + ('The symbol that will represent this asset.')}> + ('asset symbol')} + onChange={setAssetSymbol} + /> + + ('The number of decimals for this token. Max allowed via the UI is set to 20.')}> + ('asset decimals')} + onChange={setAssetDecimals} + /> + + + ); +} + +export default React.memo(Metadata); diff --git a/packages/page-assets/src/Overview/Create/Team.tsx b/packages/page-assets/src/Overview/Create/Team.tsx new file mode 100644 index 0000000000..6c5be2ee02 --- /dev/null +++ b/packages/page-assets/src/Overview/Create/Team.tsx @@ -0,0 +1,68 @@ +// Copyright 2017-2021 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type BN from 'bn.js'; +import type { TeamState } from './types'; + +import React, { useEffect, useState } from 'react'; + +import { InputAddress, Modal } from '@polkadot/react-components'; +import { useApi } from '@polkadot/react-hooks'; + +import { useTranslation } from '../../translate'; + +interface Props { + accountId?: string; + assetId?: BN; + className?: string; + onChange: (info: TeamState | null) => void; +} + +function Team ({ accountId, assetId, className = '', onChange }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api } = useApi(); + const [adminId, setAdminId] = useState(null); + const [freezerId, setFreezerId] = useState(null); + const [issuerId, setIssuerId] = useState(null); + + useEffect((): void => { + onChange( + assetId && accountId && adminId && freezerId && issuerId + ? adminId !== accountId || freezerId !== accountId || issuerId !== accountId + ? { teamTx: api.tx.assets.setTeam(assetId, issuerId, adminId, freezerId) } + : { teamTx: null } + : null + ); + }, [api, assetId, accountId, adminId, freezerId, issuerId, onChange]); + + return ( + + ('The account that is to be used for ongoing admin on the token.')}> + ('admin account')} + onChange={setAdminId} + type='account' + /> + + ('The account that is to be used for issuing for this token.')}> + ('issuer account')} + onChange={setIssuerId} + type='account' + /> + + ('The account that is to be used for performing freezing.')}> + ('freezer account')} + onChange={setFreezerId} + type='account' + /> + + + ); +} + +export default React.memo(Team); diff --git a/packages/page-assets/src/Overview/Create/index.tsx b/packages/page-assets/src/Overview/Create/index.tsx new file mode 100644 index 0000000000..32853e2706 --- /dev/null +++ b/packages/page-assets/src/Overview/Create/index.tsx @@ -0,0 +1,130 @@ +// Copyright 2017-2021 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SubmittableExtrinsic } from '@polkadot/api/types'; +import type { AssetId } from '@polkadot/types/interfaces'; +import type { InfoState, MetadataState, TeamState } from './types'; + +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import { Button, Modal, TxButton } from '@polkadot/react-components'; +import { useAccounts, useApi, useStepper, useToggle } from '@polkadot/react-hooks'; +import { isFunction } from '@polkadot/util'; + +import { useTranslation } from '../../translate'; +import Info from './Info'; +import Metadata from './Metadata'; +import Team from './Team'; + +interface Props { + assetIds?: AssetId[]; + className?: string; +} + +function Create ({ assetIds, className }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api } = useApi(); + const { hasAccounts } = useAccounts(); + const [step, nextStep, prevStep] = useStepper(); + const [isOpen, toggleOpen] = useToggle(); + const [asset, setAsset] = useState(null); + const [metadata, setMetadata] = useState(null); + const [team, setTeam] = useState(null); + const [extrinsic, setExtrinsic] = useState | null>(null); + + useEffect((): void => { + setExtrinsic( + () => asset && metadata && team + ? team.teamTx + ? api.tx.utility.batchAll([asset.createTx, metadata.metadataTx, team.teamTx]) + : api.tx.utility.batchAll([asset.createTx, metadata.metadataTx]) + : null + ); + }, [api, asset, metadata, team]); + + return ( + <> +