(Fix) [Contract Interaction] Support array-like arguments (#1009)
* Add `ArrayTypeInput` to support array type values - also updated types for ContractInteraction/utils * Update placeholder message * Support fixed size arrays - refactored how Field's key is generated due to conflicts with final-form library and `[]` in the name - also simplified validation for array-like fields, as it messed with arguments of type `T[][2][][3]`
This commit is contained in:
parent
6d1a349d87
commit
0296a670e7
|
@ -239,6 +239,7 @@
|
|||
"truffle": "5.1.29",
|
||||
"typescript": "^3.9.5",
|
||||
"wait-on": "5.0.1",
|
||||
"web3-eth-contract": "^1.2.9",
|
||||
"web3-utils": "^1.2.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export interface AbiItemExtended extends AbiItem {
|
|||
signatureHash: string
|
||||
}
|
||||
|
||||
export const getMethodSignature = ({ inputs, name }: AbiItem) => {
|
||||
export const getMethodSignature = ({ inputs, name }: AbiItem): string => {
|
||||
const params = inputs.map((x) => x.type).join(',')
|
||||
return `${name}(${params})`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react'
|
||||
|
||||
import TextareaField from 'src/components/forms/TextareaField'
|
||||
import {
|
||||
isAddress,
|
||||
isBoolean,
|
||||
isByte,
|
||||
isInt,
|
||||
isUint,
|
||||
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
|
||||
const validator = (value: string): string | undefined => {
|
||||
try {
|
||||
const values = JSON.parse(value)
|
||||
|
||||
if (!Array.isArray(values)) {
|
||||
return 'be sure to surround value with []'
|
||||
}
|
||||
} catch (e) {
|
||||
return 'invalid format'
|
||||
}
|
||||
}
|
||||
|
||||
const typePlaceholder = (text: string, type: string): string => {
|
||||
if (isAddress(type)) {
|
||||
return `${text} E.g.: ["0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E","0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e"]`
|
||||
}
|
||||
|
||||
if (isBoolean(type)) {
|
||||
return `${text} E.g.: [true, false, false, true]`
|
||||
}
|
||||
|
||||
if (isUint(type)) {
|
||||
return `${text} E.g.: [1000, 212, 320000022, 23]`
|
||||
}
|
||||
|
||||
if (isInt(type)) {
|
||||
return `${text} E.g.: [1000, -212, 1232, -1]`
|
||||
}
|
||||
|
||||
if (isByte(type)) {
|
||||
return `${text} E.g.: ["0xc00000000000000000000000000000000000", "0xc00000000000000000000000000000000001"]`
|
||||
}
|
||||
|
||||
return `${text} E.g.: ["first value", "second value", "third value"]`
|
||||
}
|
||||
|
||||
const ArrayTypeInput = ({ name, text, type }: { name: string; text: string; type: string }): JSX.Element => (
|
||||
<TextareaField name={name} placeholder={typePlaceholder(text, type)} text={text} type="text" validate={validator} />
|
||||
)
|
||||
|
||||
export default ArrayTypeInput
|
|
@ -1,3 +1,4 @@
|
|||
import { Checkbox } from '@gnosis.pm/safe-react-components'
|
||||
import React from 'react'
|
||||
|
||||
import Col from 'src/components/layout/Col'
|
||||
|
@ -5,7 +6,8 @@ import Field from 'src/components/forms/Field'
|
|||
import TextField from 'src/components/forms/TextField'
|
||||
|
||||
import { composeValidators, mustBeEthereumAddress, required } from 'src/components/forms/validator'
|
||||
import { Checkbox } from '@gnosis.pm/safe-react-components'
|
||||
import { isArrayParameter } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import ArrayTypeInput from './ArrayTypeInput'
|
||||
|
||||
type Props = {
|
||||
type: string
|
||||
|
@ -13,10 +15,11 @@ type Props = {
|
|||
placeholder: string
|
||||
}
|
||||
|
||||
const InputComponent = ({ type, keyValue, placeholder }: Props) => {
|
||||
const InputComponent = ({ type, keyValue, placeholder }: Props): JSX.Element => {
|
||||
if (!type) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'bool': {
|
||||
const inputProps = {
|
||||
|
@ -46,15 +49,19 @@ const InputComponent = ({ type, keyValue, placeholder }: Props) => {
|
|||
default: {
|
||||
return (
|
||||
<Col>
|
||||
<Field
|
||||
component={TextField}
|
||||
name={keyValue}
|
||||
placeholder={placeholder}
|
||||
testId={keyValue}
|
||||
text={placeholder}
|
||||
type="text"
|
||||
validate={required}
|
||||
/>
|
||||
{isArrayParameter(type) ? (
|
||||
<ArrayTypeInput name={keyValue} text={placeholder} type={type} />
|
||||
) : (
|
||||
<Field
|
||||
component={TextField}
|
||||
name={keyValue}
|
||||
placeholder={placeholder}
|
||||
testId={keyValue}
|
||||
text={placeholder}
|
||||
type="text"
|
||||
validate={required}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,28 +4,32 @@ import { useField } from 'react-final-form'
|
|||
import Row from 'src/components/layout/Row'
|
||||
|
||||
import InputComponent from './InputComponent'
|
||||
import { generateFormFieldKey } from '../utils'
|
||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
|
||||
const RenderInputParams = () => {
|
||||
const RenderInputParams = (): JSX.Element => {
|
||||
const {
|
||||
meta: { valid: validABI },
|
||||
} = useField('abi', { subscription: { valid: true, value: true } })
|
||||
const {
|
||||
input: { value: method },
|
||||
}: any = useField('selectedMethod', { subscription: { value: true } })
|
||||
}: { input: { value: AbiItemExtended } } = useField('selectedMethod', { subscription: { value: true } })
|
||||
const renderInputs = validABI && !!method && method.inputs.length
|
||||
|
||||
return !renderInputs
|
||||
? null
|
||||
: method.inputs.map(({ name, type }, index) => {
|
||||
return !renderInputs ? null : (
|
||||
<>
|
||||
{method.inputs.map(({ name, type }, index) => {
|
||||
const placeholder = name ? `${name} (${type})` : type
|
||||
const key = `methodInput-${method.name}_${index}_${type}`
|
||||
const key = generateFormFieldKey(type, method.signatureHash, index)
|
||||
|
||||
return (
|
||||
<Row key={key} margin="sm">
|
||||
<InputComponent type={type} keyValue={key} placeholder={placeholder} />
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RenderInputParams
|
||||
|
|
|
@ -22,7 +22,7 @@ import Header from 'src/routes/safe/components/Balances/SendModal/screens/Contra
|
|||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
|
||||
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||
import { getValueFromTxInputs } from '../utils'
|
||||
import { generateFormFieldKey, getValueFromTxInputs } from '../utils'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -133,7 +133,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => {
|
|||
</Paragraph>
|
||||
</Row>
|
||||
{tx.selectedMethod.inputs.map(({ name, type }, index) => {
|
||||
const key = `methodInput-${tx.selectedMethod.name}_${index}_${type}`
|
||||
const key = generateFormFieldKey(type, tx.selectedMethod.signatureHash, index)
|
||||
const value: string = getValueFromTxInputs(key, type, tx)
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { FORM_ERROR } from 'final-form'
|
||||
import { FORM_ERROR, Mutator, SubmissionErrors } from 'final-form'
|
||||
import createDecorator from 'final-form-calculate'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
import { ContractSendMethod } from 'web3-eth-contract'
|
||||
|
||||
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||
import { getNetwork } from 'src/config'
|
||||
import { getConfiguredSource } from 'src/logic/contractInteraction/sources'
|
||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
import { getAddressFromENS, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { TransactionReviewType } from '../Review'
|
||||
import { TransactionReviewType } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
export const NO_CONTRACT = 'no contract'
|
||||
|
@ -50,7 +50,7 @@ export const ensResolver = createDecorator({
|
|||
},
|
||||
})
|
||||
|
||||
export const formMutators = {
|
||||
export const formMutators: Record<string, Mutator<{ selectedMethod: { name: string } }>> = {
|
||||
setMax: (args, state, utils) => {
|
||||
utils.changeValue(state, 'value', () => args[0])
|
||||
},
|
||||
|
@ -73,7 +73,16 @@ export const formMutators = {
|
|||
},
|
||||
}
|
||||
|
||||
export const handleSubmitError = (error, values) => {
|
||||
export const isAddress = (type: string): boolean => type.indexOf('address') === 0
|
||||
export const isBoolean = (type: string): boolean => type.indexOf('bool') === 0
|
||||
export const isString = (type: string): boolean => type.indexOf('string') === 0
|
||||
export const isUint = (type: string): boolean => type.indexOf('uint') === 0
|
||||
export const isInt = (type: string): boolean => type.indexOf('int') === 0
|
||||
export const isByte = (type: string): boolean => type.indexOf('byte') === 0
|
||||
|
||||
export const isArrayParameter = (parameter: string): boolean => /(\[\d*])+$/.test(parameter)
|
||||
|
||||
export const handleSubmitError = (error: SubmissionErrors, values: Record<string, string>): Record<string, string> => {
|
||||
for (const key in values) {
|
||||
if (values.hasOwnProperty(key) && values[key] === error.value) {
|
||||
return { [key]: error.reason }
|
||||
|
@ -84,11 +93,30 @@ export const handleSubmitError = (error, values) => {
|
|||
return { [FORM_ERROR]: error.message }
|
||||
}
|
||||
|
||||
export const createTxObject = (method: AbiItem, contractAddress: string, values) => {
|
||||
export const generateFormFieldKey = (type: string, signatureHash: string, index: number): string => {
|
||||
const keyType = isArrayParameter(type) ? 'arrayParam' : type
|
||||
return `methodInput-${signatureHash}_${index}_${keyType}`
|
||||
}
|
||||
|
||||
const extractMethodArgs = (signatureHash: string, values: Record<string, string>) => ({ type }, index) => {
|
||||
const key = generateFormFieldKey(type, signatureHash, index)
|
||||
|
||||
if (isArrayParameter(type)) {
|
||||
return JSON.parse(values[key])
|
||||
}
|
||||
|
||||
return values[key]
|
||||
}
|
||||
|
||||
export const createTxObject = (
|
||||
method: AbiItemExtended,
|
||||
contractAddress: string,
|
||||
values: Record<string, string>,
|
||||
): ContractSendMethod => {
|
||||
const web3 = getWeb3()
|
||||
const contract: any = new web3.eth.Contract([method], contractAddress)
|
||||
const { inputs, name } = method
|
||||
const args = inputs.map(({ type }, index) => values[`methodInput-${name}_${index}_${type}`])
|
||||
const { inputs, name, signatureHash } = method
|
||||
const args = inputs.map(extractMethodArgs(signatureHash, values))
|
||||
|
||||
return contract.methods[name](...args)
|
||||
}
|
||||
|
@ -96,9 +124,15 @@ export const createTxObject = (method: AbiItem, contractAddress: string, values)
|
|||
export const isReadMethod = (method: AbiItemExtended): boolean => method && method.action === 'read'
|
||||
|
||||
export const getValueFromTxInputs = (key: string, type: string, tx: TransactionReviewType): string => {
|
||||
let value = tx[key]
|
||||
if (type === 'bool') {
|
||||
value = tx[key] ? String(tx[key]) : 'false'
|
||||
if (isArrayParameter(type)) {
|
||||
key = key.replace('[]', '')
|
||||
}
|
||||
|
||||
let value = tx[key]
|
||||
|
||||
if (type === 'bool') {
|
||||
value = String(value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue