From 35f79544d40a3b6bf001130ab0f3169dcbf18dab Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Wed, 27 Sep 2023 17:32:38 +0100 Subject: [PATCH 1/5] balance conversion for balance types --- .../components/EasySetup/ManualExtrinsic.tsx | 69 +++++++++++++++---- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx index d67ac60a..159a878f 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, @@ -14,6 +15,7 @@ import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'r import { useApi } from '../../contexts/ApiContext' import paramConversion from '../../utils/paramConversion' import { getTypeDef } from '@polkadot/types/create' +import { getGlobalMaxValue, inputToBn } from '../../utils' interface Props { extrinsicIndex?: string @@ -26,6 +28,7 @@ interface Props { interface ParamField { name: string type: string + typeName: string optional: boolean } @@ -43,9 +46,12 @@ const initFormState = { const argIsOptional = (arg: any) => arg.type.toString().startsWith('Option<') +const isTypeBalance = (typeName: string) => ['Balance', 'BalanceOf', 'Amount'].includes(typeName) + const transformParams = ( paramFields: ParamField[], inputParams: any[], + tokenDecimals: number | undefined, opts = { emptyAsNull: true } ) => { // if `opts.emptyAsNull` is true, empty param value will be added to res as `null`. @@ -69,7 +75,7 @@ const transformParams = ( value: paramVal[ind] || null })) - return params.reduce((previousValue, { type = 'string', value }) => { + return params.reduce((previousValue, { type = 'string', value, typeName }) => { if (value == null || value === '') return opts.emptyAsNull ? [...previousValue, null] : previousValue @@ -88,6 +94,26 @@ const transformParams = ( return [...previousValue, converted] } + // Deal with balance like types where the param need to + // be multiplied by the decimals + if (isTypeBalance(typeName)) { + if (!tokenDecimals) return previousValue + + if (!converted.match('^[0-9]+([.][0-9]+)?$')) { + console.error('Only numbers and "." are accepted.') + return previousValue + } + + const bnResult = inputToBn(tokenDecimals, value) + + if (bnResult.gte(getGlobalMaxValue(128))) { + console.error('Amount too large') + return previousValue + } + + return [...previousValue, bnResult.toString()] + } + // Deal with a single value if (isNumType(type)) { converted = @@ -106,7 +132,7 @@ const ManualExtrinsic = ({ extrinsicIndex, onSelectFromCallData }: Props) => { - const { api } = useApi() + const { api, chainInfo } = useApi() const [palletRPCs, setPalletRPCs] = useState([]) const [callables, setCallables] = useState([]) const [paramFields, setParamFields] = useState(null) @@ -140,8 +166,12 @@ const ManualExtrinsic = ({ useEffect(() => { !!paramFields?.length && !!inputParams.length && - setTransformedParams(transformParams(paramFields, inputParams)) - }, [inputParams, paramFields]) + setTransformedParams(transformParams(paramFields, inputParams, chainInfo?.tokenDecimals)) + }, [inputParams, paramFields, chainInfo]) + + console.log('paramFields', paramFields) + console.log('inputParams', inputParams) + console.log('transformedParams', transformedParams) const updatePalletRPCs = useCallback(() => { if (!api) { @@ -190,6 +220,7 @@ const ManualExtrinsic = ({ return { name: arg.name.toString(), type: arg.type.toString(), + typeName: arg.typeName.unwrap().toString(), optional: argIsOptional(arg) } }) @@ -209,6 +240,7 @@ const ManualExtrinsic = ({ const onPalletCallableParamChange = useCallback( (event: SelectChangeEvent, state: string) => { // reset the params + setTransformedParams(undefined) setParamFields(null) onSetErrorMessage('') @@ -336,17 +368,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 || ''} + ) + }} + /> +
  • + ) + })}
) From 6c78fd7406285ec96e39c81e08beef0ac796e5e9 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Wed, 27 Sep 2023 23:04:35 +0100 Subject: [PATCH 2/5] with proper errors --- .../components/EasySetup/ManualExtrinsic.tsx | 180 +++++++++--------- packages/ui/src/components/modals/Send.tsx | 3 +- 2 files changed, 89 insertions(+), 94 deletions(-) diff --git a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx index 159a878f..b9a1ff5c 100644 --- a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx +++ b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx @@ -14,7 +14,7 @@ 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 { getTypeDef } from '@polkadot/types/create' import { getGlobalMaxValue, inputToBn } from '../../utils' interface Props { @@ -23,6 +23,7 @@ interface Props { onSetExtrinsic: (ext: SubmittableExtrinsic<'promise', ISubmittableResult>, key?: string) => void onSetErrorMessage: React.Dispatch> onSelectFromCallData: () => void + hasErrorMessage: boolean } interface ParamField { @@ -48,81 +49,6 @@ const argIsOptional = (arg: any) => arg.type.toString().startsWith('Option<') const isTypeBalance = (typeName: string) => ['Balance', 'BalanceOf', 'Amount'].includes(typeName) -const transformParams = ( - paramFields: ParamField[], - inputParams: any[], - tokenDecimals: number | undefined, - 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, typeName }) => { - 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] - } - - // Deal with balance like types where the param need to - // be multiplied by the decimals - if (isTypeBalance(typeName)) { - if (!tokenDecimals) return previousValue - - if (!converted.match('^[0-9]+([.][0-9]+)?$')) { - console.error('Only numbers and "." are accepted.') - return previousValue - } - - const bnResult = inputToBn(tokenDecimals, value) - - if (bnResult.gte(getGlobalMaxValue(128))) { - console.error('Amount too large') - return previousValue - } - - return [...previousValue, bnResult.toString()] - } - - // 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 ManualExtrinsic = ({ @@ -130,7 +56,8 @@ const ManualExtrinsic = ({ onSetExtrinsic, onSetErrorMessage, extrinsicIndex, - onSelectFromCallData + onSelectFromCallData, + hasErrorMessage }: Props) => { const { api, chainInfo } = useApi() const [palletRPCs, setPalletRPCs] = useState([]) @@ -163,15 +90,84 @@ const ManualExtrinsic = ({ }) }, [inputParams, paramFields]) + const transformParams = useCallback( + (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, typeName }) => { + 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] + } + + // Deal with balance like types where the param need to + // be multiplied by the decimals + if (isTypeBalance(typeName)) { + if (!chainInfo?.tokenDecimals) return previousValue + + if (!converted.match('^[0-9]+([.][0-9]+)?$')) { + onSetErrorMessage('Only numbers and "." are accepted.') + return previousValue + } + + const bnResult = inputToBn(chainInfo.tokenDecimals, value) + + if (bnResult.gte(getGlobalMaxValue(128))) { + onSetErrorMessage('Amount too large') + return previousValue + } + + return [...previousValue, bnResult.toString()] + } + + // Deal with a single value + if (isNumType(type)) { + converted = + converted.indexOf('.') >= 0 ? Number.parseFloat(converted) : Number.parseInt(converted) + } + return [...previousValue, converted] + }, [] as any[]) + }, + [chainInfo, onSetErrorMessage] + ) + useEffect(() => { !!paramFields?.length && !!inputParams.length && - setTransformedParams(transformParams(paramFields, inputParams, chainInfo?.tokenDecimals)) - }, [inputParams, paramFields, chainInfo]) - - console.log('paramFields', paramFields) - console.log('inputParams', inputParams) - console.log('transformedParams', transformedParams) + setTransformedParams(transformParams(paramFields, inputParams)) + }, [inputParams, paramFields, transformParams]) const updatePalletRPCs = useCallback(() => { if (!api) { @@ -205,17 +201,14 @@ const ManualExtrinsic = ({ let paramFields: ParamField[] = [] const metaArgs = api.tx[palletRpc][callable].meta.args - console.log('metaArgs', metaArgs) + // 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(), @@ -283,7 +276,7 @@ const ManualExtrinsic = ({ return } - if (!callable || !palletRpc || !areAllParamsFilled) { + if (!callable || !palletRpc || !areAllParamsFilled || hasErrorMessage) { return } @@ -304,6 +297,7 @@ const ManualExtrinsic = ({ areAllParamsFilled, callable, extrinsicIndex, + hasErrorMessage, onSetErrorMessage, onSetExtrinsic, palletRpc, diff --git a/packages/ui/src/components/modals/Send.tsx b/packages/ui/src/components/modals/Send.tsx index 6db384ac..1295931f 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, From 719d91b138fd44d114178a0973f0402900ef0b28 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:40:22 +0200 Subject: [PATCH 3/5] Update packages/ui/src/components/EasySetup/ManualExtrinsic.tsx --- packages/ui/src/components/EasySetup/ManualExtrinsic.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx index b9a1ff5c..5c885afe 100644 --- a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx +++ b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx @@ -14,7 +14,6 @@ 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' interface Props { From bb5b4e33691b3b6949e99114a87cd5b2d1d6e399 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Thu, 28 Sep 2023 17:45:11 +0100 Subject: [PATCH 4/5] refactor --- .../components/EasySetup/ManualExtrinsic.tsx | 112 +++++++++++------- 1 file changed, 67 insertions(+), 45 deletions(-) diff --git a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx index 5c885afe..cf4c9c10 100644 --- a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx +++ b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx @@ -15,6 +15,7 @@ import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'r import { useApi } from '../../contexts/ApiContext' import paramConversion from '../../utils/paramConversion' import { getGlobalMaxValue, inputToBn } from '../../utils' +import BN from 'bn.js' interface Props { extrinsicIndex?: string @@ -50,6 +51,32 @@ const isTypeBalance = (typeName: string) => ['Balance', 'BalanceOf', 'Amount'].i const isNumType = (type: string) => paramConversion.num.includes(type) +const parseFloatOrInt = (value: any) => { + return value.indexOf('.') >= 0 ? Number.parseFloat(value) : Number.parseInt(value) +} + +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, @@ -89,77 +116,74 @@ 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 }) => { - // 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 + value: processParamValue(inputParams[ind]) })) return params.reduce((previousValue, { type = 'string', value, typeName }) => { - if (value == null || value === '') + 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 - - 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] + return [...previousValue, handleVector(value, type)] } - // Deal with balance like types where the param need to - // be multiplied by the decimals + // Deal with balance like types where the param needs to + // be multiplied by the token decimals if (isTypeBalance(typeName)) { - if (!chainInfo?.tokenDecimals) return previousValue - - if (!converted.match('^[0-9]+([.][0-9]+)?$')) { - onSetErrorMessage('Only numbers and "." are accepted.') + if (!isValidAmountString(value) || !chainInfo?.tokenDecimals) { return previousValue } const bnResult = inputToBn(chainInfo.tokenDecimals, value) - if (bnResult.gte(getGlobalMaxValue(128))) { - onSetErrorMessage('Amount too large') + if (isAmountOverflow(bnResult)) { return previousValue } return [...previousValue, bnResult.toString()] } - // Deal with a single value if (isNumType(type)) { - converted = - converted.indexOf('.') >= 0 ? Number.parseFloat(converted) : Number.parseInt(converted) + return [...previousValue, parseFloatOrInt(value)] } - return [...previousValue, converted] + + return [...previousValue, value] }, [] as any[]) }, - [chainInfo, onSetErrorMessage] + [chainInfo, isAmountOverflow, isValidAmountString] ) useEffect(() => { @@ -286,9 +310,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) } }, [ From 2971cda3a2534b3a3979204b2dec0846d0788c89 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Date: Fri, 29 Sep 2023 12:28:11 +0200 Subject: [PATCH 5/5] Update packages/ui/src/components/EasySetup/ManualExtrinsic.tsx --- packages/ui/src/components/EasySetup/ManualExtrinsic.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx index cf4c9c10..d5e34b9f 100644 --- a/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx +++ b/packages/ui/src/components/EasySetup/ManualExtrinsic.tsx @@ -224,7 +224,6 @@ 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()))