Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit 3687070

Browse files
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
1 parent 331aa9c commit 3687070

File tree

6 files changed

+378
-56
lines changed

6 files changed

+378
-56
lines changed

packages/web3-eth-abi/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,7 @@ Documentation:
195195
- `decodeLog` , `decodeParametersWith` , `decodeParameters` and `decodeParameters` now accepts first immutable param as well (#7288)
196196

197197
## [Unreleased]
198+
199+
### Added
200+
201+
- added `decodeFunctionCall` and `decodeFunctionReturn`. (#7345)

packages/web3-eth-abi/src/api/functions_api.ts

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
1919
*
2020
* @module ABI
2121
*/
22-
import { AbiError } from 'web3-errors';
22+
import { AbiError, Web3ContractError } from 'web3-errors';
2323
import { sha3Raw } from 'web3-utils';
24-
import { AbiFunctionFragment } from 'web3-types';
24+
import { AbiConstructorFragment, AbiFunctionFragment, DecodedParams, HexString } from 'web3-types';
2525
import { isAbiFunctionFragment, jsonInterfaceMethodToString } from '../utils.js';
26-
import { encodeParameters } from './parameters_api.js';
26+
import { decodeParameters, encodeParameters } from './parameters_api.js';
2727

2828
/**
2929
* 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 = (
143143
params ?? [],
144144
).replace('0x', '')}`;
145145
};
146+
147+
/**
148+
* Decodes a function call data using its `JSON interface` object.
149+
* The JSON interface spec documentation https://docs.soliditylang.org/en/latest/abi-spec.html#json
150+
* @param functionsAbi - The `JSON interface` object of the function.
151+
* @param data - The data to decode
152+
* @param methodSignatureProvided - (Optional) if `false` do not remove the first 4 bytes that would rather contain the function signature.
153+
* @returns - The data decoded according to the passed ABI.
154+
* @example
155+
* ```ts
156+
* const data =
157+
* '0xa413686200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010416e6f74686572204772656574696e6700000000000000000000000000000000';
158+
* const params = decodeFunctionCall(
159+
* {
160+
* inputs: [
161+
* { internalType: 'string', name: '_greeting', type: 'string' },
162+
* { internalType: 'string', name: '_second_greeting', type: 'string' },
163+
* ],
164+
* name: 'setGreeting',
165+
* outputs: [
166+
* { internalType: 'bool', name: '', type: 'bool' },
167+
* { internalType: 'string', name: '', type: 'string' },
168+
* ],
169+
* stateMutability: 'nonpayable',
170+
* type: 'function',
171+
* },
172+
* data,
173+
* );
174+
175+
* console.log(params);
176+
* > {
177+
* > '0': 'Hello',
178+
* > '1': 'Another Greeting',
179+
* > __length__: 2,
180+
* > __method__: 'setGreeting(string,string)',
181+
* > _greeting: 'Hello',
182+
* > _second_greeting: 'Another Greeting',
183+
* > }
184+
* ```
185+
*/
186+
export const decodeFunctionCall = (
187+
functionsAbi: AbiFunctionFragment | AbiConstructorFragment,
188+
data: HexString,
189+
methodSignatureProvided = true,
190+
): DecodedParams & { __method__: string } => {
191+
const value =
192+
methodSignatureProvided && data && data.length >= 10 && data.startsWith('0x')
193+
? data.slice(10)
194+
: data;
195+
if (!functionsAbi.inputs) {
196+
throw new Web3ContractError('No inputs found in the ABI');
197+
}
198+
const result = decodeParameters([...functionsAbi.inputs], value);
199+
return {
200+
...result,
201+
__method__: jsonInterfaceMethodToString(functionsAbi),
202+
};
203+
};
204+
205+
/**
206+
* Decodes a function call data using its `JSON interface` object.
207+
* The JSON interface spec documentation https://docs.soliditylang.org/en/latest/abi-spec.html#json
208+
* @returns - The ABI encoded function call, which, means the function signature and the parameters passed.
209+
* @param functionsAbi - The `JSON interface` object of the function.
210+
* @param returnValues - The data (the function-returned-values) to decoded
211+
* @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.
212+
* @example
213+
* ```ts
214+
* // decode a multi-value data of a method
215+
* const data =
216+
* '0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000';
217+
* const decodedResult = decodeFunctionReturn(
218+
* {
219+
* inputs: [
220+
* { internalType: 'string', name: '_greeting', type: 'string' }
221+
* ],
222+
* name: 'setGreeting',
223+
* outputs: [
224+
* { internalType: 'string', name: '', type: 'string' },
225+
* { internalType: 'bool', name: '', type: 'bool' },
226+
* ],
227+
* stateMutability: 'nonpayable',
228+
* type: 'function',
229+
* },
230+
* data,
231+
* );
232+
233+
* console.log(decodedResult);
234+
* > { '0': 'Hello', '1': true, __length__: 2 }
235+
*
236+
*
237+
* // decode a single-value data of a method
238+
* const data =
239+
* '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000';
240+
* const decodedResult = decodeFunctionReturn(
241+
* {
242+
* inputs: [
243+
* { internalType: 'string', name: '_greeting', type: 'string' }
244+
* ],
245+
* name: 'setGreeting',
246+
* outputs: [{ internalType: 'string', name: '', type: 'string' }],
247+
* stateMutability: 'nonpayable',
248+
* type: 'function',
249+
* },
250+
* data,
251+
* );
252+
253+
* console.log(decodedResult);
254+
* > 'Hello'
255+
* ```
256+
*/
257+
export const decodeFunctionReturn = (
258+
functionsAbi: AbiFunctionFragment,
259+
returnValues?: HexString,
260+
) => {
261+
// If it is a constructor there is nothing to decode!
262+
if (functionsAbi.type === 'constructor') {
263+
return returnValues;
264+
}
265+
266+
if (!returnValues) {
267+
// Using "null" value intentionally to match legacy behavior
268+
// eslint-disable-next-line no-null/no-null
269+
return null;
270+
}
271+
272+
const value = returnValues.length >= 2 ? returnValues.slice(2) : returnValues;
273+
if (!functionsAbi.outputs) {
274+
// eslint-disable-next-line no-null/no-null
275+
return null;
276+
}
277+
const result = decodeParameters([...functionsAbi.outputs], value);
278+
279+
if (result.__length__ === 1) {
280+
return result[0];
281+
}
282+
283+
return result;
284+
};
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
This file is part of web3.js.
3+
4+
web3.js is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU Lesser General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
web3.js is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU Lesser General Public License for more details.
13+
14+
You should have received a copy of the GNU Lesser General Public License
15+
along with web3.js. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
import { AbiFunctionFragment } from 'web3-types';
19+
import { decodeFunctionCall, decodeFunctionReturn } from '../../src';
20+
21+
describe('decodeFunctionCall and decodeFunctionReturn tests should pass', () => {
22+
it('decodeFunctionCall should decode single-value data of a method', async () => {
23+
const data =
24+
'0xa41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000';
25+
26+
const params = decodeFunctionCall(
27+
{
28+
inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }],
29+
name: 'setGreeting',
30+
outputs: [
31+
{ internalType: 'bool', name: '', type: 'bool' },
32+
{ internalType: 'string', name: '', type: 'string' },
33+
],
34+
stateMutability: 'nonpayable',
35+
type: 'function',
36+
},
37+
data,
38+
);
39+
40+
expect(params).toMatchObject({
41+
__method__: 'setGreeting(string)',
42+
__length__: 1,
43+
'0': 'Hello',
44+
_greeting: 'Hello',
45+
});
46+
});
47+
48+
it('decodeFunctionCall should decode data of a method without removing the method signature (if intended)', async () => {
49+
const data =
50+
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000';
51+
52+
const params = decodeFunctionCall(
53+
{
54+
inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }],
55+
name: 'setGreeting',
56+
outputs: [
57+
{ internalType: 'bool', name: '', type: 'bool' },
58+
{ internalType: 'string', name: '', type: 'string' },
59+
],
60+
stateMutability: 'nonpayable',
61+
type: 'function',
62+
},
63+
data,
64+
false,
65+
);
66+
67+
expect(params).toMatchObject({
68+
__method__: 'setGreeting(string)',
69+
__length__: 1,
70+
'0': 'Hello',
71+
_greeting: 'Hello',
72+
});
73+
});
74+
75+
it('decodeFunctionCall should throw if no inputs at the ABI', async () => {
76+
const data =
77+
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000';
78+
79+
expect(() =>
80+
decodeFunctionCall(
81+
{
82+
name: 'setGreeting',
83+
// no `inputs` provided!
84+
outputs: [
85+
{ internalType: 'bool', name: '', type: 'bool' },
86+
{ internalType: 'string', name: '', type: 'string' },
87+
],
88+
stateMutability: 'nonpayable',
89+
type: 'function',
90+
},
91+
data,
92+
false,
93+
),
94+
).toThrow('No inputs found in the ABI');
95+
});
96+
97+
it('decodeFunctionCall should decode multi-value data of a method', async () => {
98+
const data =
99+
'0xa413686200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010416e6f74686572204772656574696e6700000000000000000000000000000000';
100+
101+
const params = decodeFunctionCall(
102+
{
103+
inputs: [
104+
{ internalType: 'string', name: '_greeting', type: 'string' },
105+
{ internalType: 'string', name: '_second_greeting', type: 'string' },
106+
],
107+
name: 'setGreeting',
108+
outputs: [
109+
{ internalType: 'bool', name: '', type: 'bool' },
110+
{ internalType: 'string', name: '', type: 'string' },
111+
],
112+
stateMutability: 'nonpayable',
113+
type: 'function',
114+
},
115+
data,
116+
);
117+
118+
expect(params).toEqual({
119+
'0': 'Hello',
120+
'1': 'Another Greeting',
121+
__length__: 2,
122+
__method__: 'setGreeting(string,string)',
123+
_greeting: 'Hello',
124+
_second_greeting: 'Another Greeting',
125+
});
126+
});
127+
128+
it('decodeFunctionReturn should decode single-value data of a method', async () => {
129+
const data =
130+
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000';
131+
132+
const decodedResult = decodeFunctionReturn(
133+
{
134+
inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }],
135+
name: 'setGreeting',
136+
outputs: [{ internalType: 'string', name: '', type: 'string' }],
137+
stateMutability: 'nonpayable',
138+
type: 'function',
139+
},
140+
data,
141+
);
142+
143+
expect(decodedResult).toBe('Hello');
144+
});
145+
146+
it('decodeFunctionReturn should decode multi-value data of a method', async () => {
147+
const data =
148+
'0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000';
149+
150+
const decodedResult = decodeFunctionReturn(
151+
{
152+
inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }],
153+
name: 'setGreeting',
154+
outputs: [
155+
{ internalType: 'string', name: '', type: 'string' },
156+
{ internalType: 'bool', name: '', type: 'bool' },
157+
],
158+
stateMutability: 'nonpayable',
159+
type: 'function',
160+
},
161+
data,
162+
);
163+
164+
expect(decodedResult).toEqual({ '0': 'Hello', '1': true, __length__: 2 });
165+
});
166+
167+
it('decodeFunctionReturn should decode nothing if it is called on a constructor', async () => {
168+
const data = 'anything passed should be returned as-is';
169+
170+
const decodedResult = decodeFunctionReturn(
171+
{
172+
inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }],
173+
stateMutability: 'nonpayable',
174+
type: 'constructor',
175+
} as unknown as AbiFunctionFragment,
176+
data,
177+
);
178+
179+
expect(decodedResult).toEqual(data);
180+
});
181+
182+
it('decodeFunctionReturn should return `null` if no values passed', async () => {
183+
const data = '';
184+
185+
const decodedResult = decodeFunctionReturn(
186+
{
187+
inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }],
188+
name: 'setGreeting',
189+
outputs: [
190+
{ internalType: 'string', name: '', type: 'string' },
191+
{ internalType: 'bool', name: '', type: 'bool' },
192+
],
193+
stateMutability: 'nonpayable',
194+
type: 'function',
195+
},
196+
data,
197+
);
198+
199+
expect(decodedResult).toBeNull();
200+
});
201+
202+
it('decodeFunctionReturn should return `null` if no function output provided', async () => {
203+
const data = '0x000000';
204+
205+
const decodedResult = decodeFunctionReturn(
206+
{
207+
inputs: [{ internalType: 'string', name: '_greeting', type: 'string' }],
208+
name: 'setGreeting',
209+
stateMutability: 'nonpayable',
210+
type: 'function',
211+
},
212+
data,
213+
);
214+
215+
expect(decodedResult).toBeNull();
216+
});
217+
});

0 commit comments

Comments
 (0)