Skip to content

Commit

Permalink
fix: issues with type 2 transactions (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Jan 18, 2024
1 parent edfa126 commit b8bc9b0
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 58 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
117 changes: 85 additions & 32 deletions ape_arbitrum/ecosystem.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
49 changes: 38 additions & 11 deletions tests/test_ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down

0 comments on commit b8bc9b0

Please sign in to comment.