Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make decodeFunctionCall and decodeFunctionReturn available at web3-eth-abi #7345

Merged
merged 9 commits into from
Oct 22, 2024
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.
Muhammad-Altabba marked this conversation as resolved.
Show resolved Hide resolved
* @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;
Muhammad-Altabba marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading