Skip to content

Commit

Permalink
Balance conversion for balance types (#376)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tbaut authored Sep 29, 2023
1 parent 7bcb997 commit 8711fb0
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 81 deletions.
213 changes: 133 additions & 80 deletions packages/ui/src/components/EasySetup/ManualExtrinsic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Alert,
Box,
FormControl,
InputAdornment,
MenuItem,
Select,
SelectChangeEvent,
Expand All @@ -13,19 +14,22 @@ 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
className?: string
onSetExtrinsic: (ext: SubmittableExtrinsic<'promise', ISubmittableResult>, key?: string) => void
onSetErrorMessage: React.Dispatch<React.SetStateAction<string>>
onSelectFromCallData: () => void
hasErrorMessage: boolean
}

interface ParamField {
name: string
type: string
typeName: string
optional: boolean
}

Expand All @@ -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<any[]>([])
const [callables, setCallables] = useState<any[]>([])
const [paramFields, setParamFields] = useState<ParamField[] | null>(null)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
})
Expand All @@ -209,6 +255,7 @@ const ManualExtrinsic = ({
const onPalletCallableParamChange = useCallback(
(event: SelectChangeEvent<string>, state: string) => {
// reset the params
setTransformedParams(undefined)
setParamFields(null)
onSetErrorMessage('')

Expand Down Expand Up @@ -251,7 +298,7 @@ const ManualExtrinsic = ({
return
}

if (!callable || !palletRpc || !areAllParamsFilled) {
if (!callable || !palletRpc || !areAllParamsFilled || hasErrorMessage) {
return
}

Expand All @@ -262,16 +309,15 @@ 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)
}
}, [
api,
areAllParamsFilled,
callable,
extrinsicIndex,
hasErrorMessage,
onSetErrorMessage,
onSetExtrinsic,
palletRpc,
Expand Down Expand Up @@ -336,17 +382,24 @@ const ManualExtrinsic = ({
</Select>
</FormControl>
<ul className="paramInputs">
{paramFields?.map((paramField, ind) => (
<li key={`${paramField.name}-${paramField.type}`}>
<TextField
placeholder={paramField.type}
type="text"
label={`${paramField.name}${paramField.optional ? ' (optional)' : ''}`}
value={inputParams[ind] ? inputParams[ind].value : ''}
onChange={(event) => onParamChange(event, { ind, paramField })}
/>
</li>
))}
{paramFields?.map((paramField, ind) => {
return (
<li key={`${paramField.name}-${paramField.type}`}>
<TextField
placeholder={paramField.type}
type="text"
label={`${paramField.name}${paramField.optional ? ' (optional)' : ''}`}
value={inputParams[ind] ? inputParams[ind].value : ''}
onChange={(event) => onParamChange(event, { ind, paramField })}
InputProps={{
endAdornment: isTypeBalance(paramField.typeName) && (
<InputAdornment position="end">{chainInfo?.tokenSymbol || ''}</InputAdornment>
)
}}
/>
</li>
)
})}
</ul>
</Box>
)
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/components/modals/Send.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]: (
Expand All @@ -143,7 +144,7 @@ const Send = ({ onClose, className, onSuccess, onFinalized }: Props) => {
/>
)
}
}, [selectedOrigin, isProxySelected])
}, [selectedOrigin, easyOptionErrorMessage, isProxySelected])

const signCallback = useSigningCallback({
onSuccess,
Expand Down

0 comments on commit 8711fb0

Please sign in to comment.