Skip to content

Commit 7cb52f8

Browse files
authored
feat(ethereum): add support for EIP-7702 (#2612)
1 parent 3a92eb7 commit 7cb52f8

File tree

6 files changed

+99
-7
lines changed

6 files changed

+99
-7
lines changed

src/ape_ethereum/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,11 @@ def __getattr__(name):
6666

6767
elif name in (
6868
"AccessListTransaction",
69+
"Authorization",
6970
"BaseTransaction",
7071
"DynamicFeeTransaction",
7172
"Receipt",
73+
"SetCodeTransaction",
7274
"SharedBlobReceipt",
7375
"SharedBlobTransaction",
7476
"StaticFeeTransaction",
@@ -90,6 +92,7 @@ def __getattr__(name):
9092

9193
__all__ = [
9294
"AccessListTransaction",
95+
"Authorization",
9396
"BaseEthereumConfig",
9497
"BaseTransaction",
9598
"Block",
@@ -101,6 +104,7 @@ def __getattr__(name):
101104
"ForkedNetworkConfig",
102105
"NetworkConfig",
103106
"Receipt",
107+
"SetCodeTransaction",
104108
"SharedBlobReceipt",
105109
"SharedBlobTransaction",
106110
"SharedBlobTransaction",

src/ape_ethereum/ecosystem.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
BaseTransaction,
7171
DynamicFeeTransaction,
7272
Receipt,
73+
SetCodeTransaction,
7374
SharedBlobReceipt,
7475
SharedBlobTransaction,
7576
StaticFeeTransaction,
@@ -435,9 +436,13 @@ def decode_transaction_type(self, transaction_type_id: Any) -> type["Transaction
435436

436437
if tx_type is TransactionType.STATIC:
437438
return StaticFeeTransaction
439+
438440
elif tx_type is TransactionType.ACCESS_LIST:
439441
return AccessListTransaction
440442

443+
elif tx_type is TransactionType.SET_CODE:
444+
return SetCodeTransaction
445+
441446
return DynamicFeeTransaction
442447

443448
def encode_contract_blueprint(
@@ -898,6 +903,7 @@ def create_transaction(self, **kwargs) -> "TransactionAPI":
898903
TransactionType.ACCESS_LIST: AccessListTransaction,
899904
TransactionType.DYNAMIC: DynamicFeeTransaction,
900905
TransactionType.SHARED_BLOB: SharedBlobTransaction,
906+
TransactionType.SET_CODE: SetCodeTransaction,
901907
}
902908
if "type" in tx_data:
903909
# May be None in data.
@@ -912,14 +918,17 @@ def create_transaction(self, **kwargs) -> "TransactionAPI":
912918
# Using hex values or alike.
913919
version = TransactionType(self.conversion_manager.convert(tx_data["type"], int))
914920

915-
elif "gas_price" in tx_data:
916-
version = TransactionType.STATIC
921+
# NOTE: Determine these in reverse order
922+
elif "authorizationList" in tx_data:
923+
version = TransactionType.SET_CODE
924+
elif "maxFeePerBlobGas" in tx_data or "blobVersionedHashes" in tx_data:
925+
version = TransactionType.SHARED_BLOB
917926
elif "max_fee" in tx_data or "max_priority_fee" in tx_data:
918927
version = TransactionType.DYNAMIC
919928
elif "access_list" in tx_data or "accessList" in tx_data:
920929
version = TransactionType.ACCESS_LIST
921-
elif "maxFeePerBlobGas" in tx_data or "blobVersionedHashes" in tx_data:
922-
version = TransactionType.SHARED_BLOB
930+
elif "gas_price" in tx_data:
931+
version = TransactionType.STATIC
923932
else:
924933
version = self.default_transaction_type
925934

src/ape_ethereum/provider.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,11 @@ def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI:
11971197
and txn.gas_price is None
11981198
):
11991199
txn.gas_price = self.gas_price
1200-
elif txn_type in (TransactionType.DYNAMIC, TransactionType.SHARED_BLOB):
1200+
elif txn_type in (
1201+
TransactionType.DYNAMIC,
1202+
TransactionType.SHARED_BLOB,
1203+
TransactionType.SET_CODE,
1204+
):
12011205
if txn.max_priority_fee is None:
12021206
txn.max_priority_fee = self.priority_fee
12031207

src/ape_ethereum/transactions.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@
99
encode_transaction,
1010
serializable_unsigned_transaction_from_dict,
1111
)
12+
from eth_account.typed_transactions.set_code_transaction import Authorization as EthAcctAuth
1213
from eth_pydantic_types import HexBytes
13-
from eth_utils import decode_hex, encode_hex, keccak, to_hex, to_int
14+
from eth_utils import decode_hex, encode_hex, keccak, to_canonical_address, to_hex, to_int
1415
from ethpm_types.abi import EventABI, MethodABI
15-
from pydantic import BaseModel, Field, field_validator, model_validator
16+
from pydantic import BaseModel, Field, field_serializer, field_validator, model_validator
1617

1718
from ape.api.transactions import ReceiptAPI, TransactionAPI
1819
from ape.exceptions import OutOfGasError, SignatureError, TransactionError
1920
from ape.logging import logger
2021
from ape.types.address import AddressType
2122
from ape.types.basic import HexInt
2223
from ape.types.events import ContractLog, ContractLogContainer
24+
from ape.types.signatures import MessageSignature
2325
from ape.types.trace import SourceTraceback
2426
from ape.utils.misc import ZERO_ADDRESS
2527
from ape_ethereum.trace import Trace, _events_to_trees
@@ -55,6 +57,7 @@ class TransactionType(Enum):
5557
ACCESS_LIST = 1 # EIP-2930
5658
DYNAMIC = 2 # EIP-1559
5759
SHARED_BLOB = 3 # EIP-4844
60+
SET_CODE = 4 # EIP-7702
5861

5962

6063
class AccessList(BaseModel):
@@ -97,6 +100,17 @@ def serialize_transaction(self) -> bytes:
97100

98101
txn_data["accessList"] = adjusted_access_list
99102

103+
if "authorizationList" in txn_data:
104+
adjusted_auth_list = []
105+
106+
for item in txn_data["authorizationList"]:
107+
adjusted_item = {
108+
k: to_hex(v) if isinstance(v, bytes) else v for k, v in item.items()
109+
}
110+
adjusted_auth_list.append(adjusted_item)
111+
112+
txn_data["authorizationList"] = adjusted_auth_list
113+
100114
unsigned_txn = serializable_unsigned_transaction_from_dict(txn_data)
101115
signature = (self.signature.v, to_int(self.signature.r), to_int(self.signature.s))
102116
signed_txn = encode_transaction(unsigned_txn, signature)
@@ -186,6 +200,47 @@ class SharedBlobTransaction(DynamicFeeTransaction):
186200
"""
187201

188202

203+
class Authorization(BaseModel):
204+
"""
205+
`EIP-7702 <https://eips.ethereum.org/EIPS/eip-7702>`__ authorization list item.
206+
"""
207+
208+
chain_id: HexInt = Field(alias="chainId")
209+
address: AddressType
210+
nonce: HexInt
211+
v: HexInt = Field(alias="yParity")
212+
r: HexBytes
213+
s: HexBytes
214+
215+
@field_serializer("chain_id", "nonce", "v")
216+
def convert_int_to_hex(self, value: int) -> str:
217+
return to_hex(value)
218+
219+
@property
220+
def signature(self) -> MessageSignature:
221+
return MessageSignature(v=self.v, r=self.r, s=self.s)
222+
223+
@cached_property
224+
def authority(self) -> AddressType:
225+
auth = EthAcctAuth(self.chain_id, to_canonical_address(self.address), self.nonce)
226+
return EthAccount._recover_hash(
227+
auth.hash(),
228+
vrs=(self.signature.v, to_int(self.r), to_int(self.s)),
229+
)
230+
231+
232+
class SetCodeTransaction(DynamicFeeTransaction):
233+
"""
234+
`EIP-7702 <https://eips.ethereum.org/EIPS/eip-7702>`__ transactions.
235+
"""
236+
237+
authorizations: list[Authorization] = Field(default_factory=list, alias="authorizationList")
238+
receiver: AddressType = Field(default=ZERO_ADDRESS, alias="to")
239+
"""
240+
Overridden because EIP-7702 states it cannot be nil.
241+
"""
242+
243+
189244
class Receipt(ReceiptAPI):
190245
gas_limit: HexInt
191246
gas_price: HexInt

tests/functional/test_ecosystem.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ape_ethereum.transactions import (
2020
DynamicFeeTransaction,
2121
Receipt,
22+
SetCodeTransaction,
2223
SharedBlobReceipt,
2324
SharedBlobTransaction,
2425
StaticFeeTransaction,
@@ -921,6 +922,16 @@ def test_create_transaction_blob_versioned_hashed(kwarg_name, value, ethereum):
921922
assert tx.blob_versioned_hashes == [HexBytes(value)]
922923

923924

925+
@pytest.mark.parametrize(
926+
"tx_kwargs",
927+
[{"type": 4}, {"authorizationList": []}],
928+
)
929+
def test_create_transaction_set_code(tx_kwargs, ethereum):
930+
tx = ethereum.create_transaction(**tx_kwargs)
931+
assert isinstance(tx, SetCodeTransaction)
932+
assert tx.type == TransactionType.SET_CODE.value
933+
934+
924935
@pytest.mark.parametrize("tx_type", TransactionType)
925936
def test_encode_transaction(tx_type, ethereum, vyper_contract_instance, owner, eth_tester_provider):
926937
abi = vyper_contract_instance.contract_type.methods[0]

tests/functional/test_transaction.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@ def test_type_3_transactions(ethereum, tx_kwargs):
186186
assert txn.type == 3
187187

188188

189+
@pytest.mark.parametrize(
190+
"tx_kwargs",
191+
[{"type": 4}, {"authorizationList": []}],
192+
)
193+
def test_type_4_transactions(ethereum, tx_kwargs):
194+
txn = ethereum.create_transaction(**tx_kwargs)
195+
assert txn.type == 4
196+
197+
189198
@pytest.mark.parametrize(
190199
"fee_kwargs",
191200
(

0 commit comments

Comments
 (0)