eth-prototype 1.2.1__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eth_prototype-1.2.1.dist-info → eth_prototype-1.3.0.dist-info}/METADATA +9 -6
- eth_prototype-1.3.0.dist-info/RECORD +18 -0
- {eth_prototype-1.2.1.dist-info → eth_prototype-1.3.0.dist-info}/WHEEL +1 -1
- ethproto/aa_bundler.py +305 -160
- ethproto/test_utils/__init__.py +0 -0
- ethproto/test_utils/factories.py +70 -0
- ethproto/test_utils/hardhat.py +71 -0
- ethproto/test_utils/vcr_utils.py +23 -0
- ethproto/w3wrappers.py +4 -4
- ethproto/wrappers.py +1 -1
- eth_prototype-1.2.1.dist-info/RECORD +0 -14
- {eth_prototype-1.2.1.dist-info → eth_prototype-1.3.0.dist-info}/AUTHORS.rst +0 -0
- {eth_prototype-1.2.1.dist-info → eth_prototype-1.3.0.dist-info}/LICENSE.txt +0 -0
- {eth_prototype-1.2.1.dist-info → eth_prototype-1.3.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: eth-prototype
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.3.0
|
4
4
|
Summary: Prototype Ethereum Smart Contracts in Python
|
5
5
|
Home-page: https://github.com/gnarvaja/eth-prototype
|
6
6
|
Author: Guillermo M. Narvaja
|
@@ -19,19 +19,22 @@ Requires-Dist: environs
|
|
19
19
|
Requires-Dist: requests
|
20
20
|
Requires-Dist: hexbytes
|
21
21
|
Requires-Dist: importlib-metadata; python_version < "3.8"
|
22
|
+
Provides-Extra: web3
|
23
|
+
Requires-Dist: web3==7.*; extra == "web3"
|
22
24
|
Provides-Extra: defender
|
23
25
|
Requires-Dist: boto3; extra == "defender"
|
24
26
|
Provides-Extra: gmpy2
|
25
27
|
Requires-Dist: gmpy2; extra == "gmpy2"
|
26
28
|
Provides-Extra: testing
|
27
|
-
Requires-Dist:
|
28
|
-
Requires-Dist:
|
29
|
+
Requires-Dist: boto3; extra == "testing"
|
30
|
+
Requires-Dist: factory-boy; extra == "testing"
|
29
31
|
Requires-Dist: gmpy2; extra == "testing"
|
32
|
+
Requires-Dist: pytest; extra == "testing"
|
30
33
|
Requires-Dist: pytest-cov; extra == "testing"
|
34
|
+
Requires-Dist: pytest-mock; extra == "testing"
|
35
|
+
Requires-Dist: pytest-recording; extra == "testing"
|
36
|
+
Requires-Dist: setuptools; extra == "testing"
|
31
37
|
Requires-Dist: web3[tester]==7.*; extra == "testing"
|
32
|
-
Requires-Dist: boto3; extra == "testing"
|
33
|
-
Provides-Extra: web3
|
34
|
-
Requires-Dist: web3==7.*; extra == "web3"
|
35
38
|
|
36
39
|
# eth-prototype
|
37
40
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
ethproto/__init__.py,sha256=YWkAFysBp4tZjLWWB2FFmp5yG23pUYhQvgQW9b3soXs,579
|
2
|
+
ethproto/aa_bundler.py,sha256=bq46yF8ezLqLdIpfdLxBNi9sLNxnxbRamA5jnxVijoo,15793
|
3
|
+
ethproto/build_artifacts.py,sha256=xwCd5hJUHP82IA-y3sSfX6fV15kjCGtV19RxNRcoor0,5441
|
4
|
+
ethproto/contracts.py,sha256=rNVbCK1hURy7lWKhzSdXgVWo3wx9O_Ghk-6PfgOsRNk,18662
|
5
|
+
ethproto/defender_relay.py,sha256=05A8TfRZwiBhCpo924Pf9CjfKSir2Wvgg1p_asFxJbw,1777
|
6
|
+
ethproto/w3wrappers.py,sha256=lmyfJLhQmPYrclmbzzsthH2cShlQb6LwavKq30jqxFE,21651
|
7
|
+
ethproto/wadray.py,sha256=JBsu5KcyU9k70bDK03T2IY6qPVFO30WbYPhwrAHdXao,8262
|
8
|
+
ethproto/wrappers.py,sha256=Mj2sgZmcLVmqsnNab6PqIXtNMMPyRVvUj2_8ButEd4w,17304
|
9
|
+
ethproto/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
ethproto/test_utils/factories.py,sha256=G8DnUDG_yThRxMTCkymzcjm9lR_ni0_ZmTsb3sEfIdI,1805
|
11
|
+
ethproto/test_utils/hardhat.py,sha256=HzTqIznu6zVd_-doL96ftFJ235ktDCQen1QDQbNuwfM,2361
|
12
|
+
ethproto/test_utils/vcr_utils.py,sha256=1FH2sgJlElSjWkJLuO3C7E2J-4HKyFvjAqkCnGRZJyk,797
|
13
|
+
eth_prototype-1.3.0.dist-info/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
|
14
|
+
eth_prototype-1.3.0.dist-info/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
|
15
|
+
eth_prototype-1.3.0.dist-info/METADATA,sha256=4xarKI1kU94zClbkJmkxyhwynkcXdddaIl5BtaZWmZA,2628
|
16
|
+
eth_prototype-1.3.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
17
|
+
eth_prototype-1.3.0.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
|
18
|
+
eth_prototype-1.3.0.dist-info/RECORD,,
|
ethproto/aa_bundler.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import random
|
2
2
|
from collections import defaultdict
|
3
|
+
from dataclasses import dataclass, replace
|
3
4
|
from enum import Enum
|
4
5
|
from threading import local
|
5
6
|
from warnings import warn
|
@@ -8,15 +9,18 @@ from environs import Env
|
|
8
9
|
from eth_abi import encode
|
9
10
|
from eth_account import Account
|
10
11
|
from eth_account.messages import encode_defunct
|
11
|
-
from
|
12
|
+
from eth_typing import HexAddress
|
13
|
+
from eth_utils import add_0x_prefix, function_signature_to_4byte_selector
|
12
14
|
from hexbytes import HexBytes
|
13
15
|
from web3 import Web3
|
14
16
|
from web3.constants import ADDRESS_ZERO
|
17
|
+
from web3.types import TxParams
|
15
18
|
|
16
19
|
from .contracts import RevertError
|
17
20
|
|
18
21
|
env = Env()
|
19
22
|
|
23
|
+
AA_BUNDLER_URL = env.str("AA_BUNDLER_URL", env.str("WEB3_PROVIDER_URI", "http://localhost:8545"))
|
20
24
|
AA_BUNDLER_SENDER = env.str("AA_BUNDLER_SENDER", None)
|
21
25
|
AA_BUNDLER_ENTRYPOINT = env.str("AA_BUNDLER_ENTRYPOINT", "0x0000000071727De22E5E9d8BAf0edAc6f37da032")
|
22
26
|
AA_BUNDLER_EXECUTOR_PK = env.str("AA_BUNDLER_EXECUTOR_PK", None)
|
@@ -30,6 +34,7 @@ NonceMode = Enum(
|
|
30
34
|
"NonceMode",
|
31
35
|
[
|
32
36
|
"RANDOM_KEY", # first time initializes a random key and increments nonce locally with calling the blockchain
|
37
|
+
"RANDOM_KEY_EVERYTIME", # initializes a random key every time and increments nonce locally
|
33
38
|
"FIXED_KEY_LOCAL_NONCE", # uses a fixed key, keeps nonce locally and fetches the nonce when receiving
|
34
39
|
# 'AA25 invalid account nonce'
|
35
40
|
"FIXED_KEY_FETCH_ALWAYS", # uses a fixed key, always fetches unless received as parameter
|
@@ -57,6 +62,189 @@ GET_NONCE_ABI = [
|
|
57
62
|
NONCE_CACHE = defaultdict(lambda: 0)
|
58
63
|
RANDOM_NONCE_KEY = local()
|
59
64
|
|
65
|
+
DUMMY_SIGNATURE = HexBytes(
|
66
|
+
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007"
|
67
|
+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"
|
68
|
+
)
|
69
|
+
|
70
|
+
|
71
|
+
@dataclass(frozen=True)
|
72
|
+
class UserOpEstimation:
|
73
|
+
"""eth_estimateUserOperationGas response"""
|
74
|
+
|
75
|
+
pre_verification_gas: int
|
76
|
+
verification_gas_limit: int
|
77
|
+
call_gas_limit: int
|
78
|
+
paymaster_verification_gas_limit: int
|
79
|
+
|
80
|
+
|
81
|
+
@dataclass(frozen=True)
|
82
|
+
class GasPrice:
|
83
|
+
max_priority_fee_per_gas: int
|
84
|
+
max_fee_per_gas: int
|
85
|
+
|
86
|
+
|
87
|
+
@dataclass(frozen=True)
|
88
|
+
class Tx:
|
89
|
+
target: HexAddress
|
90
|
+
data: HexBytes
|
91
|
+
value: int
|
92
|
+
|
93
|
+
nonce_key: HexBytes = None
|
94
|
+
nonce: int = None
|
95
|
+
from_: HexAddress = ADDRESS_ZERO
|
96
|
+
chain_id: int = None
|
97
|
+
|
98
|
+
@classmethod
|
99
|
+
def from_tx_params(cls, params: TxParams) -> "Tx":
|
100
|
+
return cls(
|
101
|
+
target=params["to"],
|
102
|
+
data=HexBytes(params["data"]),
|
103
|
+
value=params["value"],
|
104
|
+
from_=params.get("from", ADDRESS_ZERO),
|
105
|
+
chain_id=params.get("chainId", None),
|
106
|
+
)
|
107
|
+
|
108
|
+
def as_execute_args(self):
|
109
|
+
return [self.target, self.value, self.data]
|
110
|
+
|
111
|
+
|
112
|
+
@dataclass(frozen=True)
|
113
|
+
class UserOperation:
|
114
|
+
EXECUTE_ARG_TYPES = ["address", "uint256", "bytes"]
|
115
|
+
EXECUTE_SELECTOR = function_signature_to_4byte_selector(f"execute({','.join(EXECUTE_ARG_TYPES)})")
|
116
|
+
|
117
|
+
sender: HexBytes
|
118
|
+
nonce: int
|
119
|
+
call_data: HexBytes
|
120
|
+
|
121
|
+
max_fee_per_gas: int = 0
|
122
|
+
max_priority_fee_per_gas: int = 0
|
123
|
+
|
124
|
+
call_gas_limit: int = 0
|
125
|
+
verification_gas_limit: int = 0
|
126
|
+
pre_verification_gas: int = 0
|
127
|
+
|
128
|
+
signature: HexBytes = DUMMY_SIGNATURE
|
129
|
+
|
130
|
+
init_code: HexBytes = HexBytes("0x")
|
131
|
+
paymaster_and_data: HexBytes = HexBytes("0x")
|
132
|
+
|
133
|
+
@classmethod
|
134
|
+
def from_tx(cls, tx: Tx, nonce):
|
135
|
+
return cls(
|
136
|
+
sender=get_sender(tx),
|
137
|
+
nonce=nonce,
|
138
|
+
call_data=add_0x_prefix(
|
139
|
+
(cls.EXECUTE_SELECTOR + encode(cls.EXECUTE_ARG_TYPES, tx.as_execute_args())).hex()
|
140
|
+
),
|
141
|
+
)
|
142
|
+
|
143
|
+
def as_reduced_dict(self):
|
144
|
+
return {
|
145
|
+
"sender": self.sender,
|
146
|
+
"nonce": "0x%x" % self.nonce,
|
147
|
+
"callData": self.call_data,
|
148
|
+
"signature": add_0x_prefix(self.signature.hex()),
|
149
|
+
}
|
150
|
+
|
151
|
+
def as_dict(self):
|
152
|
+
return {
|
153
|
+
"sender": self.sender,
|
154
|
+
"nonce": "0x%x" % self.nonce,
|
155
|
+
"callData": self.call_data,
|
156
|
+
"callGasLimit": "0x%x" % self.call_gas_limit,
|
157
|
+
"verificationGasLimit": "0x%x" % self.verification_gas_limit,
|
158
|
+
"preVerificationGas": "0x%x" % self.pre_verification_gas,
|
159
|
+
"maxPriorityFeePerGas": "0x%x" % self.max_priority_fee_per_gas,
|
160
|
+
"maxFeePerGas": "0x%x" % self.max_fee_per_gas,
|
161
|
+
"signature": add_0x_prefix(self.signature.hex()),
|
162
|
+
}
|
163
|
+
|
164
|
+
def add_estimation(self, estimation: UserOpEstimation) -> "UserOperation":
|
165
|
+
return replace(
|
166
|
+
self,
|
167
|
+
call_gas_limit=estimation.call_gas_limit,
|
168
|
+
verification_gas_limit=estimation.verification_gas_limit,
|
169
|
+
pre_verification_gas=estimation.pre_verification_gas,
|
170
|
+
)
|
171
|
+
|
172
|
+
def add_gas_price(self, gas_price: GasPrice) -> "UserOperation":
|
173
|
+
return replace(
|
174
|
+
self,
|
175
|
+
max_priority_fee_per_gas=gas_price.max_priority_fee_per_gas,
|
176
|
+
max_fee_per_gas=gas_price.max_fee_per_gas,
|
177
|
+
)
|
178
|
+
|
179
|
+
def sign(self, private_key: HexBytes, chain_id, entrypoint) -> "UserOperation":
|
180
|
+
signature = Account.sign_message(
|
181
|
+
encode_defunct(
|
182
|
+
hexstr=PackedUserOperation.from_user_operation(self)
|
183
|
+
.hash_full(chain_id=chain_id, entrypoint=entrypoint)
|
184
|
+
.hex()
|
185
|
+
),
|
186
|
+
private_key,
|
187
|
+
)
|
188
|
+
return replace(self, signature=signature.signature)
|
189
|
+
|
190
|
+
|
191
|
+
@dataclass(frozen=True)
|
192
|
+
class PackedUserOperation:
|
193
|
+
sender: HexBytes
|
194
|
+
nonce: int
|
195
|
+
call_data: HexBytes
|
196
|
+
|
197
|
+
account_gas_limits: HexBytes
|
198
|
+
pre_verification_gas: int
|
199
|
+
gas_fees: HexBytes
|
200
|
+
|
201
|
+
init_code: HexBytes = HexBytes("0x")
|
202
|
+
paymaster_and_data: HexBytes = HexBytes("0x")
|
203
|
+
signature: HexBytes = HexBytes("0x")
|
204
|
+
|
205
|
+
@classmethod
|
206
|
+
def from_user_operation(cls, user_operation: UserOperation):
|
207
|
+
return cls(
|
208
|
+
sender=user_operation.sender,
|
209
|
+
nonce=user_operation.nonce,
|
210
|
+
call_data=user_operation.call_data,
|
211
|
+
account_gas_limits=pack_two(user_operation.verification_gas_limit, user_operation.call_gas_limit),
|
212
|
+
pre_verification_gas=user_operation.pre_verification_gas,
|
213
|
+
gas_fees=pack_two(user_operation.max_priority_fee_per_gas, user_operation.max_fee_per_gas),
|
214
|
+
init_code=user_operation.init_code,
|
215
|
+
paymaster_and_data=user_operation.paymaster_and_data,
|
216
|
+
signature=user_operation.signature,
|
217
|
+
)
|
218
|
+
|
219
|
+
def hash(self):
|
220
|
+
# https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/UserOperationLib.sol#L54
|
221
|
+
hash_init_code = Web3.solidity_keccak(["bytes"], [self.init_code])
|
222
|
+
hash_call_data = Web3.solidity_keccak(["bytes"], [self.call_data])
|
223
|
+
hash_paymaster_and_data = Web3.solidity_keccak(["bytes"], [self.paymaster_and_data])
|
224
|
+
return Web3.keccak(
|
225
|
+
hexstr=encode(
|
226
|
+
["address", "uint256", "bytes32", "bytes32", "bytes32", "uint256", "bytes32", "bytes32"],
|
227
|
+
[
|
228
|
+
self.sender,
|
229
|
+
self.nonce,
|
230
|
+
hash_init_code,
|
231
|
+
hash_call_data,
|
232
|
+
HexBytes(self.account_gas_limits),
|
233
|
+
self.pre_verification_gas,
|
234
|
+
HexBytes(self.gas_fees),
|
235
|
+
hash_paymaster_and_data,
|
236
|
+
],
|
237
|
+
).hex()
|
238
|
+
)
|
239
|
+
|
240
|
+
def hash_full(self, chain_id, entrypoint):
|
241
|
+
return Web3.keccak(
|
242
|
+
hexstr=encode(
|
243
|
+
["bytes32", "address", "uint256"],
|
244
|
+
[self.hash(), entrypoint, chain_id],
|
245
|
+
).hex()
|
246
|
+
)
|
247
|
+
|
60
248
|
|
61
249
|
def pack_two(a, b):
|
62
250
|
a = HexBytes(a).hex()
|
@@ -72,98 +260,23 @@ def _to_uint(x):
|
|
72
260
|
raise RuntimeError(f"Invalid int value {x}")
|
73
261
|
|
74
262
|
|
75
|
-
def apply_factor(x, factor):
|
76
|
-
return int(_to_uint(x) * factor)
|
77
|
-
|
78
|
-
|
79
|
-
def pack_user_operation(user_operation):
|
80
|
-
# https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/interfaces/PackedUserOperation.sol
|
81
|
-
return {
|
82
|
-
"sender": user_operation["sender"],
|
83
|
-
"nonce": _to_uint(user_operation["nonce"]),
|
84
|
-
"initCode": "0x",
|
85
|
-
"callData": user_operation["callData"],
|
86
|
-
"accountGasLimits": pack_two(user_operation["verificationGasLimit"], user_operation["callGasLimit"]),
|
87
|
-
"preVerificationGas": _to_uint(user_operation["preVerificationGas"]),
|
88
|
-
"gasFees": pack_two(user_operation["maxPriorityFeePerGas"], user_operation["maxFeePerGas"]),
|
89
|
-
"paymasterAndData": "0x",
|
90
|
-
"signature": "0x",
|
91
|
-
}
|
92
|
-
|
93
|
-
|
94
|
-
def hash_packed_user_operation_only(packed_user_op):
|
95
|
-
# https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/UserOperationLib.sol#L54
|
96
|
-
hash_init_code = Web3.solidity_keccak(["bytes"], [packed_user_op["initCode"]])
|
97
|
-
hash_call_data = Web3.solidity_keccak(["bytes"], [packed_user_op["callData"]])
|
98
|
-
hash_paymaster_and_data = Web3.solidity_keccak(["bytes"], [packed_user_op["paymasterAndData"]])
|
99
|
-
return Web3.keccak(
|
100
|
-
hexstr=encode(
|
101
|
-
["address", "uint256", "bytes32", "bytes32", "bytes32", "uint256", "bytes32", "bytes32"],
|
102
|
-
[
|
103
|
-
packed_user_op["sender"],
|
104
|
-
packed_user_op["nonce"],
|
105
|
-
hash_init_code,
|
106
|
-
hash_call_data,
|
107
|
-
HexBytes(packed_user_op["accountGasLimits"]),
|
108
|
-
packed_user_op["preVerificationGas"],
|
109
|
-
HexBytes(packed_user_op["gasFees"]),
|
110
|
-
hash_paymaster_and_data,
|
111
|
-
],
|
112
|
-
).hex()
|
113
|
-
)
|
114
|
-
|
115
|
-
|
116
|
-
def hash_packed_user_operation(packed_user_op, chain_id, entry_point):
|
117
|
-
return Web3.keccak(
|
118
|
-
hexstr=encode(
|
119
|
-
["bytes32", "address", "uint256"],
|
120
|
-
[hash_packed_user_operation_only(packed_user_op), entry_point, chain_id],
|
121
|
-
).hex()
|
122
|
-
)
|
123
|
-
|
124
|
-
|
125
|
-
def sign_user_operation(private_key, user_operation, chain_id, entry_point):
|
126
|
-
packed_user_op = pack_user_operation(user_operation)
|
127
|
-
hash = hash_packed_user_operation(packed_user_op, chain_id, entry_point)
|
128
|
-
signature = Account.sign_message(encode_defunct(hexstr=hash.hex()), private_key)
|
129
|
-
return signature.signature
|
130
|
-
|
131
|
-
|
132
263
|
def make_nonce(nonce_key, nonce):
|
133
264
|
nonce_key = _to_uint(nonce_key)
|
134
265
|
nonce = _to_uint(nonce)
|
135
266
|
return (nonce_key << 64) | nonce
|
136
267
|
|
137
268
|
|
138
|
-
def fetch_nonce(w3, account,
|
139
|
-
ep = w3.eth.contract(abi=GET_NONCE_ABI, address=
|
269
|
+
def fetch_nonce(w3, account, entrypoint, nonce_key):
|
270
|
+
ep = w3.eth.contract(abi=GET_NONCE_ABI, address=entrypoint)
|
140
271
|
return ep.functions.getNonce(account, nonce_key).call()
|
141
272
|
|
142
273
|
|
143
|
-
def get_random_nonce_key():
|
144
|
-
if getattr(RANDOM_NONCE_KEY, "key", None) is None:
|
274
|
+
def get_random_nonce_key(force=False):
|
275
|
+
if force or getattr(RANDOM_NONCE_KEY, "key", None) is None:
|
145
276
|
RANDOM_NONCE_KEY.key = random.randint(1, 2**192 - 1)
|
146
277
|
return RANDOM_NONCE_KEY.key
|
147
278
|
|
148
279
|
|
149
|
-
def get_nonce_and_key(w3, tx, nonce_mode, entry_point=AA_BUNDLER_ENTRYPOINT, fetch=False):
|
150
|
-
nonce_key = tx.get("nonceKey", None)
|
151
|
-
nonce = tx.get("nonce", None)
|
152
|
-
|
153
|
-
if nonce_key is None:
|
154
|
-
if nonce_mode == NonceMode.RANDOM_KEY:
|
155
|
-
nonce_key = get_random_nonce_key()
|
156
|
-
else:
|
157
|
-
nonce_key = AA_BUNDLER_NONCE_KEY
|
158
|
-
|
159
|
-
if nonce is None:
|
160
|
-
if fetch or nonce_mode == NonceMode.FIXED_KEY_FETCH_ALWAYS:
|
161
|
-
nonce = fetch_nonce(w3, get_sender(tx), entry_point, nonce_key)
|
162
|
-
else:
|
163
|
-
nonce = NONCE_CACHE[nonce_key]
|
164
|
-
return nonce_key, nonce
|
165
|
-
|
166
|
-
|
167
280
|
def consume_nonce(nonce_key, nonce):
|
168
281
|
NONCE_CACHE[nonce_key] = max(NONCE_CACHE[nonce_key], nonce + 1)
|
169
282
|
|
@@ -182,104 +295,136 @@ def check_nonce_error(resp, retry_nonce):
|
|
182
295
|
raise RevertError(resp["error"]["message"])
|
183
296
|
|
184
297
|
|
185
|
-
def get_base_fee(w3):
|
186
|
-
blk = w3.eth.get_block("latest")
|
187
|
-
return int(_to_uint(blk["baseFeePerGas"]) * AA_BUNDLER_BASE_GAS_PRICE_FACTOR)
|
188
|
-
|
189
|
-
|
190
298
|
def get_sender(tx):
|
191
|
-
if
|
299
|
+
if tx.from_ == ADDRESS_ZERO:
|
192
300
|
if AA_BUNDLER_SENDER is None:
|
193
301
|
raise RuntimeError("Must define AA_BUNDLER_SENDER or send 'from' in the TX")
|
194
302
|
return AA_BUNDLER_SENDER
|
195
303
|
else:
|
196
|
-
return tx
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
304
|
+
return tx.from_
|
305
|
+
|
306
|
+
|
307
|
+
class Bundler:
|
308
|
+
def __init__(
|
309
|
+
self,
|
310
|
+
w3: Web3,
|
311
|
+
bundler_url: str = AA_BUNDLER_URL,
|
312
|
+
bundler_type: str = AA_BUNDLER_PROVIDER,
|
313
|
+
entrypoint: HexAddress = AA_BUNDLER_ENTRYPOINT,
|
314
|
+
nonce_mode: NonceMode = AA_BUNDLER_NONCE_MODE,
|
315
|
+
fixed_nonce_key: int = AA_BUNDLER_NONCE_KEY,
|
316
|
+
verification_gas_factor: float = AA_BUNDLER_VERIFICATION_GAS_FACTOR,
|
317
|
+
gas_limit_factor: float = AA_BUNDLER_GAS_LIMIT_FACTOR,
|
318
|
+
priority_gas_price_factor: float = AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR,
|
319
|
+
base_gas_price_factor: float = AA_BUNDLER_BASE_GAS_PRICE_FACTOR,
|
320
|
+
executor_pk: HexBytes = AA_BUNDLER_EXECUTOR_PK,
|
321
|
+
):
|
322
|
+
self.w3 = w3
|
323
|
+
self.bundler = Web3(Web3.HTTPProvider(bundler_url), middleware=[])
|
324
|
+
self.bundler_type = bundler_type
|
325
|
+
self.entrypoint = entrypoint
|
326
|
+
self.nonce_mode = nonce_mode
|
327
|
+
self.fixed_nonce_key = fixed_nonce_key
|
328
|
+
self.verification_gas_factor = verification_gas_factor
|
329
|
+
self.gas_limit_factor = gas_limit_factor
|
330
|
+
self.priority_gas_price_factor = priority_gas_price_factor
|
331
|
+
self.base_gas_price_factor = base_gas_price_factor
|
332
|
+
self.executor_pk = executor_pk
|
333
|
+
|
334
|
+
def __str__(self):
|
335
|
+
return (
|
336
|
+
f"Bundler(type={self.bundler_type}, entrypoint={self.entrypoint}, nonce_mode={self.nonce_mode}, "
|
337
|
+
f"fixed_nonce_key={self.fixed_nonce_key}, verification_gas_factor={self.verification_gas_factor}, "
|
338
|
+
f"gas_limit_factor={self.gas_limit_factor}, priority_gas_price_factor={self.priority_gas_price_factor}, "
|
339
|
+
f"base_gas_price_factor={self.base_gas_price_factor})"
|
340
|
+
)
|
218
341
|
|
219
|
-
|
220
|
-
|
221
|
-
|
342
|
+
def get_nonce_and_key(self, tx: Tx, fetch=False):
|
343
|
+
nonce_key = tx.nonce_key
|
344
|
+
nonce = tx.nonce
|
345
|
+
|
346
|
+
if nonce_key is None:
|
347
|
+
if self.nonce_mode == NonceMode.RANDOM_KEY:
|
348
|
+
nonce_key = get_random_nonce_key()
|
349
|
+
elif self.nonce_mode == NonceMode.RANDOM_KEY_EVERYTIME:
|
350
|
+
nonce_key = get_random_nonce_key(force=True)
|
351
|
+
else:
|
352
|
+
nonce_key = self.fixed_nonce_key
|
353
|
+
|
354
|
+
if nonce is None:
|
355
|
+
if fetch or self.nonce_mode == NonceMode.FIXED_KEY_FETCH_ALWAYS:
|
356
|
+
nonce = fetch_nonce(self.w3, get_sender(tx), self.entrypoint, nonce_key)
|
357
|
+
else:
|
358
|
+
nonce = NONCE_CACHE[nonce_key]
|
359
|
+
return nonce_key, nonce
|
360
|
+
|
361
|
+
def get_base_fee(self):
|
362
|
+
blk = self.w3.eth.get_block("latest")
|
363
|
+
return int(_to_uint(blk["baseFeePerGas"]) * self.base_gas_price_factor)
|
364
|
+
|
365
|
+
def estimate_user_operation_gas(self, user_operation: UserOperation) -> UserOpEstimation:
|
366
|
+
resp = self.bundler.provider.make_request(
|
367
|
+
"eth_estimateUserOperationGas", [user_operation.as_reduced_dict(), self.entrypoint]
|
222
368
|
)
|
223
369
|
if "error" in resp:
|
224
|
-
|
225
|
-
return build_user_operation(w3, tx, retry_nonce=next_nonce)
|
370
|
+
raise RevertError(resp["error"]["message"])
|
226
371
|
|
227
|
-
|
372
|
+
paymaster_verification_gas_limit = resp["result"].get("paymasterVerificationGasLimit", "0x00")
|
373
|
+
return UserOpEstimation(
|
374
|
+
pre_verification_gas=int(resp["result"].get("preVerificationGas", "0x00"), 16),
|
375
|
+
verification_gas_limit=int(
|
376
|
+
int(resp["result"].get("verificationGasLimit", "0x00"), 16) * self.verification_gas_factor
|
377
|
+
),
|
378
|
+
call_gas_limit=int(int(resp["result"].get("callGasLimit", "0x00"), 16) * self.gas_limit_factor),
|
379
|
+
paymaster_verification_gas_limit=(
|
380
|
+
int(paymaster_verification_gas_limit, 16)
|
381
|
+
if paymaster_verification_gas_limit is not None
|
382
|
+
else 0
|
383
|
+
),
|
384
|
+
)
|
228
385
|
|
229
|
-
|
386
|
+
def alchemy_gas_price(self):
|
387
|
+
resp = self.bundler.provider.make_request("rundler_maxPriorityFeePerGas", [])
|
230
388
|
if "error" in resp:
|
231
389
|
raise RevertError(resp["error"]["message"])
|
232
|
-
|
233
|
-
|
390
|
+
max_priority_fee_per_gas = int(int(resp["result"], 16) * self.priority_gas_price_factor)
|
391
|
+
max_fee_per_gas = max_priority_fee_per_gas + self.get_base_fee()
|
234
392
|
|
235
|
-
|
236
|
-
resp = w3.provider.make_request(
|
237
|
-
"eth_estimateUserOperationGas", [user_operation, AA_BUNDLER_ENTRYPOINT]
|
238
|
-
)
|
239
|
-
if "error" in resp:
|
240
|
-
next_nonce = check_nonce_error(resp, retry_nonce)
|
241
|
-
return build_user_operation(w3, tx, retry_nonce=next_nonce)
|
393
|
+
return GasPrice(max_priority_fee_per_gas=max_priority_fee_per_gas, max_fee_per_gas=max_fee_per_gas)
|
242
394
|
|
243
|
-
|
395
|
+
def build_user_operation(self, tx: Tx, retry_nonce=None) -> UserOperation:
|
396
|
+
nonce_key, nonce = self.get_nonce_and_key(tx, fetch=retry_nonce is not None)
|
397
|
+
# Consume the nonce, even if the userop may fail later
|
398
|
+
consume_nonce(nonce_key, nonce)
|
244
399
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
user_operation["verificationGasLimit"] = hex(
|
250
|
-
apply_factor(user_operation["verificationGasLimit"], AA_BUNDLER_VERIFICATION_GAS_FACTOR)
|
251
|
-
)
|
252
|
-
if "maxPriorityFeePerGas" in user_operation:
|
253
|
-
user_operation["maxPriorityFeePerGas"] = hex(
|
254
|
-
apply_factor(user_operation["maxPriorityFeePerGas"], AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR)
|
255
|
-
)
|
256
|
-
if "callGasLimit" in user_operation:
|
257
|
-
user_operation["callGasLimit"] = hex(
|
258
|
-
apply_factor(user_operation["callGasLimit"], AA_BUNDLER_GAS_LIMIT_FACTOR)
|
259
|
-
)
|
400
|
+
user_operation = UserOperation.from_tx(tx, make_nonce(nonce_key, nonce))
|
401
|
+
estimation = self.estimate_user_operation_gas(user_operation)
|
402
|
+
|
403
|
+
user_operation = user_operation.add_estimation(estimation)
|
260
404
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
user_operation.pop("paymasterVerificationGasLimit", None)
|
265
|
-
user_operation.pop("paymasterPostOpGasLimit", None)
|
405
|
+
if self.bundler_type == "alchemy":
|
406
|
+
gas_price = self.alchemy_gas_price()
|
407
|
+
user_operation = user_operation.add_gas_price(gas_price)
|
266
408
|
|
267
|
-
|
268
|
-
|
409
|
+
elif self.bundler_type == "generic":
|
410
|
+
# At the moment generic just prices the gas at 0
|
411
|
+
pass
|
412
|
+
|
413
|
+
else:
|
414
|
+
warn(f"Unknown bundler_type: {self.bundler_type}")
|
269
415
|
|
270
|
-
|
416
|
+
return user_operation
|
271
417
|
|
418
|
+
def send_transaction(self, tx: Tx, retry_nonce=None):
|
419
|
+
user_operation = self.build_user_operation(tx, retry_nonce).sign(
|
420
|
+
self.executor_pk, tx.chain_id, self.entrypoint
|
421
|
+
)
|
272
422
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
)
|
280
|
-
resp = w3.provider.make_request("eth_sendUserOperation", [user_operation, AA_BUNDLER_ENTRYPOINT])
|
281
|
-
if "error" in resp:
|
282
|
-
next_nonce = check_nonce_error(resp, retry_nonce)
|
283
|
-
return send_transaction(w3, tx, retry_nonce=next_nonce)
|
423
|
+
resp = self.bundler.provider.make_request(
|
424
|
+
"eth_sendUserOperation", [user_operation.as_dict(), self.entrypoint]
|
425
|
+
)
|
426
|
+
if "error" in resp:
|
427
|
+
next_nonce = check_nonce_error(resp, retry_nonce)
|
428
|
+
return self.send_transaction(tx, retry_nonce=next_nonce)
|
284
429
|
|
285
|
-
|
430
|
+
return {"userOpHash": resp["result"]}
|
File without changes
|
@@ -0,0 +1,70 @@
|
|
1
|
+
from random import randint
|
2
|
+
|
3
|
+
import factory
|
4
|
+
import faker
|
5
|
+
from eth_abi import encode
|
6
|
+
from eth_typing import HexAddress
|
7
|
+
from eth_utils import function_signature_to_4byte_selector, to_checksum_address
|
8
|
+
from faker.providers import BaseProvider
|
9
|
+
from hexbytes import HexBytes
|
10
|
+
from web3.constants import ADDRESS_ZERO
|
11
|
+
|
12
|
+
from ethproto import aa_bundler
|
13
|
+
|
14
|
+
|
15
|
+
class EthProvider(BaseProvider):
|
16
|
+
"""
|
17
|
+
A Provider for Ethereum related data
|
18
|
+
>>> from faker import Faker
|
19
|
+
>>> fake = Faker()
|
20
|
+
>>> fake.add_provider(EthProvider)
|
21
|
+
"""
|
22
|
+
|
23
|
+
def eth_address(self):
|
24
|
+
ret = hex(randint(0, 2**160 - 1))
|
25
|
+
if len(ret) < 42:
|
26
|
+
ret = "0x" + "0" * (42 - len(ret)) + ret[2:]
|
27
|
+
return to_checksum_address(ret)
|
28
|
+
|
29
|
+
def eth_hash(self):
|
30
|
+
ret = hex(randint(0, 2**256 - 1))
|
31
|
+
if len(ret) < 66:
|
32
|
+
return "0x" + "0" * (66 - len(ret)) + ret[2:]
|
33
|
+
return ret
|
34
|
+
|
35
|
+
|
36
|
+
fake = faker.Faker()
|
37
|
+
fake.add_provider(EthProvider)
|
38
|
+
factory.Faker.add_provider(EthProvider)
|
39
|
+
|
40
|
+
|
41
|
+
class Tx(factory.Factory):
|
42
|
+
class Meta:
|
43
|
+
model = aa_bundler.Tx
|
44
|
+
|
45
|
+
target = factory.Faker("eth_address")
|
46
|
+
data = factory.LazyAttribute(
|
47
|
+
lambda _: (
|
48
|
+
function_signature_to_4byte_selector("balanceOf(address)")
|
49
|
+
+ encode(["address"], [fake.eth_address()])
|
50
|
+
)
|
51
|
+
)
|
52
|
+
value = 0
|
53
|
+
|
54
|
+
nonce_key: HexBytes = None
|
55
|
+
nonce: int = None
|
56
|
+
from_: HexAddress = ADDRESS_ZERO
|
57
|
+
chain_id: int = None
|
58
|
+
|
59
|
+
|
60
|
+
class UserOperation(factory.Factory):
|
61
|
+
class Meta:
|
62
|
+
model = aa_bundler.UserOperation
|
63
|
+
|
64
|
+
nonce = factory.Faker("random_int", min=0, max=2**256 - 1)
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
def _create(cls, model_class, *args, **kwargs):
|
68
|
+
tx = Tx(from_=kwargs.pop("from_", fake.eth_address()))
|
69
|
+
nonce = kwargs.pop("nonce")
|
70
|
+
return model_class.from_tx(tx, nonce)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import os
|
2
|
+
import signal
|
3
|
+
import subprocess
|
4
|
+
import sys
|
5
|
+
import time
|
6
|
+
|
7
|
+
import requests
|
8
|
+
|
9
|
+
|
10
|
+
def hardhat_node(hardhat_project_path, hostname="127.0.0.1", port=8545):
|
11
|
+
"""Starts the hardhat node on the given path and returns a function to stop it"""
|
12
|
+
provider_uri = f"http://{hostname}:{port}"
|
13
|
+
|
14
|
+
# Compile the Hardhat project
|
15
|
+
compile_process = subprocess.run(
|
16
|
+
["npx", "hardhat", "compile"], cwd=hardhat_project_path, capture_output=True, text=True
|
17
|
+
)
|
18
|
+
if compile_process.returncode != 0:
|
19
|
+
raise RuntimeError(f"Hardhat compilation failed: {compile_process.stderr}")
|
20
|
+
|
21
|
+
# Start the Hardhat node
|
22
|
+
node_process = subprocess.Popen(
|
23
|
+
["npx", "hardhat", "node"],
|
24
|
+
start_new_session=True,
|
25
|
+
cwd=hardhat_project_path,
|
26
|
+
stdout=subprocess.PIPE,
|
27
|
+
stderr=subprocess.PIPE,
|
28
|
+
close_fds=True,
|
29
|
+
text=True,
|
30
|
+
)
|
31
|
+
|
32
|
+
# Wait for the node to be ready by checking eth_chainId
|
33
|
+
def is_node_ready():
|
34
|
+
try:
|
35
|
+
response = requests.post(
|
36
|
+
provider_uri,
|
37
|
+
json={"jsonrpc": "2.0", "method": "eth_chainId", "params": [], "id": 1},
|
38
|
+
timeout=1,
|
39
|
+
)
|
40
|
+
return response.status_code == 200
|
41
|
+
except Exception:
|
42
|
+
return False
|
43
|
+
|
44
|
+
def terminate_node():
|
45
|
+
node_process.terminate()
|
46
|
+
os.killpg(os.getpgid(node_process.pid), signal.SIGTERM)
|
47
|
+
try:
|
48
|
+
node_process.communicate(timeout=5)
|
49
|
+
except subprocess.TimeoutExpired:
|
50
|
+
print("Hardhat node did not terminate in time, killing the whole process group", file=sys.stderr)
|
51
|
+
os.killpg(os.getpgid(node_process.pid), signal.SIGKILL)
|
52
|
+
|
53
|
+
# Retry mechanism to check node readiness
|
54
|
+
max_attempts = 20
|
55
|
+
for _ in range(max_attempts):
|
56
|
+
if is_node_ready():
|
57
|
+
break
|
58
|
+
time.sleep(0.5)
|
59
|
+
else:
|
60
|
+
terminate_node()
|
61
|
+
raise RuntimeError("Hardhat node did not become ready in time")
|
62
|
+
|
63
|
+
# Did we actually connect to our process, or did it fail because another node was already started?
|
64
|
+
try:
|
65
|
+
_, stderr = node_process.communicate(timeout=2)
|
66
|
+
raise RuntimeError(f"Hardhat node process exited with code {node_process.returncode}: {stderr}")
|
67
|
+
except subprocess.TimeoutExpired:
|
68
|
+
# We connected to our process, everything is good
|
69
|
+
pass
|
70
|
+
|
71
|
+
return terminate_node
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import json
|
2
|
+
from urllib.parse import urlparse, urlunparse
|
3
|
+
|
4
|
+
|
5
|
+
def before_record_request(request):
|
6
|
+
# Scrub the path and query string, which might contain credentials (alchemy, infura)
|
7
|
+
scheme, netloc, path, params, query, fragment = urlparse(request.uri)
|
8
|
+
request.uri = urlunparse((scheme, netloc, "", params, "", fragment))
|
9
|
+
|
10
|
+
# Other common security-related concerns
|
11
|
+
request.headers.pop("Authorization", None)
|
12
|
+
|
13
|
+
return request
|
14
|
+
|
15
|
+
|
16
|
+
def json_rpc_matcher(r1, r2):
|
17
|
+
assert r1.headers["Content-Type"] == r2.headers["Content-Type"] == "application/json"
|
18
|
+
r1_body = json.loads(r1.body)
|
19
|
+
r2_body = json.loads(r2.body)
|
20
|
+
|
21
|
+
assert r1_body["jsonrpc"] == r2_body["jsonrpc"] == "2.0"
|
22
|
+
assert r1_body["method"] == r2_body["method"]
|
23
|
+
assert r1_body["params"] == r2_body["params"]
|
ethproto/w3wrappers.py
CHANGED
@@ -111,7 +111,7 @@ def transact(provider, function, tx_kwargs):
|
|
111
111
|
}
|
112
112
|
)
|
113
113
|
signed_tx = from_.sign_transaction(tx)
|
114
|
-
tx_hash = provider.w3.eth.send_raw_transaction(signed_tx.
|
114
|
+
tx_hash = provider.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
115
115
|
elif W3_TRANSACT_MODE == "defender-async":
|
116
116
|
from .defender_relay import send_transaction
|
117
117
|
|
@@ -211,8 +211,8 @@ class W3EnvAddressBook(AddressBook):
|
|
211
211
|
if isinstance(name, (Account, LocalAccount)):
|
212
212
|
return name
|
213
213
|
if name is None:
|
214
|
-
return self.ZERO
|
215
|
-
if
|
214
|
+
return list(self.signers.values())[0] if self.signers else self.ZERO
|
215
|
+
if isinstance(name, str) and name.startswith("0x"):
|
216
216
|
return name
|
217
217
|
if name in self.name_to_address:
|
218
218
|
return self.name_to_address[name]
|
@@ -483,7 +483,7 @@ class W3Provider(BaseProvider):
|
|
483
483
|
for lib, _ in contract_def.libraries():
|
484
484
|
if lib not in libraries:
|
485
485
|
library_def = self.get_contract_factory(lib)
|
486
|
-
library = self.construct(library_def)
|
486
|
+
library = self.construct(library_def, transact_kwargs={"from": eth_wrapper.owner})
|
487
487
|
libraries[lib] = library.address
|
488
488
|
|
489
489
|
if libraries:
|
ethproto/wrappers.py
CHANGED
@@ -303,7 +303,7 @@ class ETHWrapper:
|
|
303
303
|
constructor_args = None
|
304
304
|
initialize_args = None
|
305
305
|
|
306
|
-
def __init__(self, owner=
|
306
|
+
def __init__(self, owner=None, *init_params, **kwargs):
|
307
307
|
self.provider_key = kwargs.get("provider_key", None)
|
308
308
|
init_params = self._parse_init_params(init_params, kwargs)
|
309
309
|
self.provider.init_eth_wrapper(self, owner, init_params, kwargs)
|
@@ -1,14 +0,0 @@
|
|
1
|
-
ethproto/__init__.py,sha256=YWkAFysBp4tZjLWWB2FFmp5yG23pUYhQvgQW9b3soXs,579
|
2
|
-
ethproto/aa_bundler.py,sha256=XcSshufDoDAZU2mGaI5Kl8BSiidyIMLNhFKSmwOj5Bk,10544
|
3
|
-
ethproto/build_artifacts.py,sha256=xwCd5hJUHP82IA-y3sSfX6fV15kjCGtV19RxNRcoor0,5441
|
4
|
-
ethproto/contracts.py,sha256=rNVbCK1hURy7lWKhzSdXgVWo3wx9O_Ghk-6PfgOsRNk,18662
|
5
|
-
ethproto/defender_relay.py,sha256=05A8TfRZwiBhCpo924Pf9CjfKSir2Wvgg1p_asFxJbw,1777
|
6
|
-
ethproto/w3wrappers.py,sha256=4ZEnJFrc8bV1qHG4dNdom4FL1gEUhgoJORY6cGzlJDk,21549
|
7
|
-
ethproto/wadray.py,sha256=JBsu5KcyU9k70bDK03T2IY6qPVFO30WbYPhwrAHdXao,8262
|
8
|
-
ethproto/wrappers.py,sha256=9qDwRDOXw3wquzvGfIsub-VPWm98GBWP7dHLFOUPWzg,17307
|
9
|
-
eth_prototype-1.2.1.dist-info/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
|
10
|
-
eth_prototype-1.2.1.dist-info/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
|
11
|
-
eth_prototype-1.2.1.dist-info/METADATA,sha256=Qom0naY8c8M2mBdjBHHh9SGOsLPkhgsIOQtCA9dYVBM,2482
|
12
|
-
eth_prototype-1.2.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
13
|
-
eth_prototype-1.2.1.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
|
14
|
-
eth_prototype-1.2.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|