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

Function input decoder #991

Merged
merged 7 commits into from
Aug 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/contracts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -762,3 +762,26 @@ For example:
>>> rich_logs = contract.events.myEvent().processReceipt(tx_receipt)
>>> rich_logs[0]['args']
{'myArg': 12345}

Utils
-----

.. py:classmethod:: Contract.decode_function_input(data)

Decodes the transaction data used to invoke a smart contract function, and returns
:py:class:`ContractFunction` and decoded parameters as :py:class:`dict`.

.. code-block:: python

>>> transaction = w3.eth.getTransaction('0x5798fbc45e3b63832abc4984b0f3574a13545f415dd672cd8540cd71f735db56')
>>> transaction.input
'0x612e45a3000000000000000000000000b656b2a9c3b2416437a811e07466ca712f5a5b5a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000093a80000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000116c6f6e656c792c20736f206c6f6e656c7900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
>>> contract.decode_function_input(transaction.input)
(<Function newProposal(address,uint256,string,bytes,uint256,bool)>,
{'_recipient': '0xb656b2a9c3b2416437a811e07466ca712f5a5b5a',
'_amount': 0,
'_description': b'lonely, so lonely',
'_transactionData': b'',
'_debatingPeriod': 604800,
'_newCurator': True})

105 changes: 105 additions & 0 deletions tests/core/contracts/test_contract_method_abi_decoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from binascii import (
unhexlify,
)
import json
import pytest

ABI_A = json.loads('[{"constant":false,"inputs":[],"name":"noargfunc","outputs":[],"type":"function"}]') # noqa: E501
ABI_B = json.loads('[{"constant":false,"inputs":[{"name":"uintarg","type":"uint256"}],"name":"uintfunc","outputs":[],"type":"function"}]') # noqa: E501
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
ABI_D = json.loads('[{ "constant": false, "inputs": [ { "name": "b", "type": "bytes32[]" } ], "name": "byte_array", "outputs": [], "payable": false, "type": "function" }]') # noqa: E501
ABI_BYTES = json.loads('[{"constant":false,"inputs":[{"name":"bytesarg","type":"bytes"}],"name":"bytesfunc","outputs":[],"type":"function"}]') # noqa: E501
ABI_STRING = json.loads('[{"constant":false,"inputs":[{"name":"stringarg","type":"string"}],"name":"stringfunc","outputs":[],"type":"function"}]') # noqa: E501
ABI_ADDRESS = json.loads('[{"constant":false,"inputs":[{"name":"addressarg","type":"address"}],"name":"addressfunc","outputs":[],"type":"function"}]') # noqa: E501
a32bytes = b'a'.ljust(32, b'\x00')


@pytest.mark.parametrize(
'abi,data,method,expected',
(
(
ABI_A,
'0xc4c1a40b',
'noargfunc',
{},
),
(
ABI_B,
'0xcc6820de0000000000000000000000000000000000000000000000000000000000000001',
'uintfunc',
{'uintarg': 1},
),
(
ABI_C,
'0x22d86fa3',
'namesakefunc',
{},
),
(
ABI_C,
'0x40c05b2f0000000000000000000000000000000000000000000000000000000000000001',
'namesakefunc',
{'uintarg': 1},
),
(
ABI_C,
'0xf931d77c6100000000000000000000000000000000000000000000000000000000000000',
'namesakefunc',
{'bytesarg': a32bytes},
),
(
ABI_BYTES,
'0xb606a9f6000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000016100000000000000000000000000000000000000000000000000000000000000', # noqa: E501
'bytesfunc',
{'bytesarg': b'a'},
),
(
ABI_STRING,
'0x33b4005f000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000016100000000000000000000000000000000000000000000000000000000000000', # noqa: E501
'stringfunc',
{'stringarg': 'a'},
),
(
ABI_ADDRESS,
'0x4767be6c000000000000000000000000ffffffffffffffffffffffffffffffffffffffff',
'addressfunc',
{'addressarg': '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF'},
),
),
)
def test_contract_abi_decoding(web3, abi, data, method, expected):
contract = web3.eth.contract(abi=abi)
func, params = contract.decode_function_input(data)
assert func.fn_name == method
assert params == expected

reinvoke_func = contract.functions[func.fn_name](**params)
rebuild_txn = reinvoke_func.buildTransaction({'gas': 0, 'nonce': 0, 'to': '\x00' * 20})
assert rebuild_txn['data'] == data


@pytest.mark.parametrize(
'abi,method,expected,data',
(
(
ABI_D,
'byte_array',
{
'b': [
unhexlify('5595c210956e7721f9b692e702708556aa9aabb14ea163e96afa56ffbe9fa809'),
unhexlify('6f8d2fa18448afbfe4f82143c384484ad09a0271f3a3c0eb9f629e703f883125'),
],
},
'0xf166d6f8000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025595c210956e7721f9b692e702708556aa9aabb14ea163e96afa56ffbe9fa8096f8d2fa18448afbfe4f82143c384484ad09a0271f3a3c0eb9f629e703f883125', # noqa: E501
),
),
)
def test_contract_abi_encoding_kwargs(web3, abi, method, expected, data):
contract = web3.eth.contract(abi=abi)
func, params = contract.decode_function_input(data)
assert func.fn_name == method
assert params == expected

reinvoke_func = contract.functions[func.fn_name](**params)
rebuild_txn = reinvoke_func.buildTransaction({'gas': 0, 'nonce': 0, 'to': '\x00' * 20})
assert rebuild_txn['data'] == data
16 changes: 15 additions & 1 deletion web3/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
is_text,
to_tuple,
)
from hexbytes import (
HexBytes,
)

from web3.exceptions import (
BadFunctionCallOutput,
Expand Down Expand Up @@ -688,6 +691,17 @@ def callable_check(fn_abi):
fns = find_functions_by_identifier(self.abi, self.web3, self.address, callable_check)
return get_function_by_identifier(fns, 'selector')

@combomethod
def decode_function_input(self, data):
data = HexBytes(data)
selector, params = data[:4], data[4:]
func = self.get_function_by_selector(selector)
names = [x['name'] for x in func.abi['inputs']]
types = [x['type'] for x in func.abi['inputs']]
decoded = decode_abi(types, params)
normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded)
return func, dict(zip(names, normalized))

@combomethod
def find_functions_by_args(self, *args):
def callable_check(fn_abi):
Expand Down Expand Up @@ -1194,7 +1208,7 @@ def buildTransaction(self, transaction=None):

if not self.address and 'to' not in built_transaction:
raise ValueError(
"When using `ContractFunction.buildTransaction` from a Contract factory"
"When using `ContractFunction.buildTransaction` from a contract factory "
"you must provide a `to` address with the transaction"
)
if self.address and 'to' in built_transaction:
Expand Down