Skip to content

Commit 8b5345a

Browse files
committed
Add Web3Transaction
1 parent cdd2afa commit 8b5345a

File tree

3 files changed

+245
-0
lines changed

3 files changed

+245
-0
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import pytest
2+
3+
from hexbytes import (
4+
HexBytes,
5+
)
6+
7+
from web3.utils.transaction import (
8+
Web3Transaction,
9+
)
10+
11+
ACCESS_LIST_TRANSACTION_TEST_CASE = {
12+
"expected_raw_transaction": "0x01f8e782076c22843b9aca00830186a09409616c3d61b3331fc4109a9e41a8bdb7d9776609865af3107a400086616263646566f872f85994de0b295669a9fd93d5f28d9ec85e40f4cb697baef842a00000000000000000000000000000000000000000000000000000000000000003a00000000000000000000000000000000000000000000000000000000000000007d694bb9bc244d798123fde783fcc1c72d3bb8c189413c001a08289e85fa00f8f7f78a53cf147a87b2a7f0d27e64d7571f9d06a802e365c3430a017dc77eae36c88937db4a5179f57edc6119701652f3f1c6f194d1210d638a061", # noqa: 501
13+
"transaction": {
14+
"gas": "0x186a0",
15+
"gasPrice": "0x3b9aca00",
16+
"data": "0x616263646566",
17+
"nonce": "0x22",
18+
"to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609",
19+
"value": "0x5af3107a4000",
20+
"accessList": ( # test case from EIP-2930
21+
{
22+
"address": "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae",
23+
"storageKeys": (
24+
"0x0000000000000000000000000000000000000000000000000000000000000003", # noqa: E501
25+
"0x0000000000000000000000000000000000000000000000000000000000000007", # noqa: E501
26+
),
27+
},
28+
{
29+
"address": "0xbb9bc244d798123fde783fcc1c72d3bb8c189413",
30+
"storageKeys": (),
31+
},
32+
),
33+
"chainId": "0x76c",
34+
"v": "0x1",
35+
"r": "0x8289e85fa00f8f7f78a53cf147a87b2a7f0d27e64d7571f9d06a802e365c3430",
36+
"s": "0x17dc77eae36c88937db4a5179f57edc6119701652f3f1c6f194d1210d638a061",
37+
},
38+
}
39+
DYNAMIC_FEE_TRANSACTION_TEST_CASE = {
40+
"expected_raw_transaction": "0x02f8758205390284773594008477359400830186a09496216849c49358b10257cb55b28ea603c874b05e865af3107a4000825544c001a0c3000cd391f991169ebfd5d3b9e93c89d31a61c998a21b07a11dc6b9d66f8a8ea022cfe8424b2fbd78b16c9911da1be2349027b0a3c40adf4b6459222323773f74", # noqa: 501
41+
"transaction": {
42+
"gas": "0x186a0",
43+
"maxFeePerGas": "0x77359400",
44+
"maxPriorityFeePerGas": "0x77359400",
45+
"data": "0x5544",
46+
"nonce": "0x2",
47+
"to": "0x96216849c49358B10257cb55b28eA603c874b05E",
48+
"value": "0x5af3107a4000",
49+
"type": "0x2",
50+
"chainId": "0x539",
51+
"accessList": (),
52+
"v": "0x1",
53+
"r": "0xc3000cd391f991169ebfd5d3b9e93c89d31a61c998a21b07a11dc6b9d66f8a8e",
54+
"s": "0x22cfe8424b2fbd78b16c9911da1be2349027b0a3c40adf4b6459222323773f74",
55+
},
56+
}
57+
58+
59+
@pytest.mark.parametrize(
60+
"txn",
61+
[
62+
Web3Transaction.from_dict(ACCESS_LIST_TRANSACTION_TEST_CASE["transaction"]),
63+
Web3Transaction.from_bytes(
64+
HexBytes(ACCESS_LIST_TRANSACTION_TEST_CASE["expected_raw_transaction"])
65+
),
66+
],
67+
)
68+
def test_access_list_transaction(txn):
69+
assert txn.typed_transaction.transaction_type == 1
70+
assert txn.chain_id == 1_900
71+
assert txn.nonce == 34
72+
assert txn.gas == 100_000
73+
assert txn.to == b"\tal=a\xb33\x1f\xc4\x10\x9a\x9eA\xa8\xbd\xb7\xd9wf\t"
74+
assert txn.value == 100_000_000_000_000
75+
assert txn.data == b"abcdef"
76+
assert txn.gas_price == 1_000_000_000
77+
78+
79+
def test_encode_access_list_transaction():
80+
txn = Web3Transaction.from_dict(ACCESS_LIST_TRANSACTION_TEST_CASE["transaction"])
81+
assert txn.encode() == HexBytes(
82+
ACCESS_LIST_TRANSACTION_TEST_CASE["expected_raw_transaction"]
83+
)
84+
85+
86+
@pytest.mark.parametrize(
87+
"txn",
88+
[
89+
Web3Transaction.from_dict(DYNAMIC_FEE_TRANSACTION_TEST_CASE["transaction"]),
90+
Web3Transaction.from_bytes(
91+
HexBytes(DYNAMIC_FEE_TRANSACTION_TEST_CASE["expected_raw_transaction"])
92+
),
93+
],
94+
)
95+
def test_dynamic_fee_transaction(txn):
96+
assert txn.typed_transaction.transaction_type == 2
97+
assert txn.chain_id == 1_337
98+
assert txn.nonce == 2
99+
assert txn.gas == 100_000
100+
assert txn.to == b"\x96!hI\xc4\x93X\xb1\x02W\xcbU\xb2\x8e\xa6\x03\xc8t\xb0^"
101+
assert txn.value == 100_000_000_000_000
102+
assert txn.data == b"UD"
103+
assert txn.max_priority_fee_per_gas == 2_000_000_000
104+
assert txn.max_fee_per_gas == 2_000_000_000
105+
106+
107+
def test_encode_dynamic_fee_transaction():
108+
txn = Web3Transaction.from_dict(DYNAMIC_FEE_TRANSACTION_TEST_CASE["transaction"])
109+
assert txn.encode() == HexBytes(
110+
DYNAMIC_FEE_TRANSACTION_TEST_CASE["expected_raw_transaction"]
111+
)

web3/utils/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
from .subscriptions import (
4646
EthSubscription,
4747
)
48+
from .transaction import (
49+
Web3Transaction,
50+
)
4851

4952
__all__ = [
5053
"abi_to_signature",
@@ -75,4 +78,5 @@
7578
"SimpleCache",
7679
"EthSubscription",
7780
"handle_offchain_lookup",
81+
"Web3Transaction",
7882
]

web3/utils/transaction.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from typing import (
2+
Any,
3+
Dict,
4+
List,
5+
Tuple,
6+
)
7+
8+
from eth_account.typed_transactions import (
9+
AccessListTransaction,
10+
BlobTransaction,
11+
DynamicFeeTransaction,
12+
SetCodeTransaction,
13+
TypedTransaction,
14+
)
15+
from hexbytes import (
16+
HexBytes,
17+
)
18+
19+
20+
class Web3Transaction:
21+
def __init__(self, typed_transaction: TypedTransaction):
22+
self.typed_transaction = typed_transaction
23+
self._dict = typed_transaction.as_dict()
24+
25+
@classmethod
26+
def from_dict(cls, dictionary: Dict[str, Any]) -> "Web3Transaction":
27+
sanitized_dictionary = dict(dictionary)
28+
if (
29+
"transactionIndex" in sanitized_dictionary
30+
and sanitized_dictionary["transactionIndex"] == 0
31+
):
32+
sanitized_dictionary["transactionIndex"] = "0x0"
33+
return cls(TypedTransaction.from_dict(sanitized_dictionary))
34+
35+
@classmethod
36+
def from_bytes(cls, encoded_transaction: HexBytes) -> "Web3Transaction":
37+
return cls(TypedTransaction.from_bytes(encoded_transaction))
38+
39+
def encode(self) -> bytes:
40+
return self.typed_transaction.encode()
41+
42+
@property
43+
def transaction_type(self) -> int:
44+
return self.typed_transaction.transaction_type
45+
46+
@property
47+
def chain_id(self) -> int:
48+
return self._dict["chainId"]
49+
50+
@property
51+
def nonce(self) -> int:
52+
return self._dict["nonce"]
53+
54+
@property
55+
def gas(self) -> int:
56+
return self._dict["gas"]
57+
58+
@property
59+
def to(self) -> bytes:
60+
return self._dict["to"]
61+
62+
@property
63+
def value(self) -> int:
64+
return self._dict["value"]
65+
66+
@property
67+
def data(self) -> bytes:
68+
return self._dict["data"]
69+
70+
@property
71+
def access_list(self) -> Tuple[Any, ...]:
72+
return self._dict["accessList"]
73+
74+
@property
75+
def gas_price(self) -> int:
76+
if self.transaction_type == AccessListTransaction.transaction_type:
77+
return self._dict["gasPrice"]
78+
raise ValueError(
79+
f"Invalid transaction type {self.transaction_type} for gas_price"
80+
)
81+
82+
@property
83+
def max_priority_fee_per_gas(self) -> int:
84+
if self.transaction_type in (
85+
DynamicFeeTransaction.transaction_type,
86+
BlobTransaction.transaction_type,
87+
SetCodeTransaction.transaction_type,
88+
):
89+
return self._dict["maxPriorityFeePerGas"]
90+
raise ValueError(
91+
f"Invalid transaction type {self.transaction_type} "
92+
f"for max_priority_fee_per_gas"
93+
)
94+
95+
@property
96+
def max_fee_per_gas(self) -> int:
97+
if self.transaction_type in (
98+
DynamicFeeTransaction.transaction_type,
99+
BlobTransaction.transaction_type,
100+
SetCodeTransaction.transaction_type,
101+
):
102+
return self._dict["maxFeePerGas"]
103+
raise ValueError(
104+
f"Invalid transaction type {self.transaction_type} for max_fee_per_gas"
105+
)
106+
107+
@property
108+
def authorization_list(self) -> List[Any]:
109+
if self.transaction_type == SetCodeTransaction.transaction_type:
110+
return self._dict["authorization_list"]
111+
raise ValueError(
112+
f"Invalid transaction type {self.transaction_type} for authorization_list"
113+
)
114+
115+
@property
116+
def max_fee_per_blob_gas(self) -> int:
117+
if self.transaction_type == BlobTransaction.transaction_type:
118+
return self._dict["maxFeePerBlobGas"]
119+
raise ValueError(
120+
f"Invalid transaction type {self.transaction_type} for max_fee_per_blob_gas"
121+
)
122+
123+
@property
124+
def blob_versioned_hashes(self) -> List[Any]:
125+
if self.transaction_type == BlobTransaction.transaction_type:
126+
return self._dict["blobVersionedHashes"]
127+
raise ValueError(
128+
f"Invalid transaction type {self.transaction_type} "
129+
f"for blob_versioned_hashes"
130+
)

0 commit comments

Comments
 (0)