diff --git a/tests/contracts/conftest.py b/tests/contracts/conftest.py index a764a70..d32ee60 100644 --- a/tests/contracts/conftest.py +++ b/tests/contracts/conftest.py @@ -282,3 +282,104 @@ def StringContract(web3_tester, string_contract_abi_def): code_runtime=string_contract_abi_def["code_runtime"], source=string_contract_abi_def["source"], ) + + +CONTRACT_EMITTER_SOURCE = textwrap.dedent((""" +contract Emitter { + event LogAnonymous() anonymous; + event LogNoArguments(); + event LogSingleArg(uint arg0); + event LogDoubleArg(uint arg0, uint arg1); + event LogTripleArg(uint arg0, uint arg1, uint arg2); + event LogQuadrupleArg(uint arg0, uint arg1, uint arg2, uint arg3); + + // Indexed + event LogSingleWithIndex(uint indexed arg0); + event LogDoubleWithIndex(uint arg0, uint indexed arg1); + event LogTripleWithIndex(uint arg0, uint indexed arg1, uint indexed arg2); + event LogQuadrupleWithIndex(uint arg0, uint arg1, uint indexed arg2, uint indexed arg3); + + enum WhichEvent { + LogAnonymous, + LogNoArguments, + LogSingleArg, + LogDoubleArg, + LogTripleArg, + LogQuadrupleArg, + LogSingleWithIndex, + LogDoubleWithIndex, + LogTripleWithIndex, + LogQuadrupleWithIndex + } + + function logNoArgs(WhichEvent which) public { + if (which == WhichEvent.LogNoArguments) LogNoArguments(); + else if (which == WhichEvent.LogAnonymous) LogAnonymous(); + else throw; + } + + function logSingle(WhichEvent which, uint arg0) public { + if (which == WhichEvent.LogSingleArg) LogSingleArg(arg0); + else if (which == WhichEvent.LogSingleWithIndex) LogSingleWithIndex(arg0); + else throw; + } + + function logDouble(WhichEvent which, uint arg0, uint arg1) public { + if (which == WhichEvent.LogDoubleArg) LogDoubleArg(arg0, arg1); + else if (which == WhichEvent.LogDoubleWithIndex) LogDoubleWithIndex(arg0, arg1); + else throw; + } + + function logTriple(WhichEvent which, uint arg0, uint arg1, uint arg2) public { + if (which == WhichEvent.LogTripleArg) LogTripleArg(arg0, arg1, arg2); + else if (which == WhichEvent.LogTripleWithIndex) LogTripleWithIndex(arg0, arg1, arg2); + else throw; + } + + function logQuadruple(WhichEvent which, uint arg0, uint arg1, uint arg2, uint arg3) public { + if (which == WhichEvent.LogQuadrupleArg) LogQuadrupleArg(arg0, arg1, arg2, arg3); + else if (which == WhichEvent.LogQuadrupleWithIndex) LogQuadrupleWithIndex(arg0, arg1, arg2, arg3); + else throw; + } +} +""")) + +CONTRACT_EMITTER_CODE = "0x60606040526102c0806100126000396000f3606060405260e060020a600035046317c0c180811461004757806320f0256e1461008057806390b41d8b146100da5780639c37705314610125578063aa6fd82214610177575b005b61004560043560018114156101b9577f1e86022f78f8d04f8e3dfd13a2bdb280403e6632877c0dbee5e4eeb259908a5c60006060a15b50565b61004560043560243560443560643560843560058514156101d1576060848152608084815260a084905260c08390527ff039d147f23fe975a4254bdf6b1502b8c79132ae1833986b7ccef2638e73fdf991a15b5050505050565b610045600435602435604435600383141561021357606082815260808290527fdf0cb1dea99afceb3ea698d62e705b736f1345a7eee9eb07e63d1f8f556c1bc590604090a15b505050565b610045600435602435604435606435600484141561024e576060838152608083905260a08290527f4a25b279c7c585f25eda9788ac9420ebadae78ca6b206a0e6ab488fd81f550629080a15b50505050565b610045600435602435600282141561028b5760608181527f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d490602090a15b5050565b60008114156101cc5760006060a061007d565b610002565b60098514156101cc5760608481526080849052819083907fa30ece802b64cd2b7e57dabf4010aabf5df26d1556977affb07b98a77ad955b590604090a36100d3565b60078314156101cc57606082815281907f057bc32826fbe161da1c110afcdcae7c109a8b69149f727fc37a603c60ef94ca90602090a2610120565b60088414156101cc576060838152819083907ff16c999b533366ca5138d78e85da51611089cd05749f098d6c225d4cd42ee6ec90602090a3610171565b60068214156101cc57807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560006060a26101b556" + +CONTRACT_EMITTER_RUNTIME = "0x606060405260e060020a600035046317c0c180811461004757806320f0256e1461008057806390b41d8b146100da5780639c37705314610125578063aa6fd82214610177575b005b61004560043560018114156101b9577f1e86022f78f8d04f8e3dfd13a2bdb280403e6632877c0dbee5e4eeb259908a5c60006060a15b50565b61004560043560243560443560643560843560058514156101d1576060848152608084815260a084905260c08390527ff039d147f23fe975a4254bdf6b1502b8c79132ae1833986b7ccef2638e73fdf991a15b5050505050565b610045600435602435604435600383141561021357606082815260808290527fdf0cb1dea99afceb3ea698d62e705b736f1345a7eee9eb07e63d1f8f556c1bc590604090a15b505050565b610045600435602435604435606435600484141561024e576060838152608083905260a08290527f4a25b279c7c585f25eda9788ac9420ebadae78ca6b206a0e6ab488fd81f550629080a15b50505050565b610045600435602435600282141561028b5760608181527f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d490602090a15b5050565b60008114156101cc5760006060a061007d565b610002565b60098514156101cc5760608481526080849052819083907fa30ece802b64cd2b7e57dabf4010aabf5df26d1556977affb07b98a77ad955b590604090a36100d3565b60078314156101cc57606082815281907f057bc32826fbe161da1c110afcdcae7c109a8b69149f727fc37a603c60ef94ca90602090a2610120565b60088414156101cc576060838152819083907ff16c999b533366ca5138d78e85da51611089cd05749f098d6c225d4cd42ee6ec90602090a3610171565b60068214156101cc57807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560006060a26101b556" + +CONTRACT_EMITTER_ABI = json.loads('[{"constant":false,"inputs":[{"name":"which","type":"uint8"}],"name":"logNoArgs","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"which","type":"uint8"},{"name":"arg0","type":"uint256"},{"name":"arg1","type":"uint256"},{"name":"arg2","type":"uint256"},{"name":"arg3","type":"uint256"}],"name":"logQuadruple","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"which","type":"uint8"},{"name":"arg0","type":"uint256"},{"name":"arg1","type":"uint256"}],"name":"logDouble","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"which","type":"uint8"},{"name":"arg0","type":"uint256"},{"name":"arg1","type":"uint256"},{"name":"arg2","type":"uint256"}],"name":"logTriple","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"which","type":"uint8"},{"name":"arg0","type":"uint256"}],"name":"logSingle","outputs":[],"type":"function"},{"anonymous":true,"inputs":[],"name":"LogAnonymous","type":"event"},{"anonymous":false,"inputs":[],"name":"LogNoArguments","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"}],"name":"LogSingleArg","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":false,"name":"arg1","type":"uint256"}],"name":"LogDoubleArg","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":false,"name":"arg1","type":"uint256"},{"indexed":false,"name":"arg2","type":"uint256"}],"name":"LogTripleArg","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":false,"name":"arg1","type":"uint256"},{"indexed":false,"name":"arg2","type":"uint256"},{"indexed":false,"name":"arg3","type":"uint256"}],"name":"LogQuadrupleArg","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"arg0","type":"uint256"}],"name":"LogSingleWithIndex","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":true,"name":"arg1","type":"uint256"}],"name":"LogDoubleWithIndex","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":true,"name":"arg1","type":"uint256"},{"indexed":true,"name":"arg2","type":"uint256"}],"name":"LogTripleWithIndex","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":false,"name":"arg1","type":"uint256"},{"indexed":true,"name":"arg2","type":"uint256"},{"indexed":true,"name":"arg3","type":"uint256"}],"name":"LogQuadrupleWithIndex","type":"event"}]') + + +@pytest.fixture() +def EMITTER_SOURCE(): + return CONTRACT_EMITTER_SOURCE + + +@pytest.fixture() +def EMITTER_CODE(): + return CONTRACT_EMITTER_CODE + + +@pytest.fixture() +def EMITTER_RUNTIME(): + return CONTRACT_EMITTER_RUNTIME + + +@pytest.fixture() +def EMITTER_ABI(): + return CONTRACT_EMITTER_ABI + + +@pytest.fixture() +def EmitterContract(web3_tester, + EMITTER_SOURCE, + EMITTER_CODE, + EMITTER_RUNTIME, + EMITTER_ABI): + return web3_tester.eth.contract( + abi=EMITTER_ABI, + code=EMITTER_CODE, + code_runtime=EMITTER_RUNTIME, + source=EMITTER_SOURCE, + ) diff --git a/tests/contracts/test_contract_estimateGas.py b/tests/contracts/test_contract_estimateGas.py index 24f7f4f..647caed 100644 --- a/tests/contracts/test_contract_estimateGas.py +++ b/tests/contracts/test_contract_estimateGas.py @@ -42,7 +42,7 @@ def test_contract_estimateGas(web3, math_contract): if isinstance(web3.currentProvider, TestRPCProvider): pytest.skip("The testrpc server doesn't implement `eth_estimateGas`") - increment_abi = math_contract.find_matching_abi('increment', []) + increment_abi = math_contract.find_matching_fn_abi('increment', []) call_data = function_abi_to_4byte_selector(increment_abi) gas_estimate = math_contract.estimateGas().increment() diff --git a/tests/contracts/test_contract_method_to_argument_matching.py b/tests/contracts/test_contract_method_to_argument_matching.py index 30424e4..e20800e 100644 --- a/tests/contracts/test_contract_method_to_argument_matching.py +++ b/tests/contracts/test_contract_method_to_argument_matching.py @@ -14,7 +14,7 @@ MULTIPLE_FUNCTIONS = json.loads('[{"constant":false,"inputs":[],"name":"a","outp def test_finds_single_function_without_args(web3_tester): Contract = web3_tester.eth.contract(SINGLE_FN_NO_ARGS) - abi = Contract.find_matching_abi('a', []) + abi = Contract.find_matching_fn_abi('a', []) assert abi['name'] == 'a' assert abi['inputs'] == [] @@ -22,7 +22,7 @@ def test_finds_single_function_without_args(web3_tester): def test_finds_single_function_with_args(web3_tester): Contract = web3_tester.eth.contract(SINGLE_FN_ONE_ARG) - abi = Contract.find_matching_abi('a', [1234]) + abi = Contract.find_matching_fn_abi('a', [1234]) assert abi['name'] == 'a' assert len(abi['inputs']) == 1 assert abi['inputs'][0]['type'] == 'uint256' @@ -32,7 +32,7 @@ def test_error_when_no_function_name_match(web3_tester): Contract = web3_tester.eth.contract(SINGLE_FN_NO_ARGS) with pytest.raises(ValueError): - Contract.find_matching_abi('no_function_name', [1234]) + Contract.find_matching_fn_abi('no_function_name', [1234]) @pytest.mark.parametrize( @@ -48,7 +48,7 @@ def test_error_when_no_function_name_match(web3_tester): def test_finds_function_with_matching_args(web3_tester, arguments, expected_types): Contract = web3_tester.eth.contract(MULTIPLE_FUNCTIONS) - abi = Contract.find_matching_abi('a', arguments) + abi = Contract.find_matching_fn_abi('a', arguments) assert abi['name'] == 'a' assert len(abi['inputs']) == len(expected_types) assert set(get_abi_input_types(abi)) == set(expected_types) @@ -58,4 +58,4 @@ def test_error_when_duplicate_match(web3_tester): Contract = web3_tester.eth.contract(MULTIPLE_FUNCTIONS) with pytest.raises(ValueError): - abi = Contract.find_matching_abi('a', [100]) + abi = Contract.find_matching_fn_abi('a', [100]) diff --git a/tests/eth-module/test_eth_estimateGas.py b/tests/eth-module/test_eth_estimateGas.py index b2da582..37c847e 100644 --- a/tests/eth-module/test_eth_estimateGas.py +++ b/tests/eth-module/test_eth_estimateGas.py @@ -42,7 +42,7 @@ def test_eth_estimateGas(web3, math_contract): if isinstance(web3.currentProvider, TestRPCProvider): pytest.skip("The testrpc server doesn't implement `eth_estimateGas`") - increment_abi = math_contract.find_matching_abi('increment', []) + increment_abi = math_contract.find_matching_fn_abi('increment', []) call_data = function_abi_to_4byte_selector(increment_abi) gas_estimate = web3.eth.estimateGas({ 'to': math_contract.address, diff --git a/tests/filtering/conftest.py b/tests/filtering/conftest.py new file mode 100644 index 0000000..14888e3 --- /dev/null +++ b/tests/filtering/conftest.py @@ -0,0 +1,175 @@ +import pytest +import json +import textwrap +from sha3 import sha3_256 + +from web3.providers.rpc import TestRPCProvider + + +assert sha3_256(b'').hexdigest() == 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' + + +@pytest.fixture(autouse=True) +def skip_testrpc_and_wait_for_mining_start(web3, wait_for_block): + if isinstance(web3.currentProvider, TestRPCProvider): + pytest.skip("No miner interface on eth-testrpc") + + wait_for_block(web3) + + +CONTRACT_EMITTER_SOURCE = textwrap.dedent((""" +contract Emitter { + event LogAnonymous() anonymous; + event LogNoArguments(); + event LogSingleArg(uint arg0); + event LogDoubleArg(uint arg0, uint arg1); + event LogTripleArg(uint arg0, uint arg1, uint arg2); + event LogQuadrupleArg(uint arg0, uint arg1, uint arg2, uint arg3); + + // Indexed + event LogSingleWithIndex(uint indexed arg0); + event LogDoubleWithIndex(uint arg0, uint indexed arg1); + event LogTripleWithIndex(uint arg0, uint indexed arg1, uint indexed arg2); + event LogQuadrupleWithIndex(uint arg0, uint arg1, uint indexed arg2, uint indexed arg3); + + enum WhichEvent { + LogAnonymous, + LogNoArguments, + LogSingleArg, + LogDoubleArg, + LogTripleArg, + LogQuadrupleArg, + LogSingleWithIndex, + LogDoubleWithIndex, + LogTripleWithIndex, + LogQuadrupleWithIndex + } + + function logNoArgs(WhichEvent which) public { + if (which == WhichEvent.LogNoArguments) LogNoArguments(); + else if (which == WhichEvent.LogAnonymous) LogAnonymous(); + else throw; + } + + function logSingle(WhichEvent which, uint arg0) public { + if (which == WhichEvent.LogSingleArg) LogSingleArg(arg0); + else if (which == WhichEvent.LogSingleWithIndex) LogSingleWithIndex(arg0); + else throw; + } + + function logDouble(WhichEvent which, uint arg0, uint arg1) public { + if (which == WhichEvent.LogDoubleArg) LogDoubleArg(arg0, arg1); + else if (which == WhichEvent.LogDoubleWithIndex) LogDoubleWithIndex(arg0, arg1); + else throw; + } + + function logTriple(WhichEvent which, uint arg0, uint arg1, uint arg2) public { + if (which == WhichEvent.LogTripleArg) LogTripleArg(arg0, arg1, arg2); + else if (which == WhichEvent.LogTripleWithIndex) LogTripleWithIndex(arg0, arg1, arg2); + else throw; + } + + function logQuadruple(WhichEvent which, uint arg0, uint arg1, uint arg2, uint arg3) public { + if (which == WhichEvent.LogQuadrupleArg) LogQuadrupleArg(arg0, arg1, arg2, arg3); + else if (which == WhichEvent.LogQuadrupleWithIndex) LogQuadrupleWithIndex(arg0, arg1, arg2, arg3); + else throw; + } +} +""")) + +CONTRACT_EMITTER_CODE = "0x60606040526102c0806100126000396000f3606060405260e060020a600035046317c0c180811461004757806320f0256e1461008057806390b41d8b146100da5780639c37705314610125578063aa6fd82214610177575b005b61004560043560018114156101b9577f1e86022f78f8d04f8e3dfd13a2bdb280403e6632877c0dbee5e4eeb259908a5c60006060a15b50565b61004560043560243560443560643560843560058514156101d1576060848152608084815260a084905260c08390527ff039d147f23fe975a4254bdf6b1502b8c79132ae1833986b7ccef2638e73fdf991a15b5050505050565b610045600435602435604435600383141561021357606082815260808290527fdf0cb1dea99afceb3ea698d62e705b736f1345a7eee9eb07e63d1f8f556c1bc590604090a15b505050565b610045600435602435604435606435600484141561024e576060838152608083905260a08290527f4a25b279c7c585f25eda9788ac9420ebadae78ca6b206a0e6ab488fd81f550629080a15b50505050565b610045600435602435600282141561028b5760608181527f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d490602090a15b5050565b60008114156101cc5760006060a061007d565b610002565b60098514156101cc5760608481526080849052819083907fa30ece802b64cd2b7e57dabf4010aabf5df26d1556977affb07b98a77ad955b590604090a36100d3565b60078314156101cc57606082815281907f057bc32826fbe161da1c110afcdcae7c109a8b69149f727fc37a603c60ef94ca90602090a2610120565b60088414156101cc576060838152819083907ff16c999b533366ca5138d78e85da51611089cd05749f098d6c225d4cd42ee6ec90602090a3610171565b60068214156101cc57807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560006060a26101b556" + +CONTRACT_EMITTER_RUNTIME = "0x606060405260e060020a600035046317c0c180811461004757806320f0256e1461008057806390b41d8b146100da5780639c37705314610125578063aa6fd82214610177575b005b61004560043560018114156101b9577f1e86022f78f8d04f8e3dfd13a2bdb280403e6632877c0dbee5e4eeb259908a5c60006060a15b50565b61004560043560243560443560643560843560058514156101d1576060848152608084815260a084905260c08390527ff039d147f23fe975a4254bdf6b1502b8c79132ae1833986b7ccef2638e73fdf991a15b5050505050565b610045600435602435604435600383141561021357606082815260808290527fdf0cb1dea99afceb3ea698d62e705b736f1345a7eee9eb07e63d1f8f556c1bc590604090a15b505050565b610045600435602435604435606435600484141561024e576060838152608083905260a08290527f4a25b279c7c585f25eda9788ac9420ebadae78ca6b206a0e6ab488fd81f550629080a15b50505050565b610045600435602435600282141561028b5760608181527f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d490602090a15b5050565b60008114156101cc5760006060a061007d565b610002565b60098514156101cc5760608481526080849052819083907fa30ece802b64cd2b7e57dabf4010aabf5df26d1556977affb07b98a77ad955b590604090a36100d3565b60078314156101cc57606082815281907f057bc32826fbe161da1c110afcdcae7c109a8b69149f727fc37a603c60ef94ca90602090a2610120565b60088414156101cc576060838152819083907ff16c999b533366ca5138d78e85da51611089cd05749f098d6c225d4cd42ee6ec90602090a3610171565b60068214156101cc57807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560006060a26101b556" + +CONTRACT_EMITTER_ABI = json.loads('[{"constant":false,"inputs":[{"name":"which","type":"uint8"}],"name":"logNoArgs","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"which","type":"uint8"},{"name":"arg0","type":"uint256"},{"name":"arg1","type":"uint256"},{"name":"arg2","type":"uint256"},{"name":"arg3","type":"uint256"}],"name":"logQuadruple","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"which","type":"uint8"},{"name":"arg0","type":"uint256"},{"name":"arg1","type":"uint256"}],"name":"logDouble","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"which","type":"uint8"},{"name":"arg0","type":"uint256"},{"name":"arg1","type":"uint256"},{"name":"arg2","type":"uint256"}],"name":"logTriple","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"which","type":"uint8"},{"name":"arg0","type":"uint256"}],"name":"logSingle","outputs":[],"type":"function"},{"anonymous":true,"inputs":[],"name":"LogAnonymous","type":"event"},{"anonymous":false,"inputs":[],"name":"LogNoArguments","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"}],"name":"LogSingleArg","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":false,"name":"arg1","type":"uint256"}],"name":"LogDoubleArg","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":false,"name":"arg1","type":"uint256"},{"indexed":false,"name":"arg2","type":"uint256"}],"name":"LogTripleArg","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":false,"name":"arg1","type":"uint256"},{"indexed":false,"name":"arg2","type":"uint256"},{"indexed":false,"name":"arg3","type":"uint256"}],"name":"LogQuadrupleArg","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"arg0","type":"uint256"}],"name":"LogSingleWithIndex","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":true,"name":"arg1","type":"uint256"}],"name":"LogDoubleWithIndex","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":true,"name":"arg1","type":"uint256"},{"indexed":true,"name":"arg2","type":"uint256"}],"name":"LogTripleWithIndex","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg0","type":"uint256"},{"indexed":false,"name":"arg1","type":"uint256"},{"indexed":true,"name":"arg2","type":"uint256"},{"indexed":true,"name":"arg3","type":"uint256"}],"name":"LogQuadrupleWithIndex","type":"event"}]') + + +@pytest.fixture() +def EMITTER_SOURCE(): + return CONTRACT_EMITTER_SOURCE + + +@pytest.fixture() +def EMITTER_CODE(): + return CONTRACT_EMITTER_CODE + + +@pytest.fixture() +def EMITTER_RUNTIME(): + return CONTRACT_EMITTER_RUNTIME + + +@pytest.fixture() +def EMITTER_ABI(): + return CONTRACT_EMITTER_ABI + + +@pytest.fixture() +def EMITTER(EMITTER_CODE, + EMITTER_RUNTIME, + EMITTER_ABI, + EMITTER_SOURCE): + return { + 'code': EMITTER_CODE, + 'code_runtime': EMITTER_RUNTIME, + 'source': EMITTER_SOURCE, + 'abi': EMITTER_ABI, + } + + +@pytest.fixture() +def Emitter(web3, EMITTER): + return web3.eth.contract(**EMITTER) + + +@pytest.fixture() +def emitter(web3, Emitter, wait_for_transaction, wait_for_block): + wait_for_block(web3) + deploy_txn_hash = Emitter.deploy({'from': web3.eth.coinbase, 'gas': 1000000}) + deploy_receipt = wait_for_transaction(deploy_txn_hash) + contract_address = deploy_receipt['contractAddress'] + + code = web3.eth.getCode(contract_address) + assert code == Emitter.code_runtime + return Emitter(address=contract_address) + + +class LogFunctions(object): + LogAnonymous = 0 + LogNoArguments = 1 + LogSingleArg = 2 + LogDoubleArg = 3 + LogTripleArg = 4 + LogQuadrupleArg = 5 + LogSingleWithIndex = 6 + LogDoubleWithIndex = 7 + LogTripleWithIndex = 8 + LogQuadrupleWithIndex = 9 + + +@pytest.fixture() +def emitter_event_ids(): + return LogFunctions + + +def event_topic(event_signature): + from web3.utils.string import force_bytes + return force_bytes("0x" + sha3_256(force_bytes(event_signature)).hexdigest()) + + +class LogTopics(object): + LogAnonymous = event_topic("LogAnonymous()") + LogNoArguments = event_topic("LogNoArguments()") + LogSingleArg = event_topic("LogSingleArg(uint256)") + LogSingleWithIndex = event_topic("LogSingleWithIndex(uint256)") + LogDoubleArg = event_topic("LogDoubleArg(uint256,uint256)") + LogDoubleWithIndex = event_topic("LogDoubleWithIndex(uint256,uint256)") + LogTripleArg = event_topic("LogTripleArg(uint256,uint256,uint256)") + LogTripleWithIndex = event_topic("LogTripleWithIndex(uint256,uint256,uint256)") + LogQuadrupleArg = event_topic("LogQuadrupleArg(uint256,uint256,uint256,uint256)") + LogQuadrupleWithIndex = event_topic("LogQuadrupleWithIndex(uint256,uint256,uint256,uint256)") + + +@pytest.fixture() +def emitter_log_topics(): + return LogTopics diff --git a/tests/filtering/test_contract_on_event_filtering.py b/tests/filtering/test_contract_on_event_filtering.py new file mode 100644 index 0000000..ab6c279 --- /dev/null +++ b/tests/filtering/test_contract_on_event_filtering.py @@ -0,0 +1,56 @@ +import random +import gevent + + +def test_on_filter_with_only_event_name(web3, + emitter, + wait_for_transaction, + emitter_log_topics, + emitter_event_ids): + + seen_logs = [] + + filter = emitter.on('LogNoArguments', {}, seen_logs.append) + + txn_hash = emitter.transact().logNoArgs(emitter_event_ids.LogNoArguments) + txn_receipt = wait_for_transaction(txn_hash) + + gevent.sleep(1) + + filter.stop_watching(10) + + assert len(seen_logs) == 1 + assert seen_logs[0]['transactionHash'] == txn_hash + + +def test_on_filter_with_event_name_and_single_argument(web3, + emitter, + wait_for_transaction, + emitter_log_topics, + emitter_event_ids): + + seen_logs = [] + + filter = emitter.on('LogTripleWithIndex', {'filter': { + 'arg1': 2, + }}, seen_logs.append) + + txn_hashes = [] + txn_hashes.append( + emitter.transact().logTriple(emitter_event_ids.LogTripleWithIndex, 2, 1, 3) + ) + txn_hashes.append( + emitter.transact().logTriple(emitter_event_ids.LogTripleWithIndex, 1, 2, 3) + ) + txn_hashes.append( + emitter.transact().logTriple(emitter_event_ids.LogTripleWithIndex, 12345, 2, 54321) + ) + for txn_hash in txn_hashes: + wait_for_transaction(txn_hash) + + gevent.sleep(1) + + filter.stop_watching(10) + + assert len(seen_logs) == 2 + assert {l['transactionHash'] for l in seen_logs} == set(txn_hashes[1:]) diff --git a/tests/filtering/test_contract_past_event_filtering.py b/tests/filtering/test_contract_past_event_filtering.py new file mode 100644 index 0000000..66b5a81 --- /dev/null +++ b/tests/filtering/test_contract_past_event_filtering.py @@ -0,0 +1,24 @@ +import random +import gevent + + +def test_past_events_filter_with_only_event_name(web3, + emitter, + wait_for_transaction, + emitter_log_topics, + emitter_event_ids): + txn_hash = emitter.transact().logNoArgs(emitter_event_ids.LogNoArguments) + txn_receipt = wait_for_transaction(txn_hash) + + seen_logs = [] + + filter = emitter.pastEvents('LogNoArguments', {}, seen_logs.append) + + with gevent.Timeout(5): + while not seen_logs: + gevent.sleep(random.random()) + + filter.stop_watching(10) + + assert len(seen_logs) == 1 + assert seen_logs[0]['transactionHash'] == txn_hash diff --git a/tests/filtering/test_filter_against_latest_blocks.py b/tests/filtering/test_filter_against_latest_blocks.py new file mode 100644 index 0000000..1f41739 --- /dev/null +++ b/tests/filtering/test_filter_against_latest_blocks.py @@ -0,0 +1,20 @@ +def test_filter_against_latest_blocks(web3, wait_for_block): + seen_blocks = [] + txn_filter = web3.eth.filter("latest") + txn_filter.watch(seen_blocks.append) + + current_block = web3.eth.blockNumber + + wait_for_block(web3, current_block + 3) + + txn_filter.stop_watching(3) + + # give the gevent threads a moment to catch the latest block. + wait_for_block(web3, web3.eth.blockNumber + 1) + + expected_block_hashes = [ + web3.eth.getBlock(n)['hash'] for n in range(current_block + 1, current_block + 4) + ] + assert len(seen_blocks) > 2 + + assert set(expected_block_hashes).issuperset(seen_blocks) diff --git a/tests/filtering/test_filter_against_pending_transactions.py b/tests/filtering/test_filter_against_pending_transactions.py new file mode 100644 index 0000000..f6e865f --- /dev/null +++ b/tests/filtering/test_filter_against_pending_transactions.py @@ -0,0 +1,23 @@ +def test_filter_against_pending_transactions(web3, wait_for_transaction): + seen_txns = [] + txn_filter = web3.eth.filter("pending") + txn_filter.watch(seen_txns.append) + + txn_1_hash = web3.eth.sendTransaction({ + 'from': web3.eth.coinbase, + 'to': '0xd3cda913deb6f67967b99d67acdfa1712c293601', + 'value': 12345, + }) + txn_2_hash = web3.eth.sendTransaction({ + 'from': web3.eth.coinbase, + 'to': '0xd3cda913deb6f67967b99d67acdfa1712c293601', + 'value': 54321, + }) + + wait_for_transaction(txn_1_hash) + wait_for_transaction(txn_2_hash) + + txn_filter.stop_watching(30) + + assert txn_1_hash in seen_txns + assert txn_2_hash in seen_txns diff --git a/tests/filtering/test_filter_against_transaction_logs.py b/tests/filtering/test_filter_against_transaction_logs.py new file mode 100644 index 0000000..2d8cb27 --- /dev/null +++ b/tests/filtering/test_filter_against_transaction_logs.py @@ -0,0 +1,20 @@ +def test_filter_against_log_events(web3, + emitter, + wait_for_transaction, + emitter_log_topics, + emitter_event_ids): + + seen_logs = [] + txn_filter = web3.eth.filter({}) + txn_filter.watch(seen_logs.append) + + txn_hashes = [] + + txn_hashes.append(emitter.transact().logNoArgs(emitter_event_ids.LogNoArguments)) + + for txn_hash in txn_hashes: + wait_for_transaction(txn_hash) + + txn_filter.stop_watching(30) + + assert set(txn_hashes) == set(log['transactionHash'] for log in seen_logs) diff --git a/tests/utilities/test_abi_filtering_by_argument_name.py b/tests/utilities/test_abi_filtering_by_argument_name.py new file mode 100644 index 0000000..4d76d98 --- /dev/null +++ b/tests/utilities/test_abi_filtering_by_argument_name.py @@ -0,0 +1,61 @@ +import pytest + +from web3.utils.abi import ( + filter_by_argument_name, +) + +ABI = [ + { + "constant": False, + "inputs": [], + "name": "func_1", + "outputs": [], + "type": "function", + }, + { + "constant": False, + "inputs": [ + {"name": "a", "type": "uint256"}, + ], + "name": "func_2", + "outputs": [], + "type": "function", + }, + { + "constant": False, + "inputs": [ + {"name": "a", "type": "uint256"}, + {"name": "b", "type": "uint256"}, + ], + "name": "func_3", + "outputs": [], + "type": "function", + }, + { + "constant": False, + "inputs": [ + {"name": "a", "type": "uint256"}, + {"name": "b", "type": "uint256"}, + {"name": "c", "type": "uint256"}, + ], + "name": "func_4", + "outputs": [], + "type": "function", + }, +] + + +@pytest.mark.parametrize( + 'argument_names,expected', + ( + ([], ['func_1', 'func_2', 'func_3', 'func_4']), + (['a'], ['func_2', 'func_3', 'func_4']), + (['a', 'c'], ['func_4']), + (['c'], ['func_4']), + (['b'], ['func_3', 'func_4']), + ) +) +def test_filter_by_arguments_1(argument_names, expected): + actual_matches = filter_by_argument_name(argument_names, ABI) + function_names = [match['name'] for match in actual_matches] + assert set(function_names) == set(expected) diff --git a/tests/utilities/test_construct_event_data_set.py b/tests/utilities/test_construct_event_data_set.py new file mode 100644 index 0000000..a3cb746 --- /dev/null +++ b/tests/utilities/test_construct_event_data_set.py @@ -0,0 +1,76 @@ +import pytest + + +from web3.utils.abi import ( + construct_event_data_set, +) + + +EVENT_1_ABI = { + "anonymous": False, + "inputs": [ + {"indexed": False,"name":"arg0","type":"uint256"}, + {"indexed": True,"name":"arg1","type":"uint256"}, + {"indexed": True,"name":"arg2","type":"uint256"}, + {"indexed": False,"name":"arg3","type":"uint256"}, + {"indexed": True,"name":"arg4","type":"uint256"}, + {"indexed": False,"name":"arg5","type":"uint256"}, + ], + "name": "Event_1", + "type":"event", +} +EVENT_1_TOPIC = '0xa7144ed450ecab4a6283d3b1e290ff6c889232d922b84d88203eb7619222fb32' + + +def hex_and_pad(i): + unpadded_hex_value = hex(i) + return '0x' + unpadded_hex_value[2:].zfill(65 - len(unpadded_hex_value[2:])) + + +@pytest.mark.parametrize( + 'event_abi,arguments,expected', + ( + ( + EVENT_1_ABI, + {}, + [[]], + ), + ( + EVENT_1_ABI, + {'arg1': 1}, + [[]], + ), + ( + EVENT_1_ABI, + {'arg0': 1}, + [[hex_and_pad(1), None, None]], + ), + ( + EVENT_1_ABI, + {'arg0': [1]}, + [[hex_and_pad(1), None, None]], + ), + ( + EVENT_1_ABI, + {'arg0': [1, 2]}, + [ + [hex_and_pad(1), None, None], + [hex_and_pad(2), None, None], + ], + ), + ( + EVENT_1_ABI, + {'arg0': [1, 3], 'arg3': [2, 4]}, + [ + [hex_and_pad(1), hex_and_pad(2), None], + [hex_and_pad(1), hex_and_pad(4), None], + [hex_and_pad(3), hex_and_pad(2), None], + [hex_and_pad(3), hex_and_pad(4), None], + ], + ), + ) +) +def test_construct_event_data_set(event_abi, arguments, expected): + actual = construct_event_data_set(event_abi, arguments) + assert actual == expected + diff --git a/tests/utilities/test_construct_event_filter_params.py b/tests/utilities/test_construct_event_filter_params.py new file mode 100644 index 0000000..c0cdd7e --- /dev/null +++ b/tests/utilities/test_construct_event_filter_params.py @@ -0,0 +1,55 @@ +import pytest + +from web3.utils.abi import ( + event_abi_to_log_topic, +) +from web3.utils.filters import ( + construct_event_filter_params, +) + +EVENT_1_ABI = { + "anonymous": False, + "inputs": [ + {"indexed": False,"name":"arg0","type":"uint256"}, + {"indexed": True,"name":"arg1","type":"uint256"}, + ], + "name": "Event_1", + "type":"event", +} + + +@pytest.mark.parametrize( + "event_abi,fn_kwargs,expected", + ( + (EVENT_1_ABI, {}, { + "topics": ['0xb470a829ed7792f06947f0ca3730a570cb378329ddcf09f2b4efabd6326f51f6'], + }), + (EVENT_1_ABI, {'topics': ['should-be-preserved']}, { + "topics": [ + ['should-be-preserved'], + ['0xb470a829ed7792f06947f0ca3730a570cb378329ddcf09f2b4efabd6326f51f6'], + ] + }), + (EVENT_1_ABI, {'contract_address': '0xd3cda913deb6f67967b99d67acdfa1712c293601'}, { + "topics": ['0xb470a829ed7792f06947f0ca3730a570cb378329ddcf09f2b4efabd6326f51f6'], + 'address': '0xd3cda913deb6f67967b99d67acdfa1712c293601', + }), + (EVENT_1_ABI, { + 'contract_address': '0xd3cda913deb6f67967b99d67acdfa1712c293601', + 'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', + }, { + "topics": ['0xb470a829ed7792f06947f0ca3730a570cb378329ddcf09f2b4efabd6326f51f6'], + 'address': [ + '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', + '0xd3cda913deb6f67967b99d67acdfa1712c293601', + ], + }), + (EVENT_1_ABI, {'address': '0xd3cda913deb6f67967b99d67acdfa1712c293601'}, { + "topics": ['0xb470a829ed7792f06947f0ca3730a570cb378329ddcf09f2b4efabd6326f51f6'], + 'address': '0xd3cda913deb6f67967b99d67acdfa1712c293601', + }), + ), +) +def test_construct_event_filter_params(event_abi, fn_kwargs, expected): + actual = construct_event_filter_params(event_abi, **fn_kwargs) + assert actual == expected diff --git a/tests/utilities/test_construct_event_topics.py b/tests/utilities/test_construct_event_topics.py new file mode 100644 index 0000000..01b919e --- /dev/null +++ b/tests/utilities/test_construct_event_topics.py @@ -0,0 +1,84 @@ +import pytest + + +from web3.utils.abi import ( + construct_event_topic_set, +) + + +EVENT_1_ABI = { + "anonymous": False, + "inputs": [ + {"indexed": False,"name":"arg0","type":"uint256"}, + {"indexed": True,"name":"arg1","type":"uint256"}, + {"indexed": True,"name":"arg2","type":"uint256"}, + {"indexed": False,"name":"arg3","type":"uint256"}, + {"indexed": True,"name":"arg4","type":"uint256"}, + {"indexed": False,"name":"arg5","type":"uint256"}, + ], + "name": "Event_1", + "type":"event", +} +EVENT_1_TOPIC = '0xa7144ed450ecab4a6283d3b1e290ff6c889232d922b84d88203eb7619222fb32' + + +def hex_and_pad(i): + unpadded_hex_value = hex(i) + return '0x' + unpadded_hex_value[2:].zfill(65 - len(unpadded_hex_value[2:])) + + +@pytest.mark.parametrize( + 'event_abi,arguments,expected', + ( + ( + EVENT_1_ABI, + {}, + [[EVENT_1_TOPIC]], + ), + ( + EVENT_1_ABI, + {'arg0': 1}, + [[EVENT_1_TOPIC]], + ), + ( + EVENT_1_ABI, + {'arg0': 1, 'arg3': [1, 2]}, + [[EVENT_1_TOPIC]], + ), + ( + EVENT_1_ABI, + {'arg1': 1}, + [ + [EVENT_1_TOPIC, hex_and_pad(1), None, None], + ], + ), + ( + EVENT_1_ABI, + {'arg1': [1, 2]}, + [ + [EVENT_1_TOPIC, hex_and_pad(1), None, None], + [EVENT_1_TOPIC, hex_and_pad(2), None, None], + ], + ), + ( + EVENT_1_ABI, + {'arg1': [1], 'arg2': [2]}, + [ + [EVENT_1_TOPIC, hex_and_pad(1), hex_and_pad(2), None], + ], + ), + ( + EVENT_1_ABI, + {'arg1': [1, 3], 'arg2': [2, 4]}, + [ + [EVENT_1_TOPIC, hex_and_pad(1), hex_and_pad(2), None], + [EVENT_1_TOPIC, hex_and_pad(1), hex_and_pad(4), None], + [EVENT_1_TOPIC, hex_and_pad(3), hex_and_pad(2), None], + [EVENT_1_TOPIC, hex_and_pad(3), hex_and_pad(4), None], + ], + ), + ) +) +def test_construct_event_topics(event_abi, arguments, expected): + actual = construct_event_topic_set(event_abi, arguments) + assert actual == expected diff --git a/web3/contract.py b/web3/contract.py index e98c65d..91df3e3 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -16,6 +16,9 @@ from web3.utils.formatting import ( add_0x_prefix, remove_0x_prefix, ) +from web3.utils.types import ( + is_array, +) from web3.utils.string import ( force_bytes, coerce_return_to_text, @@ -25,12 +28,21 @@ from web3.utils.abi import ( filter_by_type, filter_by_name, filter_by_argument_count, + filter_by_argument_name, filter_by_encodability, get_abi_input_types, get_abi_output_types, get_constructor_abi, check_if_arguments_can_be_encoded, function_abi_to_4byte_selector, + event_abi_to_log_topic, +) +from web3.utils.functional import ( + compose, +) +from web3.utils.filters import ( + construct_event_filter_params, + LogFilter, ) @@ -232,7 +244,7 @@ class Contract(object): # ABI Helpers # @classmethod - def find_matching_abi(cls, fn_name, arguments): + def find_matching_fn_abi(cls, fn_name, arguments): filters = [ functools.partial(filter_by_name, fn_name), functools.partial(filter_by_argument_count, arguments), @@ -254,13 +266,30 @@ class Contract(object): else: raise ValueError("Multiple functions found") + @classmethod + def find_matching_event_abi(cls, event_name, argument_names): + filter_fn = compose( + functools.partial(filter_by_type, 'event'), + functools.partial(filter_by_name, event_name), + functools.partial(filter_by_argument_name, argument_names), + ) + + event_abi_candidates = filter_fn(cls.abi) + + if len(event_abi_candidates) == 1: + return event_abi_candidates[0] + elif not event_abi_candidates: + raise ValueError("No matching functions found") + else: + raise ValueError("Multiple functions found") + @classmethod @coerce_return_to_text def encodeABI(cls, fn_name, arguments, data=None): """ encodes the arguments using the Ethereum ABI. """ - function_abi = cls.find_matching_abi(fn_name, force_obj_to_bytes(arguments)) + function_abi = cls.find_matching_fn_abi(fn_name, force_obj_to_bytes(arguments)) return cls._encodeABI(function_abi, arguments, data) @classmethod @@ -309,17 +338,61 @@ class Contract(object): return deploy_data - def on(self, event, filters, callback): + def on(self, event_name, default_filter_params=None, *callbacks): """ register a callback to be triggered on the appropriate events. """ - raise NotImplementedError('Not implemented') + if default_filter_params is None: + default_filter_params = {} - def pastEvents(self, event, filters, callback): + argument_filters = default_filter_params.pop('filter', {}) + argument_filter_names = list(argument_filters.keys()) + event_abi = self.find_matching_event_abi(event_name, argument_filter_names) + + filter_params = construct_event_filter_params( + event_abi, + contract_address=self.address, + argument_filters=argument_filters, + **default_filter_params + ) + + filter = self.web3.eth.filter(filter_params) + + if callbacks: + filter.watch(*callbacks) + + filter.filter_params = filter_params + return filter + + def pastEvents(self, event_name, default_filter_params=None, *callbacks): """ register a callback to be triggered on all past events. """ - raise NotImplementedError('Not implemented') + if default_filter_params is None: + default_filter_params = {} + + if 'fromBlock' in default_filter_params or 'toBlock' in default_filter_params: + raise ValueError("Cannot provide `fromBlock` or `toBlock` in `pastEvents` calls") + + argument_filters = default_filter_params.pop('filter', {}) + argument_filter_names = list(argument_filters.keys()) + event_abi = self.find_matching_event_abi(event_name, argument_filter_names) + + filter_params = construct_event_filter_params( + event_abi, + contract_address=self.address, + argument_filters=argument_filters, + fromBlock="earliest", + toBlock=self.web3.eth.blockNumber, + **default_filter_params + ) + + filter = self.web3.eth.filter(filter_params) + + if callbacks: + filter.watch(*callbacks) + + return filter def estimateGas(self, transaction=None): """ @@ -506,7 +579,7 @@ def call_contract_function(contract=None, if not arguments: arguments = [] - function_abi = contract.find_matching_abi(function_name, arguments) + function_abi = contract.find_matching_fn_abi(function_name, arguments) function_selector = function_abi_to_4byte_selector(function_abi) transaction['data'] = contract.encodeABI( @@ -590,7 +663,7 @@ def transact_with_contract_function(contract=None, if not arguments: arguments = [] - function_abi = contract.find_matching_abi(function_name, arguments) + function_abi = contract.find_matching_fn_abi(function_name, arguments) function_selector = function_abi_to_4byte_selector(function_abi) transaction['data'] = contract.encodeABI( @@ -615,7 +688,7 @@ def estimate_gas_for_function(contract=None, if not arguments: arguments = [] - function_abi = contract.find_matching_abi(function_name, arguments) + function_abi = contract.find_matching_fn_abi(function_name, arguments) function_selector = function_abi_to_4byte_selector(function_abi) transaction['data'] = contract.encodeABI( diff --git a/web3/eth.py b/web3/eth.py index 04ea56c..7f07a6d 100644 --- a/web3/eth.py +++ b/web3/eth.py @@ -8,10 +8,16 @@ from web3.utils.encoding import ( ) from web3.utils.types import ( is_integer, + is_string, ) from web3.utils.functional import ( apply_formatters_to_return, ) +from web3.utils.filters import ( + BlockFilter, + TransactionFilter, + LogFilter, +) from web3.contract import construct_contract_class @@ -218,13 +224,35 @@ class Eth(object): def estimateGas(self, transaction): return self.request_manager.request_blocking("eth_estimateGas", [transaction]) - def filter(self, *args, **kwargs): - """ - `eth_newFilter` - `eth_newBlockFilter` - `eth_uninstallFilter` - """ - raise NotImplementedError("TODO") + def filter(self, filter_params): + if is_string(filter_params): + if filter_params == "latest": + filter_id = self.request_manager.request_blocking("eth_newBlockFilter", []) + return BlockFilter(self.web3, filter_id) + elif filter_params == "pending": + filter_id = self.request_manager.request_blocking( + "eth_newPendingTransactionFilter", [], + ) + return TransactionFilter(self.web3, filter_id) + else: + raise ValueError( + "The filter API only accepts the values of `pending` or " + "`latest` for string based filters" + ) + elif isinstance(filter_params, dict): + filter_id = self.request_manager.request_blocking("eth_newFilter", [filter_params]) + return LogFilter(self.web3, filter_id) + else: + raise ValueError("Must provide either a string or a valid filter object") + + def getFilterChanges(self, filter_id): + return self.request_manager.request_blocking("eth_getFilterChanges", [filter_id]) + + def getFilterLogs(self, filter_id): + return self.request_manager.request_blocking("eth_getFilterLogs", [filter_id]) + + def uninstallFilter(self, filter_id): + return self.request_manager.request_blocking("eth_uninstallFilter", [filter_id]) def contract(self, abi, address=None, **kwargs): contract_class = construct_contract_class(self.web3, abi, **kwargs) diff --git a/web3/utils/abi.py b/web3/utils/abi.py index 87f7a61..28bafb7 100644 --- a/web3/utils/abi.py +++ b/web3/utils/abi.py @@ -1,9 +1,16 @@ +import itertools + from eth_abi.abi import ( process_type, + encode_single, ) +from .encoding import encode_hex from .crypto import sha3 -from .string import coerce_args_to_bytes +from .string import ( + coerce_args_to_bytes, + coerce_return_to_text, +) from .formatting import ( add_0x_prefix, ) @@ -34,6 +41,18 @@ def get_abi_output_types(abi): return [arg['type'] for arg in abi['outputs']] +def get_abi_input_names(abi): + return [arg['name'] for arg in abi['inputs']] + + +def get_indexed_event_inputs(event_abi): + return [arg for arg in event_abi['inputs'] if arg['indexed'] is True] + + +def exclude_indexed_event_inputs(event_abi): + return [arg for arg in event_abi['inputs'] if arg['indexed'] is False] + + def filter_by_argument_count(arguments, contract_abi): return [ abi @@ -43,6 +62,16 @@ def filter_by_argument_count(arguments, contract_abi): ] +def filter_by_argument_name(argument_names, contract_abi): + return [ + abi + for abi in contract_abi + if set(argument_names).intersection( + get_abi_input_names(abi) + ) == set(argument_names) + ] + + def is_encodable(_type, value): try: base, sub, arrlist = _type @@ -124,7 +153,7 @@ def get_constructor_abi(contract_abi): raise ValueError("Found multiple constructors.") -def abi_to_4byte_function_selector(function_abi): +def abi_to_signature(function_abi): function_signature = "{fn_name}({fn_input_types})".format( fn_name=function_abi['name'], fn_input_types=','.join([ @@ -135,5 +164,95 @@ def abi_to_4byte_function_selector(function_abi): def function_abi_to_4byte_selector(function_abi): - function_signature = abi_to_4byte_function_selector(function_abi) + function_signature = abi_to_signature(function_abi) return add_0x_prefix(sha3(function_signature)[:8]) + + +def event_abi_to_log_topic(event_abi): + event_signature = abi_to_signature(event_abi) + return add_0x_prefix(sha3(event_signature)) + + +@coerce_return_to_text +def construct_event_topic_set(event_abi, arguments=None): + if arguments is None: + arguments = {} + if isinstance(arguments, (list, tuple)): + if len(arguments) != len(event_abi['inputs']): + raise ValueError( + "When passing an argument list, the number of arguments must " + "match the event constructor." + ) + arguments = { + arg['name']: [arg_value] + for arg, arg_value + in zip(event_abi['inputs'], arguments) + } + + normalized_args = { + key: value if is_array(value) else [value] + for key, value in arguments.items() + } + + event_topic = event_abi_to_log_topic(event_abi) + indexed_args = get_indexed_event_inputs(event_abi) + zipped_abi_and_args = [ + (arg, normalized_args.get(arg['name'], [None])) + for arg in indexed_args + ] + encoded_args = [ + [ + None if option is None else encode_hex(encode_single(arg['type'], option)) + for option in arg_options] + for arg, arg_options in zipped_abi_and_args + ] + + topics = [ + [event_topic] + list(permutation) + if any(value is not None for value in permutation) + else [event_topic] + for permutation in itertools.product(*encoded_args) + ] + return topics + + +@coerce_return_to_text +def construct_event_data_set(event_abi, arguments=None): + if arguments is None: + arguments = {} + if isinstance(arguments, (list, tuple)): + if len(arguments) != len(event_abi['inputs']): + raise ValueError( + "When passing an argument list, the number of arguments must " + "match the event constructor." + ) + arguments = { + arg['name']: [arg_value] + for arg, arg_value + in zip(event_abi['inputs'], arguments) + } + + normalized_args = { + key: value if is_array(value) else [value] + for key, value in arguments.items() + } + + indexed_args = exclude_indexed_event_inputs(event_abi) + zipped_abi_and_args = [ + (arg, normalized_args.get(arg['name'], [None])) + for arg in indexed_args + ] + encoded_args = [ + [ + None if option is None else encode_hex(encode_single(arg['type'], option)) + for option in arg_options] + for arg, arg_options in zipped_abi_and_args + ] + + topics = [ + list(permutation) + if any(value is not None for value in permutation) + else [] + for permutation in itertools.product(*encoded_args) + ] + return topics diff --git a/web3/utils/filters.py b/web3/utils/filters.py new file mode 100644 index 0000000..e762192 --- /dev/null +++ b/web3/utils/filters.py @@ -0,0 +1,123 @@ +import random +import gevent + +from .types import ( + is_string, + is_array, +) +from .abi import ( + construct_event_topic_set, + construct_event_data_set, +) + + +def construct_event_filter_params(event_abi, + contract_address=None, + argument_filters=None, + topics=None, + fromBlock=None, + toBlock=None, + address=None): + filter_params = {} + + if topics is None: + topic_set = construct_event_topic_set(event_abi, argument_filters) + else: + topic_set = [topics] + construct_event_topic_set(event_abi, argument_filters) + + if len(topic_set) == 1 and is_array(topic_set[0]): + filter_params['topics'] = topic_set[0] + else: + filter_params['topics'] = topic_set + + if address and contract_address: + if is_array(address): + filter_params['address'] = address + [contract_address] + elif is_string(address): + filter_params['address'] = [address, contract_address] + else: + raise ValueError( + "Unsupported type for `address` parameter: {0}".format(type(address)) + ) + elif address: + filter_params['address'] = address + elif contract_address: + filter_params['address'] = contract_address + + if fromBlock is not None: + filter_params['fromBlock'] = fromBlock + + if toBlock is not None: + filter_params['toBlock'] = toBlock + + return filter_params + + +class BaseFilter(gevent.Greenlet): + callbacks = None + running = None + stopped = False + + def __init__(self, web3, filter_id): + self.web3 = web3 + self.filter_id = filter_id + self.callbacks = [] + gevent.Greenlet.__init__(self) + + def __str__(self): + return "Filter for {0}".format(self.filter_id) + + def _run(self): + if self.stopped: + raise ValueError("Cannot restart a Filter") + self.running = True + + previous_logs = self.web3.eth.getFilterLogs(self.filter_id) + if previous_logs: + for log in previous_logs: + for callback_fn in self.callbacks: + callback_fn(log) + + while self.running: + changes = self.web3.eth.getFilterChanges(self.filter_id) + if changes: + for log in changes: + for callback_fn in self.callbacks: + callback_fn(log) + gevent.sleep(random.random()) + + def watch(self, *callbacks): + if self.stopped: + raise ValueError("Cannot watch on a filter that has been stopped") + self.callbacks.extend(callbacks) + + if not self.running: + self.start() + + def stop_watching(self, timeout=0): + self.running = False + self.stopped = True + self.web3.eth.uninstallFilter(self.filter_id) + self.join(timeout) + + stopWatching = stop_watching + + +class BlockFilter(BaseFilter): + pass + + +class TransactionFilter(BaseFilter): + pass + + +class LogFilter(BaseFilter): + def get(self, only_changes=True): + if self.running: + raise ValueError( + "Cannot call `get` on a filter object which is actively watching" + ) + if only_changes: + return self.web3.eth.getFilterChanges(self.filter_id) + else: + return self.web3.eth.getFilterChanges(self.filter_id)