diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3a221cc..98efbdd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,18 +10,18 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black name: black - repo: https://github.com/pycqa/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy additional_dependencies: [types-setuptools, pydantic] diff --git a/ape_arbitrum/ecosystem.py b/ape_arbitrum/ecosystem.py index a9c1e63..c0cc909 100644 --- a/ape_arbitrum/ecosystem.py +++ b/ape_arbitrum/ecosystem.py @@ -1,5 +1,5 @@ import time -from typing import Dict, Optional, Type, cast +from typing import Dict, Optional, Tuple, Type, cast from ape.api.config import PluginConfig from ape.api.networks import LOCAL_NETWORK_NAME @@ -10,6 +10,7 @@ from ape.utils import DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT from ape_ethereum.ecosystem import Ethereum, ForkedNetworkConfig, NetworkConfig from ape_ethereum.transactions import ( + AccessListTransaction, DynamicFeeTransaction, Receipt, StaticFeeTransaction, @@ -145,65 +146,100 @@ def create_transaction(self, **kwargs) -> TransactionAPI: :class:`~ape.api.transactions.TransactionAPI` """ + # Handle all aliases. + tx_data = dict(kwargs) + tx_data = _correct_key( + "max_priority_fee", + tx_data, + ("max_priority_fee_per_gas", "maxPriorityFeePerGas", "maxPriorityFee"), + ) + tx_data = _correct_key("max_fee", tx_data, ("max_fee_per_gas", "maxFeePerGas", "maxFee")) + tx_data = _correct_key("gas", tx_data, ("gas_limit", "gasLimit")) + tx_data = _correct_key("gas_price", tx_data, ("gasPrice",)) + tx_data = _correct_key( + "type", + tx_data, + ("txType", "tx_type", "txnType", "txn_type", "transactionType", "transaction_type"), + ) + + # Handle unique value specifications, such as "1 ether". + if "value" in tx_data and not isinstance(tx_data["value"], int): + value = tx_data["value"] or 0 # Convert None to 0. + tx_data["value"] = self.conversion_manager.convert(value, int) + + # None is not allowed, the user likely means `b""`. + if "data" in tx_data and tx_data["data"] is None: + tx_data["data"] = b"" + + # Deduce the transaction type. transaction_types: Dict[int, Type[TransactionAPI]] = { EthTransactionType.STATIC.value: StaticFeeTransaction, EthTransactionType.DYNAMIC.value: DynamicFeeTransaction, + EthTransactionType.ACCESS_LIST.value: AccessListTransaction, INTERNAL_TRANSACTION_TYPE: InternalTransaction, } - if "type" in kwargs: - if kwargs["type"] is None: - # The Default is pre-EIP-1559. + if "type" in tx_data: + if tx_data["type"] is None: + # Explicit `None` means used default. version = self.default_transaction_type.value - elif not isinstance(kwargs["type"], int): - version = self.conversion_manager.convert(kwargs["type"], int) + elif isinstance(tx_data["type"], EthTransactionType): + version = tx_data["type"].value + elif isinstance(tx_data["type"], int): + version = tx_data["type"] else: - version = kwargs["type"] + # Using hex values or alike. + version = self.conversion_manager.convert(tx_data["type"], int) - elif "gas_price" in kwargs: + elif "gas_price" in tx_data: version = EthTransactionType.STATIC.value + elif "max_fee" in tx_data or "max_priority_fee" in tx_data: + version = EthTransactionType.DYNAMIC.value + elif "access_list" in tx_data or "accessList" in tx_data: + version = EthTransactionType.ACCESS_LIST.value else: version = self.default_transaction_type.value - kwargs["type"] = version + tx_data["type"] = version + + # This causes problems in pydantic for some reason. + # NOTE: This must happen after deducing the tx type! + if "gas_price" in tx_data and tx_data["gas_price"] is None: + del tx_data["gas_price"] + txn_class = transaction_types[version] - if "required_confirmations" not in kwargs or kwargs["required_confirmations"] is None: + if "required_confirmations" not in tx_data or tx_data["required_confirmations"] is None: # Attempt to use default required-confirmations from `ape-config.yaml`. required_confirmations = 0 active_provider = self.network_manager.active_provider if active_provider: required_confirmations = active_provider.network.required_confirmations - kwargs["required_confirmations"] = required_confirmations + tx_data["required_confirmations"] = required_confirmations - if isinstance(kwargs.get("chainId"), str): - kwargs["chainId"] = int(kwargs["chainId"], 16) + if isinstance(tx_data.get("chainId"), str): + tx_data["chainId"] = int(tx_data["chainId"], 16) - elif "chainId" not in kwargs and self.network_manager.active_provider is not None: - kwargs["chainId"] = self.provider.chain_id + elif ( + "chainId" not in tx_data or tx_data["chainId"] is None + ) and self.network_manager.active_provider is not None: + tx_data["chainId"] = self.provider.chain_id - if "input" in kwargs: - kwargs["data"] = kwargs.pop("input") + if "input" in tx_data: + tx_data["data"] = tx_data.pop("input") - if all(field in kwargs for field in ("v", "r", "s")): - kwargs["signature"] = TransactionSignature( - v=kwargs["v"], - r=bytes(kwargs["r"]), - s=bytes(kwargs["s"]), + if all(field in tx_data for field in ("v", "r", "s")): + tx_data["signature"] = TransactionSignature( + v=tx_data["v"], + r=bytes(tx_data["r"]), + s=bytes(tx_data["s"]), ) - if "max_priority_fee_per_gas" in kwargs: - kwargs["max_priority_fee"] = kwargs.pop("max_priority_fee_per_gas") - if "max_fee_per_gas" in kwargs: - kwargs["max_fee"] = kwargs.pop("max_fee_per_gas") + if "gas" not in tx_data: + tx_data["gas"] = None - kwargs["gas"] = kwargs.pop("gas_limit", kwargs.get("gas")) - - if "value" in kwargs and not isinstance(kwargs["value"], int): - kwargs["value"] = self.conversion_manager.convert(kwargs["value"], int) - - return txn_class(**kwargs) + return txn_class(**tx_data) def decode_receipt(self, data: dict) -> ReceiptAPI: """ @@ -246,3 +282,20 @@ def decode_receipt(self, data: dict) -> ReceiptAPI: txn_hash=txn_hash, transaction=self.create_transaction(**data), ) + + +def _correct_key(key: str, data: Dict, alt_keys: Tuple[str, ...]) -> Dict: + if key in data: + return data + + # Check for alternative. + for possible_key in alt_keys: + if possible_key not in data: + continue + + # Alt found: use it. + new_data = {k: v for k, v in data.items() if k not in alt_keys} + new_data[key] = data[possible_key] + return new_data + + return data diff --git a/setup.py b/setup.py index 1931df9..ea569f6 100644 --- a/setup.py +++ b/setup.py @@ -11,10 +11,10 @@ "hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer ], "lint": [ - "black>=23.12.0,<24", # Auto-formatter and linter - "mypy>=1.7.1,<2", # Static type analyzer + "black>=23.12.1,<24", # Auto-formatter and linter + "mypy>=1.8.0,<2", # Static type analyzer "types-setuptools", # Needed for mypy type shed - "flake8>=6.1.0,<7", # Style linter + "flake8>=7.0.0,<8", # Style linter "flake8-breakpoint>=1.1.0,<2", # Detect breakpoints left in code "flake8-print>=5.0.0,<6", # Detect print statements left in code "isort>=5.10.1,<6", # Import sorting linter diff --git a/tests/conftest.py b/tests/conftest.py index 5ad004f..cdbcecf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,15 @@ import pytest +@pytest.fixture(autouse=True) +def eth_tester_provider(): + if not ape.networks.active_provider or ape.networks.provider.name != "test": + with ape.networks.arbitrum.local.use_provider("test") as provider: + yield provider + else: + yield ape.networks.provider + + @pytest.fixture def networks(): return ape.networks @@ -22,15 +31,6 @@ def arbitrum(networks): return networks.arbitrum -@pytest.fixture -def eth_tester_provider(): - if not ape.networks.active_provider or ape.networks.provider.name != "test": - with ape.networks.arbitrum.local.use_provider("test") as provider: - yield provider - else: - yield ape.networks.provider - - @pytest.fixture def account(accounts): return accounts.test_accounts[0] diff --git a/tests/test_ecosystem.py b/tests/test_ecosystem.py index da846a5..f607e45 100644 --- a/tests/test_ecosystem.py +++ b/tests/test_ecosystem.py @@ -12,12 +12,44 @@ def test_gas_limit(arbitrum): assert arbitrum.config.local.gas_limit == LOCAL_GAS_LIMIT -# NOTE: None because we want to show the default is STATIC -@pytest.mark.parametrize("tx_type", (None, 0, "0x0")) -def test_create_transaction(arbitrum, tx_type, eth_tester_provider): - tx = arbitrum.create_transaction(type=tx_type) - assert tx.type == TransactionType.STATIC.value - assert tx.gas_limit == LOCAL_GAS_LIMIT +@pytest.mark.parametrize( + "tx_kwargs", + [ + {}, # Default is type 0 in Arbitrum. + {"type": 0}, + {"gas_price": 0}, + {"gasPrice": 0}, + ], +) +def test_create_transaction_type_0(arbitrum, tx_kwargs): + txn = arbitrum.create_transaction(**tx_kwargs) + assert txn.type == TransactionType.STATIC.value + + +@pytest.mark.parametrize( + "tx_kwargs", + [ + {"type": 2}, + {"max_fee": 0}, + {"max_fee_per_gas": 0}, + {"maxFee": 0}, + {"max_priority_fee_per_gas": 0}, + {"max_priority_fee": 0}, + {"maxPriorityFeePerGas": 0}, + ], +) +def test_create_transaction_type_2(arbitrum, tx_kwargs): + """ + Show is smart-enough to deduce type 2 transactions. + """ + + txn = arbitrum.create_transaction(**tx_kwargs) + assert txn.type == TransactionType.DYNAMIC.value + + +def test_create_transaction_internal(arbitrum): + tx = arbitrum.create_transaction(type=INTERNAL_TRANSACTION_TYPE, gas_limit=10000) + assert tx.type == INTERNAL_TRANSACTION_TYPE @pytest.mark.parametrize( @@ -39,11 +71,6 @@ def test_encode_transaction(tx_type, arbitrum, eth_tester_provider): assert actual.gas_limit == LOCAL_GAS_LIMIT -def test_internal_tx(arbitrum): - tx = arbitrum.create_transaction(type=INTERNAL_TRANSACTION_TYPE, gas_limit=10000) - assert tx.type == INTERNAL_TRANSACTION_TYPE - - def test_decode_receipt(arbitrum): data = { "required_confirmations": 0,