Skip to content

Commit 3a924fc

Browse files
authored
Merge pull request #991 from banteg/function-decoder
Function input decoder
2 parents 53f2a9d + 2cf7c34 commit 3a924fc

File tree

3 files changed

+143
-1
lines changed

3 files changed

+143
-1
lines changed

docs/contracts.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,3 +762,26 @@ For example:
762762
>>> rich_logs = contract.events.myEvent().processReceipt(tx_receipt)
763763
>>> rich_logs[0]['args']
764764
{'myArg': 12345}
765+
766+
Utils
767+
-----
768+
769+
.. py:classmethod:: Contract.decode_function_input(data)
770+
771+
Decodes the transaction data used to invoke a smart contract function, and returns
772+
:py:class:`ContractFunction` and decoded parameters as :py:class:`dict`.
773+
774+
.. code-block:: python
775+
776+
>>> transaction = w3.eth.getTransaction('0x5798fbc45e3b63832abc4984b0f3574a13545f415dd672cd8540cd71f735db56')
777+
>>> transaction.input
778+
'0x612e45a3000000000000000000000000b656b2a9c3b2416437a811e07466ca712f5a5b5a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000093a80000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000116c6f6e656c792c20736f206c6f6e656c7900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
779+
>>> contract.decode_function_input(transaction.input)
780+
(<Function newProposal(address,uint256,string,bytes,uint256,bool)>,
781+
{'_recipient': '0xb656b2a9c3b2416437a811e07466ca712f5a5b5a',
782+
'_amount': 0,
783+
'_description': b'lonely, so lonely',
784+
'_transactionData': b'',
785+
'_debatingPeriod': 604800,
786+
'_newCurator': True})
787+
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from binascii import (
2+
unhexlify,
3+
)
4+
import json
5+
import pytest
6+
7+
ABI_A = json.loads('[{"constant":false,"inputs":[],"name":"noargfunc","outputs":[],"type":"function"}]') # noqa: E501
8+
ABI_B = json.loads('[{"constant":false,"inputs":[{"name":"uintarg","type":"uint256"}],"name":"uintfunc","outputs":[],"type":"function"}]') # noqa: E501
9+
ABI_C = json.loads('[{"constant":false,"inputs":[],"name":"namesakefunc","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"bytesarg","type":"bytes32"}],"name":"namesakefunc","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"uintarg","type":"uint256"}],"name":"namesakefunc","outputs":[],"type":"function"}]') # noqa: E501
10+
ABI_D = json.loads('[{ "constant": false, "inputs": [ { "name": "b", "type": "bytes32[]" } ], "name": "byte_array", "outputs": [], "payable": false, "type": "function" }]') # noqa: E501
11+
ABI_BYTES = json.loads('[{"constant":false,"inputs":[{"name":"bytesarg","type":"bytes"}],"name":"bytesfunc","outputs":[],"type":"function"}]') # noqa: E501
12+
ABI_STRING = json.loads('[{"constant":false,"inputs":[{"name":"stringarg","type":"string"}],"name":"stringfunc","outputs":[],"type":"function"}]') # noqa: E501
13+
ABI_ADDRESS = json.loads('[{"constant":false,"inputs":[{"name":"addressarg","type":"address"}],"name":"addressfunc","outputs":[],"type":"function"}]') # noqa: E501
14+
a32bytes = b'a'.ljust(32, b'\x00')
15+
16+
17+
@pytest.mark.parametrize(
18+
'abi,data,method,expected',
19+
(
20+
(
21+
ABI_A,
22+
'0xc4c1a40b',
23+
'noargfunc',
24+
{},
25+
),
26+
(
27+
ABI_B,
28+
'0xcc6820de0000000000000000000000000000000000000000000000000000000000000001',
29+
'uintfunc',
30+
{'uintarg': 1},
31+
),
32+
(
33+
ABI_C,
34+
'0x22d86fa3',
35+
'namesakefunc',
36+
{},
37+
),
38+
(
39+
ABI_C,
40+
'0x40c05b2f0000000000000000000000000000000000000000000000000000000000000001',
41+
'namesakefunc',
42+
{'uintarg': 1},
43+
),
44+
(
45+
ABI_C,
46+
'0xf931d77c6100000000000000000000000000000000000000000000000000000000000000',
47+
'namesakefunc',
48+
{'bytesarg': a32bytes},
49+
),
50+
(
51+
ABI_BYTES,
52+
'0xb606a9f6000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000016100000000000000000000000000000000000000000000000000000000000000', # noqa: E501
53+
'bytesfunc',
54+
{'bytesarg': b'a'},
55+
),
56+
(
57+
ABI_STRING,
58+
'0x33b4005f000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000016100000000000000000000000000000000000000000000000000000000000000', # noqa: E501
59+
'stringfunc',
60+
{'stringarg': 'a'},
61+
),
62+
(
63+
ABI_ADDRESS,
64+
'0x4767be6c000000000000000000000000ffffffffffffffffffffffffffffffffffffffff',
65+
'addressfunc',
66+
{'addressarg': '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF'},
67+
),
68+
),
69+
)
70+
def test_contract_abi_decoding(web3, abi, data, method, expected):
71+
contract = web3.eth.contract(abi=abi)
72+
func, params = contract.decode_function_input(data)
73+
assert func.fn_name == method
74+
assert params == expected
75+
76+
reinvoke_func = contract.functions[func.fn_name](**params)
77+
rebuild_txn = reinvoke_func.buildTransaction({'gas': 0, 'nonce': 0, 'to': '\x00' * 20})
78+
assert rebuild_txn['data'] == data
79+
80+
81+
@pytest.mark.parametrize(
82+
'abi,method,expected,data',
83+
(
84+
(
85+
ABI_D,
86+
'byte_array',
87+
{
88+
'b': [
89+
unhexlify('5595c210956e7721f9b692e702708556aa9aabb14ea163e96afa56ffbe9fa809'),
90+
unhexlify('6f8d2fa18448afbfe4f82143c384484ad09a0271f3a3c0eb9f629e703f883125'),
91+
],
92+
},
93+
'0xf166d6f8000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025595c210956e7721f9b692e702708556aa9aabb14ea163e96afa56ffbe9fa8096f8d2fa18448afbfe4f82143c384484ad09a0271f3a3c0eb9f629e703f883125', # noqa: E501
94+
),
95+
),
96+
)
97+
def test_contract_abi_encoding_kwargs(web3, abi, method, expected, data):
98+
contract = web3.eth.contract(abi=abi)
99+
func, params = contract.decode_function_input(data)
100+
assert func.fn_name == method
101+
assert params == expected
102+
103+
reinvoke_func = contract.functions[func.fn_name](**params)
104+
rebuild_txn = reinvoke_func.buildTransaction({'gas': 0, 'nonce': 0, 'to': '\x00' * 20})
105+
assert rebuild_txn['data'] == data

web3/contract.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
is_text,
1919
to_tuple,
2020
)
21+
from hexbytes import (
22+
HexBytes,
23+
)
2124

2225
from web3.exceptions import (
2326
BadFunctionCallOutput,
@@ -688,6 +691,17 @@ def callable_check(fn_abi):
688691
fns = find_functions_by_identifier(self.abi, self.web3, self.address, callable_check)
689692
return get_function_by_identifier(fns, 'selector')
690693

694+
@combomethod
695+
def decode_function_input(self, data):
696+
data = HexBytes(data)
697+
selector, params = data[:4], data[4:]
698+
func = self.get_function_by_selector(selector)
699+
names = [x['name'] for x in func.abi['inputs']]
700+
types = [x['type'] for x in func.abi['inputs']]
701+
decoded = decode_abi(types, params)
702+
normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded)
703+
return func, dict(zip(names, normalized))
704+
691705
@combomethod
692706
def find_functions_by_args(self, *args):
693707
def callable_check(fn_abi):
@@ -1194,7 +1208,7 @@ def buildTransaction(self, transaction=None):
11941208

11951209
if not self.address and 'to' not in built_transaction:
11961210
raise ValueError(
1197-
"When using `ContractFunction.buildTransaction` from a Contract factory"
1211+
"When using `ContractFunction.buildTransaction` from a contract factory "
11981212
"you must provide a `to` address with the transaction"
11991213
)
12001214
if self.address and 'to' in built_transaction:

0 commit comments

Comments
 (0)