From 8711fb0ccbd67bdd1451b8024d64092de1a0db50 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:28:34 +0100 Subject: [PATCH] Balance conversion for balance types (#376) --- .../components/EasySetup/ManualExtrinsic.tsx | 213 +++++++++++------- packages/ui/src/components/modals/Send.tsx | 3 +- 2 files changed, 135 insertions(+), 81 deletions(-) diff --git a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx index d67ac60a..d5e34b9f 100644 --- a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx +++ b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx @@ -2,6 +2,7 @@ import { Alert, Box, FormControl, + InputAdornment, MenuItem, Select, SelectChangeEvent, @@ -13,7 +14,8 @@ import { ISubmittableResult } from '@polkadot/types/types' import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' import { useApi } from '../../contexts/ApiContext' import paramConversion from '../../utils/paramConversion' -import { getTypeDef } from '@polkadot/types/create' +import { getGlobalMaxValue, inputToBn } from '../../utils' +import BN from 'bn.js' interface Props { extrinsicIndex?: string @@ -21,11 +23,13 @@ interface Props { onSetExtrinsic: (ext: SubmittableExtrinsic<'promise', ISubmittableResult>, key?: string) => void onSetErrorMessage: React.Dispatch> onSelectFromCallData: () => void + hasErrorMessage: boolean } interface ParamField { name: string type: string + typeName: string optional: boolean } @@ -43,70 +47,45 @@ const initFormState = { const argIsOptional = (arg: any) => arg.type.toString().startsWith('Option<') -const transformParams = ( - paramFields: ParamField[], - inputParams: any[], - opts = { emptyAsNull: true } -) => { - // if `opts.emptyAsNull` is true, empty param value will be added to res as `null`. - // otherwise, it will not be added - const paramVal = inputParams.map((inputParam) => { - // to cater the js quirk that `null` is a type of `object`. - if ( - typeof inputParam === 'object' && - inputParam !== null && - typeof inputParam.value === 'string' - ) { - return inputParam.value.trim() - } else if (typeof inputParam === 'string') { - return inputParam.trim() - } - return inputParam - }) - - const params = paramFields.map((field, ind) => ({ - ...field, - value: paramVal[ind] || null - })) - - return params.reduce((previousValue, { type = 'string', value }) => { - if (value == null || value === '') - return opts.emptyAsNull ? [...previousValue, null] : previousValue - - let converted = value - - // Deal with a vector - if (type.indexOf('Vec<') >= 0) { - converted = converted.split(',').map((e: string) => e.trim()) - converted = converted.map((single: any) => - isNumType(type) - ? single.indexOf('.') >= 0 - ? Number.parseFloat(single) - : Number.parseInt(single) - : single - ) - return [...previousValue, converted] - } +const isTypeBalance = (typeName: string) => ['Balance', 'BalanceOf', 'Amount'].includes(typeName) - // Deal with a single value - if (isNumType(type)) { - converted = - converted.indexOf('.') >= 0 ? Number.parseFloat(converted) : Number.parseInt(converted) - } - return [...previousValue, converted] - }, [] as any[]) +const isNumType = (type: string) => paramConversion.num.includes(type) + +const parseFloatOrInt = (value: any) => { + return value.indexOf('.') >= 0 ? Number.parseFloat(value) : Number.parseInt(value) } -const isNumType = (type: string) => paramConversion.num.includes(type) +const handleVector = (val: any, type: string) => { + return val + .split(',') + .map((e: string) => e.trim()) + .map((single: any) => (isNumType(type) ? parseFloatOrInt(single) : single)) +} + +// create an array of all the params with their type +const processParamValue = (inputParam: any) => { + // to cater the js quirk that `null` is a type of `object`. + if ( + typeof inputParam === 'object' && + inputParam !== null && + typeof inputParam.value === 'string' + ) { + return inputParam.value.trim() + } else if (typeof inputParam === 'string') { + return inputParam.trim() + } + return inputParam +} const ManualExtrinsic = ({ className, onSetExtrinsic, onSetErrorMessage, extrinsicIndex, - onSelectFromCallData + onSelectFromCallData, + hasErrorMessage }: Props) => { - const { api } = useApi() + const { api, chainInfo } = useApi() const [palletRPCs, setPalletRPCs] = useState([]) const [callables, setCallables] = useState([]) const [paramFields, setParamFields] = useState(null) @@ -137,11 +116,81 @@ const ManualExtrinsic = ({ }) }, [inputParams, paramFields]) + const isValidAmountString = useCallback( + (value: any) => { + if (!value.match(/^[0-9]+([.][0-9]+)?$/)) { + console.log('wrong boom') + onSetErrorMessage('Only numbers and "." are accepted.') + return false + } + + return true + }, + [onSetErrorMessage] + ) + + const isAmountOverflow = useCallback( + (bnResult: BN) => { + if (bnResult.gte(getGlobalMaxValue(128))) { + onSetErrorMessage('Amount too large') + return true + } + + return false + }, + [onSetErrorMessage] + ) + + const transformParams = useCallback( + (paramFields: ParamField[], inputParams: any[], opts = { emptyAsNull: true }) => { + const params = paramFields.map((field, ind) => ({ + ...field, + value: processParamValue(inputParams[ind]) + })) + + return params.reduce((previousValue, { type = 'string', value, typeName }) => { + if (value == null || value === '') { + // if `opts.emptyAsNull` is true, empty param value will be added to res as `null`. + // otherwise, it will not be added + return opts.emptyAsNull ? [...previousValue, null] : previousValue + } + + // Deal with a vector + if (type.indexOf('Vec<') >= 0) { + return [...previousValue, handleVector(value, type)] + } + + // Deal with balance like types where the param needs to + // be multiplied by the token decimals + if (isTypeBalance(typeName)) { + if (!isValidAmountString(value) || !chainInfo?.tokenDecimals) { + return previousValue + } + + const bnResult = inputToBn(chainInfo.tokenDecimals, value) + + if (isAmountOverflow(bnResult)) { + return previousValue + } + + return [...previousValue, bnResult.toString()] + } + + if (isNumType(type)) { + return [...previousValue, parseFloatOrInt(value)] + } + + return [...previousValue, value] + }, [] as any[]) + }, + [chainInfo, isAmountOverflow, isValidAmountString] + ) + useEffect(() => { !!paramFields?.length && !!inputParams.length && setTransformedParams(transformParams(paramFields, inputParams)) - }, [inputParams, paramFields]) + }, [inputParams, paramFields, transformParams]) const updatePalletRPCs = useCallback(() => { if (!api) { @@ -175,21 +224,18 @@ const ManualExtrinsic = ({ let paramFields: ParamField[] = [] const metaArgs = api.tx[palletRpc][callable].meta.args - console.log('metaArgs', metaArgs) if (metaArgs && metaArgs.length > 0) { paramFields = metaArgs.map((arg) => { - console.log('getTypeDef', getTypeDef(arg.type.toString())) - const instance = api.registry.createType(arg.type as unknown as 'u32') - console.log('instance', instance) - const raw = getTypeDef(instance.toRawType()) - console.log('raw', raw) - - arg.typeName.isSome && - console.log('typeName.unwrap().toString()', arg.typeName.unwrap().toString()) + // console.log('getTypeDef', getTypeDef(arg.type.toString())) + // const instance = api.registry.createType(arg.type as unknown as 'u32') + // console.log('instance', instance) + // const raw = getTypeDef(instance.toRawType()) + // console.log('raw', raw) return { name: arg.name.toString(), type: arg.type.toString(), + typeName: arg.typeName.unwrap().toString(), optional: argIsOptional(arg) } }) @@ -209,6 +255,7 @@ const ManualExtrinsic = ({ const onPalletCallableParamChange = useCallback( (event: SelectChangeEvent, state: string) => { // reset the params + setTransformedParams(undefined) setParamFields(null) onSetErrorMessage('') @@ -251,7 +298,7 @@ const ManualExtrinsic = ({ return } - if (!callable || !palletRpc || !areAllParamsFilled) { + if (!callable || !palletRpc || !areAllParamsFilled || hasErrorMessage) { return } @@ -262,9 +309,7 @@ const ManualExtrinsic = ({ !!extrinsic && onSetExtrinsic(extrinsic, extrinsicIndex) } catch (e) { - console.error('Error in ManualExtrinsic') - console.error(e) - onSetErrorMessage('An error occured') + onSetErrorMessage('Some parameters are invalid.') console.error(e) } }, [ @@ -272,6 +317,7 @@ const ManualExtrinsic = ({ areAllParamsFilled, callable, extrinsicIndex, + hasErrorMessage, onSetErrorMessage, onSetExtrinsic, palletRpc, @@ -336,17 +382,24 @@ const ManualExtrinsic = ({
    - {paramFields?.map((paramField, ind) => ( -
  • - onParamChange(event, { ind, paramField })} - /> -
  • - ))} + {paramFields?.map((paramField, ind) => { + return ( +
  • + onParamChange(event, { ind, paramField })} + InputProps={{ + endAdornment: isTypeBalance(paramField.typeName) && ( + {chainInfo?.tokenSymbol || ''} + ) + }} + /> +
  • + ) + })}
) diff --git a/packages/ui/src/components/modals/Send.tsx b/packages/ui/src/components/modals/Send.tsx index db009b74..4d3ef223 100644 --- a/packages/ui/src/components/modals/Send.tsx +++ b/packages/ui/src/components/modals/Send.tsx @@ -133,6 +133,7 @@ const Send = ({ onClose, className, onSuccess, onFinalized }: Props) => { onSetExtrinsic={setExtrinsicToCall} onSetErrorMessage={setEasyOptionErrorMessage} onSelectFromCallData={() => setSelectedEasyOption(FROM_CALL_DATA_MENU)} + hasErrorMessage={!!easyOptionErrorMessage} /> ), [FROM_CALL_DATA_MENU]: ( @@ -143,7 +144,7 @@ const Send = ({ onClose, className, onSuccess, onFinalized }: Props) => { /> ) } - }, [selectedOrigin, isProxySelected]) + }, [selectedOrigin, easyOptionErrorMessage, isProxySelected]) const signCallback = useSigningCallback({ onSuccess,