From 3687070dc165d414a0e7f2cbed91cd448b8da13a Mon Sep 17 00:00:00 2001 From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:49:41 +0200 Subject: [PATCH] make `decodeFunctionCall` and `decodeFunctionReturn` available at `web3-eth-abi` (#7345) * move `decodeFunctionCall` and `decodeFunctionReturn` to web3-eth-abi * add unit tests * update CHANGELOG.md * add functions docs --- packages/web3-eth-abi/CHANGELOG.md | 4 + .../web3-eth-abi/src/api/functions_api.ts | 145 +++++++++++- .../unit/decodeMethodParamsAndReturn.test.ts | 217 ++++++++++++++++++ .../src/contract-deployer-method-class.ts | 5 +- packages/web3-eth-contract/src/contract.ts | 15 +- packages/web3-eth-contract/src/encoding.ts | 48 +--- 6 files changed, 378 insertions(+), 56 deletions(-) create mode 100644 packages/web3-eth-abi/test/unit/decodeMethodParamsAndReturn.test.ts diff --git a/packages/web3-eth-abi/CHANGELOG.md b/packages/web3-eth-abi/CHANGELOG.md index 9cb5bd2d7b9..7a6951386d9 100644 --- a/packages/web3-eth-abi/CHANGELOG.md +++ b/packages/web3-eth-abi/CHANGELOG.md @@ -195,3 +195,7 @@ Documentation: - `decodeLog` , `decodeParametersWith` , `decodeParameters` and `decodeParameters` now accepts first immutable param as well (#7288) ## [Unreleased] + +### Added + +- added `decodeFunctionCall` and `decodeFunctionReturn`. (#7345) diff --git a/packages/web3-eth-abi/src/api/functions_api.ts b/packages/web3-eth-abi/src/api/functions_api.ts index 7504f352275..8518deeab24 100644 --- a/packages/web3-eth-abi/src/api/functions_api.ts +++ b/packages/web3-eth-abi/src/api/functions_api.ts @@ -19,11 +19,11 @@ along with web3.js. If not, see . * * @module ABI */ -import { AbiError } from 'web3-errors'; +import { AbiError, Web3ContractError } from 'web3-errors'; import { sha3Raw } from 'web3-utils'; -import { AbiFunctionFragment } from 'web3-types'; +import { AbiConstructorFragment, AbiFunctionFragment, DecodedParams, HexString } from 'web3-types'; import { isAbiFunctionFragment, jsonInterfaceMethodToString } from '../utils.js'; -import { encodeParameters } from './parameters_api.js'; +import { decodeParameters, encodeParameters } from './parameters_api.js'; /** * Encodes the function name to its ABI representation, which are the first 4 bytes of the sha3 of the function name including types. @@ -143,3 +143,142 @@ export const encodeFunctionCall = ( params ?? [], ).replace('0x', '')}`; }; + +/** + * Decodes a function call data using its `JSON interface` object. + * The JSON interface spec documentation https://docs.soliditylang.org/en/latest/abi-spec.html#json + * @param functionsAbi - The `JSON interface` object of the function. + * @param data - The data to decode + * @param methodSignatureProvided - (Optional) if `false` do not remove the first 4 bytes that would rather contain the function signature. + * @returns - The data decoded according to the passed ABI. + * @example + * ```ts + * const data = + * '0xa413686200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010416e6f74686572204772656574696e6700000000000000000000000000000000'; + * const params = decodeFunctionCall( + * { + * inputs: [ + * { internalType: 'string', name: '_greeting', type: 'string' }, + * { internalType: 'string', name: '_second_greeting', type: 'string' }, + * ], + * name: 'setGreeting', + * outputs: [ + * { internalType: 'bool', name: '', type: 'bool' }, + * { internalType: 'string', name: '', type: 'string' }, + * ], + * stateMutability: 'nonpayable', + * type: 'function', + * }, + * data, + * ); + + * console.log(params); + * > { + * > '0': 'Hello', + * > '1': 'Another Greeting', + * > __length__: 2, + * > __method__: 'setGreeting(string,string)', + * > _greeting: 'Hello', + * > _second_greeting: 'Another Greeting', + * > } + * ``` + */ +export const decodeFunctionCall = ( + functionsAbi: AbiFunctionFragment | AbiConstructorFragment, + data: HexString, + methodSignatureProvided = true, +): DecodedParams & { __method__: string } => { + const value = + methodSignatureProvided && data && data.length >= 10 && data.startsWith('0x') + ? data.slice(10) + : data; + if (!functionsAbi.inputs) { + throw new Web3ContractError('No inputs found in the ABI'); + } + const result = decodeParameters([...functionsAbi.inputs], value); + return { + ...result, + __method__: jsonInterfaceMethodToString(functionsAbi), + }; +}; + +/** + * Decodes a function call data using its `JSON interface` object. + * The JSON interface spec documentation https://docs.soliditylang.org/en/latest/abi-spec.html#json + * @returns - The ABI encoded function call, which, means the function signature and the parameters passed. + * @param functionsAbi - The `JSON interface` object of the function. + * @param returnValues - The data (the function-returned-values) to decoded + * @returns - The function-returned-values decoded according to the passed ABI. If there are multiple values, it returns them as an object as the example below. But if it is a single value, it returns it only for simplicity. + * @example + * ```ts + * // decode a multi-value data of a method + * const data = + * '0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + * const decodedResult = decodeFunctionReturn( + * { + * inputs: [ + * { internalType: 'string', name: '_greeting', type: 'string' } + * ], + * name: 'setGreeting', + * outputs: [ + * { internalType: 'string', name: '', type: 'string' }, + * { internalType: 'bool', name: '', type: 'bool' }, + * ], + * stateMutability: 'nonpayable', + * type: 'function', + * }, + * data, + * ); + + * console.log(decodedResult); + * > { '0': 'Hello', '1': true, __length__: 2 } + * + * + * // decode a single-value data of a method + * const data = + * '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + * const decodedResult = decodeFunctionReturn( + * { + * inputs: [ + * { internalType: 'string', name: '_greeting', type: 'string' } + * ], + * name: 'setGreeting', + * outputs: [{ internalType: 'string', name: '', type: 'string' }], + * stateMutability: 'nonpayable', + * type: 'function', + * }, + * data, + * ); + + * console.log(decodedResult); + * > 'Hello' + * ``` + */ +export const decodeFunctionReturn = ( + functionsAbi: AbiFunctionFragment, + returnValues?: HexString, +) => { + // If it is a constructor there is nothing to decode! + if (functionsAbi.type === 'constructor') { + return returnValues; + } + + if (!returnValues) { + // Using "null" value intentionally to match legacy behavior + // eslint-disable-next-line no-null/no-null + return null; + } + + const value = returnValues.length >= 2 ? returnValues.slice(2) : returnValues; + if (!functionsAbi.outputs) { + // eslint-disable-next-line no-null/no-null + return null; + } + const result = decodeParameters([...functionsAbi.outputs], value); + + if (result.__length__ === 1) { + return result[0]; + } + + return result; +}; diff --git a/packages/web3-eth-abi/test/unit/decodeMethodParamsAndReturn.test.ts b/packages/web3-eth-abi/test/unit/decodeMethodParamsAndReturn.test.ts new file mode 100644 index 00000000000..acf96c2aa41 --- /dev/null +++ b/packages/web3-eth-abi/test/unit/decodeMethodParamsAndReturn.test.ts @@ -0,0 +1,217 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { AbiFunctionFragment } from 'web3-types'; +import { decodeFunctionCall, decodeFunctionReturn } from '../../src'; + +describe('decodeFunctionCall and decodeFunctionReturn tests should pass', () => { + it('decodeFunctionCall should decode single-value data of a method', async () => { + const data = + '0xa41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + + const params = decodeFunctionCall( + { + inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }], + name: 'setGreeting', + outputs: [ + { internalType: 'bool', name: '', type: 'bool' }, + { internalType: 'string', name: '', type: 'string' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + data, + ); + + expect(params).toMatchObject({ + __method__: 'setGreeting(string)', + __length__: 1, + '0': 'Hello', + _greeting: 'Hello', + }); + }); + + it('decodeFunctionCall should decode data of a method without removing the method signature (if intended)', async () => { + const data = + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + + const params = decodeFunctionCall( + { + inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }], + name: 'setGreeting', + outputs: [ + { internalType: 'bool', name: '', type: 'bool' }, + { internalType: 'string', name: '', type: 'string' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + data, + false, + ); + + expect(params).toMatchObject({ + __method__: 'setGreeting(string)', + __length__: 1, + '0': 'Hello', + _greeting: 'Hello', + }); + }); + + it('decodeFunctionCall should throw if no inputs at the ABI', async () => { + const data = + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + + expect(() => + decodeFunctionCall( + { + name: 'setGreeting', + // no `inputs` provided! + outputs: [ + { internalType: 'bool', name: '', type: 'bool' }, + { internalType: 'string', name: '', type: 'string' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + data, + false, + ), + ).toThrow('No inputs found in the ABI'); + }); + + it('decodeFunctionCall should decode multi-value data of a method', async () => { + const data = + '0xa413686200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010416e6f74686572204772656574696e6700000000000000000000000000000000'; + + const params = decodeFunctionCall( + { + inputs: [ + { internalType: 'string', name: '_greeting', type: 'string' }, + { internalType: 'string', name: '_second_greeting', type: 'string' }, + ], + name: 'setGreeting', + outputs: [ + { internalType: 'bool', name: '', type: 'bool' }, + { internalType: 'string', name: '', type: 'string' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + data, + ); + + expect(params).toEqual({ + '0': 'Hello', + '1': 'Another Greeting', + __length__: 2, + __method__: 'setGreeting(string,string)', + _greeting: 'Hello', + _second_greeting: 'Another Greeting', + }); + }); + + it('decodeFunctionReturn should decode single-value data of a method', async () => { + const data = + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + + const decodedResult = decodeFunctionReturn( + { + inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }], + name: 'setGreeting', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'nonpayable', + type: 'function', + }, + data, + ); + + expect(decodedResult).toBe('Hello'); + }); + + it('decodeFunctionReturn should decode multi-value data of a method', async () => { + const data = + '0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + + const decodedResult = decodeFunctionReturn( + { + inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }], + name: 'setGreeting', + outputs: [ + { internalType: 'string', name: '', type: 'string' }, + { internalType: 'bool', name: '', type: 'bool' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + data, + ); + + expect(decodedResult).toEqual({ '0': 'Hello', '1': true, __length__: 2 }); + }); + + it('decodeFunctionReturn should decode nothing if it is called on a constructor', async () => { + const data = 'anything passed should be returned as-is'; + + const decodedResult = decodeFunctionReturn( + { + inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }], + stateMutability: 'nonpayable', + type: 'constructor', + } as unknown as AbiFunctionFragment, + data, + ); + + expect(decodedResult).toEqual(data); + }); + + it('decodeFunctionReturn should return `null` if no values passed', async () => { + const data = ''; + + const decodedResult = decodeFunctionReturn( + { + inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }], + name: 'setGreeting', + outputs: [ + { internalType: 'string', name: '', type: 'string' }, + { internalType: 'bool', name: '', type: 'bool' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + data, + ); + + expect(decodedResult).toBeNull(); + }); + + it('decodeFunctionReturn should return `null` if no function output provided', async () => { + const data = '0x000000'; + + const decodedResult = decodeFunctionReturn( + { + inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }], + name: 'setGreeting', + stateMutability: 'nonpayable', + type: 'function', + }, + data, + ); + + expect(decodedResult).toBeNull(); + }); +}); diff --git a/packages/web3-eth-contract/src/contract-deployer-method-class.ts b/packages/web3-eth-contract/src/contract-deployer-method-class.ts index 33018541390..08d04b16c8f 100644 --- a/packages/web3-eth-contract/src/contract-deployer-method-class.ts +++ b/packages/web3-eth-contract/src/contract-deployer-method-class.ts @@ -17,6 +17,7 @@ along with web3.js. If not, see . import { Web3ContractError } from 'web3-errors'; import { sendTransaction, SendTransactionEvents, SendTransactionOptions } from 'web3-eth'; +import { decodeFunctionCall } from 'web3-eth-abi'; import { AbiConstructorFragment, AbiFunctionFragment, @@ -34,7 +35,7 @@ import { import { format } from 'web3-utils'; import { isNullish } from 'web3-validator'; import { Web3PromiEvent } from 'web3-core'; -import { decodeMethodParams, encodeMethodABI } from './encoding.js'; +import { encodeMethodABI } from './encoding.js'; import { NonPayableTxOptions, PayableTxOptions } from './types.js'; import { getSendTxParams } from './utils.js'; // eslint-disable-next-line import/no-cycle @@ -209,7 +210,7 @@ export class DeployerMethodClass { public decodeData(data: HexString) { return { - ...decodeMethodParams( + ...decodeFunctionCall( this.constructorAbi, data.replace(this.deployData as string, ''), false, diff --git a/packages/web3-eth-contract/src/contract.ts b/packages/web3-eth-contract/src/contract.ts index a99dc27628e..60602497e65 100644 --- a/packages/web3-eth-contract/src/contract.ts +++ b/packages/web3-eth-contract/src/contract.ts @@ -42,6 +42,8 @@ import { TransactionMiddleware, } from 'web3-eth'; import { + decodeFunctionCall, + decodeFunctionReturn, encodeEventSignature, encodeFunctionSignature, decodeContractErrorData, @@ -99,12 +101,7 @@ import { ValidationSchemaInput, Web3ValidatorError, } from 'web3-validator'; -import { - decodeMethodReturn, - decodeMethodParams, - encodeEventABI, - encodeMethodABI, -} from './encoding.js'; +import { encodeEventABI, encodeMethodABI } from './encoding.js'; import { ContractLogsSubscription } from './contract_log_subscription.js'; import { ContractEventOptions, @@ -1026,7 +1023,7 @@ export class Contract `The ABI for the provided method signature ${methodSignature} was not found.`, ); } - return { ...decodeMethodParams(abi, data), __method__: jsonInterfaceMethodToString(abi) }; + return decodeFunctionCall(abi, data); } private _parseAndSetJsonInterface( @@ -1251,7 +1248,7 @@ export class Contract }), encodeABI: () => encodeMethodABI(methodAbi, abiParams), - decodeData: (data: HexString) => decodeMethodParams(methodAbi, data), + decodeData: (data: HexString) => decodeFunctionCall(methodAbi, data), createAccessList: async ( options?: PayableCallOptions | NonPayableCallOptions, @@ -1305,7 +1302,7 @@ export class Contract block, this.defaultReturnFormat as typeof DEFAULT_RETURN_FORMAT, ); - return decodeMethodReturn(abi, result); + return decodeFunctionReturn(abi, result); } catch (error: unknown) { if (error instanceof ContractExecutionError) { // this will parse the error data by trying to decode the ABI error inputs according to EIP-838 diff --git a/packages/web3-eth-contract/src/encoding.ts b/packages/web3-eth-contract/src/encoding.ts index 48531277518..2b347269d36 100644 --- a/packages/web3-eth-contract/src/encoding.ts +++ b/packages/web3-eth-contract/src/encoding.ts @@ -30,7 +30,8 @@ import { } from 'web3-types'; import { - decodeParameters, + decodeFunctionCall, + decodeFunctionReturn, encodeEventSignature, encodeFunctionSignature, encodeParameter, @@ -153,44 +154,7 @@ export const encodeMethodABI = ( return `${encodeFunctionSignature(abi)}${params}`; }; -export const decodeMethodParams = ( - abi: AbiFunctionFragment | AbiConstructorFragment, - data: HexString, - methodSignatureProvided = true, -) => { - const value = - methodSignatureProvided && data && data.length >= 10 && data.startsWith('0x') - ? data.slice(10) - : data; - if (!abi.inputs) { - throw new Web3ContractError('No inputs found in the ABI'); - } - const result = decodeParameters([...abi.inputs], value); - return result; -}; - -export const decodeMethodReturn = (abi: AbiFunctionFragment, returnValues?: HexString) => { - // If it was constructor then we need to return contract address - if (abi.type === 'constructor') { - return returnValues; - } - - if (!returnValues) { - // Using "null" value intentionally to match legacy behavior - // eslint-disable-next-line no-null/no-null - return null; - } - - const value = returnValues.length >= 2 ? returnValues.slice(2) : returnValues; - if (!abi.outputs) { - // eslint-disable-next-line no-null/no-null - return null; - } - const result = decodeParameters([...abi.outputs], value); - - if (result.__length__ === 1) { - return result[0]; - } - - return result; -}; +/** @deprecated import `decodeFunctionCall` from ''web3-eth-abi' instead. */ +export const decodeMethodParams = decodeFunctionCall; +/** @deprecated import `decodeFunctionReturn` from ''web3-eth-abi' instead. */ +export const decodeMethodReturn = decodeFunctionReturn;