Asset modal rework (min balance with decimals) (#5126)

* Asset modal rework (min balance with decimals)

* Language cleanups
This commit is contained in:
Jaco 2021-04-23 11:54:51 +03:00 committed by GitHub
parent 957dc068aa
commit c231b66f6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 126 additions and 166 deletions

View File

@ -2,16 +2,15 @@
// SPDX-License-Identifier: Apache-2.0
import type { AssetId } from '@polkadot/types/interfaces';
import type { InfoState, MetadataState, TeamState } from './types';
import type { InfoState, TeamState } from './types';
import React, { useMemo, useState } from 'react';
import { Button, Modal, TxButton } from '@polkadot/react-components';
import { useStepper, useTxBatch } from '@polkadot/react-hooks';
import { useApi, useStepper, useTxBatch } from '@polkadot/react-hooks';
import { useTranslation } from '../../translate';
import Info from './Info';
import Metadata from './Metadata';
import Team from './Team';
interface Props {
@ -26,21 +25,43 @@ const BATCH_OPTIONS = {
function Create ({ assetIds, className, onClose }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [step, nextStep, prevStep] = useStepper();
const [asset, setAsset] = useState<InfoState | null>(null);
const [metadata, setMetadata] = useState<MetadataState | null>(null);
const [team, setTeam] = useState<TeamState | null>(null);
const txs = useMemo(() => asset && metadata && team && (
team.teamTx
? [asset.createTx, metadata.metadataTx, team.teamTx]
: [asset.createTx, metadata.metadataTx]
), [asset, metadata, team]);
const [createTx, metadataTx] = useMemo(
() => asset
? [
api.tx.assets.create(asset.assetId, asset.accountId, asset.minBalance),
api.tx.assets.setMetadata(asset.assetId, asset.assetName, asset.assetSymbol, asset.assetDecimals)
]
: [null, null],
[api, asset]
);
const teamTx = useMemo(
() => asset && team && (team.adminId !== asset.accountId || team.freezerId !== asset.accountId || team.issuerId !== asset.accountId)
? api.tx.assets.setTeam(asset.assetId, team.issuerId, team.adminId, team.freezerId)
: null,
[api, asset, team]
);
const txs = useMemo(
() => createTx && metadataTx && team && (
teamTx
? [createTx, metadataTx, teamTx]
: [createTx, metadataTx]
),
[createTx, metadataTx, team, teamTx]
);
const extrinsic = useTxBatch(txs, BATCH_OPTIONS);
return (
<Modal
className={className}
header={t<string>('create asset {{step}}/{{steps}}', { replace: { step, steps: 3 } })}
header={t<string>('create asset {{step}}/{{steps}}', { replace: { step, steps: 2 } })}
size='large'
>
{step === 1 && (
@ -50,17 +71,9 @@ function Create ({ assetIds, className, onClose }: Props): React.ReactElement<Pr
onChange={setAsset}
/>
)}
{step === 2 && (
<Metadata
assetId={asset?.assetId}
defaultValue={metadata}
onChange={setMetadata}
/>
)}
{step === 3 && (
{step === 2 && asset && (
<Team
accountId={asset?.accountId}
assetId={asset?.assetId}
accountId={asset.accountId}
defaultValue={team}
onChange={setTeam}
/>
@ -75,21 +88,6 @@ function Create ({ assetIds, className, onClose }: Props): React.ReactElement<Pr
/>
}
{step === 2 && (
<>
<Button
icon='step-backward'
label={t<string>('Prev')}
onClick={prevStep}
/>
<Button
icon='step-forward'
isDisabled={!metadata}
label={t<string>('Next')}
onClick={nextStep}
/>
</>
)}
{step === 3 && (
<>
<Button
icon='step-backward'

View File

@ -7,7 +7,7 @@ import type { InfoState } from './types';
import React, { useEffect, useMemo, useState } from 'react';
import { InputAddress, InputNumber, Modal } from '@polkadot/react-components';
import { Input, InputAddress, InputBalance, InputNumber, Modal } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { BN_ONE, BN_ZERO } from '@polkadot/util';
@ -33,20 +33,45 @@ function Info ({ assetIds, className = '', defaultValue, onChange }: Props): Rea
const [initialId] = useState(() => findAvailbleId(assetIds));
const [accountId, setAccountId] = useState<string | null>(null);
const [assetId, setAssetId] = useState<BN | null>(null);
const [assetDecimals, setAssetDecimals] = useState<BN | null>(null);
const [assetName, setAssetName] = useState<string | null | undefined>(() => defaultValue?.assetName);
const [assetSymbol, setAssetSymbol] = useState<string | null | undefined>(() => defaultValue?.assetSymbol);
const [minBalance, setMinBalance] = useState<BN | null>(null);
const isIdValid = useMemo(
const [siDecimals, siSymbol] = useMemo(
() => assetDecimals && assetSymbol
? [assetDecimals.toNumber(), assetSymbol.toUpperCase()]
: [0, 'NONE'],
[assetDecimals, assetSymbol]
);
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.length <= 7,
[assetSymbol]
);
const isValidId = 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), minBalance }
assetId && assetName && assetSymbol && assetDecimals && isValidId && isValidName && isValidSymbol && isValidDecimals && accountId && minBalance && !minBalance.isZero()
? { accountId, assetDecimals, assetId, assetName, assetSymbol, minBalance }
: null
);
}, [api, accountId, assetId, assetIds, isIdValid, minBalance, onChange]);
}, [api, accountId, assetDecimals, assetId, assetIds, assetName, assetSymbol, isValidId, isValidName, isValidSymbol, isValidDecimals, minBalance, onChange]);
return (
<Modal.Content className={className}>
@ -58,22 +83,48 @@ function Info ({ assetIds, className = '', defaultValue, onChange }: Props): Rea
type='account'
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The selected id for the asset. This should not match an already-existing asset id.')}>
<InputNumber
<Modal.Columns hint={t<string>('The descriptive name for this asset.')}>
<Input
autoFocus
defaultValue={initial?.assetId || initialId}
isError={!isIdValid}
isZeroable={false}
label={t<string>('asset id')}
onChange={setAssetId}
defaultValue={initial?.assetName}
isError={!isValidName}
label={t<string>('asset name')}
onChange={setAssetName}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The minimum balance for the asset.')}>
<Modal.Columns hint={t<string>('The symbol that will represent this asset.')}>
<Input
defaultValue={initial?.assetSymbol}
isError={!isValidSymbol}
label={t<string>('asset symbol')}
onChange={setAssetSymbol}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The number of decimals for this token. Max allowed via the UI is set to 20.')}>
<InputNumber
defaultValue={initial?.minBalance || BN_ONE}
defaultValue={initial?.assetDecimals}
isError={!isValidDecimals}
label={t<string>('asset decimals')}
onChange={setAssetDecimals}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The minimum balance for the asset. This is specified in the units and decimals as requested.')}>
<InputBalance
defaultValue={initial?.minBalance}
isZeroable={false}
label={t<string>('minimum balance')}
onChange={setMinBalance}
siDecimals={siDecimals}
siSymbol={siSymbol}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The selected id for the asset. This should not match an already-existing asset id.')}>
<InputNumber
defaultValue={initial?.assetId || initialId}
isError={!isValidId}
isZeroable={false}
label={t<string>('asset id')}
onChange={setAssetId}
/>
</Modal.Columns>
</Modal.Content>

View File

@ -1,83 +0,0 @@
// 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;
defaultValue: MetadataState | null;
onChange: (info: MetadataState | null) => void;
}
function Metadata ({ assetId, className = '', defaultValue, onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [initial] = useState(() => defaultValue);
const [assetDecimals, setAssetDecimals] = useState<BN | null>(null);
const [assetName, setAssetName] = useState<string | null | undefined>(() => defaultValue?.assetName);
const [assetSymbol, setAssetSymbol] = useState<string | null | undefined>(() => defaultValue?.assetSymbol);
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
? { assetDecimals, assetName, assetSymbol, metadataTx: api.tx.assets.setMetadata(assetId, assetName, assetSymbol, assetDecimals) }
: null
);
}, [api, assetDecimals, assetId, assetName, assetSymbol, isValidName, isValidSymbol, isValidDecimals, onChange]);
return (
<Modal.Content className={className}>
<Modal.Columns hint={t<string>('The descriptive name for this asset.')}>
<Input
autoFocus
defaultValue={initial?.assetName}
isError={!isValidName}
label={t<string>('asset name')}
onChange={setAssetName}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The symbol that will represent this asset.')}>
<Input
defaultValue={initial?.assetSymbol}
isError={!isValidSymbol}
label={t<string>('asset symbol')}
onChange={setAssetSymbol}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The number of decimals for this token. Max allowed via the UI is set to 20.')}>
<InputNumber
defaultValue={initial?.assetDecimals}
isError={!isValidDecimals}
label={t<string>('asset decimals')}
onChange={setAssetDecimals}
/>
</Modal.Columns>
</Modal.Content>
);
}
export default React.memo(Metadata);

View File

@ -1,7 +1,6 @@
// 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';
@ -12,14 +11,13 @@ import { useApi } from '@polkadot/react-hooks';
import { useTranslation } from '../../translate';
interface Props {
accountId?: string;
assetId?: BN;
accountId: string;
className?: string;
defaultValue: TeamState | null;
onChange: (info: TeamState | null) => void;
}
function Team ({ accountId, assetId, className = '', defaultValue, onChange }: Props): React.ReactElement<Props> {
function Team ({ accountId, className = '', defaultValue, onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [initial] = useState(() => defaultValue);
@ -29,13 +27,11 @@ function Team ({ accountId, assetId, className = '', defaultValue, onChange }: P
useEffect((): void => {
onChange(
assetId && accountId && adminId && freezerId && issuerId
? adminId !== accountId || freezerId !== accountId || issuerId !== accountId
? { adminId, freezerId, issuerId, teamTx: api.tx.assets.setTeam(assetId, issuerId, adminId, freezerId) }
: { adminId, freezerId, issuerId, teamTx: null }
adminId && freezerId && issuerId
? { adminId, freezerId, issuerId }
: null
);
}, [api, assetId, accountId, adminId, freezerId, issuerId, onChange]);
}, [api, adminId, freezerId, issuerId, onChange]);
return (
<Modal.Content className={className}>
@ -47,7 +43,7 @@ function Team ({ accountId, assetId, className = '', defaultValue, onChange }: P
type='account'
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The account that is to be used for issuing for this token.')}>
<Modal.Columns hint={t<string>('The account that is to be used for issuing this token.')}>
<InputAddress
defaultValue={initial?.issuerId || accountId}
label={t<string>('issuer account')}

View File

@ -2,25 +2,18 @@
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { SubmittableExtrinsic } from '@polkadot/api/types';
export interface InfoState {
accountId: string;
assetId: BN;
createTx: SubmittableExtrinsic<'promise'>;
minBalance: BN;
}
export interface MetadataState {
assetDecimals: BN;
assetId: BN;
assetName: string;
assetSymbol: string;
metadataTx: SubmittableExtrinsic<'promise'>;
minBalance: BN;
}
export interface TeamState {
adminId: string;
issuerId: string;
freezerId: string;
teamTx: SubmittableExtrinsic<'promise'> | null;
}

View File

@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { useApi } from '@polkadot/react-hooks';
import { BN_ONE, BN_TEN, BN_TWO, BN_ZERO, formatBalance, isBn } from '@polkadot/util';
import { BN_ONE, BN_TEN, BN_TWO, BN_ZERO, formatBalance, isBn, isUndefined } from '@polkadot/util';
import { BitLengthOption } from './constants';
import Dropdown from './Dropdown';
@ -80,12 +80,14 @@ function getSiOptions (symbol: string, decimals?: number): { text: string; value
}));
}
function getSiPowers (si: SiDef | null): [BN, number, number] {
function getSiPowers (si: SiDef | null, decimals?: number): [BN, number, number] {
if (!si) {
return [BN_ZERO, 0, 0];
}
const basePower = formatBalance.getDefaults().decimals;
const basePower = isUndefined(decimals)
? formatBalance.getDefaults().decimals
: decimals;
return [new BN(basePower + si.power), basePower, si.power];
}
@ -109,8 +111,8 @@ function isValidNumber (bn: BN, bitLength: BitLength, isZeroable: boolean, maxVa
return true;
}
function inputToBn (api: ApiPromise, input: string, si: SiDef | null, bitLength: BitLength, isZeroable: boolean, maxValue?: BN): [BN, boolean] {
const [siPower, basePower, siUnitPower] = getSiPowers(si);
function inputToBn (api: ApiPromise, input: string, si: SiDef | null, bitLength: BitLength, isZeroable: boolean, maxValue?: BN, decimals?: number): [BN, boolean] {
const [siPower, basePower, siUnitPower] = getSiPowers(si, decimals);
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
const isDecimalValue = input.match(/^(\d+)\.(\d+)$/);
@ -140,8 +142,8 @@ function inputToBn (api: ApiPromise, input: string, si: SiDef | null, bitLength:
];
}
function getValuesFromString (api: ApiPromise, value: string, si: SiDef | null, bitLength: BitLength, isZeroable: boolean, maxValue?: BN): [string, BN, boolean] {
const [valueBn, isValid] = inputToBn(api, value, si, bitLength, isZeroable, maxValue);
function getValuesFromString (api: ApiPromise, value: string, si: SiDef | null, bitLength: BitLength, isZeroable: boolean, maxValue?: BN, decimals?: number): [string, BN, boolean] {
const [valueBn, isValid] = inputToBn(api, value, si, bitLength, isZeroable, maxValue, decimals);
return [
value,
@ -150,9 +152,12 @@ function getValuesFromString (api: ApiPromise, value: string, si: SiDef | null,
];
}
function getValuesFromBn (valueBn: BN, si: SiDef | null, isZeroable: boolean): [string, BN, boolean] {
function getValuesFromBn (valueBn: BN, si: SiDef | null, isZeroable: boolean, _decimals?: number): [string, BN, boolean] {
const decimals = isUndefined(_decimals)
? formatBalance.getDefaults().decimals
: _decimals;
const value = si
? valueBn.div(BN_TEN.pow(new BN(formatBalance.getDefaults().decimals + si.power))).toString()
? valueBn.div(BN_TEN.pow(new BN(decimals + si.power))).toString()
: valueBn.toString();
return [
@ -162,10 +167,10 @@ function getValuesFromBn (valueBn: BN, si: SiDef | null, isZeroable: boolean): [
];
}
function getValues (api: ApiPromise, value: BN | string = BN_ZERO, si: SiDef | null, bitLength: BitLength, isZeroable: boolean, maxValue?: BN): [string, BN, boolean] {
function getValues (api: ApiPromise, value: BN | string = BN_ZERO, si: SiDef | null, bitLength: BitLength, isZeroable: boolean, maxValue?: BN, decimals?: number): [string, BN, boolean] {
return isBn(value)
? getValuesFromBn(value, si, isZeroable)
: getValuesFromString(api, value, si, bitLength, isZeroable, maxValue);
? getValuesFromBn(value, si, isZeroable, decimals)
: getValuesFromString(api, value, si, bitLength, isZeroable, maxValue, decimals);
}
function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, className = '', defaultValue, help, isDecimal, isFull, isSi, isDisabled, isError = false, isWarning, isZeroable = true, label, labelExtra, maxLength, maxValue, onChange, onEnter, onEscape, placeholder, siDecimals, siSymbol, value: propsValue }: Props): React.ReactElement<Props> {
@ -174,7 +179,7 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas
const [si, setSi] = useState<SiDef | null>(isSi ? formatBalance.findSi('-') : null);
const [isPreKeyDown, setIsPreKeyDown] = useState(false);
const [[value, valueBn, isValid], setValues] = useState<[string, BN, boolean]>(
getValues(api, propsValue || defaultValue, si, bitLength, isZeroable, maxValue)
getValues(api, propsValue || defaultValue, si, bitLength, isZeroable, maxValue, siDecimals)
);
const siOptions = useMemo(
@ -188,9 +193,9 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas
const _onChangeWithSi = useCallback(
(input: string, si: SiDef | null) => setValues(
getValuesFromString(api, input, si, bitLength, isZeroable, maxValue)
getValuesFromString(api, input, si, bitLength, isZeroable, maxValue, siDecimals)
),
[api, bitLength, isZeroable, maxValue]
[api, bitLength, isZeroable, maxValue, siDecimals]
);
const _onChange = useCallback(