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;