From 4b4b6b60338f830d26b62d468351e8660de7710a Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Tue, 30 Jul 2024 14:47:41 -0500 Subject: [PATCH] chore: wip --- src/ape/api/transactions.py | 11 +++++++++ src/ape_ethereum/ecosystem.py | 2 +- src/ape_ethereum/provider.py | 39 ++++++++++++++----------------- src/ape_test/provider.py | 26 +++++++++++++++------ tests/functional/test_provider.py | 16 ++++++------- 5 files changed, 57 insertions(+), 37 deletions(-) diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 36e4f26dc1..cbfe847059 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -74,6 +74,17 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._raise_on_revert = raise_on_revert + @field_validator("nonce", mode="before") + @classmethod + def validate_nonce(cls, value): + if value is None or isinstance(value, int): + return value + + elif isinstance(value, str) and value.startswith("0x"): + return to_int(hexstr=value) + + return to_int(value) + @field_validator("gas_limit", mode="before") @classmethod def validate_gas_limit(cls, value): diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 5c35f28c75..09039d6abe 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -950,7 +950,7 @@ def create_transaction(self, **kwargs) -> TransactionAPI: if "gas" not in tx_data: tx_data["gas"] = None - return txn_class(**tx_data) + return txn_class.model_validate(tx_data) def decode_logs(self, logs: Sequence[dict], *events: EventABI) -> Iterator["ContractLog"]: if not logs: diff --git a/src/ape_ethereum/provider.py b/src/ape_ethereum/provider.py index bfcbeae889..848514b620 100644 --- a/src/ape_ethereum/provider.py +++ b/src/ape_ethereum/provider.py @@ -214,7 +214,7 @@ def client_version(self) -> str: @property def base_fee(self) -> int: - latest_block_number = self._get_latest_block().number + latest_block_number = self._get_latest_block_rpc().get("number") if latest_block_number is None: # Possibly no blocks yet. logger.debug("Latest block has no number. Using base fee of '0'.") @@ -261,8 +261,7 @@ def _get_fee_history(self, block_number: int) -> FeeHistory: raise APINotImplementedError(str(err)) from err def _get_last_base_fee(self) -> int: - block = self._get_latest_block() - base_fee = getattr(block, "base_fee", None) + base_fee = self._get_latest_block_rpc().get("baseFeePerGas", None) if base_fee is not None: return base_fee @@ -277,7 +276,7 @@ def is_connected(self) -> bool: @property def max_gas(self) -> int: - return self.make_request("eth_getBlockByNumber", ["latest"])["gasLimit"] + return int(self._get_latest_block_rpc()["gasLimit"], 16) @cached_property def supports_tracing(self) -> bool: @@ -395,12 +394,14 @@ def get_block(self, block_id: BlockID) -> BlockAPI: def _get_latest_block(self) -> BlockAPI: # perf: By-pass as much as possible since this is a common action. - data = self.make_request("eth_getBlockByNumber", ["latest"]) + data = self._get_latest_block_rpc() return self.network.ecosystem.decode_block(data) + def _get_latest_block_rpc(self) -> dict: + return self.make_request("eth_getBlockByNumber", ["latest", False]) + def get_nonce(self, address: AddressType, block_id: Optional[BlockID] = None) -> int: - arguments = [address] if block_id is None else [address, block_id] # type: ignore - return self.make_request("eth_getTransactionCount", arguments) + return self.web3.eth.get_transaction_count(address, block_identifier=block_id) def get_balance(self, address: AddressType, block_id: Optional[BlockID] = None) -> int: return self.web3.eth.get_balance(address, block_identifier=block_id) @@ -577,20 +578,16 @@ def get_receipt( ) hex_hash = HexBytes(txn_hash) - # perf: avoid the "wait" method when using dev networks. - if self.network.is_dev: - receipt_data = self.web3.eth.get_transaction_receipt(hex_hash) - else: - try: - receipt_data = self.web3.eth.wait_for_transaction_receipt(hex_hash, timeout=timeout) - except TimeExhausted as err: - msg_str = str(err) - if f"HexBytes('{txn_hash}')" in msg_str: - msg_str = msg_str.replace(f"HexBytes('{txn_hash}')", f"'{txn_hash}'") - - raise TransactionNotFoundError( - transaction_hash=txn_hash, error_message=msg_str - ) from err + try: + receipt_data = self.web3.eth.wait_for_transaction_receipt(hex_hash, timeout=timeout) + except TimeExhausted as err: + msg_str = str(err) + if f"HexBytes('{txn_hash}')" in msg_str: + msg_str = msg_str.replace(f"HexBytes('{txn_hash}')", f"'{txn_hash}'") + + raise TransactionNotFoundError( + transaction_hash=txn_hash, error_message=msg_str + ) from err ecosystem_config = self.network.ecosystem_config.model_dump(by_alias=True) network_config: dict = ecosystem_config.get(self.network.name, {}) diff --git a/src/ape_test/provider.py b/src/ape_test/provider.py index 76739a9119..7c0cbf1895 100644 --- a/src/ape_test/provider.py +++ b/src/ape_test/provider.py @@ -19,6 +19,7 @@ from ape.api import PluginConfig, ReceiptAPI, TestProviderAPI, TransactionAPI from ape.exceptions import ( + APINotImplementedError, ContractLogicError, ProviderError, ProviderNotConnectedError, @@ -90,6 +91,10 @@ def auto_mine(self, value: Any) -> None: else: raise TypeError("Expecting bool-value for auto_mine setter.") + @property + def max_gas(self) -> int: + return self.evm_backend.get_block_by_number("latest")["gas_limit"] + def connect(self): if "tester" in self.__dict__: del self.__dict__["tester"] @@ -120,9 +125,7 @@ def estimate_gas_cost( estimate_gas = self.web3.eth.estimate_gas # NOTE: Using JSON mode since used as request data. - txn_dict = txn.model_dump(mode="json") - - txn_dict.pop("gas", None) + txn_dict = txn.model_dump(by_alias=True, mode="json", exclude=("gas_limit", "chain_id")) txn_data = cast(TxParams, txn_dict) try: @@ -235,7 +238,9 @@ def send_call( def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI: vm_err = None try: - txn_hash = self.web3.eth.send_raw_transaction(txn.serialize_transaction()).hex() + txn_hash = self.tester.ethereum_tester.send_raw_transaction( + txn.serialize_transaction().hex() + ) except (ValidationError, TransactionFailed, Web3ContractLogicError) as err: vm_err = self.get_virtual_machine_error(err, txn=txn) if txn.raise_on_revert: @@ -249,7 +254,7 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI: required_confirmations=required_confirmations, error=vm_err, txn_hash=txn_hash ) else: - receipt_data = self.make_request("eth_getTransactionReceipt", [txn_hash]) + receipt_data = self.tester.ethereum_tester.get_transaction_receipt(txn_hash) receipt = self._create_receipt( error=vm_err, txn_hash=txn_hash, @@ -293,7 +298,7 @@ def snapshot(self) -> SnapshotID: def restore(self, snapshot_id: SnapshotID): if snapshot_id: - current_hash = self._get_latest_block().hash + current_hash = self._get_latest_block_rpc().get("hash") if current_hash != snapshot_id: try: return self.evm_backend.revert_to_snapshot(snapshot_id) @@ -337,7 +342,7 @@ def get_contract_logs(self, log_filter: LogFilter) -> Iterator[ContractLog]: if log_filter.stop_block is None: to_block = None else: - latest_block = self._get_latest_block().number + latest_block = self._get_latest_block_rpc().get("number") to_block = ( min(latest_block, log_filter.stop_block) if latest_block is not None @@ -369,6 +374,13 @@ def get_test_account(self, index: int) -> "TestAccountAPI": def add_account(self, private_key: str): self.evm_backend.add_account(private_key) + def _get_last_base_fee(self) -> int: + base_fee = self._get_latest_block_rpc().get("base_fee_per_gas", None) + if base_fee is not None: + return base_fee + + raise APINotImplementedError("No base fee found in block.") + def get_virtual_machine_error(self, exception: Exception, **kwargs) -> VirtualMachineError: if isinstance(exception, ValidationError): match = self._CANNOT_AFFORD_GAS_PATTERN.match(str(exception)) diff --git a/tests/functional/test_provider.py b/tests/functional/test_provider.py index ac72f69a26..f826ce5c6f 100644 --- a/tests/functional/test_provider.py +++ b/tests/functional/test_provider.py @@ -6,7 +6,6 @@ from eth_tester.exceptions import TransactionFailed # type: ignore from eth_typing import HexStr from eth_utils import ValidationError -from hexbytes import HexBytes from requests import HTTPError from web3.exceptions import ContractPanicError @@ -298,17 +297,18 @@ def test_no_comma_in_rpc_url(): def test_send_transaction_when_no_error_and_receipt_fails( - mock_transaction, mock_web3, eth_tester_provider, owner, vyper_contract_instance + mocker, mock_transaction, eth_tester_provider, owner, vyper_contract_instance ): - start_web3 = eth_tester_provider._web3 - eth_tester_provider._web3 = mock_web3 + mock_eth_tester = mocker.MagicMock() + original_tester = eth_tester_provider.tester + eth_tester_provider.__dict__["tester"] = mock_eth_tester try: # NOTE: Value is meaningless. tx_hash = HashBytes32.__eth_pydantic_validate__(123**36) # Sending tx "works" meaning no vm error. - mock_web3.eth.send_raw_transaction.return_value = tx_hash + mock_eth_tester.ethereum_tester.send_raw_transaction.return_value = tx_hash # Getting a receipt "works", but you get a failed one. receipt_data = { @@ -322,17 +322,17 @@ def test_send_transaction_when_no_error_and_receipt_fails( "gasUsed": 123, "gasLimit": 100, } - mock_web3.eth.wait_for_transaction_receipt.return_value = receipt_data + mock_eth_tester.ethereum_tester.get_transaction_receipt.return_value = receipt_data # Attempting to replay the tx does not produce any error. - mock_web3.eth.call.return_value = HexBytes("") + # mock_web3.eth.call.return_value = HexBytes("") # Execute test. with pytest.raises(TransactionError): eth_tester_provider.send_transaction(mock_transaction) finally: - eth_tester_provider._web3 = start_web3 + eth_tester_provider.__dict__["tester"] = original_tester def test_network_choice(eth_tester_provider):