Skip to content

Commit

Permalink
fix: various bugs regarding AccessList usage in transactions (type …
Browse files Browse the repository at this point in the history
…1 and 2) (#1853)
  • Loading branch information
antazoey authored Jan 10, 2024
1 parent 56ea74b commit fb6a57a
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 58 deletions.
123 changes: 80 additions & 43 deletions src/ape_ethereum/ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,78 +600,98 @@ 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[TransactionType, Type[TransactionAPI]] = {
TransactionType.STATIC: StaticFeeTransaction,
TransactionType.DYNAMIC: DynamicFeeTransaction,
TransactionType.ACCESS_LIST: AccessListTransaction,
}

if "type" in kwargs:
if kwargs["type"] is None:
version = TransactionType.DYNAMIC
elif isinstance(kwargs["type"], TransactionType):
version = kwargs["type"]
elif isinstance(kwargs["type"], int):
version = TransactionType(kwargs["type"])
if "type" in tx_data:
if tx_data["type"] is None:
# Explicit `None` means used default.
version = self.default_transaction_type
elif isinstance(tx_data["type"], TransactionType):
version = tx_data["type"]
elif isinstance(tx_data["type"], int):
version = TransactionType(tx_data["type"])
else:
# Using hex values or alike.
version = TransactionType(self.conversion_manager.convert(kwargs["type"], int))
version = TransactionType(self.conversion_manager.convert(tx_data["type"], int))

elif "gas_price" in kwargs:
elif "gas_price" in tx_data:
version = TransactionType.STATIC
elif "max_fee" in tx_data or "max_priority_fee" in tx_data:
version = TransactionType.DYNAMIC
elif "access_list" in tx_data or "accessList" in tx_data:
version = TransactionType.ACCESS_LIST
else:
version = self.default_transaction_type

kwargs["type"] = version.value
tx_data["type"] = version.value

# 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 or kwargs["chainId"] is None
"chainId" not in tx_data or tx_data["chainId"] is None
) and self.network_manager.active_provider is not None:
kwargs["chainId"] = self.provider.chain_id
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")

kwargs["gas"] = kwargs.pop("gas_limit", kwargs.get("gas"))
if "gas" not in tx_data:
tx_data["gas"] = None

if "value" in kwargs and not isinstance(kwargs["value"], int):
value = kwargs["value"] or 0 # Convert None to 0.
kwargs["value"] = self.conversion_manager.convert(value, int)

# This causes problems in pydantic for some reason.
if "gas_price" in kwargs and kwargs["gas_price"] is None:
del kwargs["gas_price"]

# None is not allowed, the user likely means `b""`.
if "data" in kwargs and kwargs["data"] is None:
kwargs["data"] = b""

return txn_class(**kwargs)
return txn_class(**tx_data)

def decode_logs(self, logs: Sequence[Dict], *events: EventABI) -> Iterator["ContractLog"]:
if not logs:
Expand Down Expand Up @@ -958,3 +978,20 @@ def parse_type(type_: Dict[str, Any]) -> Union[str, Tuple, List]:

result = tuple([parse_type(c) for c in type_["components"]])
return [result] if is_array(type_["type"]) else result


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
1 change: 1 addition & 0 deletions src/ape_ethereum/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,7 @@ def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI:
if txn.max_fee is None:
multiplier = self.network.base_fee_multiplier
txn.max_fee = int(self.base_fee * multiplier + txn.max_priority_fee)

# else: Assume user specified the correct amount or txn will fail and waste gas

gas_limit = self.network.gas_limit if txn.gas_limit is None else txn.gas_limit
Expand Down
19 changes: 18 additions & 1 deletion src/ape_ethereum/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ class TransactionType(Enum):

class AccessList(BaseModel):
address: str
storage_keys: List[Union[str, bytes, int]] = Field(default_factory=list, alias="storageKeys")
storage_keys: List[Union[HexBytes, bytes, str, int]] = Field(
default_factory=list, alias="storageKeys"
)


class BaseTransaction(TransactionAPI):
Expand All @@ -72,6 +74,21 @@ def serialize_transaction(self) -> bytes:
if txn_data.get("to") == ZERO_ADDRESS:
del txn_data["to"]

# Adjust bytes in the access list if necessary.
if "accessList" in txn_data:
adjusted_access_list = []

for item in txn_data["accessList"]:
adjusted_item = {**item}
storage_keys_corrected = [HexBytes(k).hex() for k in item.get("storageKeys", [])]

if storage_keys_corrected:
adjusted_item["storageKeys"] = storage_keys_corrected

adjusted_access_list.append(adjusted_item)

txn_data["accessList"] = adjusted_access_list

unsigned_txn = serializable_unsigned_transaction_from_dict(txn_data)
signature = (self.signature.v, to_int(self.signature.r), to_int(self.signature.s))
signed_txn = encode_transaction(unsigned_txn, signature)
Expand Down
12 changes: 9 additions & 3 deletions src/ape_test/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from hexbytes import HexBytes

from ape.api import TestAccountAPI, TestAccountContainerAPI, TransactionAPI
from ape.exceptions import SignatureError
from ape.types import AddressType, MessageSignature, TransactionSignature
from ape.utils import GeneratedDevAccount, generate_dev_accounts

Expand Down Expand Up @@ -122,9 +123,14 @@ def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]

def sign_transaction(self, txn: TransactionAPI, **signer_options) -> Optional[TransactionAPI]:
# Signs anything that's given to it
signature = EthAccount.sign_transaction(
txn.model_dump(mode="json", by_alias=True), self.private_key
)
tx_data = txn.model_dump(mode="json", by_alias=True)

try:
signature = EthAccount.sign_transaction(tx_data, self.private_key)
except TypeError as err:
# Occurs when missing properties on the txn that are needed to sign.
raise SignatureError(str(err)) from err

txn.signature = TransactionSignature(
v=signature.v,
r=to_bytes(signature.r),
Expand Down
7 changes: 7 additions & 0 deletions tests/functional/test_ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,13 @@ def test_create_transaction_with_none_values(ethereum, eth_tester_provider):
assert dynamic.max_priority_fee is None


@pytest.mark.parametrize("kwarg_name", ("max_fee", "max_fee_per_gas", "maxFee", "maxFeePerGas"))
def test_create_transaction_max_fee_per_gas(kwarg_name, ethereum):
fee = 1_000_000_000
tx = ethereum.create_transaction(**{kwarg_name: fee})
assert tx.max_fee == fee


@pytest.mark.parametrize("tx_type", TransactionType)
def test_encode_transaction(tx_type, ethereum, vyper_contract_instance, owner, eth_tester_provider):
abi = vyper_contract_instance.contract_type.methods[0]
Expand Down
Loading

0 comments on commit fb6a57a

Please sign in to comment.