Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: issues with type 2 transactions #26

Merged
merged 2 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading