diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 4606f5c3db..b051b56ca7 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -306,13 +306,13 @@ def encode_transaction( """ @abstractmethod - def decode_logs(self, abi: EventABI, raw_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: - abi (EventABI): The event producing the logs. - raw_logs (List[Dict]): A list of raw log data from the chain. + 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 281ff27aa5..4b63638778 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 @@ -21,6 +21,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 @@ -740,7 +741,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) @@ -758,20 +759,26 @@ 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): 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]) + 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: diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 7691594c6f..5a7cbe179a 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -215,14 +215,12 @@ def _confirmations_occurred(self) -> int: return latest_block.number - self.block_number - def raise_for_status(self): - """ - Handle provider-specific errors regarding a non-successful - :class:`~api.providers.TransactionStatusEnum`. - """ - + @abstractmethod def decode_logs( - self, abi: Union[List[Union[EventABI, "ContractEvent"]], Union[EventABI, "ContractEvent"]] + self, + abi: Optional[ + Union[List[Union[EventABI, "ContractEvent"]], Union[EventABI, "ContractEvent"]] + ] = None, ) -> Iterator[ContractLog]: """ Decode the logs on the receipt. @@ -233,14 +231,12 @@ def decode_logs( Returns: Iterator[:class:`~ape.types.ContractLog`] """ - if not isinstance(abi, (list, tuple)): - abi = [abi] - for event_abi in abi: - if not isinstance(event_abi, EventABI): - event_abi = event_abi.abi - - yield from self.provider.network.ecosystem.decode_logs(event_abi, self.logs) + def raise_for_status(self): + """ + Handle provider-specific errors regarding a non-successful + :class:`~api.providers.TransactionStatusEnum`. + """ def await_confirmations(self) -> "ReceiptAPI": """ 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/managers/chain.py b/src/ape/managers/chain.py index 45630e3b73..9deb3b000f 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( @@ -596,6 +595,13 @@ def get( self._local_proxies[address_key] = proxy_info 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. contract_type = self._get_contract_type_from_explorer(address_key) diff --git a/src/ape/types/__init__.py b/src/ape/types/__init__.py index 40e841c19e..50935d8ca8 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 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 ) @@ -147,7 +148,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 @@ -168,6 +169,25 @@ 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. + """ + + @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..98193914c1 100644 --- a/src/ape/utils/__init__.py +++ b/src/ape/utils/__init__.py @@ -34,6 +34,7 @@ raises_not_implemented, singledispatchmethod, stream_response, + to_int, ) 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 +86,7 @@ "Struct", "StructParser", "TraceStyles", + "to_int", "use_temp_sys_path", "USER_AGENT", "ZERO_ADDRESS", diff --git a/src/ape/utils/trace.py b/src/ape/utils/trace.py index d5963e7bed..da56b973ea 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 81d7f17d45..7dc552d17c 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -26,7 +26,6 @@ parse_type, returns_array, ) -from ape.utils.misc import to_int from ape_ethereum.transactions import ( AccessListTransaction, BaseTransaction, @@ -148,6 +147,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 @@ -444,13 +445,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 } @@ -470,13 +465,13 @@ def decode_logs( continue event_arguments = abi.decode(topics, log["data"]) - yield ContractLog( - name=abi.event_name, + 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["logIndex"], transaction_hash=log["transactionHash"], - block_number=to_int(log["blockNumber"]), - block_hash=log["blockHash"], - log_index=to_int(log["logIndex"]), - ) + transaction_index=log["transactionIndex"], + ) # type: ignore diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index f91e56101b..b974125644 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, 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 @@ -79,7 +82,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) @@ -97,7 +100,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): @@ -115,7 +118,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): @@ -177,3 +180,77 @@ 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]: + 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(self.logs, *event_abis) + + 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._decode_ds_note(log) + if library_log: + yield library_log + else: + 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 + 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:]) + address = self.provider.network.ecosystem.decode_address(log["address"]) + + return ContractLog( + block_hash=log["blockHash"], + 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"], + transaction_hash=log["transactionHash"], + transaction_index=log["transactionIndex"], + ) # type: ignore diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index bd95d834af..d76a0ef0ed 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -21,19 +21,20 @@ from ape.types import AddressType, ContractLog -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()) ALIAS = "__FUNCTIONAL_TESTS_ALIAS__" ALIAS_2 = "__FUNCTIONAL_TESTS_ALIAS_2__" -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") APE_PROJECT_FOLDER = BASE_PROJECTS_DIRECTORY / "ApeProject" SOLIDITY_CONTRACT_ADDRESS = "0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951" VYPER_CONTRACT_ADDRESS = "0x274b028b03A250cA03644E6c578D81f019eE1323" @@ -200,6 +201,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 @@ -259,6 +267,46 @@ 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 + + +@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, + } + + @pytest.fixture def chain_at_block_5(chain): snapshot_id = chain.snapshot() 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/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 diff --git a/tests/functional/test_contract_event.py b/tests/functional/test_contract_event.py index 7c744b7a3c..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): @@ -252,3 +259,12 @@ def test_contract_two_events_with_same_name(owner, chain, networks_connected_to_ assert event_from_impl_contract.abi.signature == expected_sig_from_impl event_from_interface = impl_instance.get_event_by_signature(expected_sig_from_interface) assert event_from_interface.abi.signature == expected_sig_from_interface + + +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].event_name == "NumberChange" + assert events[0].newNum == 1 + assert events[0].transaction_index == 0 diff --git a/tests/functional/test_ecosystem.py b/tests/functional/test_ecosystem.py new file mode 100644 index 0000000000..1645f7a15e --- /dev/null +++ b/tests/functional/test_ecosystem.py @@ -0,0 +1,101 @@ +import pytest +from eth_typing import HexAddress, HexStr +from hexbytes import HexBytes + +from ape.types import AddressType +from ape.utils import DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT +from ape_ethereum.ecosystem import Block + +LOG = { + "removed": False, + "logIndex": "0x0", + "transactionIndex": "0x0", + "transactionHash": "0x74dd040dfa06f0af9af8ca95d7aae409978400151c746f55ecce19e7356cfc5a", + "blockHash": "0x2c99950b07accf3e442512a3352a11e6fed37b2331de5f71b7743b357d96e4e8", + "blockNumber": "0xa946ac", + "address": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "data": "0xabffd4675206dab5d04a6b0d62c975049665d1f512f29f303908abdd20bc08a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000744796e616d696300000000000000000000000000000000000000000000000000", # noqa: E501 + "topics": [ + "0xa84473122c11e32cd505595f246a28418b8ecd6cf819f4e3915363fad1b8f968", + "0x0000000000000000000000000000000000000000000000000000000000000006", + "0x9f3d45ac20ccf04b45028b8080bb191eab93e29f7898ed43acf480dd80bba94d", + ], +} + + +@pytest.fixture +def event_abi(vyper_contract_instance): + return vyper_contract_instance.NumberChange.abi + + +@pytest.mark.parametrize( + "address", + ( + "0x63953eB1B3D8DB28334E7C1C69456C851F934199".lower(), + 0x63953EB1B3D8DB28334E7C1C69456C851F934199, + ), +) +def test_decode_address(ethereum, address): + expected = "0x63953eB1B3D8DB28334E7C1C69456C851F934199" + actual = ethereum.decode_address(address) + assert actual == expected + + +def test_encode_address(ethereum): + raw_address = "0x63953eB1B3D8DB28334E7C1C69456C851F934199" + address = AddressType(HexAddress(HexStr(raw_address))) + actual = ethereum.encode_address(address) + assert actual == raw_address + + +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") + + # Replace 'parentHash' key with 'parent_hash' + latest_block = eth_tester_provider.get_block("latest") + latest_block_dict = eth_tester_provider.get_block("latest").dict() + latest_block_dict["parent_hash"] = latest_block_dict.pop("parentHash") + + redefined_block = Block.parse_obj(latest_block_dict) + assert redefined_block.parent_hash == latest_block.parent_hash + + +def test_transaction_acceptance_timeout(temp_config, config, networks): + assert ( + networks.provider.network.transaction_acceptance_timeout + == DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT + ) + new_value = DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT + 1 + timeout_config = {"ethereum": {"local": {"transaction_acceptance_timeout": new_value}}} + with temp_config(timeout_config): + assert networks.provider.network.transaction_acceptance_timeout == new_value + + +def test_decode_logs(ethereum, vyper_contract_instance): + abi = vyper_contract_instance.NumberChange.abi + result = [x for x in ethereum.decode_logs([LOG], 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, + } + + +def test_decode_logs_empty_list(ethereum, event_abi): + actual = [x for x in ethereum.decode_logs([], event_abi)] + assert actual == [] diff --git a/tests/functional/test_ethereum.py b/tests/functional/test_ethereum.py deleted file mode 100644 index da1bd1cccd..0000000000 --- a/tests/functional/test_ethereum.py +++ /dev/null @@ -1,123 +0,0 @@ -import pytest -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 - - -@pytest.mark.parametrize( - "address", - ( - "0x63953eB1B3D8DB28334E7C1C69456C851F934199".lower(), - 0x63953EB1B3D8DB28334E7C1C69456C851F934199, - ), -) -def test_decode_address(ethereum, address): - expected = "0x63953eB1B3D8DB28334E7C1C69456C851F934199" - actual = ethereum.decode_address(address) - assert actual == expected - - -def test_encode_address(ethereum): - raw_address = "0x63953eB1B3D8DB28334E7C1C69456C851F934199" - address = AddressType(HexAddress(HexStr(raw_address))) - actual = ethereum.encode_address(address) - 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") - - # Replace 'parentHash' key with 'parent_hash' - latest_block = eth_tester_provider.get_block("latest") - latest_block_dict = eth_tester_provider.get_block("latest").dict() - latest_block_dict["parent_hash"] = latest_block_dict.pop("parentHash") - - redefined_block = Block.parse_obj(latest_block_dict) - assert redefined_block.parent_hash == latest_block.parent_hash - - -def test_transaction_acceptance_timeout(temp_config, config, networks): - assert ( - networks.provider.network.transaction_acceptance_timeout - == DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT - ) - new_value = DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT + 1 - timeout_config = {"ethereum": {"local": {"transaction_acceptance_timeout": new_value}}} - with temp_config(timeout_config): - assert networks.provider.network.transaction_acceptance_timeout == new_value diff --git a/tests/functional/test_receipt.py b/tests/functional/test_receipt.py index c5995af40d..9869307cf1 100644 --- a/tests/functional/test_receipt.py +++ b/tests/functional/test_receipt.py @@ -1,11 +1,13 @@ import pytest from ape.api import ReceiptAPI +from ape.exceptions import OutOfGasError +from ape_ethereum.transactions import Receipt, TransactionStatusEnum @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): @@ -15,6 +17,60 @@ def test_show_trace(invoke_receipt): invoke_receipt.show_trace() +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 + 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): + 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 + 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): + 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].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].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].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].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): event_type = contract_instance.NumberChange @@ -43,6 +99,14 @@ def test_decode_logs_multiple_event_types(owner, contract_instance, assert_log_v 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 + + def test_get_failed_receipt(owner, vyper_contract_instance, eth_tester_provider): # Setting to '5' always fails. transaction = vyper_contract_instance.setNumber.as_transaction( @@ -50,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 cb13ed6a0c..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 @@ -9,14 +10,16 @@ BLOCK_NUMBER = 323423 EVENT_NAME = "MyEvent" LOG_INDEX = 7 +TXN_INDEX = 2 RAW_LOG = { "block_hash": BLOCK_HASH, "block_number": BLOCK_NUMBER, "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, + "transaction_index": TXN_INDEX, } RAW_EVENT_ABI = """ { @@ -45,13 +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.name == EVENT_NAME - assert log.log_index == 7 - assert log.transaction_hash == TXN_HASH + 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):