Skip to content

Commit

Permalink
make decodeFunctionCall and decodeFunctionReturn available at `we…
Browse files Browse the repository at this point in the history
…b3-eth-abi` (#7345)

* move `decodeFunctionCall` and `decodeFunctionReturn` to web3-eth-abi

* add unit tests

* update CHANGELOG.md

* add functions docs
  • Loading branch information
Muhammad-Altabba authored Oct 22, 2024
1 parent 331aa9c commit 3687070
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 56 deletions.
4 changes: 4 additions & 0 deletions packages/web3-eth-abi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
145 changes: 142 additions & 3 deletions packages/web3-eth-abi/src/api/functions_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*
* @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.
Expand Down Expand Up @@ -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;
};
217 changes: 217 additions & 0 deletions packages/web3-eth-abi/test/unit/decodeMethodParamsAndReturn.test.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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();
});
});
Loading

1 comment on commit 3687070

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: 3687070 Previous: 331aa9c Ratio
processingTx 22856 ops/sec (±6.62%) 20042 ops/sec (±7.50%) 0.88
processingContractDeploy 37573 ops/sec (±8.57%) 38574 ops/sec (±5.63%) 1.03
processingContractMethodSend 15453 ops/sec (±7.92%) 14751 ops/sec (±9.38%) 0.95
processingContractMethodCall 27091 ops/sec (±7.83%) 28271 ops/sec (±6.08%) 1.04
abiEncode 42669 ops/sec (±7.08%) 41366 ops/sec (±7.53%) 0.97
abiDecode 30350 ops/sec (±6.23%) 28375 ops/sec (±8.41%) 0.93
sign 1520 ops/sec (±3.42%) 1500 ops/sec (±1.06%) 0.99
verify 362 ops/sec (±0.69%) 356 ops/sec (±0.80%) 0.98

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.