From 66ff53e74055bf7a8502893097125c322ddacfc2 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 02:48:56 +0400 Subject: [PATCH 01/53] feat: add receipt events and ds-note decoding --- src/ape/api/transactions.py | 53 +++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 16e3be0a7d..74a74c045e 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -3,6 +3,9 @@ from typing import IO, TYPE_CHECKING, Iterator, List, Optional, Union from ethpm_types import HexBytes +from eth_abi import decode_abi +from eth_utils import decode_hex, keccak +from ethpm_types import ContractType from ethpm_types.abi import EventABI from evm_trace import TraceFrame from pydantic.fields import Field @@ -236,6 +239,56 @@ def decode_logs(self, abi: Union[EventABI, "ContractEvent"]) -> Iterator[Contrac yield from self.provider.network.ecosystem.decode_logs(abi, self.logs) + @property + def events(self): + contracts = {log["address"] for log in self.logs} + contract_types = {addr: self.chain_manager.contracts.get(addr) for addr in contracts} + decoded = [] + for log in self.logs: + note = self.decode_ds_note(contract_types[log["address"]], log) + if note: + decoded.append(note) + else: + try: + event_abi = next( + abi + for abi in contract_types[log["address"]].events + if keccak(text=abi.selector) == log["topics"][0] + ) + decoded.extend(self.provider.network.ecosystem.decode_logs(event_abi, [log])) + except StopIteration: + decoded.append(log) + + return decoded + + def decode_ds_note(self, contract_type: ContractType, log: dict) -> Optional[ContractLog]: + selector, tail = log["topics"][0][:4], log["topics"][0][4:] + if sum(tail): + return None + try: + abi = next( + func + for func in contract_type.mutable_methods + if selector == keccak(text=func.selector)[:4] + ) + except StopIteration: + return None + + # in versions of ds-note the data field uses either (uint256,bytes) or (bytes) encoding + # instead of guessing, assume the payload starts right after the selector + data = decode_hex(log["data"]) + input_types = [i.canonical_type for i in abi.inputs] + values = decode_abi(input_types, data[data.index(selector) + 4 :]) # noqa: E203 + + return ContractLog( # type: ignore + name=abi.name, + event_arguments={input.name: value for input, value in zip(abi.inputs, values)}, + transaction_hash=log["transactionHash"], + block_number=log["blockNumber"], + block_hash=log["blockHash"], + index=log["logIndex"], + ) + def await_confirmations(self) -> "ReceiptAPI": """ Wait for a transaction to be considered confirmed. From fc20c38a7dbe5632b0d7583058b98518d16467d9 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 02:48:56 +0400 Subject: [PATCH 02/53] refactor: move ds-note decoding to ecosystem api --- src/ape/api/transactions.py | 29 +--------------------------- src/ape_ethereum/ecosystem.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 74a74c045e..ee962f65f3 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -6,6 +6,7 @@ from eth_abi import decode_abi from eth_utils import decode_hex, keccak from ethpm_types import ContractType +from eth_utils import keccak from ethpm_types.abi import EventABI from evm_trace import TraceFrame from pydantic.fields import Field @@ -261,34 +262,6 @@ def events(self): return decoded - def decode_ds_note(self, contract_type: ContractType, log: dict) -> Optional[ContractLog]: - selector, tail = log["topics"][0][:4], log["topics"][0][4:] - if sum(tail): - return None - try: - abi = next( - func - for func in contract_type.mutable_methods - if selector == keccak(text=func.selector)[:4] - ) - except StopIteration: - return None - - # in versions of ds-note the data field uses either (uint256,bytes) or (bytes) encoding - # instead of guessing, assume the payload starts right after the selector - data = decode_hex(log["data"]) - input_types = [i.canonical_type for i in abi.inputs] - values = decode_abi(input_types, data[data.index(selector) + 4 :]) # noqa: E203 - - return ContractLog( # type: ignore - name=abi.name, - event_arguments={input.name: value for input, value in zip(abi.inputs, values)}, - transaction_hash=log["transactionHash"], - block_number=log["blockNumber"], - block_hash=log["blockHash"], - index=log["logIndex"], - ) - def await_confirmations(self) -> "ReceiptAPI": """ Wait for a transaction to be considered confirmed. diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 7a31542c2e..db50e02a3d 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -490,3 +490,39 @@ def decode_value(t, v) -> Any: block_hash=log["blockHash"], block_number=log["blockNumber"], ) # type: ignore + + def decode_ds_note(self, log: dict) -> Optional[ContractLog]: + """ + Decode anonymous events emitted by DSNote library. + """ + contract_type = self.chain_manager.contracts.get(log["address"]) + if contract_type is None: + raise DecodingError("contract type not available") + + # topic 0 encodes selector, the tail must be zeros + selector, tail = log["topics"][0][:4], log["topics"][0][4:] + if sum(tail): + return None + try: + abi = next( + func + for func in contract_type.mutable_methods + if selector == keccak(text=func.selector)[:4] + ) + except StopIteration: + return None + + # in versions of ds-note the data field uses either (uint256,bytes) or (bytes) encoding + # instead of guessing, assume the payload starts right after the selector + data = decode_hex(log["data"]) + input_types = [i.canonical_type for i in abi.inputs] + values = decode_abi(input_types, data[data.index(selector) + 4 :]) # noqa: E203 + + return ContractLog( # type: ignore + name=abi.name, + event_arguments={input.name: value for input, value in zip(abi.inputs, values)}, + transaction_hash=log["transactionHash"], + block_number=log["blockNumber"], + block_hash=log["blockHash"], + index=log["logIndex"], + ) From 41347d3852b2310f2e27fe37c3b0755efa3745fc Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 02:48:56 +0400 Subject: [PATCH 03/53] fix: consistent exceptions --- src/ape_ethereum/ecosystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index db50e02a3d..df1a88264e 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -497,7 +497,7 @@ def decode_ds_note(self, log: dict) -> Optional[ContractLog]: """ contract_type = self.chain_manager.contracts.get(log["address"]) if contract_type is None: - raise DecodingError("contract type not available") + raise DecodingError(f"ds-note: contract type for {log['address']} not found") # topic 0 encodes selector, the tail must be zeros selector, tail = log["topics"][0][:4], log["topics"][0][4:] @@ -510,7 +510,7 @@ def decode_ds_note(self, log: dict) -> Optional[ContractLog]: if selector == keccak(text=func.selector)[:4] ) except StopIteration: - return None + raise DecodingError(f"ds-note: selector '{selector.hex()}' not found") # in versions of ds-note the data field uses either (uint256,bytes) or (bytes) encoding # instead of guessing, assume the payload starts right after the selector From 4b4539afed441e2df0d65548f9924e3543cc787a Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 03:44:25 +0400 Subject: [PATCH 04/53] feat: optimize ds-log decoding --- src/ape/api/transactions.py | 28 ++++++++++++---------------- src/ape_ethereum/ecosystem.py | 17 +++++++++-------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index ee962f65f3..6eaf65ffbd 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -2,18 +2,15 @@ import time from typing import IO, TYPE_CHECKING, Iterator, List, Optional, Union -from ethpm_types import HexBytes -from eth_abi import decode_abi -from eth_utils import decode_hex, keccak -from ethpm_types import ContractType from eth_utils import keccak +from ethpm_types import HexBytes from ethpm_types.abi import EventABI from evm_trace import TraceFrame from pydantic.fields import Field from tqdm import tqdm # type: ignore from ape.api.explorers import ExplorerAPI -from ape.exceptions import TransactionError +from ape.exceptions import DecodingError, TransactionError from ape.logging import logger from ape.types import ContractLog, TransactionSignature from ape.utils import BaseInterfaceModel, abstractmethod, raises_not_implemented @@ -246,18 +243,17 @@ def events(self): contract_types = {addr: self.chain_manager.contracts.get(addr) for addr in contracts} decoded = [] for log in self.logs: - note = self.decode_ds_note(contract_types[log["address"]], log) - if note: - decoded.append(note) - else: + try: + event_abi = next( + abi + for abi in contract_types[log["address"]].events + if keccak(text=abi.selector) == log["topics"][0] + ) + decoded.extend(self.provider.network.ecosystem.decode_logs(event_abi, [log])) + except StopIteration: try: - event_abi = next( - abi - for abi in contract_types[log["address"]].events - if keccak(text=abi.selector) == log["topics"][0] - ) - decoded.extend(self.provider.network.ecosystem.decode_logs(event_abi, [log])) - except StopIteration: + decoded.append(self.decode_ds_note(contract_types[log["address"]], log)) + except DecodingError: decoded.append(log) return decoded diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index df1a88264e..973b7a8f1f 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -493,16 +493,17 @@ def decode_value(t, v) -> Any: def decode_ds_note(self, log: dict) -> Optional[ContractLog]: """ - Decode anonymous events emitted by DSNote library. + Decode anonymous events emitted by the DSNote library. """ + # the first topic encodes the function selector + selector, tail = log["topics"][0][:4], log["topics"][0][4:] + if sum(tail): + raise DecodingError("ds-note: non-zero bytes found after selector") + contract_type = self.chain_manager.contracts.get(log["address"]) if contract_type is None: raise DecodingError(f"ds-note: contract type for {log['address']} not found") - # topic 0 encodes selector, the tail must be zeros - selector, tail = log["topics"][0][:4], log["topics"][0][4:] - if sum(tail): - return None try: abi = next( func @@ -510,10 +511,10 @@ def decode_ds_note(self, log: dict) -> Optional[ContractLog]: if selector == keccak(text=func.selector)[:4] ) except StopIteration: - raise DecodingError(f"ds-note: selector '{selector.hex()}' not found") + raise DecodingError(f"ds-note: selector {selector.hex()} not found in {log['address']}") - # in versions of ds-note the data field uses either (uint256,bytes) or (bytes) encoding - # instead of guessing, assume the payload starts right after the selector + # ds-note data field uses either (uint256,bytes) or (bytes) encoding + # instead of guessing, assume the payload begins right after the selector data = decode_hex(log["data"]) input_types = [i.canonical_type for i in abi.inputs] values = decode_abi(input_types, data[data.index(selector) + 4 :]) # noqa: E203 From 269545423a41e918cd24c73978301c1b2bfddc8a Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 03:56:54 +0400 Subject: [PATCH 05/53] test: add makerdao vat contract --- .../mainnet/0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/functional/data/contracts/ethereum/mainnet/0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B.json diff --git a/tests/functional/data/contracts/ethereum/mainnet/0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B.json b/tests/functional/data/contracts/ethereum/mainnet/0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B.json new file mode 100644 index 0000000000..0d799b8f85 --- /dev/null +++ b/tests/functional/data/contracts/ethereum/mainnet/0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B.json @@ -0,0 +1 @@ +{"abi":[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg3","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"inputs":[],"name":"Line","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"can","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"dai","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"debt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"flux","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"i","type":"bytes32"},{"internalType":"address","name":"u","type":"address"},{"internalType":"int256","name":"rate","type":"int256"}],"name":"fold","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"fork","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"i","type":"bytes32"},{"internalType":"address","name":"u","type":"address"},{"internalType":"address","name":"v","type":"address"},{"internalType":"address","name":"w","type":"address"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"frob","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"name":"gem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"i","type":"bytes32"},{"internalType":"address","name":"u","type":"address"},{"internalType":"address","name":"v","type":"address"},{"internalType":"address","name":"w","type":"address"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"grab","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"heal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"hope","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"ilks","outputs":[{"internalType":"uint256","name":"Art","type":"uint256"},{"internalType":"uint256","name":"rate","type":"uint256"},{"internalType":"uint256","name":"spot","type":"uint256"},{"internalType":"uint256","name":"line","type":"uint256"},{"internalType":"uint256","name":"dust","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"init","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"move","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"nope","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"sin","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"usr","type":"address"},{"internalType":"int256","name":"wad","type":"int256"}],"name":"slip","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"u","type":"address"},{"internalType":"address","name":"v","type":"address"},{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"suck","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"name":"urns","outputs":[{"internalType":"uint256","name":"ink","type":"uint256"},{"internalType":"uint256","name":"art","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"vice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}],"contractName":"Vat"} \ No newline at end of file From 946536ad57929745e91ae7b4096df00470a8a130 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 04:19:58 +0400 Subject: [PATCH 06/53] test: ds note --- tests/functional/conftest.py | 18 +++++++++++++++++ tests/functional/test_ethereum.py | 32 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index d49e7b3652..cb375f7587 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -210,3 +210,21 @@ def dependency_config(temp_config): @pytest.fixture def base_projects_directory(): return BASE_PROJECTS_DIRECTORY + + +@pytest.fixture +def mainnet_contract(chain): + def contract_getter(address): + path = ( + Path(__file__).parent + / "data" + / "contracts" + / "ethereum" + / "mainnet" + / f"{address}.json" + ) + contract = ContractType.parse_file(path) + chain.contracts._local_contracts[address] = contract + return contract + + return contract_getter diff --git a/tests/functional/test_ethereum.py b/tests/functional/test_ethereum.py index 8f035e3752..868c2b37ef 100644 --- a/tests/functional/test_ethereum.py +++ b/tests/functional/test_ethereum.py @@ -1,4 +1,5 @@ import pytest +from eth_abi import encode_single from eth_typing import HexAddress, HexStr from hexbytes import HexBytes @@ -109,3 +110,34 @@ def test_block_handles_snake_case_parent_hash(eth_tester_provider, sender, recei redefined_block = Block.parse_obj(latest_block_dict) assert redefined_block.parent_hash == latest_block.parent_hash + + +def test_decode_ds_note(ethereum, mainnet_contract, chain): + mainnet_contract("0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B") + log = { + "address": "0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B", + "topics": [ + HexBytes("0x7608870300000000000000000000000000000000000000000000000000000000"), + HexBytes("0x5946492d41000000000000000000000000000000000000000000000000000000"), + HexBytes("0x0000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a45"), + HexBytes("0x0000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a45"), + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e0760887035946492d410000000000000000000000000000000000000000000000000000000000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a450000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a450000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a450000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffa050e82a57b7fc6b6020c00000000000000000000000000000000000000000000000000000000", # noqa: E501 + "blockNumber": 14623434, + "transactionHash": HexBytes( + "0xa322a9fd0e627e22bfe1b0877cca1d1f2e697d076007231d0b7a366d1a0fdd51" + ), + "transactionIndex": 333, + "blockHash": HexBytes("0x0fd77b0af3fa471aa040a02d4fcd1ec0a35122a4166d0bb7c31354e23823de49"), + "logIndex": 376, + "removed": False, + } + ilk = encode_single("bytes32", b"YFI-A") + assert ethereum.decode_ds_note(log).event_arguments == { + "i": ilk, + "u": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", + "v": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", + "w": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", + "dink": 0, + "dart": -7229675416790010075676148, + } From cc8be009ee58bd724c18558f48a40072287af84b Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 04:48:18 +0400 Subject: [PATCH 07/53] feat: make abi optional for decode_logs, add ds-note decoding --- src/ape/api/transactions.py | 48 ++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 6eaf65ffbd..140cf85ab4 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -2,7 +2,6 @@ import time from typing import IO, TYPE_CHECKING, Iterator, List, Optional, Union -from eth_utils import keccak from ethpm_types import HexBytes from ethpm_types.abi import EventABI from evm_trace import TraceFrame @@ -222,7 +221,9 @@ def raise_for_status(self): :class:`~api.providers.TransactionStatusEnum`. """ - def decode_logs(self, abi: Union[EventABI, "ContractEvent"]) -> Iterator[ContractLog]: + def decode_logs( + self, abi: Optional[Union[EventABI, "ContractEvent"]] = None + ) -> Iterator[ContractLog]: """ Decode the logs on the receipt. @@ -232,31 +233,28 @@ def decode_logs(self, abi: Union[EventABI, "ContractEvent"]) -> Iterator[Contrac Returns: Iterator[:class:`~ape.types.ContractLog`] """ - if not isinstance(abi, EventABI): - abi = abi.abi + if abi: + if not isinstance(abi, EventABI): + abi = abi.abi - yield from self.provider.network.ecosystem.decode_logs(abi, self.logs) - - @property - def events(self): - contracts = {log["address"] for log in self.logs} - contract_types = {addr: self.chain_manager.contracts.get(addr) for addr in contracts} - decoded = [] - for log in self.logs: - try: - event_abi = next( - abi - for abi in contract_types[log["address"]].events - if keccak(text=abi.selector) == log["topics"][0] - ) - decoded.extend(self.provider.network.ecosystem.decode_logs(event_abi, [log])) - except StopIteration: + yield from self.provider.network.ecosystem.decode_logs(abi, self.logs) + else: + # if abi is not provided, decode all events + contract_addresses = {log["address"] for log in self.logs} + contract_types = { + address: self.chain_manager.contracts.get(address) for address in contract_addresses + } + for log in self.logs: try: - decoded.append(self.decode_ds_note(contract_types[log["address"]], log)) - except DecodingError: - decoded.append(log) - - return decoded + event_abi = contract_types[log["address"]].events[ # type: ignore + log["topics"][0] + ] + yield from self.provider.network.ecosystem.decode_logs(event_abi, [log]) + except (StopIteration, KeyError): + try: + yield self.provider.network.ecosystem.decode_ds_note(log) # type: ignore + except (DecodingError, AttributeError): + yield log # type: ignore def await_confirmations(self) -> "ReceiptAPI": """ From 1622abcf1c881b454da7cf49a8419ecefcce69b5 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 04:48:39 +0400 Subject: [PATCH 08/53] test: decode events without providing abi --- tests/functional/test_contracts.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/functional/test_contracts.py b/tests/functional/test_contracts.py index de744d46b7..9f4d1f6b61 100644 --- a/tests/functional/test_contracts.py +++ b/tests/functional/test_contracts.py @@ -100,6 +100,14 @@ def assert_receipt_logs(receipt: ReceiptAPI, num: int): assert_receipt_logs(receipt_2, 3) +def test_contract_decode_logs_no_abi(owner, contract_instance): + receipt = contract_instance.setNumber(1, sender=owner) + events = list(receipt.decode_logs()) # no abi + assert len(events) == 1 + assert events[0].name == "NumberChange" + assert events[0].newNum == 1 + + def test_contract_logs_from_event_type(contract_instance, owner, assert_log_values): event_type = contract_instance.NumberChange From 1ec62147a4ef954d7872f98d12fd6988340586fc Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 04:54:02 +0400 Subject: [PATCH 09/53] refactor: simplify finding selector for ds-note --- src/ape_ethereum/ecosystem.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 973b7a8f1f..1091e2c3ad 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -505,12 +505,8 @@ def decode_ds_note(self, log: dict) -> Optional[ContractLog]: raise DecodingError(f"ds-note: contract type for {log['address']} not found") try: - abi = next( - func - for func in contract_type.mutable_methods - if selector == keccak(text=func.selector)[:4] - ) - except StopIteration: + abi = contract_type.mutable_methods[selector] + except KeyError: raise DecodingError(f"ds-note: selector {selector.hex()} not found in {log['address']}") # ds-note data field uses either (uint256,bytes) or (bytes) encoding From 0c6666e6acf0096128c8d1fae5b4b2aa4461c3b5 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 04:57:38 +0400 Subject: [PATCH 10/53] fix: black/flake8 clash --- src/ape_ethereum/ecosystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 1091e2c3ad..31c976a9c8 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -513,7 +513,8 @@ def decode_ds_note(self, log: dict) -> Optional[ContractLog]: # instead of guessing, assume the payload begins right after the selector data = decode_hex(log["data"]) input_types = [i.canonical_type for i in abi.inputs] - values = decode_abi(input_types, data[data.index(selector) + 4 :]) # noqa: E203 + start_index = data.index(selector) + 4 + values = decode_abi(input_types, data[start_index:]) return ContractLog( # type: ignore name=abi.name, From 7952e32b5a9a6d6a253a4b25cf56cfc40c0e8ade Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 05:01:26 +0400 Subject: [PATCH 11/53] feat: lazy fetching of contract types --- src/ape/api/transactions.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 140cf85ab4..7e71dbd412 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -240,15 +240,10 @@ def decode_logs( yield from self.provider.network.ecosystem.decode_logs(abi, self.logs) else: # if abi is not provided, decode all events - contract_addresses = {log["address"] for log in self.logs} - contract_types = { - address: self.chain_manager.contracts.get(address) for address in contract_addresses - } for log in self.logs: + contract_type = self.chain_manager.contracts.get(log["address"]) try: - event_abi = contract_types[log["address"]].events[ # type: ignore - log["topics"][0] - ] + event_abi = contract_type.events[log["topics"][0]] # type: ignore yield from self.provider.network.ecosystem.decode_logs(event_abi, [log]) except (StopIteration, KeyError): try: From 8fdc8dea456217a62568bfe3a016831443528603 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 7 Jul 2022 13:01:45 +0400 Subject: [PATCH 12/53] fix: catch decoding error --- src/ape/api/transactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 7e71dbd412..1ea3f38e5c 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -245,7 +245,7 @@ def decode_logs( try: event_abi = contract_type.events[log["topics"][0]] # type: ignore yield from self.provider.network.ecosystem.decode_logs(event_abi, [log]) - except (StopIteration, KeyError): + except (StopIteration, KeyError, DecodingError): try: yield self.provider.network.ecosystem.decode_ds_note(log) # type: ignore except (DecodingError, AttributeError): From 10c25df32e27bfdf1734e0332cd01652ebd8406b Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Thu, 7 Jul 2022 16:10:38 -0500 Subject: [PATCH 13/53] fix: handle None contract types --- src/ape/api/transactions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 1ea3f38e5c..b797440149 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -242,8 +242,12 @@ def decode_logs( # if abi is not provided, decode all events for log in self.logs: contract_type = self.chain_manager.contracts.get(log["address"]) + if not contract_type: + logger.warning(f"Failed to locate contract at '{log['address']}'.") + continue + try: - event_abi = contract_type.events[log["topics"][0]] # type: ignore + event_abi = contract_type.events[log["topics"][0]] yield from self.provider.network.ecosystem.decode_logs(event_abi, [log]) except (StopIteration, KeyError, DecodingError): try: From 10d062a50d3be5ebb8627de71852db15a1702e15 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Thu, 7 Jul 2022 16:12:45 -0500 Subject: [PATCH 14/53] chore: rm optional annotation --- src/ape_ethereum/ecosystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 31c976a9c8..0015c81751 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -491,7 +491,7 @@ def decode_value(t, v) -> Any: block_number=log["blockNumber"], ) # type: ignore - def decode_ds_note(self, log: dict) -> Optional[ContractLog]: + def decode_ds_note(self, log: dict) -> ContractLog: """ Decode anonymous events emitted by the DSNote library. """ From 4cdc91d52f8c3cf3c73f8aadbe95e5aa96ec56ee Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Thu, 7 Jul 2022 17:24:37 -0500 Subject: [PATCH 15/53] refactor: move stuff to ethereum and handle anon logs more --- src/ape/api/transactions.py | 42 ++++++++++++++++++++++++----------- src/ape_ethereum/ecosystem.py | 13 ++++++----- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index b797440149..b689214125 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -1,6 +1,6 @@ import sys import time -from typing import IO, TYPE_CHECKING, Iterator, List, Optional, Union +from typing import IO, TYPE_CHECKING, Dict, Iterator, List, Optional, Tuple, Union from ethpm_types import HexBytes from ethpm_types.abi import EventABI @@ -9,7 +9,7 @@ from tqdm import tqdm # type: ignore from ape.api.explorers import ExplorerAPI -from ape.exceptions import DecodingError, TransactionError +from ape.exceptions import TransactionError from ape.logging import logger from ape.types import ContractLog, TransactionSignature from ape.utils import BaseInterfaceModel, abstractmethod, raises_not_implemented @@ -239,21 +239,37 @@ def decode_logs( yield from self.provider.network.ecosystem.decode_logs(abi, self.logs) else: - # if abi is not provided, decode all events + # If ABI is not provided, decode all events + logs_map: Dict[str, Dict[str, Tuple[EventABI, List[Dict]]]] = {} for log in self.logs: - contract_type = self.chain_manager.contracts.get(log["address"]) + address = log["address"] + contract_type = self.chain_manager.contracts.get(address) + if not contract_type: - logger.warning(f"Failed to locate contract at '{log['address']}'.") + logger.warning(f"Failed to locate contract at '{address}'.") continue - try: - event_abi = contract_type.events[log["topics"][0]] - yield from self.provider.network.ecosystem.decode_logs(event_abi, [log]) - except (StopIteration, KeyError, DecodingError): - try: - yield self.provider.network.ecosystem.decode_ds_note(log) # type: ignore - except (DecodingError, AttributeError): - yield log # type: ignore + log_topics = log.get("topics", []) + if not log_topics: + raise ValueError("Missing 'topics' in log data") + + selector = log["topics"][0] + if selector not in contract_type.events: + raise ValueError("Log missing event selector.") + + # Track logs + event_abi = contract_type.events[selector] + event_selector = selector.hex() + if address not in logs_map: + logs_map[address] = {} + if event_selector not in logs_map[address]: + logs_map[address][event_selector] = (event_abi, [log]) + else: + logs_map[address][event_selector][1].append(log) + + for addr in logs_map: + for _, (evt_abi, log_items) in logs_map[addr].items(): + yield from self.provider.network.ecosystem.decode_logs(evt_abi, log_items) def await_confirmations(self) -> "ReceiptAPI": """ diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 0015c81751..3b5deb0504 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -424,7 +424,7 @@ def create_transaction(self, **kwargs) -> TransactionAPI: return txn_class(**kwargs) # type: ignore - def decode_logs(self, abi: EventABI, data: List[Dict]) -> Iterator["ContractLog"]: + def decode_logs(self, abi: EventABI, data: List[Dict]) -> Iterator[ContractLog]: if not abi.anonymous: event_id_bytes = keccak(to_bytes(text=abi.selector)) matching_logs = [log for log in data if log["topics"][0] == event_id_bytes] @@ -491,11 +491,12 @@ def decode_value(t, v) -> Any: block_number=log["blockNumber"], ) # type: ignore - def decode_ds_note(self, log: dict) -> ContractLog: + def _decode_ds_note(self, log: Dict) -> ContractLog: """ Decode anonymous events emitted by the DSNote library. """ - # the first topic encodes the function selector + + # The first topic encodes the function selector selector, tail = log["topics"][0][:4], log["topics"][0][4:] if sum(tail): raise DecodingError("ds-note: non-zero bytes found after selector") @@ -518,9 +519,9 @@ def decode_ds_note(self, log: dict) -> ContractLog: return ContractLog( # type: ignore name=abi.name, - event_arguments={input.name: value for input, value in zip(abi.inputs, values)}, - transaction_hash=log["transactionHash"], - block_number=log["blockNumber"], block_hash=log["blockHash"], + block_number=log["blockNumber"], + event_arguments={input.name: value for input, value in zip(abi.inputs, values)}, index=log["logIndex"], + transaction_hash=log["transactionHash"], ) From d876f3d692bc83e91329293e737f245b8cc22dd5 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Thu, 7 Jul 2022 17:31:58 -0500 Subject: [PATCH 16/53] test: add simple tests --- tests/functional/test_receipt.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_receipt.py b/tests/functional/test_receipt.py index e214bb39f2..96b23563dc 100644 --- a/tests/functional/test_receipt.py +++ b/tests/functional/test_receipt.py @@ -2,8 +2,8 @@ @pytest.fixture -def invoke_receipt(solidity_contract_instance, owner): - return solidity_contract_instance.setNumber(1, sender=owner) +def invoke_receipt(vyper_contract_instance, owner): + return vyper_contract_instance.setNumber(1, sender=owner) def test_show_trace(invoke_receipt): @@ -11,3 +11,23 @@ def test_show_trace(invoke_receipt): # such as ape-hardhat. with pytest.raises(NotImplementedError): invoke_receipt.show_trace() + + +def test_decode_logs(invoke_receipt): + logs = [log for log in invoke_receipt.decode_logs()] + assert len(logs) == 1 + assert logs[0].newNum == 1 + + +def test_decode_logs_specify_abi(invoke_receipt, vyper_contract_instance): + abi = vyper_contract_instance.NumberChange.abi + logs = [log for log in invoke_receipt.decode_logs(abi=abi)] + assert len(logs) == 1 + assert logs[0].newNum == 1 + + +def test_decode_logs_specify_abi_as_event(invoke_receipt, vyper_contract_instance): + abi = vyper_contract_instance.NumberChange + logs = [log for log in invoke_receipt.decode_logs(abi=abi)] + assert len(logs) == 1 + assert logs[0].newNum == 1 From 1660f863159acf509e4787dbc3beaf5d8ee16e1b Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Thu, 7 Jul 2022 17:34:37 -0500 Subject: [PATCH 17/53] test: make fixture --- tests/functional/conftest.py | 23 +++++++++++++++++++++++ tests/functional/test_ethereum.py | 22 ++-------------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index cb375f7587..94d371d49d 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -8,6 +8,7 @@ import yaml from eth.exceptions import HeaderNotFound from ethpm_types import ContractType +from hexbytes import HexBytes import ape from ape.api import EcosystemAPI, NetworkAPI, PluginConfig, TransactionAPI @@ -228,3 +229,25 @@ def contract_getter(address): return contract return contract_getter + + +@pytest.fixture +def ds_note(): + return { + "address": "0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B", + "topics": [ + HexBytes("0x7608870300000000000000000000000000000000000000000000000000000000"), + HexBytes("0x5946492d41000000000000000000000000000000000000000000000000000000"), + HexBytes("0x0000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a45"), + HexBytes("0x0000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a45"), + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e0760887035946492d410000000000000000000000000000000000000000000000000000000000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a450000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a450000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a450000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffa050e82a57b7fc6b6020c00000000000000000000000000000000000000000000000000000000", # noqa: E501 + "blockNumber": 14623434, + "transactionHash": HexBytes( + "0xa322a9fd0e627e22bfe1b0877cca1d1f2e697d076007231d0b7a366d1a0fdd51" + ), + "transactionIndex": 333, + "blockHash": HexBytes("0x0fd77b0af3fa471aa040a02d4fcd1ec0a35122a4166d0bb7c31354e23823de49"), + "logIndex": 376, + "removed": False, + } diff --git a/tests/functional/test_ethereum.py b/tests/functional/test_ethereum.py index 868c2b37ef..11cf4ee66e 100644 --- a/tests/functional/test_ethereum.py +++ b/tests/functional/test_ethereum.py @@ -112,28 +112,10 @@ def test_block_handles_snake_case_parent_hash(eth_tester_provider, sender, recei assert redefined_block.parent_hash == latest_block.parent_hash -def test_decode_ds_note(ethereum, mainnet_contract, chain): +def test_decode_ds_note(ethereum, mainnet_contract, chain, ds_note): mainnet_contract("0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B") - log = { - "address": "0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B", - "topics": [ - HexBytes("0x7608870300000000000000000000000000000000000000000000000000000000"), - HexBytes("0x5946492d41000000000000000000000000000000000000000000000000000000"), - HexBytes("0x0000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a45"), - HexBytes("0x0000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a45"), - ], - "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e0760887035946492d410000000000000000000000000000000000000000000000000000000000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a450000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a450000000000000000000000000abb839063ef747c8432b2acc60bf8f70ec09a450000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffa050e82a57b7fc6b6020c00000000000000000000000000000000000000000000000000000000", # noqa: E501 - "blockNumber": 14623434, - "transactionHash": HexBytes( - "0xa322a9fd0e627e22bfe1b0877cca1d1f2e697d076007231d0b7a366d1a0fdd51" - ), - "transactionIndex": 333, - "blockHash": HexBytes("0x0fd77b0af3fa471aa040a02d4fcd1ec0a35122a4166d0bb7c31354e23823de49"), - "logIndex": 376, - "removed": False, - } ilk = encode_single("bytes32", b"YFI-A") - assert ethereum.decode_ds_note(log).event_arguments == { + assert ethereum._decode_ds_note(ds_note).event_arguments == { "i": ilk, "u": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", "v": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", From c5ed093b887c09e12b24da3acf84e4b730d28db5 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Thu, 7 Jul 2022 19:49:31 -0500 Subject: [PATCH 18/53] test: tests for getting ds notes from receipt --- src/ape_ethereum/ecosystem.py | 35 ----------- src/ape_ethereum/transactions.py | 63 +++++++++++++++++-- tests/functional/conftest.py | 16 +++-- .../ethereum/local/ds_note_test.json | 1 + tests/functional/test_contract_event.py | 0 tests/functional/test_receipt.py | 26 ++++++++ 6 files changed, 96 insertions(+), 45 deletions(-) create mode 100644 tests/functional/data/contracts/ethereum/local/ds_note_test.json create mode 100644 tests/functional/test_contract_event.py diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 3b5deb0504..e3380b5d62 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -490,38 +490,3 @@ def decode_value(t, v) -> Any: block_hash=log["blockHash"], block_number=log["blockNumber"], ) # type: ignore - - def _decode_ds_note(self, log: Dict) -> ContractLog: - """ - Decode anonymous events emitted by the DSNote library. - """ - - # The first topic encodes the function selector - selector, tail = log["topics"][0][:4], log["topics"][0][4:] - if sum(tail): - raise DecodingError("ds-note: non-zero bytes found after selector") - - contract_type = self.chain_manager.contracts.get(log["address"]) - if contract_type is None: - raise DecodingError(f"ds-note: contract type for {log['address']} not found") - - try: - abi = contract_type.mutable_methods[selector] - except KeyError: - raise DecodingError(f"ds-note: selector {selector.hex()} not found in {log['address']}") - - # ds-note data field uses either (uint256,bytes) or (bytes) encoding - # instead of guessing, assume the payload begins right after the selector - data = decode_hex(log["data"]) - input_types = [i.canonical_type for i in abi.inputs] - start_index = data.index(selector) + 4 - values = decode_abi(input_types, data[start_index:]) - - return ContractLog( # type: ignore - name=abi.name, - block_hash=log["blockHash"], - block_number=log["blockNumber"], - event_arguments={input.name: value for input, value in zip(abi.inputs, values)}, - index=log["logIndex"], - transaction_hash=log["transactionHash"], - ) diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index b05ac0eb1a..e92742fbd7 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -1,6 +1,6 @@ import sys from enum import Enum, IntEnum -from typing import IO, Dict, List, Optional, Union +from typing import IO, Dict, Iterator, List, Optional, Union from eth_abi import decode_abi from eth_account import Account as EthAccount # type: ignore @@ -8,13 +8,16 @@ encode_transaction, serializable_unsigned_transaction_from_dict, ) -from eth_utils import keccak, to_int +from eth_utils import decode_hex, keccak, to_int from ethpm_types import HexBytes +from ethpm_types.abi import EventABI from pydantic import BaseModel, Field, root_validator, validator from rich.console import Console as RichConsole from ape.api import ReceiptAPI, TransactionAPI -from ape.exceptions import OutOfGasError, SignatureError, TransactionError +from ape.contracts import ContractEvent +from ape.exceptions import DecodingError, OutOfGasError, SignatureError, TransactionError +from ape.types import ContractLog from ape.utils import CallTraceParser, TraceStyles @@ -82,7 +85,7 @@ class StaticFeeTransaction(BaseTransaction): type: Union[str, int, bytes] = Field(TransactionType.STATIC.value, exclude=True) max_fee: Optional[int] = Field(None, exclude=True) - @root_validator(pre=True) + @root_validator(pre=True, allow_reuse=True) def calculate_read_only_max_fee(cls, values) -> Dict: # NOTE: Work-around, Pydantic doesn't handle calculated fields well. values["max_fee"] = values.get("gas_limit", 0) * values.get("gas_price", 0) @@ -100,7 +103,7 @@ class DynamicFeeTransaction(BaseTransaction): type: Union[int, str, bytes] = Field(TransactionType.DYNAMIC.value) access_list: List[AccessList] = Field(default_factory=list, alias="accessList") - @validator("type") + @validator("type", allow_reuse=True) def check_type(cls, value): if isinstance(value, TransactionType): @@ -118,7 +121,7 @@ class AccessListTransaction(BaseTransaction): type: Union[int, str, bytes] = Field(TransactionType.ACCESS_LIST.value) access_list: List[AccessList] = Field(default_factory=list, alias="accessList") - @validator("type") + @validator("type", allow_reuse=True) def check_type(cls, value): if isinstance(value, TransactionType): @@ -156,6 +159,54 @@ def raise_for_status(self): txn_hash = HexBytes(self.txn_hash).hex() raise TransactionError(message=f"Transaction '{txn_hash}' failed.") + def decode_logs( + self, abi: Optional[Union[EventABI, ContractEvent]] = None + ) -> Iterator[ContractLog]: + if not abi: + # Check for DS-Note library logs. + for log in self.logs: + try: + yield self._decode_ds_note(log) + except DecodingError: + continue + + return super().decode_logs(abi) + + def _decode_ds_note(self, log: Dict) -> ContractLog: + """ + Decode anonymous events emitted by the DSNote library. + """ + + # The first topic encodes the function selector + selector, tail = log["topics"][0][:4], log["topics"][0][4:] + if sum(tail): + raise DecodingError("ds-note: non-zero bytes found after selector") + + contract_type = self.chain_manager.contracts.get(log["address"]) + if contract_type is None: + raise DecodingError(f"ds-note: contract type for {log['address']} not found") + + try: + method_abi = contract_type.mutable_methods[selector] + except KeyError: + raise DecodingError(f"ds-note: selector {selector.hex()} not found in {log['address']}") + + # ds-note data field uses either (uint256,bytes) or (bytes) encoding + # instead of guessing, assume the payload begins right after the selector + data = decode_hex(log["data"]) + input_types = [i.canonical_type for i in method_abi.inputs] + start_index = data.index(selector) + 4 + values = decode_abi(input_types, data[start_index:]) + + return ContractLog( # type: ignore + name=method_abi.name, + block_hash=log["blockHash"], + block_number=log["blockNumber"], + event_arguments={input.name: value for input, value in zip(method_abi.inputs, values)}, + index=log["logIndex"], + transaction_hash=log["transactionHash"], + ) + def show_trace(self, verbose: bool = False, file: IO[str] = sys.stdout): tree_factory = CallTraceParser(self, verbose=verbose) call_tree = self.provider.get_call_tree(self.txn_hash) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 94d371d49d..da87b97724 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -17,17 +17,18 @@ from ape.managers.config import CONFIG_FILE_NAME -def _get_raw_contract(compiler: str) -> Dict: +def _get_raw_contract(name: str) -> Dict: here = Path(__file__).parent contracts_dir = here / "data" / "contracts" / "ethereum" / "local" - return json.loads((contracts_dir / f"{compiler}_contract.json").read_text()) + return json.loads((contracts_dir / f"{name}.json").read_text()) -RAW_SOLIDITY_CONTRACT_TYPE = _get_raw_contract("solidity") -RAW_VYPER_CONTRACT_TYPE = _get_raw_contract("vyper") +RAW_SOLIDITY_CONTRACT_TYPE = _get_raw_contract("solidity_contract") +RAW_VYPER_CONTRACT_TYPE = _get_raw_contract("vyper_contract") TEST_ADDRESS = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" BASE_PROJECTS_DIRECTORY = (Path(__file__).parent / "data" / "projects").absolute() PROJECT_WITH_LONG_CONTRACTS_FOLDER = BASE_PROJECTS_DIRECTORY / "LongContractsFolder" +DS_NOTE_TEST_CONTRACT_TYPE = _get_raw_contract("ds_note_test") class _ContractLogicError(ContractLogicError): @@ -164,6 +165,13 @@ def contract_instance(request, solidity_contract_instance, vyper_contract_instan return solidity_contract_instance if request.param == "solidity" else vyper_contract_instance +@pytest.fixture +def ds_note_test_contract(vyper_contract_type, owner, eth_tester_provider): + contract_type = ContractType.parse_obj(DS_NOTE_TEST_CONTRACT_TYPE) + contract_container = ContractContainer(contract_type=contract_type) + return contract_container.deploy(sender=owner) + + @pytest.fixture(scope="session") def temp_config(config): @contextmanager diff --git a/tests/functional/data/contracts/ethereum/local/ds_note_test.json b/tests/functional/data/contracts/ethereum/local/ds_note_test.json new file mode 100644 index 0000000000..2dc160b2f9 --- /dev/null +++ b/tests/functional/data/contracts/ethereum/local/ds_note_test.json @@ -0,0 +1 @@ +{"abi":[{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"guy","type":"address"},{"indexed":true,"internalType":"bytes32","name":"foo","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"bar","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"inputs":[],"name":"bar","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"a","type":"uint256"},{"internalType":"uint256","name":"b","type":"uint256"}],"name":"foo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"a","type":"uint256"},{"internalType":"uint256","name":"b","type":"uint256"},{"internalType":"uint256","name":"c","type":"uint256"}],"name":"foo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"a","type":"uint256"}],"name":"foo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"foo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"test_0","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"test_1","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"test_2","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"test_3","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"test_4","outputs":[],"stateMutability":"nonpayable","type":"function"}],"contractName":"DSNoteTest","deploymentBytecode":{"bytecode":"0x608060405234801561001057600080fd5b50610429806100206000396000f3fe6080604052600436106100915760003560e01c8063a3e2b70b11610059578063a3e2b70b14610122578063b9e2fa3514610137578063c29855781461014c578063e5935e3a14610161578063febb0f7e1461016d57600080fd5b806304bc52f8146100965780630b94e4f7146100b85780632fbebd38146100d8578063663bc990146100f8578063899eb49c1461010d575b600080fd5b3480156100a257600080fd5b506100b66100b1366004610356565b610175565b005b3480156100c457600080fd5b506100b66100d3366004610378565b6101b3565b3480156100e457600080fd5b506100b66100f33660046103a4565b6101f2565b34801561010457600080fd5b506100b661022f565b34801561011957600080fd5b506100b6610283565b34801561012e57600080fd5b506100b66102ad565b34801561014357600080fd5b506100b66102e8565b34801561015857600080fd5b506100b661031a565b3480156100b657600080fd5b6100b661031a565b60405160043590602435903490829084903390600080356001600160e01b031916916101a491879136906103bd565b60405180910390a45050505050565b60405160043590602435903490829084903390600080356001600160e01b031916916101e291879136906103bd565b60405180910390a4505050505050565b60405160043590602435903490829084903390600080356001600160e01b0319169161022191879136906103bd565b60405180910390a450505050565b6040516305f7d7a760e31b8152600160048201523090632fbebd38906024015b600060405180830381600087803b15801561026957600080fd5b505af115801561027d573d6000803e3d6000fd5b50505050565b60405162978a5f60e31b8152600160048201526002602482015230906304bc52f89060440161024f565b306001600160a01b031663c29855786040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561026957600080fd5b604051630b94e4f760e01b81526001600482015260026024820152600360448201523090630b94e4f79060640161024f565b60405160043590602435903490829084903390600080356001600160e01b0319169161034991879136906103bd565b60405180910390a4505050565b6000806040838503121561036957600080fd5b50508035926020909101359150565b60008060006060848603121561038d57600080fd5b505081359360208301359350604090920135919050565b6000602082840312156103b657600080fd5b5035919050565b83815260406020820152816040820152818360608301376000818301606090810191909152601f909201601f191601019291505056fea264697066735822122041bc79efc5b0bbc9372eefac7897741dc81a8fe37fac8759cc850946bb8bcec564736f6c634300080f0033"},"devdoc":{"kind":"dev","methods":{},"version":1},"runtimeBytecode":{"bytecode":"0x608060405234801561001057600080fd5b50610429806100206000396000f3fe6080604052600436106100915760003560e01c8063a3e2b70b11610059578063a3e2b70b14610122578063b9e2fa3514610137578063c29855781461014c578063e5935e3a14610161578063febb0f7e1461016d57600080fd5b806304bc52f8146100965780630b94e4f7146100b85780632fbebd38146100d8578063663bc990146100f8578063899eb49c1461010d575b600080fd5b3480156100a257600080fd5b506100b66100b1366004610356565b610175565b005b3480156100c457600080fd5b506100b66100d3366004610378565b6101b3565b3480156100e457600080fd5b506100b66100f33660046103a4565b6101f2565b34801561010457600080fd5b506100b661022f565b34801561011957600080fd5b506100b6610283565b34801561012e57600080fd5b506100b66102ad565b34801561014357600080fd5b506100b66102e8565b34801561015857600080fd5b506100b661031a565b3480156100b657600080fd5b6100b661031a565b60405160043590602435903490829084903390600080356001600160e01b031916916101a491879136906103bd565b60405180910390a45050505050565b60405160043590602435903490829084903390600080356001600160e01b031916916101e291879136906103bd565b60405180910390a4505050505050565b60405160043590602435903490829084903390600080356001600160e01b0319169161022191879136906103bd565b60405180910390a450505050565b6040516305f7d7a760e31b8152600160048201523090632fbebd38906024015b600060405180830381600087803b15801561026957600080fd5b505af115801561027d573d6000803e3d6000fd5b50505050565b60405162978a5f60e31b8152600160048201526002602482015230906304bc52f89060440161024f565b306001600160a01b031663c29855786040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561026957600080fd5b604051630b94e4f760e01b81526001600482015260026024820152600360448201523090630b94e4f79060640161024f565b60405160043590602435903490829084903390600080356001600160e01b0319169161034991879136906103bd565b60405180910390a4505050565b6000806040838503121561036957600080fd5b50508035926020909101359150565b60008060006060848603121561038d57600080fd5b505081359360208301359350604090920135919050565b6000602082840312156103b657600080fd5b5035919050565b83815260406020820152816040820152818360608301376000818301606090810191909152601f909201601f191601019291505056fea264697066735822122041bc79efc5b0bbc9372eefac7897741dc81a8fe37fac8759cc850946bb8bcec564736f6c634300080f0033"},"sourceId":"DSNoteTest.sol","userdoc":{"kind":"user","methods":{},"version":1}} diff --git a/tests/functional/test_contract_event.py b/tests/functional/test_contract_event.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/functional/test_receipt.py b/tests/functional/test_receipt.py index 96b23563dc..64f1a8a7f3 100644 --- a/tests/functional/test_receipt.py +++ b/tests/functional/test_receipt.py @@ -31,3 +31,29 @@ def test_decode_logs_specify_abi_as_event(invoke_receipt, vyper_contract_instanc logs = [log for log in invoke_receipt.decode_logs(abi=abi)] assert len(logs) == 1 assert logs[0].newNum == 1 + + +def test_decode_logs_with_ds_notes(ds_note_test_contract, owner): + contract = ds_note_test_contract + receipt = contract.test_0(sender=owner) + logs = [log for log in receipt.decode_logs()] + assert len(logs) == 1 + assert logs[0].name == "foo" + + receipt = contract.test_1(sender=owner) + logs = [log for log in receipt.decode_logs()] + assert len(logs) == 1 + assert logs[0].name == "foo" + assert logs[0].event_arguments == {"a": 1} + + receipt = contract.test_2(sender=owner) + logs = [log for log in receipt.decode_logs()] + assert len(logs) == 1 + assert logs[0].name == "foo" + assert logs[0].event_arguments == {"a": 1, "b": 2} + + receipt = contract.test_3(sender=owner) + logs = [log for log in receipt.decode_logs()] + assert len(logs) == 1 + assert logs[0].name == "foo" + assert logs[0].event_arguments == {"a": 1, "b": 2, "c": 3} From 0185ab24c6191d2a9df6a678e31aa19a0cc6e88d Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Thu, 7 Jul 2022 22:14:21 -0500 Subject: [PATCH 19/53] chore: del unneeded file --- tests/functional/test_contract_event.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/functional/test_contract_event.py diff --git a/tests/functional/test_contract_event.py b/tests/functional/test_contract_event.py deleted file mode 100644 index e69de29bb2..0000000000 From e0330509285cb18d51f069f24387d8bd5218f492 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Sun, 10 Jul 2022 09:03:24 -0500 Subject: [PATCH 20/53] fix: decode lib log optimization --- src/ape/api/transactions.py | 11 ++++++++++- src/ape_ethereum/transactions.py | 33 ++++++++++++-------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index b689214125..7f799fe091 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -255,7 +255,12 @@ def decode_logs( selector = log["topics"][0] if selector not in contract_type.events: - raise ValueError("Log missing event selector.") + # Likely a library log + library_log = self.decode_library_log(log) + if library_log: + yield library_log + + continue # Track logs event_abi = contract_type.events[selector] @@ -271,6 +276,10 @@ def decode_logs( for _, (evt_abi, log_items) in logs_map[addr].items(): yield from self.provider.network.ecosystem.decode_logs(evt_abi, log_items) + def decode_library_log(self, log: Dict) -> Optional[ContractLog]: + # Override in ecosystem implementations to handle common library logs, such as DSNote. + return None + def await_confirmations(self) -> "ReceiptAPI": """ Wait for a transaction to be considered confirmed. diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index e92742fbd7..7f6f676645 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -1,6 +1,6 @@ import sys from enum import Enum, IntEnum -from typing import IO, Dict, Iterator, List, Optional, Union +from typing import IO, Dict, List, Optional, Union from eth_abi import decode_abi from eth_account import Account as EthAccount # type: ignore @@ -10,13 +10,11 @@ ) from eth_utils import decode_hex, keccak, to_int from ethpm_types import HexBytes -from ethpm_types.abi import EventABI from pydantic import BaseModel, Field, root_validator, validator from rich.console import Console as RichConsole from ape.api import ReceiptAPI, TransactionAPI -from ape.contracts import ContractEvent -from ape.exceptions import DecodingError, OutOfGasError, SignatureError, TransactionError +from ape.exceptions import OutOfGasError, SignatureError, TransactionError from ape.types import ContractLog from ape.utils import CallTraceParser, TraceStyles @@ -159,20 +157,10 @@ def raise_for_status(self): txn_hash = HexBytes(self.txn_hash).hex() raise TransactionError(message=f"Transaction '{txn_hash}' failed.") - def decode_logs( - self, abi: Optional[Union[EventABI, ContractEvent]] = None - ) -> Iterator[ContractLog]: - if not abi: - # Check for DS-Note library logs. - for log in self.logs: - try: - yield self._decode_ds_note(log) - except DecodingError: - continue + def decode_library_log(self, log: Dict) -> Optional[ContractLog]: + return self._decode_ds_note(log) - return super().decode_logs(abi) - - def _decode_ds_note(self, log: Dict) -> ContractLog: + def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: """ Decode anonymous events emitted by the DSNote library. """ @@ -180,16 +168,19 @@ def _decode_ds_note(self, log: Dict) -> ContractLog: # The first topic encodes the function selector selector, tail = log["topics"][0][:4], log["topics"][0][4:] if sum(tail): - raise DecodingError("ds-note: non-zero bytes found after selector") + # non-zero bytes found after selector + return None contract_type = self.chain_manager.contracts.get(log["address"]) if contract_type is None: - raise DecodingError(f"ds-note: contract type for {log['address']} not found") + # contract type for {log['address']} not found + return None try: method_abi = contract_type.mutable_methods[selector] except KeyError: - raise DecodingError(f"ds-note: selector {selector.hex()} not found in {log['address']}") + # selector {selector.hex()} not found in {log['address']} + return None # ds-note data field uses either (uint256,bytes) or (bytes) encoding # instead of guessing, assume the payload begins right after the selector @@ -202,7 +193,7 @@ def _decode_ds_note(self, log: Dict) -> ContractLog: name=method_abi.name, block_hash=log["blockHash"], block_number=log["blockNumber"], - event_arguments={input.name: value for input, value in zip(method_abi.inputs, values)}, + event_arguments={i.name: value for i, value in zip(method_abi.inputs, values)}, index=log["logIndex"], transaction_hash=log["transactionHash"], ) From 44eee653b5f82531e6d954f3050c674303e1d969 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Sun, 10 Jul 2022 09:53:24 -0500 Subject: [PATCH 21/53] refactor: move decode lib logs to ecosystem --- src/ape/api/networks.py | 13 ++++++ src/ape/api/transactions.py | 6 +-- src/ape_ethereum/ecosystem.py | 41 +++++++++++++++++ src/ape_ethereum/transactions.py | 44 +------------------ tests/functional/conftest.py | 1 + tests/functional/test_chain.py | 4 +- .../{test_ethereum.py => test_ecosystem.py} | 4 +- 7 files changed, 60 insertions(+), 53 deletions(-) rename tests/functional/{test_ethereum.py => test_ecosystem.py} (96%) diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 5fc29741bb..d45ffcc4fe 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -311,6 +311,19 @@ def decode_logs(self, abi: EventABI, raw_logs: List[Dict]) -> Iterator[ContractL Iterator[:class:`~ape.types.ContractLog`] """ + def decode_library_log(self, log: Dict) -> Optional[ContractLog]: + """ + Decode logs that come from contract libraries such as DS-Note. + + Args: + log (Dict): Raw log data. + + Returns: + Optional[:class:`~ape.types.ContractLog`]: A contract log object + if it is able to decode one else ``None``. + """ + return None + @raises_not_implemented def decode_primitive_value( self, value: Any, output_type: Union[str, Tuple, List] diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 7f799fe091..6b2c281785 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -256,7 +256,7 @@ def decode_logs( selector = log["topics"][0] if selector not in contract_type.events: # Likely a library log - library_log = self.decode_library_log(log) + library_log = self.provider.network.ecosystem.decode_library_log(log) if library_log: yield library_log @@ -276,10 +276,6 @@ def decode_logs( for _, (evt_abi, log_items) in logs_map[addr].items(): yield from self.provider.network.ecosystem.decode_logs(evt_abi, log_items) - def decode_library_log(self, log: Dict) -> Optional[ContractLog]: - # Override in ecosystem implementations to handle common library logs, such as DSNote. - return None - def await_confirmations(self) -> "ReceiptAPI": """ Wait for a transaction to be considered confirmed. diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index e3380b5d62..c8e1ec0af9 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -490,3 +490,44 @@ def decode_value(t, v) -> Any: block_hash=log["blockHash"], block_number=log["blockNumber"], ) # type: ignore + + def decode_library_log(self, log: Dict) -> Optional[ContractLog]: + return self._decode_ds_note(log) + + def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: + """ + Decode anonymous events emitted by the DSNote library. + """ + + # The first topic encodes the function selector + selector, tail = log["topics"][0][:4], log["topics"][0][4:] + if sum(tail): + # non-zero bytes found after selector + return None + + contract_type = self.chain_manager.contracts.get(log["address"]) + if contract_type is None: + # contract type for {log['address']} not found + return None + + try: + method_abi = contract_type.mutable_methods[selector] + except KeyError: + # selector {selector.hex()} not found in {log['address']} + return None + + # ds-note data field uses either (uint256,bytes) or (bytes) encoding + # instead of guessing, assume the payload begins right after the selector + data = decode_hex(log["data"]) + input_types = [i.canonical_type for i in method_abi.inputs] + start_index = data.index(selector) + 4 + values = decode_abi(input_types, data[start_index:]) + + return ContractLog( # type: ignore + name=method_abi.name, + block_hash=log["blockHash"], + block_number=log["blockNumber"], + event_arguments={i.name: value for i, value in zip(method_abi.inputs, values)}, + index=log["logIndex"], + transaction_hash=log["transactionHash"], + ) diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index 7f6f676645..29b273e5bd 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -8,14 +8,13 @@ encode_transaction, serializable_unsigned_transaction_from_dict, ) -from eth_utils import decode_hex, keccak, to_int +from eth_utils import keccak, to_int from ethpm_types import HexBytes from pydantic import BaseModel, Field, root_validator, validator from rich.console import Console as RichConsole from ape.api import ReceiptAPI, TransactionAPI from ape.exceptions import OutOfGasError, SignatureError, TransactionError -from ape.types import ContractLog from ape.utils import CallTraceParser, TraceStyles @@ -157,47 +156,6 @@ def raise_for_status(self): txn_hash = HexBytes(self.txn_hash).hex() raise TransactionError(message=f"Transaction '{txn_hash}' failed.") - def decode_library_log(self, log: Dict) -> Optional[ContractLog]: - return self._decode_ds_note(log) - - def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: - """ - Decode anonymous events emitted by the DSNote library. - """ - - # The first topic encodes the function selector - selector, tail = log["topics"][0][:4], log["topics"][0][4:] - if sum(tail): - # non-zero bytes found after selector - return None - - contract_type = self.chain_manager.contracts.get(log["address"]) - if contract_type is None: - # contract type for {log['address']} not found - return None - - try: - method_abi = contract_type.mutable_methods[selector] - except KeyError: - # selector {selector.hex()} not found in {log['address']} - return None - - # ds-note data field uses either (uint256,bytes) or (bytes) encoding - # instead of guessing, assume the payload begins right after the selector - data = decode_hex(log["data"]) - input_types = [i.canonical_type for i in method_abi.inputs] - start_index = data.index(selector) + 4 - values = decode_abi(input_types, data[start_index:]) - - return ContractLog( # type: ignore - name=method_abi.name, - block_hash=log["blockHash"], - block_number=log["blockNumber"], - event_arguments={i.name: value for i, value in zip(method_abi.inputs, values)}, - index=log["logIndex"], - transaction_hash=log["transactionHash"], - ) - def show_trace(self, verbose: bool = False, file: IO[str] = sys.stdout): tree_factory = CallTraceParser(self, verbose=verbose) call_tree = self.provider.get_call_tree(self.txn_hash) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 2a1c0e9ab0..485f39d0f7 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -275,6 +275,7 @@ def ds_note(): } +@pytest.fixture def remove_disk_writes_deployments(chain): if chain.contracts._deployments_mapping_cache.exists(): chain.contracts._deployments_mapping_cache.unlink() diff --git a/tests/functional/test_chain.py b/tests/functional/test_chain.py index df9947aea5..3eb41585c9 100644 --- a/tests/functional/test_chain.py +++ b/tests/functional/test_chain.py @@ -316,9 +316,7 @@ def test_get_deployments_live( assert my_contracts_list_1[-1].address == deployed_contract_1.address -def test_get_multiple_deployments_live( - chain, project_with_contract, owner, remove_disk_writes_deployments, dummy_live_network -): +def test_get_multiple_deployments_live(chain, project_with_contract, owner, dummy_live_network): # Arrange starting_contracts_list_0 = chain.contracts.get_deployments(project_with_contract.ApeContract0) starting_contracts_list_1 = chain.contracts.get_deployments(project_with_contract.ApeContract1) diff --git a/tests/functional/test_ethereum.py b/tests/functional/test_ecosystem.py similarity index 96% rename from tests/functional/test_ethereum.py rename to tests/functional/test_ecosystem.py index 11cf4ee66e..5f8b094e51 100644 --- a/tests/functional/test_ethereum.py +++ b/tests/functional/test_ecosystem.py @@ -112,10 +112,10 @@ def test_block_handles_snake_case_parent_hash(eth_tester_provider, sender, recei assert redefined_block.parent_hash == latest_block.parent_hash -def test_decode_ds_note(ethereum, mainnet_contract, chain, ds_note): +def test_decode_library_log(ethereum, mainnet_contract, chain, ds_note): mainnet_contract("0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B") ilk = encode_single("bytes32", b"YFI-A") - assert ethereum._decode_ds_note(ds_note).event_arguments == { + assert ethereum.decode_library_log(ds_note).event_arguments == { "i": ilk, "u": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", "v": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", From 7d12143ade1b86344d5a74ec632dfb22bedbba2e Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Sun, 10 Jul 2022 09:56:19 -0500 Subject: [PATCH 22/53] chore: put back fixture use --- tests/functional/test_chain.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_chain.py b/tests/functional/test_chain.py index 3eb41585c9..df9947aea5 100644 --- a/tests/functional/test_chain.py +++ b/tests/functional/test_chain.py @@ -316,7 +316,9 @@ def test_get_deployments_live( assert my_contracts_list_1[-1].address == deployed_contract_1.address -def test_get_multiple_deployments_live(chain, project_with_contract, owner, dummy_live_network): +def test_get_multiple_deployments_live( + chain, project_with_contract, owner, remove_disk_writes_deployments, dummy_live_network +): # Arrange starting_contracts_list_0 = chain.contracts.get_deployments(project_with_contract.ApeContract0) starting_contracts_list_1 = chain.contracts.get_deployments(project_with_contract.ApeContract1) From 83536ffb4e8c739f5a02a0e31da41bcc0f802eee Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 19 Jul 2022 19:49:25 -0500 Subject: [PATCH 23/53] chore: resolve merge conflicts --- src/ape_ethereum/ecosystem.py | 8 ++++---- tests/functional/conftest.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index d45a74ba55..704c5c53c9 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -441,14 +441,14 @@ def decode_logs( continue event_arguments = abi.decode(topics, log["data"]) - yield ContractLog( - name=abi.event_name, + block_hash=log["blockHash"], + block_number=to_int(log["blockNumber"]), contract_address=self.decode_address(log["address"]), event_arguments=event_arguments, + log_index=log["logIndex"], + name=abi.event_name, transaction_hash=log["transactionHash"], - block_hash=log["blockHash"], - block_number=to_int(log["blockNumber"]), ) # type: ignore def decode_library_log(self, log: Dict) -> Optional[ContractLog]: diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 5f281eef0d..826ffc014f 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -273,6 +273,7 @@ def ds_note(): } +@pytest.fixture def chain_at_block_5(chain): snapshot_id = chain.snapshot() chain.mine(5) From eddf2d39fae9e988e113ca5d42b8c18168dea7d4 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 19 Jul 2022 19:55:34 -0500 Subject: [PATCH 24/53] fix: address regressions from merge --- src/ape/api/transactions.py | 2 +- src/ape_ethereum/ecosystem.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 5b7956ff9f..b47ba07449 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -275,7 +275,7 @@ def decode_logs( if address not in logs_map: logs_map[address] = {} if event_selector not in logs_map[address]: - logs_map[address][event_selector] = (event_abi, [log]) + logs_map[address][event_selector] = (event_abi, [log]) # type: ignore else: logs_map[address][event_selector][1].append(log) diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 704c5c53c9..54af43600a 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -483,11 +483,12 @@ def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: start_index = data.index(selector) + 4 values = abi_decode(input_types, data[start_index:]) - return ContractLog( # type: ignore - name=method_abi.name, + return ContractLog( block_hash=log["blockHash"], block_number=log["blockNumber"], + contract_address=self.decode_address(log["address"]), event_arguments={i.name: value for i, value in zip(method_abi.inputs, values)}, - index=log["logIndex"], + log_index=log["logIndex"], + name=method_abi.name, transaction_hash=log["transactionHash"], - ) + ) # type: ignore From 69f9f22f16b59e2acaa9222b1c3d008a4e034a3b Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 20 Jul 2022 07:48:11 -0500 Subject: [PATCH 25/53] refator: make list of abis --- src/ape/api/transactions.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index b47ba07449..6a8ec6c427 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -240,11 +240,9 @@ def decode_logs( if not isinstance(abi, (list, tuple)): abi = [abi] - for event_abi in abi: - if not isinstance(event_abi, EventABI): - event_abi = event_abi.abi + event_abis: List[EventABI] = [a.abi if not isinstance(a, EventABI) else a for a in abi] + yield from self.provider.network.ecosystem.decode_logs(event_abis, self.logs) - yield from self.provider.network.ecosystem.decode_logs(event_abi, self.logs) else: # If ABI is not provided, decode all events logs_map: Dict[str, Dict[str, Tuple[EventABI, List[Dict]]]] = {} From 3125cdd6ac4d3e1f56c121d1f625f46fa4d1173d Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 20 Jul 2022 08:11:34 -0500 Subject: [PATCH 26/53] chore: pr feedback --- src/ape/api/transactions.py | 22 +++++++++++++++++----- tests/functional/test_receipt.py | 8 ++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 6a8ec6c427..a572af301f 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -1,5 +1,6 @@ import sys import time +from concurrent.futures import ThreadPoolExecutor from typing import IO, TYPE_CHECKING, Dict, Iterator, List, Optional, Tuple, Union from ethpm_types import HexBytes @@ -11,7 +12,7 @@ from ape.api.explorers import ExplorerAPI from ape.exceptions import TransactionError from ape.logging import logger -from ape.types import ContractLog, TransactionSignature +from ape.types import AddressType, ContractLog, TransactionSignature from ape.utils import BaseInterfaceModel, abstractmethod, raises_not_implemented if TYPE_CHECKING: @@ -245,14 +246,25 @@ def decode_logs( else: # If ABI is not provided, decode all events - logs_map: Dict[str, Dict[str, Tuple[EventABI, List[Dict]]]] = {} - for log in self.logs: - address = log["address"] + addressess = {x["address"] for x in self.logs} + num_threads = min(len(addressess), 4) + contract_types = {} + + def get_contract_type(address: AddressType): contract_type = self.chain_manager.contracts.get(address) if not contract_type: logger.warning(f"Failed to locate contract at '{address}'.") - continue + else: + contract_types[address] = contract_type + + with ThreadPoolExecutor(num_threads) as pool: + pool.map(get_contract_type, addressess) + + logs_map: Dict[str, Dict[str, Tuple[EventABI, List[Dict]]]] = {} + for log in self.logs: + address = log["address"] + contract_type = contract_types[address] log_topics = log.get("topics", []) if not log_topics: diff --git a/tests/functional/test_receipt.py b/tests/functional/test_receipt.py index 8b24c26b94..2455c2b102 100644 --- a/tests/functional/test_receipt.py +++ b/tests/functional/test_receipt.py @@ -81,3 +81,11 @@ def test_decode_logs_multiple_event_types(owner, contract_instance, assert_log_v assert len(logs) == 2 assert logs[0].foo == 0 assert logs[1].bar == 1 + + +def test_decode_logs_unspecified_abi_gets_all_logs(owner, contract_instance): + receipt = contract_instance.fooAndBar(sender=owner) + logs = [log for log in receipt.decode_logs()] + assert len(logs) == 2 + assert logs[0].foo == 0 + assert logs[1].bar == 1 From 25d9288ab44eaed93f8e4802a75a2b69085cabba Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 20 Jul 2022 08:26:58 -0500 Subject: [PATCH 27/53] fix: base method param type correct --- src/ape/api/networks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index d45ffcc4fe..deaa044826 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -299,7 +299,9 @@ def encode_transaction( """ @abstractmethod - def decode_logs(self, abi: EventABI, raw_logs: List[Dict]) -> Iterator[ContractLog]: + def decode_logs( + self, events: Union[EventABI, List[EventABI]], logs: List[Dict] + ) -> Iterator["ContractLog"]: """ Decode any contract logs that match the given event ABI from the raw log data. From c22bb8a94fa1de68070bd83094e9d7aa256a7cc5 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 20 Jul 2022 08:44:44 -0500 Subject: [PATCH 28/53] fix: skip unfound contracttypes --- src/ape/api/transactions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index a572af301f..6319f9647e 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -264,8 +264,10 @@ def get_contract_type(address: AddressType): logs_map: Dict[str, Dict[str, Tuple[EventABI, List[Dict]]]] = {} for log in self.logs: address = log["address"] - contract_type = contract_types[address] + if address not in contract_types: + continue + contract_type = contract_types[address] log_topics = log.get("topics", []) if not log_topics: raise ValueError("Missing 'topics' in log data") From 0b924bccbd25091a8d7aa6356c82dd0f46a2dc4f Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 20 Jul 2022 11:48:58 -0500 Subject: [PATCH 29/53] refactor: use return from pool map --- src/ape/api/transactions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 6319f9647e..fe99bbc4aa 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -248,7 +248,6 @@ def decode_logs( # If ABI is not provided, decode all events addressess = {x["address"] for x in self.logs} num_threads = min(len(addressess), 4) - contract_types = {} def get_contract_type(address: AddressType): contract_type = self.chain_manager.contracts.get(address) @@ -256,10 +255,12 @@ def get_contract_type(address: AddressType): if not contract_type: logger.warning(f"Failed to locate contract at '{address}'.") else: - contract_types[address] = contract_type + return contract_type, address + contract_types = {} with ThreadPoolExecutor(num_threads) as pool: - pool.map(get_contract_type, addressess) + for contract_type, address in pool.map(get_contract_type, addressess): + contract_types[address] = contract_type logs_map: Dict[str, Dict[str, Tuple[EventABI, List[Dict]]]] = {} for log in self.logs: From fbc160ca5325c69db92630c754fc2ba0262b7664 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 20 Jul 2022 12:24:34 -0500 Subject: [PATCH 30/53] refactor: make get_all method in contract cache --- src/ape/api/transactions.py | 21 +++----------------- src/ape/managers/chain.py | 35 +++++++++++++++++++++++++++++++++- tests/functional/test_chain.py | 11 +++++++++++ 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index fe99bbc4aa..de77672c7f 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -1,6 +1,5 @@ import sys import time -from concurrent.futures import ThreadPoolExecutor from typing import IO, TYPE_CHECKING, Dict, Iterator, List, Optional, Tuple, Union from ethpm_types import HexBytes @@ -12,7 +11,7 @@ from ape.api.explorers import ExplorerAPI from ape.exceptions import TransactionError from ape.logging import logger -from ape.types import AddressType, ContractLog, TransactionSignature +from ape.types import ContractLog, TransactionSignature from ape.utils import BaseInterfaceModel, abstractmethod, raises_not_implemented if TYPE_CHECKING: @@ -246,22 +245,8 @@ def decode_logs( else: # If ABI is not provided, decode all events - addressess = {x["address"] for x in self.logs} - num_threads = min(len(addressess), 4) - - def get_contract_type(address: AddressType): - contract_type = self.chain_manager.contracts.get(address) - - if not contract_type: - logger.warning(f"Failed to locate contract at '{address}'.") - else: - return contract_type, address - - contract_types = {} - with ThreadPoolExecutor(num_threads) as pool: - for contract_type, address in pool.map(get_contract_type, addressess): - contract_types[address] = contract_type - + addresses = {x["address"] for x in self.logs} + contract_types = self.chain_manager.contracts.get_all(addresses) logs_map: Dict[str, Dict[str, Tuple[EventABI, List[Dict]]]] = {} for log in self.logs: address = log["address"] diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index 7a46b05c20..71f46d5e45 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -1,7 +1,8 @@ import json import time +from concurrent.futures import ThreadPoolExecutor from pathlib import Path -from typing import Callable, Dict, Iterator, List, Optional, Tuple, Union, cast +from typing import Callable, Collection, Dict, Iterator, List, Optional, Tuple, Union, cast import pandas as pd from ethpm_types import ContractType @@ -504,6 +505,38 @@ def __getitem__(self, address: AddressType) -> ContractType: return contract_type + def get_all( + self, addresses: Collection[AddressType], concurrency: Optional[int] = None + ) -> Dict[AddressType, ContractType]: + """ + Get contract types for all given addresses. + + Args: + addresses (List[AddressType): A list of addresses to get contract types for. + concurrency (Optional[int]): The number of threads to use. Defaults to + ``min(4, len(addresses))``. + + Returns: + Dict[AddressType, ContractType]: A mapping of addresses to their respective + contract types. + """ + + def get_contract_type(address: AddressType): + contract_type = self.get(address) + + if not contract_type: + logger.warning(f"Failed to locate contract at '{address}'.") + else: + return contract_type, address + + contract_types = {} + num_threads = concurrency if concurrency is not None else min(len(addresses), 4) + with ThreadPoolExecutor(num_threads) as pool: + for contract_type, address in pool.map(get_contract_type, addresses): + contract_types[address] = contract_type + + return contract_types + def get( self, address: AddressType, default: Optional[ContractType] = None ) -> Optional[ContractType]: diff --git a/tests/functional/test_chain.py b/tests/functional/test_chain.py index 83bcd972f0..8e6396713b 100644 --- a/tests/functional/test_chain.py +++ b/tests/functional/test_chain.py @@ -406,3 +406,14 @@ def test_poll_blocks_timeout( time.sleep(1.5) assert "Timed out waiting for new block (time_waited=1" in str(err.value) + + +def test_contracts_get_all(vyper_contract_instance, solidity_contract_instance, chain): + contract_map = chain.contracts.get_all( + [vyper_contract_instance.address, solidity_contract_instance.address] + ) + assert len(contract_map) == 2 + assert contract_map[vyper_contract_instance.address] == vyper_contract_instance.contract_type + assert ( + contract_map[solidity_contract_instance.address] == solidity_contract_instance.contract_type + ) From d1b8c89f18d8e3a7fd96402068fffe33661887b2 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 20 Jul 2022 13:04:20 -0500 Subject: [PATCH 31/53] fix: handle conversion and non contract types --- src/ape/managers/chain.py | 40 ++++++++++++++++++++-------------- tests/functional/test_chain.py | 25 +++++++++++++++++++-- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index 71f46d5e45..0caced2c64 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -522,6 +522,7 @@ def get_all( """ def get_contract_type(address: AddressType): + address = self.conversion_manager.convert(address, AddressType) contract_type = self.get(address) if not contract_type: @@ -532,8 +533,12 @@ def get_contract_type(address: AddressType): contract_types = {} num_threads = concurrency if concurrency is not None else min(len(addresses), 4) with ThreadPoolExecutor(num_threads) as pool: - for contract_type, address in pool.map(get_contract_type, addresses): - contract_types[address] = contract_type + + for result in pool.map(get_contract_type, addresses): + if not result: + continue + + contract_types[result[1]] = result[0] return contract_types @@ -556,11 +561,12 @@ def get( otherwise the default parameter. """ - contract_type = self._local_contracts.get(address) + address_key: AddressType = self.conversion_manager.convert(address, AddressType) + contract_type = self._local_contracts.get(address_key) if contract_type: if default and default != contract_type: # Replacing contract type - self._local_contracts[address] = default + self._local_contracts[address_key] = default return default return contract_type @@ -568,43 +574,45 @@ def get( if self._network.name == LOCAL_NETWORK_NAME: # Don't check disk-cache or explorer when using local if default: - self._local_contracts[address] = default + self._local_contracts[address_key] = default return default - contract_type = self._get_contract_type_from_disk(address) + contract_type = self._get_contract_type_from_disk(address_key) if not contract_type: # Contract could be a minimal proxy - proxy_info = self._local_proxies.get(address) or self._get_proxy_info_from_disk(address) + proxy_info = self._local_proxies.get(address_key) or self._get_proxy_info_from_disk( + address_key + ) if not proxy_info: - proxy_info = self.provider.network.ecosystem.get_proxy_info(address) + proxy_info = self.provider.network.ecosystem.get_proxy_info(address_key) if proxy_info and self._is_live_network: - self._cache_proxy_info_to_disk(address, proxy_info) + self._cache_proxy_info_to_disk(address_key, proxy_info) if proxy_info: - self._local_proxies[address] = proxy_info + self._local_proxies[address_key] = proxy_info return self.get(proxy_info.target) # Also gets cached to disk for faster lookup next time. - contract_type = self._get_contract_type_from_explorer(address) + contract_type = self._get_contract_type_from_explorer(address_key) # Cache locally for faster in-session look-up. if contract_type: - self._local_contracts[address] = contract_type + self._local_contracts[address_key] = contract_type if not contract_type: if default: - self._local_contracts[address] = default - self._cache_contract_to_disk(address, default) + self._local_contracts[address_key] = default + self._cache_contract_to_disk(address_key, default) return default if default and default != contract_type: # Replacing contract type - self._local_contracts[address] = default - self._cache_contract_to_disk(address, default) + self._local_contracts[address_key] = default + self._cache_contract_to_disk(address_key, default) return default return contract_type diff --git a/tests/functional/test_chain.py b/tests/functional/test_chain.py index 8e6396713b..7141b850d0 100644 --- a/tests/functional/test_chain.py +++ b/tests/functional/test_chain.py @@ -8,7 +8,7 @@ import ape from ape.api.networks import LOCAL_NETWORK_NAME from ape.contracts import ContractInstance -from ape.exceptions import APINotImplementedError, ChainError +from ape.exceptions import APINotImplementedError, ChainError, ConversionError @pytest.fixture(scope="module", autouse=True) @@ -410,10 +410,31 @@ def test_poll_blocks_timeout( def test_contracts_get_all(vyper_contract_instance, solidity_contract_instance, chain): contract_map = chain.contracts.get_all( - [vyper_contract_instance.address, solidity_contract_instance.address] + (vyper_contract_instance.address, solidity_contract_instance.address) ) assert len(contract_map) == 2 assert contract_map[vyper_contract_instance.address] == vyper_contract_instance.contract_type assert ( contract_map[solidity_contract_instance.address] == solidity_contract_instance.contract_type ) + + +def test_contracts_get_all_include_non_contract_address(vyper_contract_instance, chain, owner): + actual = chain.contracts.get_all((vyper_contract_instance.address, owner.address)) + assert len(actual) == 1 + assert actual[vyper_contract_instance.address] == vyper_contract_instance.contract_type + + +def test_contracts_get_all_attempts_to_convert(chain): + with pytest.raises(ConversionError): + chain.contracts.get_all(("test.eth",)) + + +def test_contracts_get_non_contract_address(chain, owner): + actual = chain.contracts.get(owner.address) + assert actual is None + + +def test_contracts_get_attempts_to_convert(chain): + with pytest.raises(ConversionError): + chain.contracts.get("test.eth") From afd7915ba906028e2a369661885801aa944dc8f6 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Wed, 20 Jul 2022 23:20:14 +0400 Subject: [PATCH 32/53] refactor: get contract type --- src/ape/managers/chain.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index 0caced2c64..e8c94d0feb 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -527,18 +527,19 @@ def get_contract_type(address: AddressType): if not contract_type: logger.warning(f"Failed to locate contract at '{address}'.") + return address, None else: - return contract_type, address + return address, contract_type contract_types = {} num_threads = concurrency if concurrency is not None else min(len(addresses), 4) with ThreadPoolExecutor(num_threads) as pool: - for result in pool.map(get_contract_type, addresses): - if not result: + for address, contract_type in pool.map(get_contract_type, addresses): + if contract_type is None: continue - contract_types[result[1]] = result[0] + contract_types[address] = contract_type return contract_types From 659191af91feb1d02fce0626c26232ddb4af0c51 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Wed, 20 Jul 2022 23:33:42 +0400 Subject: [PATCH 33/53] feat: exit early for empty code --- src/ape/managers/chain.py | 2 ++ src/ape_ethereum/ecosystem.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index e8c94d0feb..01040b27d6 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -563,6 +563,8 @@ def get( """ address_key: AddressType = self.conversion_manager.convert(address, AddressType) + if not self.provider.get_code(address_key): + return None contract_type = self._local_contracts.get(address_key) if contract_type: if default and default != contract_type: diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 54af43600a..99269c3620 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -125,6 +125,8 @@ def encode_address(cls, address: AddressType) -> RawAddress: def get_proxy_info(self, address: AddressType) -> Optional[ProxyInfo]: code = self.provider.get_code(address).hex()[2:] + if not code: + return None patterns = { ProxyType.Minimal: r"363d3d373d3d3d363d73(.{40})5af43d82803e903d91602b57fd5bf3", ProxyType.Vyper: r"366000600037611000600036600073(.{40})5af4602c57600080fd5b6110006000f3", # noqa: E501 From 09013664872085b8d1ba10263b48af4751bc2e02 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Wed, 20 Jul 2022 23:55:49 +0400 Subject: [PATCH 34/53] fix: exit early --- src/ape/managers/chain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index 01040b27d6..1d66eaf4d7 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -563,8 +563,6 @@ def get( """ address_key: AddressType = self.conversion_manager.convert(address, AddressType) - if not self.provider.get_code(address_key): - return None contract_type = self._local_contracts.get(address_key) if contract_type: if default and default != contract_type: @@ -598,6 +596,8 @@ def get( self._local_proxies[address_key] = proxy_info return self.get(proxy_info.target) + if not self.provider.get_code(address_key): + return None # Also gets cached to disk for faster lookup next time. contract_type = self._get_contract_type_from_explorer(address_key) From 2b6d167ace53e2fd70d5d164466516e9b320e0bc Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 21 Jul 2022 01:33:13 +0400 Subject: [PATCH 35/53] refactor: get multiple --- src/ape/managers/chain.py | 2 +- tests/functional/test_chain.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index 1d66eaf4d7..708fafe335 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -505,7 +505,7 @@ def __getitem__(self, address: AddressType) -> ContractType: return contract_type - def get_all( + def get_multiple( self, addresses: Collection[AddressType], concurrency: Optional[int] = None ) -> Dict[AddressType, ContractType]: """ diff --git a/tests/functional/test_chain.py b/tests/functional/test_chain.py index 7141b850d0..052d9be504 100644 --- a/tests/functional/test_chain.py +++ b/tests/functional/test_chain.py @@ -409,7 +409,7 @@ def test_poll_blocks_timeout( def test_contracts_get_all(vyper_contract_instance, solidity_contract_instance, chain): - contract_map = chain.contracts.get_all( + contract_map = chain.contracts.get_multiple( (vyper_contract_instance.address, solidity_contract_instance.address) ) assert len(contract_map) == 2 @@ -420,14 +420,14 @@ def test_contracts_get_all(vyper_contract_instance, solidity_contract_instance, def test_contracts_get_all_include_non_contract_address(vyper_contract_instance, chain, owner): - actual = chain.contracts.get_all((vyper_contract_instance.address, owner.address)) + actual = chain.contracts.get_multiple((vyper_contract_instance.address, owner.address)) assert len(actual) == 1 assert actual[vyper_contract_instance.address] == vyper_contract_instance.contract_type def test_contracts_get_all_attempts_to_convert(chain): with pytest.raises(ConversionError): - chain.contracts.get_all(("test.eth",)) + chain.contracts.get_multiple(("test.eth",)) def test_contracts_get_non_contract_address(chain, owner): From 832616e78a9a167d96a7df8ca7564672e7c267f5 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 21 Jul 2022 01:45:48 +0400 Subject: [PATCH 36/53] fix: decode in correct order --- src/ape/api/transactions.py | 41 ++++++++++++------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index de77672c7f..09517f4f2c 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -1,7 +1,8 @@ import sys import time -from typing import IO, TYPE_CHECKING, Dict, Iterator, List, Optional, Tuple, Union +from typing import IO, TYPE_CHECKING, Iterator, List, Optional, Union +from eth_utils import keccak from ethpm_types import HexBytes from ethpm_types.abi import EventABI from evm_trace import TraceFrame @@ -246,40 +247,24 @@ def decode_logs( else: # If ABI is not provided, decode all events addresses = {x["address"] for x in self.logs} - contract_types = self.chain_manager.contracts.get_all(addresses) - logs_map: Dict[str, Dict[str, Tuple[EventABI, List[Dict]]]] = {} + contract_types = self.chain_manager.contracts.get_multiple(addresses) + # address → selector → abi + selectors = { + address: {keccak(text=abi.selector): abi for abi in contract.events} + for address, contract in contract_types.items() + } for log in self.logs: - address = log["address"] - if address not in contract_types: + if log["address"] not in selectors: continue - - contract_type = contract_types[address] - log_topics = log.get("topics", []) - if not log_topics: - raise ValueError("Missing 'topics' in log data") - - selector = log["topics"][0] - if selector not in contract_type.events: + try: + event_abi = selectors[log["address"]][log["topics"][0]] + except KeyError: # Likely a library log library_log = self.provider.network.ecosystem.decode_library_log(log) if library_log: yield library_log - - continue - - # Track logs - event_abi = contract_type.events[selector] - event_selector = selector.hex() - if address not in logs_map: - logs_map[address] = {} - if event_selector not in logs_map[address]: - logs_map[address][event_selector] = (event_abi, [log]) # type: ignore else: - logs_map[address][event_selector][1].append(log) - - for addr in logs_map: - for _, (evt_abi, log_items) in logs_map[addr].items(): - yield from self.provider.network.ecosystem.decode_logs(evt_abi, log_items) + yield from self.provider.network.ecosystem.decode_logs([event_abi], [log]) def await_confirmations(self) -> "ReceiptAPI": """ From 9a20e3096b3c83831e3576bce90c818259b696db Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Fri, 22 Jul 2022 04:44:00 +0400 Subject: [PATCH 37/53] feat: add event selector to ecosystem --- src/ape/api/networks.py | 6 ++++++ src/ape/api/transactions.py | 13 +++++++++---- src/ape_ethereum/ecosystem.py | 7 ++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index deaa044826..b6aff69329 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -298,6 +298,12 @@ def encode_transaction( class:`~ape.api.transactions.TransactionAPI` """ + @raises_not_implemented + def event_selector(self, abi: EventABI) -> str: + """ + Convert EventABI to a selector which usually appears as topics[0]. + """ + @abstractmethod def decode_logs( self, events: Union[EventABI, List[EventABI]], logs: List[Dict] diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 09517f4f2c..fd1b592299 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -2,7 +2,7 @@ import time from typing import IO, TYPE_CHECKING, Iterator, List, Optional, Union -from eth_utils import keccak +from eth_utils import encode_hex from ethpm_types import HexBytes from ethpm_types.abi import EventABI from evm_trace import TraceFrame @@ -250,14 +250,19 @@ def decode_logs( contract_types = self.chain_manager.contracts.get_multiple(addresses) # address → selector → abi selectors = { - address: {keccak(text=abi.selector): abi for abi in contract.events} + address: { + self.provider.network.ecosystem.event_selector(abi): abi + for abi in contract.events + } for address, contract in contract_types.items() } for log in self.logs: - if log["address"] not in selectors: + contract_address = log["address"] + if contract_address not in selectors: continue try: - event_abi = selectors[log["address"]][log["topics"][0]] + selector = encode_hex(log["topics"][0]) + event_abi = selectors[contract_address][selector] except KeyError: # Likely a library log library_log = self.provider.network.ecosystem.decode_library_log(log) diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 99269c3620..d4c6414346 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -418,15 +418,16 @@ def create_transaction(self, **kwargs) -> TransactionAPI: return txn_class(**kwargs) # type: ignore + def event_selector(self, abi: EventABI) -> str: + return encode_hex(keccak(text=abi.selector)) + def decode_logs( self, events: Union[EventABI, List[EventABI]], logs: List[Dict] ) -> Iterator["ContractLog"]: if not isinstance(events, list): events = [events] - abi_inputs = { - encode_hex(keccak(text=abi.selector)): LogInputABICollection(abi) for abi in events - } + abi_inputs = {self.event_selector(abi): LogInputABICollection(abi) for abi in events} for log in logs: if log.get("anonymous"): From 08323a608ceafef77995c27f5acbed7330f7604d Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Fri, 22 Jul 2022 08:51:32 -0500 Subject: [PATCH 38/53] refactor: make decode_logs method in ape_eth txn --- src/ape/api/networks.py | 10 ++---- src/ape/api/transactions.py | 58 ++------------------------------ src/ape_ethereum/ecosystem.py | 7 ++-- src/ape_ethereum/transactions.py | 53 +++++++++++++++++++++++++++-- 4 files changed, 58 insertions(+), 70 deletions(-) diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index b6aff69329..0d1c45b272 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -298,12 +298,6 @@ def encode_transaction( class:`~ape.api.transactions.TransactionAPI` """ - @raises_not_implemented - def event_selector(self, abi: EventABI) -> str: - """ - Convert EventABI to a selector which usually appears as topics[0]. - """ - @abstractmethod def decode_logs( self, events: Union[EventABI, List[EventABI]], logs: List[Dict] @@ -312,8 +306,8 @@ def decode_logs( Decode any contract logs that match the given event ABI from the raw log data. Args: - abi (EventABI): The event producing the logs. - raw_logs (List[Dict]): A list of raw log data from the chain. + events (Union[EventABI, List[EventABI]]): Event definitions to decode. + logs (List[Dict]): A list of raw log data from the chain. Returns: Iterator[:class:`~ape.types.ContractLog`] diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index fd1b592299..28b6a4f659 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -1,10 +1,8 @@ import sys import time -from typing import IO, TYPE_CHECKING, Iterator, List, Optional, Union +from typing import IO, Iterator, List, Optional, Union -from eth_utils import encode_hex from ethpm_types import HexBytes -from ethpm_types.abi import EventABI from evm_trace import TraceFrame from pydantic.fields import Field from tqdm import tqdm # type: ignore @@ -12,12 +10,9 @@ from ape.api.explorers import ExplorerAPI from ape.exceptions import TransactionError from ape.logging import logger -from ape.types import ContractLog, TransactionSignature +from ape.types import TransactionSignature from ape.utils import BaseInterfaceModel, abstractmethod, raises_not_implemented -if TYPE_CHECKING: - from ape.contracts import ContractEvent - class TransactionAPI(BaseInterfaceModel): """ @@ -222,55 +217,6 @@ def raise_for_status(self): :class:`~api.providers.TransactionStatusEnum`. """ - def decode_logs( - self, - abi: Optional[ - Union[List[Union[EventABI, "ContractEvent"]], Union[EventABI, "ContractEvent"]] - ] = None, - ) -> Iterator[ContractLog]: - """ - Decode the logs on the receipt. - - Args: - abi (``EventABI``): The ABI of the event to decode into logs. - - Returns: - Iterator[:class:`~ape.types.ContractLog`] - """ - if abi: - if not isinstance(abi, (list, tuple)): - abi = [abi] - - event_abis: List[EventABI] = [a.abi if not isinstance(a, EventABI) else a for a in abi] - yield from self.provider.network.ecosystem.decode_logs(event_abis, self.logs) - - else: - # If ABI is not provided, decode all events - addresses = {x["address"] for x in self.logs} - contract_types = self.chain_manager.contracts.get_multiple(addresses) - # address → selector → abi - selectors = { - address: { - self.provider.network.ecosystem.event_selector(abi): abi - for abi in contract.events - } - for address, contract in contract_types.items() - } - for log in self.logs: - contract_address = log["address"] - if contract_address not in selectors: - continue - try: - selector = encode_hex(log["topics"][0]) - event_abi = selectors[contract_address][selector] - except KeyError: - # Likely a library log - library_log = self.provider.network.ecosystem.decode_library_log(log) - if library_log: - yield library_log - else: - yield from self.provider.network.ecosystem.decode_logs([event_abi], [log]) - def await_confirmations(self) -> "ReceiptAPI": """ Wait for a transaction to be considered confirmed. diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index d4c6414346..99269c3620 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -418,16 +418,15 @@ def create_transaction(self, **kwargs) -> TransactionAPI: return txn_class(**kwargs) # type: ignore - def event_selector(self, abi: EventABI) -> str: - return encode_hex(keccak(text=abi.selector)) - def decode_logs( self, events: Union[EventABI, List[EventABI]], logs: List[Dict] ) -> Iterator["ContractLog"]: if not isinstance(events, list): events = [events] - abi_inputs = {self.event_selector(abi): LogInputABICollection(abi) for abi in events} + abi_inputs = { + encode_hex(keccak(text=abi.selector)): LogInputABICollection(abi) for abi in events + } for log in logs: if log.get("anonymous"): diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index 486e749ac5..86e48de25e 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -1,6 +1,6 @@ import sys from enum import Enum, IntEnum -from typing import IO, Dict, List, Optional, Union +from typing import IO, Dict, Iterator, List, Optional, Union from eth_abi import decode_abi from eth_account import Account as EthAccount # type: ignore @@ -8,13 +8,16 @@ encode_transaction, serializable_unsigned_transaction_from_dict, ) -from eth_utils import keccak, to_int +from eth_utils import encode_hex, keccak, to_int from ethpm_types import HexBytes +from ethpm_types.abi import EventABI from pydantic import BaseModel, Field, root_validator, validator from rich.console import Console as RichConsole from ape.api import ReceiptAPI, TransactionAPI +from ape.contracts import ContractEvent from ape.exceptions import OutOfGasError, SignatureError, TransactionError +from ape.types import ContractLog from ape.utils import CallTraceParser, TraceStyles @@ -177,3 +180,49 @@ def show_trace(self, verbose: bool = False, file: IO[str] = sys.stdout): console.print(f"txn.origin=[{TraceStyles.CONTRACTS}]{self.sender}[/]") console.print(root) + + def decode_logs( + self, + abi: Optional[ + Union[List[Union[EventABI, "ContractEvent"]], Union[EventABI, "ContractEvent"]] + ] = None, + ) -> Iterator[ContractLog]: + """ + Decode the logs on the receipt. + + Args: + abi (``EventABI``): The ABI of the event to decode into logs. + + Returns: + Iterator[:class:`~ape.types.ContractLog`] + """ + if abi: + if not isinstance(abi, (list, tuple)): + abi = [abi] + + event_abis: List[EventABI] = [a.abi if not isinstance(a, EventABI) else a for a in abi] + yield from self.provider.network.ecosystem.decode_logs(event_abis, self.logs) + + else: + # If ABI is not provided, decode all events + addresses = {x["address"] for x in self.logs} + contract_types = self.chain_manager.contracts.get_multiple(addresses) + # address → selector → abi + selectors = { + address: {encode_hex(keccak(text=abi.selector)): abi for abi in contract.events} + for address, contract in contract_types.items() + } + for log in self.logs: + contract_address = log["address"] + if contract_address not in selectors: + continue + try: + selector = encode_hex(log["topics"][0]) + event_abi = selectors[contract_address][selector] + except KeyError: + # Likely a library log + library_log = self.provider.network.ecosystem.decode_library_log(log) + if library_log: + yield library_log + else: + yield from self.provider.network.ecosystem.decode_logs([event_abi], [log]) From 57bb7b32c466a464728f4f056682d7bc61e3eff4 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Fri, 22 Jul 2022 09:05:23 -0500 Subject: [PATCH 39/53] fix: add abstract method --- src/ape/api/transactions.py | 25 +++++++++++++++++++++++-- src/ape_ethereum/transactions.py | 9 --------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 28b6a4f659..5a7cbe179a 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -1,8 +1,9 @@ import sys import time -from typing import IO, Iterator, List, Optional, Union +from typing import IO, TYPE_CHECKING, Iterator, List, Optional, Union from ethpm_types import HexBytes +from ethpm_types.abi import EventABI from evm_trace import TraceFrame from pydantic.fields import Field from tqdm import tqdm # type: ignore @@ -10,9 +11,12 @@ from ape.api.explorers import ExplorerAPI from ape.exceptions import TransactionError from ape.logging import logger -from ape.types import TransactionSignature +from ape.types import ContractLog, TransactionSignature from ape.utils import BaseInterfaceModel, abstractmethod, raises_not_implemented +if TYPE_CHECKING: + from ape.contracts import ContractEvent + class TransactionAPI(BaseInterfaceModel): """ @@ -211,6 +215,23 @@ def _confirmations_occurred(self) -> int: return latest_block.number - self.block_number + @abstractmethod + def decode_logs( + self, + abi: Optional[ + Union[List[Union[EventABI, "ContractEvent"]], Union[EventABI, "ContractEvent"]] + ] = None, + ) -> Iterator[ContractLog]: + """ + Decode the logs on the receipt. + + Args: + abi (``EventABI``): The ABI of the event to decode into logs. + + Returns: + Iterator[:class:`~ape.types.ContractLog`] + """ + def raise_for_status(self): """ Handle provider-specific errors regarding a non-successful diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index 86e48de25e..d27d903a35 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -187,15 +187,6 @@ def decode_logs( Union[List[Union[EventABI, "ContractEvent"]], Union[EventABI, "ContractEvent"]] ] = None, ) -> Iterator[ContractLog]: - """ - Decode the logs on the receipt. - - Args: - abi (``EventABI``): The ABI of the event to decode into logs. - - Returns: - Iterator[:class:`~ape.types.ContractLog`] - """ if abi: if not isinstance(abi, (list, tuple)): abi = [abi] From 2861def845b3108020caf0ec4d32c9cdce489c31 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Mon, 25 Jul 2022 07:48:50 -0500 Subject: [PATCH 40/53] feat: rm get lib logs method --- src/ape/api/networks.py | 13 --------- src/ape_ethereum/ecosystem.py | 42 ------------------------------ src/ape_ethereum/transactions.py | 39 +++++++++++++++++++++++++-- tests/functional/test_ecosystem.py | 14 ---------- 4 files changed, 37 insertions(+), 71 deletions(-) diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 0d1c45b272..5aa7a4f636 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -313,19 +313,6 @@ def decode_logs( Iterator[:class:`~ape.types.ContractLog`] """ - def decode_library_log(self, log: Dict) -> Optional[ContractLog]: - """ - Decode logs that come from contract libraries such as DS-Note. - - Args: - log (Dict): Raw log data. - - Returns: - Optional[:class:`~ape.types.ContractLog`]: A contract log object - if it is able to decode one else ``None``. - """ - return None - @raises_not_implemented def decode_primitive_value( self, value: Any, output_type: Union[str, Tuple, List] diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 99269c3620..1817f692a0 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -452,45 +452,3 @@ def decode_logs( name=abi.event_name, transaction_hash=log["transactionHash"], ) # type: ignore - - def decode_library_log(self, log: Dict) -> Optional[ContractLog]: - return self._decode_ds_note(log) - - def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: - """ - Decode anonymous events emitted by the DSNote library. - """ - - # The first topic encodes the function selector - selector, tail = log["topics"][0][:4], log["topics"][0][4:] - if sum(tail): - # non-zero bytes found after selector - return None - - contract_type = self.chain_manager.contracts.get(log["address"]) - if contract_type is None: - # contract type for {log['address']} not found - return None - - try: - method_abi = contract_type.mutable_methods[selector] - except KeyError: - # selector {selector.hex()} not found in {log['address']} - return None - - # ds-note data field uses either (uint256,bytes) or (bytes) encoding - # instead of guessing, assume the payload begins right after the selector - data = decode_hex(log["data"]) - input_types = [i.canonical_type for i in method_abi.inputs] - start_index = data.index(selector) + 4 - values = abi_decode(input_types, data[start_index:]) - - return ContractLog( - block_hash=log["blockHash"], - block_number=log["blockNumber"], - contract_address=self.decode_address(log["address"]), - event_arguments={i.name: value for i, value in zip(method_abi.inputs, values)}, - log_index=log["logIndex"], - name=method_abi.name, - transaction_hash=log["transactionHash"], - ) # type: ignore diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index d27d903a35..187a103868 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -8,7 +8,7 @@ encode_transaction, serializable_unsigned_transaction_from_dict, ) -from eth_utils import encode_hex, keccak, to_int +from eth_utils import decode_hex, encode_hex, keccak, to_int from ethpm_types import HexBytes from ethpm_types.abi import EventABI from pydantic import BaseModel, Field, root_validator, validator @@ -212,8 +212,43 @@ def decode_logs( event_abi = selectors[contract_address][selector] except KeyError: # Likely a library log - library_log = self.provider.network.ecosystem.decode_library_log(log) + library_log = self._decode_ds_note(log) if library_log: yield library_log else: yield from self.provider.network.ecosystem.decode_logs([event_abi], [log]) + + def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: + # The first topic encodes the function selector + selector, tail = log["topics"][0][:4], log["topics"][0][4:] + if sum(tail): + # non-zero bytes found after selector + return None + + contract_type = self.chain_manager.contracts.get(log["address"]) + if contract_type is None: + # contract type for {log['address']} not found + return None + + try: + method_abi = contract_type.mutable_methods[selector] + except KeyError: + # selector {selector.hex()} not found in {log['address']} + return None + + # ds-note data field uses either (uint256,bytes) or (bytes) encoding + # instead of guessing, assume the payload begins right after the selector + data = decode_hex(log["data"]) + input_types = [i.canonical_type for i in method_abi.inputs] + start_index = data.index(selector) + 4 + values = decode_abi(input_types, data[start_index:]) + + return ContractLog( + block_hash=log["blockHash"], + block_number=log["blockNumber"], + contract_address=self.decode_address(log["address"]), + event_arguments={i.name: value for i, value in zip(method_abi.inputs, values)}, + log_index=log["logIndex"], + name=method_abi.name, + transaction_hash=log["transactionHash"], + ) # type: ignore diff --git a/tests/functional/test_ecosystem.py b/tests/functional/test_ecosystem.py index 5f8b094e51..8f035e3752 100644 --- a/tests/functional/test_ecosystem.py +++ b/tests/functional/test_ecosystem.py @@ -1,5 +1,4 @@ import pytest -from eth_abi import encode_single from eth_typing import HexAddress, HexStr from hexbytes import HexBytes @@ -110,16 +109,3 @@ def test_block_handles_snake_case_parent_hash(eth_tester_provider, sender, recei redefined_block = Block.parse_obj(latest_block_dict) assert redefined_block.parent_hash == latest_block.parent_hash - - -def test_decode_library_log(ethereum, mainnet_contract, chain, ds_note): - mainnet_contract("0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B") - ilk = encode_single("bytes32", b"YFI-A") - assert ethereum.decode_library_log(ds_note).event_arguments == { - "i": ilk, - "u": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", - "v": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", - "w": "0x0abb839063ef747c8432b2acc60bf8f70ec09a45", - "dink": 0, - "dart": -7229675416790010075676148, - } From 5dc6276c8d5eb04c476188b6259088cd44fd0690 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Mon, 25 Jul 2022 08:14:53 -0500 Subject: [PATCH 41/53] fix: contract cache issue suddenly --- src/ape/managers/chain.py | 4 ++-- src/ape_ethereum/transactions.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index 0090a22f48..8b8546a0f1 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -580,7 +580,6 @@ def get( return default contract_type = self._get_contract_type_from_disk(address_key) - if not contract_type: # Contract could be a minimal proxy proxy_info = self._local_proxies.get(address_key) or self._get_proxy_info_from_disk( @@ -597,7 +596,8 @@ def get( return self.get(proxy_info.target) if not self.provider.get_code(address_key): - return None + return default + # Also gets cached to disk for faster lookup next time. contract_type = self._get_contract_type_from_explorer(address_key) diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index 187a103868..55fbc9a0fd 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -242,11 +242,12 @@ def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: input_types = [i.canonical_type for i in method_abi.inputs] start_index = data.index(selector) + 4 values = decode_abi(input_types, data[start_index:]) + address = self.provider.network.ecosystem.decode_address(log["address"]) return ContractLog( block_hash=log["blockHash"], block_number=log["blockNumber"], - contract_address=self.decode_address(log["address"]), + contract_address=address, event_arguments={i.name: value for i, value in zip(method_abi.inputs, values)}, log_index=log["logIndex"], name=method_abi.name, From c8d0e6784b27f9e68b473cb3a7934cd1b37ae00d Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Mon, 25 Jul 2022 08:16:07 -0500 Subject: [PATCH 42/53] fix: cache before return --- src/ape/managers/chain.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index 8b8546a0f1..9deb3b000f 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -596,6 +596,10 @@ def get( return self.get(proxy_info.target) if not self.provider.get_code(address_key): + if default: + self._local_contracts[address_key] = default + self._cache_contract_to_disk(address_key, default) + return default # Also gets cached to disk for faster lookup next time. From 64cb17d36230dc028216293caaf1b876810f54a4 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Mon, 25 Jul 2022 22:25:26 -0500 Subject: [PATCH 43/53] refactor: use * not for event abis --- src/ape/api/networks.py | 6 ++---- src/ape/api/providers.py | 2 +- src/ape/contracts/base.py | 2 +- src/ape_ethereum/ecosystem.py | 7 +------ src/ape_ethereum/transactions.py | 4 ++-- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 45d905386a..825486f4ee 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -305,15 +305,13 @@ def encode_transaction( """ @abstractmethod - def decode_logs( - self, events: Union[EventABI, List[EventABI]], logs: List[Dict] - ) -> Iterator["ContractLog"]: + def decode_logs(self, logs: List[Dict], *events: EventABI) -> Iterator["ContractLog"]: """ Decode any contract logs that match the given event ABI from the raw log data. Args: - events (Union[EventABI, List[EventABI]]): Event definitions to decode. logs (List[Dict]): A list of raw log data from the chain. + *events (EventABI): Event definitions to decode. Returns: Iterator[:class:`~ape.types.ContractLog`] diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index 31241856c8..725fe26008 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -750,7 +750,7 @@ def fetch_log_page(block_range): # eth-tester expects a different format, let web3 handle the conversions for it raw = "EthereumTester" not in self.client_version logs = self._get_logs(page_filter.dict(), raw) - return self.network.ecosystem.decode_logs(log_filter.events, logs) + return self.network.ecosystem.decode_logs(logs, *log_filter.events) with ThreadPoolExecutor(self.concurrency) as pool: for page in pool.map(fetch_log_page, block_ranges): diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py index 9187242112..3268ca42d1 100644 --- a/src/ape/contracts/base.py +++ b/src/ape/contracts/base.py @@ -440,7 +440,7 @@ def from_receipt(self, receipt: ReceiptAPI) -> Iterator[ContractLog]: Iterator[:class:`~ape.contracts.base.ContractLog`] """ ecosystem = self.provider.network.ecosystem - yield from ecosystem.decode_logs(self.abi, receipt.logs) + yield from ecosystem.decode_logs(receipt.logs, self.abi) def poll_logs( self, diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index d9c43cd0cd..10151f45e8 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -444,12 +444,7 @@ def create_transaction(self, **kwargs) -> TransactionAPI: return txn_class(**kwargs) # type: ignore - def decode_logs( - self, events: Union[EventABI, List[EventABI]], logs: List[Dict] - ) -> Iterator["ContractLog"]: - if not isinstance(events, list): - events = [events] - + def decode_logs(self, logs: List[Dict], *events: EventABI) -> Iterator["ContractLog"]: abi_inputs = { encode_hex(keccak(text=abi.selector)): LogInputABICollection(abi) for abi in events } diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index 55fbc9a0fd..e3acbf8544 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -192,7 +192,7 @@ def decode_logs( abi = [abi] event_abis: List[EventABI] = [a.abi if not isinstance(a, EventABI) else a for a in abi] - yield from self.provider.network.ecosystem.decode_logs(event_abis, self.logs) + yield from self.provider.network.ecosystem.decode_logs(self.logs, *event_abis) else: # If ABI is not provided, decode all events @@ -216,7 +216,7 @@ def decode_logs( if library_log: yield library_log else: - yield from self.provider.network.ecosystem.decode_logs([event_abi], [log]) + yield from self.provider.network.ecosystem.decode_logs([log], event_abi) def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: # The first topic encodes the function selector From 55625f280ba64806f49adf371eeff70e271387a2 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 26 Jul 2022 10:57:40 -0500 Subject: [PATCH 44/53] refactor: rename name to event_name --- src/ape/types/__init__.py | 2 +- src/ape_ethereum/ecosystem.py | 2 +- src/ape_ethereum/transactions.py | 2 +- tests/functional/test_types.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ape/types/__init__.py b/src/ape/types/__init__.py index 40e841c19e..4e2b1aef78 100644 --- a/src/ape/types/__init__.py +++ b/src/ape/types/__init__.py @@ -147,7 +147,7 @@ class ContractLog(BaseModel): An instance of a log from a contract. """ - name: str + event_name: str """The name of the event.""" contract_address: AddressType diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 10151f45e8..db8c49110b 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -469,7 +469,7 @@ def decode_logs(self, logs: List[Dict], *events: EventABI) -> Iterator["Contract block_number=to_int(log["blockNumber"]), contract_address=self.decode_address(log["address"]), event_arguments=event_arguments, + event_name=abi.event_name, log_index=log["logIndex"], - name=abi.event_name, transaction_hash=log["transactionHash"], ) # type: ignore diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index e3acbf8544..3ac07aec62 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -249,7 +249,7 @@ def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: block_number=log["blockNumber"], contract_address=address, event_arguments={i.name: value for i, value in zip(method_abi.inputs, values)}, + event_name=method_abi.name, log_index=log["logIndex"], - name=method_abi.name, transaction_hash=log["transactionHash"], ) # type: ignore diff --git a/tests/functional/test_types.py b/tests/functional/test_types.py index cb13ed6a0c..f278045f91 100644 --- a/tests/functional/test_types.py +++ b/tests/functional/test_types.py @@ -15,7 +15,7 @@ "contract_address": ZERO_ADDRESS, "event_arguments": {"foo": 0, "bar": 1}, "log_index": LOG_INDEX, - "name": EVENT_NAME, + "event_name": EVENT_NAME, "transaction_hash": TXN_HASH, } RAW_EVENT_ABI = """ From d320ded0cfeb11ffa1a91e32e271cacb924df385 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 26 Jul 2022 11:06:39 -0500 Subject: [PATCH 45/53] feat: support transaction_index --- src/ape/types/__init__.py | 6 ++++++ src/ape_ethereum/ecosystem.py | 1 + src/ape_ethereum/transactions.py | 1 + tests/functional/test_contract_event.py | 3 ++- tests/functional/test_receipt.py | 22 ++++++++++++++++++---- tests/functional/test_types.py | 5 ++++- 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/ape/types/__init__.py b/src/ape/types/__init__.py index 4e2b1aef78..bd11b49354 100644 --- a/src/ape/types/__init__.py +++ b/src/ape/types/__init__.py @@ -168,6 +168,12 @@ class ContractLog(BaseModel): log_index: int """The index of the log on the transaction.""" + transaction_index: Optional[int] = None + """ + The index of the transaction's position when the log was created. + Is `None` when from the pending block. + """ + def __str__(self) -> str: args = " ".join(f"{key}={val}" for key, val in self.event_arguments.items()) return f"{self.name} {args}" diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index db8c49110b..8b6864e451 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -472,4 +472,5 @@ def decode_logs(self, logs: List[Dict], *events: EventABI) -> Iterator["Contract event_name=abi.event_name, log_index=log["logIndex"], transaction_hash=log["transactionHash"], + transaction_index=log["transactionIndex"], ) # type: ignore diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index 3ac07aec62..b974125644 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -252,4 +252,5 @@ def _decode_ds_note(self, log: Dict) -> Optional[ContractLog]: event_name=method_abi.name, log_index=log["logIndex"], transaction_hash=log["transactionHash"], + transaction_index=log["transactionIndex"], ) # type: ignore diff --git a/tests/functional/test_contract_event.py b/tests/functional/test_contract_event.py index 2a8a2438a3..8fed9d7d4a 100644 --- a/tests/functional/test_contract_event.py +++ b/tests/functional/test_contract_event.py @@ -258,5 +258,6 @@ def test_contract_decode_logs_no_abi(owner, contract_instance): receipt = contract_instance.setNumber(1, sender=owner) events = list(receipt.decode_logs()) # no abi assert len(events) == 1 - assert events[0].name == "NumberChange" + assert events[0].event_name == "NumberChange" assert events[0].newNum == 1 + assert events[0].transaction_index == 0 diff --git a/tests/functional/test_receipt.py b/tests/functional/test_receipt.py index b96ae738d7..0ccf611ca4 100644 --- a/tests/functional/test_receipt.py +++ b/tests/functional/test_receipt.py @@ -20,6 +20,9 @@ def test_decode_logs_specify_abi(invoke_receipt, vyper_contract_instance): logs = [log for log in invoke_receipt.decode_logs(abi=abi)] assert len(logs) == 1 assert logs[0].newNum == 1 + assert logs[0].event_name == "NumberChange" + assert logs[0].log_index == 0 + assert logs[0].transaction_index == 0 def test_decode_logs_specify_abi_as_event(invoke_receipt, vyper_contract_instance): @@ -27,6 +30,9 @@ def test_decode_logs_specify_abi_as_event(invoke_receipt, vyper_contract_instanc logs = [log for log in invoke_receipt.decode_logs(abi=abi)] assert len(logs) == 1 assert logs[0].newNum == 1 + assert logs[0].event_name == "NumberChange" + assert logs[0].log_index == 0 + assert logs[0].transaction_index == 0 def test_decode_logs_with_ds_notes(ds_note_test_contract, owner): @@ -34,25 +40,33 @@ def test_decode_logs_with_ds_notes(ds_note_test_contract, owner): receipt = contract.test_0(sender=owner) logs = [log for log in receipt.decode_logs()] assert len(logs) == 1 - assert logs[0].name == "foo" + assert logs[0].event_name == "foo" + assert logs[0].log_index == 0 + assert logs[0].transaction_index == 0 receipt = contract.test_1(sender=owner) logs = [log for log in receipt.decode_logs()] assert len(logs) == 1 - assert logs[0].name == "foo" + assert logs[0].event_name == "foo" assert logs[0].event_arguments == {"a": 1} + assert logs[0].log_index == 0 + assert logs[0].transaction_index == 0 receipt = contract.test_2(sender=owner) logs = [log for log in receipt.decode_logs()] assert len(logs) == 1 - assert logs[0].name == "foo" + assert logs[0].event_name == "foo" assert logs[0].event_arguments == {"a": 1, "b": 2} + assert logs[0].log_index == 0 + assert logs[0].transaction_index == 0 receipt = contract.test_3(sender=owner) logs = [log for log in receipt.decode_logs()] assert len(logs) == 1 - assert logs[0].name == "foo" + assert logs[0].event_name == "foo" assert logs[0].event_arguments == {"a": 1, "b": 2, "c": 3} + assert logs[0].log_index == 0 + assert logs[0].transaction_index == 0 def test_decode_logs(owner, contract_instance, assert_log_values): diff --git a/tests/functional/test_types.py b/tests/functional/test_types.py index f278045f91..7738ddb7a6 100644 --- a/tests/functional/test_types.py +++ b/tests/functional/test_types.py @@ -9,6 +9,7 @@ BLOCK_NUMBER = 323423 EVENT_NAME = "MyEvent" LOG_INDEX = 7 +TXN_INDEX = 2 RAW_LOG = { "block_hash": BLOCK_HASH, "block_number": BLOCK_NUMBER, @@ -17,6 +18,7 @@ "log_index": LOG_INDEX, "event_name": EVENT_NAME, "transaction_hash": TXN_HASH, + "transaction_index": TXN_INDEX, } RAW_EVENT_ABI = """ { @@ -49,9 +51,10 @@ def test_contract_log_serialization(log): assert log.contract_address == ZERO_ADDRESS assert log.block_hash == BLOCK_HASH assert log.block_number == BLOCK_NUMBER - assert log.name == EVENT_NAME + assert log.event_name == EVENT_NAME assert log.log_index == 7 assert log.transaction_hash == TXN_HASH + assert log.transaction_index == TXN_INDEX def test_contract_log_access(log): From cc0a676268eedcaaeda6d11068f1d0492bdcf2c0 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Tue, 26 Jul 2022 23:37:15 +0400 Subject: [PATCH 46/53] fix: to int --- src/ape_ethereum/ecosystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 8b6864e451..7b20fb6c8a 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -470,7 +470,7 @@ def decode_logs(self, logs: List[Dict], *events: EventABI) -> Iterator["Contract contract_address=self.decode_address(log["address"]), event_arguments=event_arguments, event_name=abi.event_name, - log_index=log["logIndex"], + log_index=to_int(log["logIndex"]), transaction_hash=log["transactionHash"], - transaction_index=log["transactionIndex"], + transaction_index=to_int(log["transactionIndex"]), ) # type: ignore From a6b3232d40fb5b49e2c542bd5f0d46a58b77ac6e Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 26 Jul 2022 16:01:35 -0500 Subject: [PATCH 47/53] feat: handle raw responses --- src/ape/types/__init__.py | 21 ++++- src/ape/utils/__init__.py | 4 + src/ape/utils/misc.py | 5 ++ src/ape/utils/trace.py | 4 +- src/ape_ethereum/ecosystem.py | 13 +-- tests/functional/test_ecosystem.py | 126 ++++++++++++--------------- tests/functional/test_receipt.py | 20 +++++ tests/functional/test_transaction.py | 51 +++++++++++ tests/functional/test_types.py | 35 ++++++-- 9 files changed, 190 insertions(+), 89 deletions(-) create mode 100644 tests/functional/test_transaction.py diff --git a/src/ape/types/__init__.py b/src/ape/types/__init__.py index bd11b49354..3262e17155 100644 --- a/src/ape/types/__init__.py +++ b/src/ape/types/__init__.py @@ -21,6 +21,7 @@ from web3.types import FilterParams from ape._compat import Literal +from ape.utils.misc import to_int, to_snake_case from .signatures import MessageSignature, SignableMessage, TransactionSignature @@ -78,8 +79,8 @@ def validate_addresses(cls, value): def dict(self, client=None): return FilterParams( address=self.addresses, - fromBlock=hex(self.start_block), - toBlock=hex(self.stop_block), + fromBlock=hex(self.start_block), # type: ignore + toBlock=hex(self.stop_block), # type: ignore topics=self.topic_filter, # type: ignore ) @@ -174,6 +175,22 @@ class ContractLog(BaseModel): Is `None` when from the pending block. """ + class Config: + alias_generator = to_snake_case + + @validator("block_number", "log_index", "transaction_index", pre=True) + def validate_hex_ints(cls, value): + if not isinstance(value, int): + return to_int(value) + + return value + + @validator("contract_address", pre=True) + def validate_address(cls, value): + from ape import convert + + return convert(value, AddressType) + def __str__(self) -> str: args = " ".join(f"{key}={val}" for key, val in self.event_arguments.items()) return f"{self.name} {args}" diff --git a/src/ape/utils/__init__.py b/src/ape/utils/__init__.py index d34a2b9558..565ab76f2d 100644 --- a/src/ape/utils/__init__.py +++ b/src/ape/utils/__init__.py @@ -34,6 +34,8 @@ raises_not_implemented, singledispatchmethod, stream_response, + to_int, + to_snake_case, ) from ape.utils.os import get_all_files_in_directory, get_relative_path, use_temp_sys_path from ape.utils.process import JoinableQueue, spawn @@ -85,6 +87,8 @@ "Struct", "StructParser", "TraceStyles", + "to_int", + "to_snake_case", "use_temp_sys_path", "USER_AGENT", "ZERO_ADDRESS", diff --git a/src/ape/utils/misc.py b/src/ape/utils/misc.py index 0198e6fa26..5245ffddf7 100644 --- a/src/ape/utils/misc.py +++ b/src/ape/utils/misc.py @@ -251,6 +251,10 @@ def to_int(value) -> int: raise ValueError(f"cannot convert {repr(value)} to int") +def to_snake_case(string: str) -> str: + return "".join(["_" + i.lower() if i.isupper() else i for i in string]).lstrip("_") + + __all__ = [ "cached_property", "expand_environment_variables", @@ -261,5 +265,6 @@ def to_int(value) -> int: "raises_not_implemented", "singledispatchmethod", "stream_response", + "to_snake_case", "USER_AGENT", ] diff --git a/src/ape/utils/trace.py b/src/ape/utils/trace.py index 8c52529158..34dab8341e 100644 --- a/src/ape/utils/trace.py +++ b/src/ape/utils/trace.py @@ -12,12 +12,12 @@ from hexbytes import HexBytes from rich.tree import Tree -from ape.api.networks import EcosystemAPI from ape.exceptions import ContractError, DecodingError from ape.utils.abi import Struct, parse_type from ape.utils.misc import ZERO_ADDRESS if TYPE_CHECKING: + from ape.api.networks import EcosystemAPI from ape.api.transactions import ReceiptAPI @@ -85,7 +85,7 @@ def __init__( self.colors = color_set @property - def _ecosystem(self) -> EcosystemAPI: + def _ecosystem(self) -> "EcosystemAPI": return self._receipt.provider.network.ecosystem def parse_as_tree(self, call: CallTreeNode) -> Tree: diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 7b20fb6c8a..463fc46f52 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -25,8 +25,8 @@ is_array, parse_type, returns_array, + to_snake_case, ) -from ape.utils.misc import to_int from ape_ethereum.transactions import ( AccessListTransaction, BaseTransaction, @@ -464,13 +464,14 @@ def decode_logs(self, logs: List[Dict], *events: EventABI) -> Iterator["Contract continue event_arguments = abi.decode(topics, log["data"]) + log = {to_snake_case(k): v for k, v in log.items()} yield ContractLog( - block_hash=log["blockHash"], - block_number=to_int(log["blockNumber"]), + block_hash=log["block_hash"], + block_number=log["block_number"], contract_address=self.decode_address(log["address"]), event_arguments=event_arguments, event_name=abi.event_name, - log_index=to_int(log["logIndex"]), - transaction_hash=log["transactionHash"], - transaction_index=to_int(log["transactionIndex"]), + log_index=log["log_index"], + transaction_hash=log["transaction_hash"], + transaction_index=log["transaction_index"], ) # type: ignore diff --git a/tests/functional/test_ecosystem.py b/tests/functional/test_ecosystem.py index da1bd1cccd..7477fcb104 100644 --- a/tests/functional/test_ecosystem.py +++ b/tests/functional/test_ecosystem.py @@ -2,34 +2,40 @@ from eth_typing import HexAddress, HexStr from hexbytes import HexBytes -from ape.exceptions import OutOfGasError from ape.types import AddressType from ape.utils import DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT from ape_ethereum.ecosystem import Block -from ape_ethereum.transactions import ( - Receipt, - StaticFeeTransaction, - TransactionStatusEnum, - TransactionType, -) - - -@pytest.mark.parametrize("type_kwarg", (0, "0x0", b"\x00", "0", HexBytes("0x0"), HexBytes("0x00"))) -def test_create_static_fee_transaction(ethereum, type_kwarg): - txn = ethereum.create_transaction(type=type_kwarg) - assert txn.type == TransactionType.STATIC.value - - -@pytest.mark.parametrize("type_kwarg", (1, "0x01", b"\x01", "1", "01", HexBytes("0x01"))) -def test_create_access_list_transaction(ethereum, type_kwarg): - txn = ethereum.create_transaction(type=type_kwarg) - assert txn.type == TransactionType.ACCESS_LIST.value - -@pytest.mark.parametrize("type_kwarg", (None, 2, "0x02", b"\x02", "2", "02", HexBytes("0x02"))) -def test_create_dynamic_fee_transaction(ethereum, type_kwarg): - txn = ethereum.create_transaction(type=type_kwarg) - assert txn.type == TransactionType.DYNAMIC.value +LOG_FROM_RESPONSE = { + "removed": False, + "logIndex": "0x0", + "transactionIndex": "0x0", + "transactionHash": "0x74dd040dfa06f0af9af8ca95d7aae409978400151c746f55ecce19e7356cfc5a", + "blockHash": "0x2c99950b07accf3e442512a3352a11e6fed37b2331de5f71b7743b357d96e4e8", + "blockNumber": "0xa946ac", + "address": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "data": "0xabffd4675206dab5d04a6b0d62c975049665d1f512f29f303908abdd20bc08a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000744796e616d696300000000000000000000000000000000000000000000000000", # noqa: E501 + "topics": [ + "0xa84473122c11e32cd505595f246a28418b8ecd6cf819f4e3915363fad1b8f968", + "0x0000000000000000000000000000000000000000000000000000000000000006", + "0x9f3d45ac20ccf04b45028b8080bb191eab93e29f7898ed43acf480dd80bba94d", + ], +} +LOG_FROM_ETH_TESTER = { + "removed": False, + "log_index": 0, + "transaction_index": 0, + "transaction_hash": "0x74dd040dfa06f0af9af8ca95d7aae409978400151c746f55ecce19e7356cfc5a", + "block_hash": "0x2c99950b07accf3e442512a3352a11e6fed37b2331de5f71b7743b357d96e4e8", + "block_number": 11093676, + "address": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "data": "0xabffd4675206dab5d04a6b0d62c975049665d1f512f29f303908abdd20bc08a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000744796e616d696300000000000000000000000000000000000000000000000000", # noqa: E501 + "topics": [ + "0xa84473122c11e32cd505595f246a28418b8ecd6cf819f4e3915363fad1b8f968", + "0x0000000000000000000000000000000000000000000000000000000000000006", + "0x9f3d45ac20ccf04b45028b8080bb191eab93e29f7898ed43acf480dd80bba94d", + ], +} @pytest.mark.parametrize( @@ -52,53 +58,6 @@ def test_encode_address(ethereum): assert actual == raw_address -def test_transaction_dict_excludes_none_values(): - txn = StaticFeeTransaction() - txn.value = 1000000 - actual = txn.dict() - assert "value" in actual - txn.value = None - actual = txn.dict() - assert "value" not in actual - - -def test_receipt_raise_for_status_out_of_gas_error(mocker): - gas_limit = 100000 - receipt = Receipt( - provider=mocker.MagicMock(), - txn_hash="", - gas_used=gas_limit, - gas_limit=gas_limit, - status=TransactionStatusEnum.FAILING, - gas_price=0, - block_number=0, - sender="", - receiver="", - nonce=0, - ) - with pytest.raises(OutOfGasError): - receipt.raise_for_status() - - -def test_txn_hash(owner, eth_tester_provider): - txn = StaticFeeTransaction() - txn = owner.prepare_transaction(txn) - txn.signature = owner.sign_transaction(txn) - - actual = txn.txn_hash.hex() - receipt = eth_tester_provider.send_transaction(txn) - expected = receipt.txn_hash - - assert actual == expected - - -def test_whitespace_in_transaction_data(): - data = b"Should not clip whitespace\t\n" - txn_dict = {"data": data} - txn = StaticFeeTransaction.parse_obj(txn_dict) - assert txn.data == data, "Whitespace should not be removed from data" - - def test_block_handles_snake_case_parent_hash(eth_tester_provider, sender, receiver): # Transaction to change parent hash of next block sender.transfer(receiver, "1 gwei") @@ -121,3 +80,28 @@ def test_transaction_acceptance_timeout(temp_config, config, networks): timeout_config = {"ethereum": {"local": {"transaction_acceptance_timeout": new_value}}} with temp_config(timeout_config): assert networks.provider.network.transaction_acceptance_timeout == new_value + + +@pytest.mark.parametrize("log_data", (LOG_FROM_RESPONSE, LOG_FROM_ETH_TESTER)) +def test_decode_logs(ethereum, project, vyper_contract_instance, log_data): + abi = vyper_contract_instance.NumberChange.abi + result = [x for x in ethereum.decode_logs([log_data], abi)] + assert len(result) == 1 + assert result[0] == { + "event_name": "NumberChange", + "contract_address": "0x274b028b03A250cA03644E6c578D81f019eE1323", + "event_arguments": { + "newNum": 6, + "dynIndexed": HexBytes( + "0x9f3d45ac20ccf04b45028b8080bb191eab93e29f7898ed43acf480dd80bba94d" + ), + "b": HexBytes("0xabffd4675206dab5d04a6b0d62c975049665d1f512f29f303908abdd20bc08a1"), + "prevNum": 0, + "dynData": "Dynamic", + }, + "transaction_hash": "0x74dd040dfa06f0af9af8ca95d7aae409978400151c746f55ecce19e7356cfc5a", + "block_number": 11093676, + "block_hash": "0x2c99950b07accf3e442512a3352a11e6fed37b2331de5f71b7743b357d96e4e8", + "log_index": 0, + "transaction_index": 0, + } diff --git a/tests/functional/test_receipt.py b/tests/functional/test_receipt.py index 0ccf611ca4..9869307cf1 100644 --- a/tests/functional/test_receipt.py +++ b/tests/functional/test_receipt.py @@ -1,6 +1,8 @@ import pytest from ape.api import ReceiptAPI +from ape.exceptions import OutOfGasError +from ape_ethereum.transactions import Receipt, TransactionStatusEnum @pytest.fixture @@ -112,3 +114,21 @@ def test_get_failed_receipt(owner, vyper_contract_instance, eth_tester_provider) ) receipt = owner.call(transaction) assert receipt.failed + + +def test_receipt_raise_for_status_out_of_gas_error(mocker): + gas_limit = 100000 + receipt = Receipt( + provider=mocker.MagicMock(), + txn_hash="", + gas_used=gas_limit, + gas_limit=gas_limit, + status=TransactionStatusEnum.FAILING, + gas_price=0, + block_number=0, + sender="", + receiver="", + nonce=0, + ) + with pytest.raises(OutOfGasError): + receipt.raise_for_status() diff --git a/tests/functional/test_transaction.py b/tests/functional/test_transaction.py new file mode 100644 index 0000000000..a50f9cd786 --- /dev/null +++ b/tests/functional/test_transaction.py @@ -0,0 +1,51 @@ +import pytest +from hexbytes import HexBytes + +from ape_ethereum.transactions import StaticFeeTransaction, TransactionType + + +@pytest.mark.parametrize("type_kwarg", (0, "0x0", b"\x00", "0", HexBytes("0x0"), HexBytes("0x00"))) +def test_create_static_fee_transaction(ethereum, type_kwarg): + txn = ethereum.create_transaction(type=type_kwarg) + assert txn.type == TransactionType.STATIC.value + + +@pytest.mark.parametrize("type_kwarg", (1, "0x01", b"\x01", "1", "01", HexBytes("0x01"))) +def test_create_access_list_transaction(ethereum, type_kwarg): + txn = ethereum.create_transaction(type=type_kwarg) + assert txn.type == TransactionType.ACCESS_LIST.value + + +@pytest.mark.parametrize("type_kwarg", (None, 2, "0x02", b"\x02", "2", "02", HexBytes("0x02"))) +def test_create_dynamic_fee_transaction(ethereum, type_kwarg): + txn = ethereum.create_transaction(type=type_kwarg) + assert txn.type == TransactionType.DYNAMIC.value + + +def test_txn_hash(owner, eth_tester_provider): + txn = StaticFeeTransaction() + txn = owner.prepare_transaction(txn) + txn.signature = owner.sign_transaction(txn) + + actual = txn.txn_hash.hex() + receipt = eth_tester_provider.send_transaction(txn) + expected = receipt.txn_hash + + assert actual == expected + + +def test_whitespace_in_transaction_data(): + data = b"Should not clip whitespace\t\n" + txn_dict = {"data": data} + txn = StaticFeeTransaction.parse_obj(txn_dict) + assert txn.data == data, "Whitespace should not be removed from data" + + +def test_transaction_dict_excludes_none_values(): + txn = StaticFeeTransaction() + txn.value = 1000000 + actual = txn.dict() + assert "value" in actual + txn.value = None + actual = txn.dict() + assert "value" not in actual diff --git a/tests/functional/test_types.py b/tests/functional/test_types.py index 7738ddb7a6..6b44ccc386 100644 --- a/tests/functional/test_types.py +++ b/tests/functional/test_types.py @@ -1,4 +1,5 @@ import pytest +from eth_utils import to_hex from ethpm_types.abi import EventABI from ape.types import ContractLog, LogFilter @@ -47,14 +48,32 @@ def log(): def test_contract_log_serialization(log): - log = ContractLog.parse_obj(log.dict()) - assert log.contract_address == ZERO_ADDRESS - assert log.block_hash == BLOCK_HASH - assert log.block_number == BLOCK_NUMBER - assert log.event_name == EVENT_NAME - assert log.log_index == 7 - assert log.transaction_hash == TXN_HASH - assert log.transaction_index == TXN_INDEX + obj = ContractLog.parse_obj(log.dict()) + assert obj.contract_address == ZERO_ADDRESS + assert obj.block_hash == BLOCK_HASH + assert obj.block_number == BLOCK_NUMBER + assert obj.event_name == EVENT_NAME + assert obj.log_index == 7 + assert obj.transaction_hash == TXN_HASH + assert obj.transaction_index == TXN_INDEX + + +def test_contract_log_serialization_with_hex_strings_and_non_checksum_addresses(log): + data = log.dict() + data["log_index"] = to_hex(log.log_index) + data["transaction_index"] = to_hex(log.transaction_index) + data["block_number"] = to_hex(log.block_number) + data["contract_address"] = log.contract_address.lower() + + obj = ContractLog(**data) + + assert obj.contract_address == ZERO_ADDRESS + assert obj.block_hash == BLOCK_HASH + assert obj.block_number == BLOCK_NUMBER + assert obj.event_name == EVENT_NAME + assert obj.log_index == 7 + assert obj.transaction_hash == TXN_HASH + assert obj.transaction_index == TXN_INDEX def test_contract_log_access(log): From ae68813e3970bab7c0f352d03d3c46e48d4b3ae0 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 26 Jul 2022 18:13:58 -0500 Subject: [PATCH 48/53] test: improve test --- tests/functional/test_contract_event.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/functional/test_contract_event.py b/tests/functional/test_contract_event.py index 8fed9d7d4a..32fefee998 100644 --- a/tests/functional/test_contract_event.py +++ b/tests/functional/test_contract_event.py @@ -52,16 +52,23 @@ def assert_receipt_logs(receipt: ReceiptAPI, num: int): def test_contract_logs_from_event_type(contract_instance, owner, assert_log_values): event_type = contract_instance.NumberChange + start_num = 6 + size = 20 + num_range = range(start_num, start_num + size) - contract_instance.setNumber(1, sender=owner) - contract_instance.setNumber(2, sender=owner) - contract_instance.setNumber(3, sender=owner) + # Generate 20 logs + for i in num_range: + contract_instance.setNumber(i, sender=owner) + # Collect 20 logs logs = [log for log in event_type] - assert len(logs) == 3, "Unexpected number of logs" - assert_log_values(logs[0], 1) - assert_log_values(logs[1], 2) - assert_log_values(logs[2], 3) + + assert len(logs) == size, "Unexpected number of logs" + for num, log in zip(num_range, logs): + if num == start_num: + assert_log_values(log, num, previous_number=0) + else: + assert_log_values(log, num) def test_contract_logs_index_access(contract_instance, owner, assert_log_values): From 084f5af0443d03f94e6f424a6d52388517806ae7 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 26 Jul 2022 18:28:11 -0500 Subject: [PATCH 49/53] fix: issue when not using web3 provideR --- src/ape/api/providers.py | 9 +++++---- tests/functional/test_ecosystem.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index 725fe26008..9e9a43ac5f 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -11,7 +11,7 @@ from pathlib import Path from signal import SIGINT, SIGTERM, signal from subprocess import PIPE, Popen -from typing import Any, Iterator, List, Optional +from typing import Any, Dict, Iterator, List, Optional from eth_typing import HexStr from eth_utils import add_0x_prefix @@ -732,7 +732,7 @@ def block_ranges(self, start=0, stop=None, page=None): if stop is None: stop = self.chain_manager.blocks.height if page is None: - page = self.chain_manager.provider.block_page_size + page = self.block_page_size for start_block in range(start, stop + 1, page): stop_block = min(stop, start_block + page - 1) @@ -748,7 +748,7 @@ def fetch_log_page(block_range): start, stop = block_range page_filter = log_filter.copy(update=dict(start_block=start, stop_block=stop)) # eth-tester expects a different format, let web3 handle the conversions for it - raw = "EthereumTester" not in self.client_version + raw = True if not hasattr(self, "client_version") else "EthereumTester" not in self.client_version logs = self._get_logs(page_filter.dict(), raw) return self.network.ecosystem.decode_logs(logs, *log_filter.events) @@ -756,11 +756,12 @@ def fetch_log_page(block_range): for page in pool.map(fetch_log_page, block_ranges): yield from page - def _get_logs(self, filter_params, raw=True): + def _get_logs(self, filter_params, raw=True) -> List[Dict]: if raw: response = self.web3.provider.make_request("eth_getLogs", [filter_params]) if "error" in response: raise ValueError(response["error"]["message"]) + return response["result"] else: return self.web3.eth.get_logs(filter_params) diff --git a/tests/functional/test_ecosystem.py b/tests/functional/test_ecosystem.py index 7477fcb104..45234be23a 100644 --- a/tests/functional/test_ecosystem.py +++ b/tests/functional/test_ecosystem.py @@ -38,6 +38,11 @@ } +@pytest.fixture +def event_abi(vyper_contract_instance): + return vyper_contract_instance.NumberChange.abi + + @pytest.mark.parametrize( "address", ( @@ -83,7 +88,7 @@ def test_transaction_acceptance_timeout(temp_config, config, networks): @pytest.mark.parametrize("log_data", (LOG_FROM_RESPONSE, LOG_FROM_ETH_TESTER)) -def test_decode_logs(ethereum, project, vyper_contract_instance, log_data): +def test_decode_logs(ethereum, vyper_contract_instance, log_data): abi = vyper_contract_instance.NumberChange.abi result = [x for x in ethereum.decode_logs([log_data], abi)] assert len(result) == 1 @@ -105,3 +110,8 @@ def test_decode_logs(ethereum, project, vyper_contract_instance, log_data): "log_index": 0, "transaction_index": 0, } + + +def test_decode_logs_empty_list(ethereum, event_abi): + actual = [x for x in ethereum.decode_logs([], event_abi)] + assert actual == [] From 8f223c4e4bd1f3aabbb1170e4a881bf5df841ebe Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 26 Jul 2022 18:29:33 -0500 Subject: [PATCH 50/53] fix: revert unneeded change --- src/ape/api/providers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index 9e9a43ac5f..50aa27886c 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -748,7 +748,7 @@ def fetch_log_page(block_range): start, stop = block_range page_filter = log_filter.copy(update=dict(start_block=start, stop_block=stop)) # eth-tester expects a different format, let web3 handle the conversions for it - raw = True if not hasattr(self, "client_version") else "EthereumTester" not in self.client_version + raw = "EthereumTester" not in self.client_version logs = self._get_logs(page_filter.dict(), raw) return self.network.ecosystem.decode_logs(logs, *log_filter.events) From d2ee85256848a36cb5c770cb02a895a1bbccf730 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 26 Jul 2022 18:36:56 -0500 Subject: [PATCH 51/53] chore: new found mypy issues --- src/ape/api/providers.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index 50aa27886c..8627de0ed7 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -7,6 +7,7 @@ import time from abc import ABC from concurrent.futures import ThreadPoolExecutor +from dataclasses import asdict from logging import FileHandler, Formatter, Logger, getLogger from pathlib import Path from signal import SIGINT, SIGTERM, signal @@ -21,6 +22,7 @@ from web3 import Web3 from web3.exceptions import ContractLogicError as Web3ContractLogicError from web3.exceptions import TimeExhausted +from web3.types import RPCEndpoint from ape.api.config import PluginConfig from ape.api.networks import LOCAL_NETWORK_NAME, NetworkAPI @@ -758,13 +760,18 @@ def fetch_log_page(block_range): def _get_logs(self, filter_params, raw=True) -> List[Dict]: if raw: - response = self.web3.provider.make_request("eth_getLogs", [filter_params]) + response = self.web3.provider.make_request(RPCEndpoint("eth_getLogs"), [filter_params]) if "error" in response: - raise ValueError(response["error"]["message"]) + error = response["error"] + if isinstance(error, dict) and "message" in error: + raise ValueError(error["message"]) + else: + # Should never get here, mostly for mypy + raise ValueError(str(error)) return response["result"] else: - return self.web3.eth.get_logs(filter_params) + return [vars(d) for d in self.web3.eth.get_logs(filter_params)] def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI: try: From 299b048d9b150945121e6ddcd2c5509f2b7afcc8 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Tue, 26 Jul 2022 19:53:46 -0500 Subject: [PATCH 52/53] chore: rm unused import --- src/ape/api/providers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index 8627de0ed7..787e5523e0 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -7,7 +7,6 @@ import time from abc import ABC from concurrent.futures import ThreadPoolExecutor -from dataclasses import asdict from logging import FileHandler, Formatter, Logger, getLogger from pathlib import Path from signal import SIGINT, SIGTERM, signal From e5b764b39cbc806f5960e70ed6637965eeb4bc6e Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 27 Jul 2022 08:22:31 -0500 Subject: [PATCH 53/53] feat: remove support for snake_case --- src/ape/types/__init__.py | 5 +---- src/ape/utils/__init__.py | 2 -- src/ape/utils/misc.py | 5 ----- src/ape_ethereum/ecosystem.py | 12 +++++------- tests/functional/test_ecosystem.py | 22 +++------------------- 5 files changed, 9 insertions(+), 37 deletions(-) diff --git a/src/ape/types/__init__.py b/src/ape/types/__init__.py index 3262e17155..50935d8ca8 100644 --- a/src/ape/types/__init__.py +++ b/src/ape/types/__init__.py @@ -21,7 +21,7 @@ from web3.types import FilterParams from ape._compat import Literal -from ape.utils.misc import to_int, to_snake_case +from ape.utils.misc import to_int from .signatures import MessageSignature, SignableMessage, TransactionSignature @@ -175,9 +175,6 @@ class ContractLog(BaseModel): Is `None` when from the pending block. """ - class Config: - alias_generator = to_snake_case - @validator("block_number", "log_index", "transaction_index", pre=True) def validate_hex_ints(cls, value): if not isinstance(value, int): diff --git a/src/ape/utils/__init__.py b/src/ape/utils/__init__.py index 565ab76f2d..98193914c1 100644 --- a/src/ape/utils/__init__.py +++ b/src/ape/utils/__init__.py @@ -35,7 +35,6 @@ singledispatchmethod, stream_response, to_int, - to_snake_case, ) from ape.utils.os import get_all_files_in_directory, get_relative_path, use_temp_sys_path from ape.utils.process import JoinableQueue, spawn @@ -88,7 +87,6 @@ "StructParser", "TraceStyles", "to_int", - "to_snake_case", "use_temp_sys_path", "USER_AGENT", "ZERO_ADDRESS", diff --git a/src/ape/utils/misc.py b/src/ape/utils/misc.py index 5245ffddf7..0198e6fa26 100644 --- a/src/ape/utils/misc.py +++ b/src/ape/utils/misc.py @@ -251,10 +251,6 @@ def to_int(value) -> int: raise ValueError(f"cannot convert {repr(value)} to int") -def to_snake_case(string: str) -> str: - return "".join(["_" + i.lower() if i.isupper() else i for i in string]).lstrip("_") - - __all__ = [ "cached_property", "expand_environment_variables", @@ -265,6 +261,5 @@ def to_snake_case(string: str) -> str: "raises_not_implemented", "singledispatchmethod", "stream_response", - "to_snake_case", "USER_AGENT", ] diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 463fc46f52..c22437b104 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -25,7 +25,6 @@ is_array, parse_type, returns_array, - to_snake_case, ) from ape_ethereum.transactions import ( AccessListTransaction, @@ -464,14 +463,13 @@ def decode_logs(self, logs: List[Dict], *events: EventABI) -> Iterator["Contract continue event_arguments = abi.decode(topics, log["data"]) - log = {to_snake_case(k): v for k, v in log.items()} yield ContractLog( - block_hash=log["block_hash"], - block_number=log["block_number"], + block_hash=log["blockHash"], + block_number=log["blockNumber"], contract_address=self.decode_address(log["address"]), event_arguments=event_arguments, event_name=abi.event_name, - log_index=log["log_index"], - transaction_hash=log["transaction_hash"], - transaction_index=log["transaction_index"], + log_index=log["logIndex"], + transaction_hash=log["transactionHash"], + transaction_index=log["transactionIndex"], ) # type: ignore diff --git a/tests/functional/test_ecosystem.py b/tests/functional/test_ecosystem.py index 45234be23a..1645f7a15e 100644 --- a/tests/functional/test_ecosystem.py +++ b/tests/functional/test_ecosystem.py @@ -6,7 +6,7 @@ from ape.utils import DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT from ape_ethereum.ecosystem import Block -LOG_FROM_RESPONSE = { +LOG = { "removed": False, "logIndex": "0x0", "transactionIndex": "0x0", @@ -21,21 +21,6 @@ "0x9f3d45ac20ccf04b45028b8080bb191eab93e29f7898ed43acf480dd80bba94d", ], } -LOG_FROM_ETH_TESTER = { - "removed": False, - "log_index": 0, - "transaction_index": 0, - "transaction_hash": "0x74dd040dfa06f0af9af8ca95d7aae409978400151c746f55ecce19e7356cfc5a", - "block_hash": "0x2c99950b07accf3e442512a3352a11e6fed37b2331de5f71b7743b357d96e4e8", - "block_number": 11093676, - "address": "0x274b028b03a250ca03644e6c578d81f019ee1323", - "data": "0xabffd4675206dab5d04a6b0d62c975049665d1f512f29f303908abdd20bc08a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000744796e616d696300000000000000000000000000000000000000000000000000", # noqa: E501 - "topics": [ - "0xa84473122c11e32cd505595f246a28418b8ecd6cf819f4e3915363fad1b8f968", - "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x9f3d45ac20ccf04b45028b8080bb191eab93e29f7898ed43acf480dd80bba94d", - ], -} @pytest.fixture @@ -87,10 +72,9 @@ def test_transaction_acceptance_timeout(temp_config, config, networks): assert networks.provider.network.transaction_acceptance_timeout == new_value -@pytest.mark.parametrize("log_data", (LOG_FROM_RESPONSE, LOG_FROM_ETH_TESTER)) -def test_decode_logs(ethereum, vyper_contract_instance, log_data): +def test_decode_logs(ethereum, vyper_contract_instance): abi = vyper_contract_instance.NumberChange.abi - result = [x for x in ethereum.decode_logs([log_data], abi)] + result = [x for x in ethereum.decode_logs([LOG], abi)] assert len(result) == 1 assert result[0] == { "event_name": "NumberChange",