eth-prototype 1.2.1b1__py3-none-any.whl → 1.3.0b2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eth-prototype
3
- Version: 1.2.1b1
3
+ Version: 1.3.0b2
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: setuptools; extra == "testing"
28
- Requires-Dist: pytest; extra == "testing"
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=HupCu7fRCwlE556WXDqytRU5GEFLdBoFYzjpqDHBtE4,15789
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.0b2.dist-info/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
14
+ eth_prototype-1.3.0b2.dist-info/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
15
+ eth_prototype-1.3.0b2.dist-info/METADATA,sha256=VGLInjZc46w_OraflEsO2vG9yXmA_crJL0DVP5dFLvM,2630
16
+ eth_prototype-1.3.0b2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
17
+ eth_prototype-1.3.0b2.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
18
+ eth_prototype-1.3.0b2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
ethproto/aa_bundler.py CHANGED
@@ -1,21 +1,26 @@
1
1
  import random
2
2
  from collections import defaultdict
3
+ from dataclasses import dataclass, replace
3
4
  from enum import Enum
5
+ from threading import local
4
6
  from warnings import warn
5
7
 
6
8
  from environs import Env
7
9
  from eth_abi import encode
8
10
  from eth_account import Account
9
11
  from eth_account.messages import encode_defunct
10
- from eth_utils import add_0x_prefix
12
+ from eth_typing import HexAddress
13
+ from eth_utils import add_0x_prefix, function_signature_to_4byte_selector
11
14
  from hexbytes import HexBytes
12
15
  from web3 import Web3
13
16
  from web3.constants import ADDRESS_ZERO
17
+ from web3.types import TxParams
14
18
 
15
19
  from .contracts import RevertError
16
20
 
17
21
  env = Env()
18
22
 
23
+ AA_BUNDLER_URL = env.str("AA_BUNDLER_URL", env.str("WEB3_PROVIDER_URI", "http://localhost:8545"))
19
24
  AA_BUNDLER_SENDER = env.str("AA_BUNDLER_SENDER", None)
20
25
  AA_BUNDLER_ENTRYPOINT = env.str("AA_BUNDLER_ENTRYPOINT", "0x0000000071727De22E5E9d8BAf0edAc6f37da032")
21
26
  AA_BUNDLER_EXECUTOR_PK = env.str("AA_BUNDLER_EXECUTOR_PK", None)
@@ -29,6 +34,7 @@ NonceMode = Enum(
29
34
  "NonceMode",
30
35
  [
31
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
32
38
  "FIXED_KEY_LOCAL_NONCE", # uses a fixed key, keeps nonce locally and fetches the nonce when receiving
33
39
  # 'AA25 invalid account nonce'
34
40
  "FIXED_KEY_FETCH_ALWAYS", # uses a fixed key, always fetches unless received as parameter
@@ -54,7 +60,190 @@ GET_NONCE_ABI = [
54
60
  ]
55
61
 
56
62
  NONCE_CACHE = defaultdict(lambda: 0)
57
- RANDOM_NONCE_KEY = None
63
+ RANDOM_NONCE_KEY = local()
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
+ )
58
247
 
59
248
 
60
249
  def pack_two(a, b):
@@ -71,97 +260,21 @@ def _to_uint(x):
71
260
  raise RuntimeError(f"Invalid int value {x}")
72
261
 
73
262
 
74
- def apply_factor(x, factor):
75
- return int(_to_uint(x) * factor)
76
-
77
-
78
- def pack_user_operation(user_operation):
79
- # https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/interfaces/PackedUserOperation.sol
80
- return {
81
- "sender": user_operation["sender"],
82
- "nonce": _to_uint(user_operation["nonce"]),
83
- "initCode": "0x",
84
- "callData": user_operation["callData"],
85
- "accountGasLimits": pack_two(user_operation["verificationGasLimit"], user_operation["callGasLimit"]),
86
- "preVerificationGas": _to_uint(user_operation["preVerificationGas"]),
87
- "gasFees": pack_two(user_operation["maxPriorityFeePerGas"], user_operation["maxFeePerGas"]),
88
- "paymasterAndData": "0x",
89
- "signature": "0x",
90
- }
91
-
92
-
93
- def hash_packed_user_operation_only(packed_user_op):
94
- # https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/UserOperationLib.sol#L54
95
- hash_init_code = Web3.solidity_keccak(["bytes"], [packed_user_op["initCode"]])
96
- hash_call_data = Web3.solidity_keccak(["bytes"], [packed_user_op["callData"]])
97
- hash_paymaster_and_data = Web3.solidity_keccak(["bytes"], [packed_user_op["paymasterAndData"]])
98
- return Web3.keccak(
99
- hexstr=encode(
100
- ["address", "uint256", "bytes32", "bytes32", "bytes32", "uint256", "bytes32", "bytes32"],
101
- [
102
- packed_user_op["sender"],
103
- packed_user_op["nonce"],
104
- hash_init_code,
105
- hash_call_data,
106
- HexBytes(packed_user_op["accountGasLimits"]),
107
- packed_user_op["preVerificationGas"],
108
- HexBytes(packed_user_op["gasFees"]),
109
- hash_paymaster_and_data,
110
- ],
111
- ).hex()
112
- )
113
-
114
-
115
- def hash_packed_user_operation(packed_user_op, chain_id, entry_point):
116
- return Web3.keccak(
117
- hexstr=encode(
118
- ["bytes32", "address", "uint256"],
119
- [hash_packed_user_operation_only(packed_user_op), entry_point, chain_id],
120
- ).hex()
121
- )
122
-
123
-
124
- def sign_user_operation(private_key, user_operation, chain_id, entry_point):
125
- packed_user_op = pack_user_operation(user_operation)
126
- hash = hash_packed_user_operation(packed_user_op, chain_id, entry_point)
127
- signature = Account.sign_message(encode_defunct(hexstr=hash.hex()), private_key)
128
- return signature.signature
129
-
130
-
131
263
  def make_nonce(nonce_key, nonce):
132
264
  nonce_key = _to_uint(nonce_key)
133
265
  nonce = _to_uint(nonce)
134
266
  return (nonce_key << 64) | nonce
135
267
 
136
268
 
137
- def fetch_nonce(w3, account, entry_point, nonce_key):
138
- ep = w3.eth.contract(abi=GET_NONCE_ABI, address=entry_point)
269
+ def fetch_nonce(w3, account, entrypoint, nonce_key):
270
+ ep = w3.eth.contract(abi=GET_NONCE_ABI, address=entrypoint)
139
271
  return ep.functions.getNonce(account, nonce_key).call()
140
272
 
141
273
 
142
- def get_random_nonce_key():
143
- global RANDOM_NONCE_KEY
144
- if RANDOM_NONCE_KEY is None:
145
- RANDOM_NONCE_KEY = random.randint(1, 2**192 - 1)
146
- return RANDOM_NONCE_KEY
147
-
148
-
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
274
+ def get_random_nonce_key(force=False):
275
+ if force or getattr(RANDOM_NONCE_KEY, "key", None) is None:
276
+ RANDOM_NONCE_KEY.key = random.randint(1, 2**192 - 1)
277
+ return RANDOM_NONCE_KEY.key
165
278
 
166
279
 
167
280
  def consume_nonce(nonce_key, nonce):
@@ -182,117 +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 "from" not in tx or tx["from"] == ADDRESS_ZERO:
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["from"]
197
-
198
-
199
- def build_user_operation(w3, tx, retry_nonce=None):
200
- nonce_key, nonce = get_nonce_and_key(
201
- w3, tx, AA_BUNDLER_NONCE_MODE, entry_point=AA_BUNDLER_ENTRYPOINT, fetch=retry_nonce is not None
202
- )
203
- # "0xb61d27f6" = bytes4 hash of execute(address,uint256,bytes)
204
- call_data = (
205
- "0xb61d27f6"
206
- + encode(["address", "uint256", "bytes"], [tx["to"], tx["value"], HexBytes(tx["data"])]).hex()
207
- )
208
- dummy_signature = (
209
- "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007"
210
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"
211
- )
212
- user_operation = {
213
- "sender": get_sender(tx),
214
- "nonce": hex(make_nonce(nonce_key, nonce)),
215
- "callData": call_data,
216
- "signature": dummy_signature,
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
- if AA_BUNDLER_PROVIDER == "alchemy":
220
- resp = w3.provider.make_request(
221
- "eth_estimateUserOperationGas", [user_operation, AA_BUNDLER_ENTRYPOINT]
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
- next_nonce = check_nonce_error(resp, retry_nonce)
225
- return build_user_operation(w3, tx, retry_nonce=next_nonce)
370
+ raise RevertError(resp["error"]["message"])
226
371
 
227
- user_operation.update(resp["result"])
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
- resp = w3.provider.make_request("rundler_maxPriorityFeePerGas", [])
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
- max_priority_fee_per_gas = apply_factor(resp["result"], AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR)
233
- user_operation["maxPriorityFeePerGas"] = hex(max_priority_fee_per_gas)
234
- user_operation["maxFeePerGas"] = hex(max_priority_fee_per_gas + get_base_fee(w3))
235
- user_operation["callGasLimit"] = hex(
236
- apply_factor(user_operation["callGasLimit"], AA_BUNDLER_GAS_LIMIT_FACTOR)
237
- )
238
- elif AA_BUNDLER_PROVIDER == "gelato":
239
- user_operation.update(
240
- {
241
- "preVerificationGas": "0x00",
242
- "callGasLimit": "0x00",
243
- "verificationGasLimit": "0x00",
244
- "maxFeePerGas": "0x00",
245
- "maxPriorityFeePerGas": "0x00",
246
- }
247
- )
248
- elif AA_BUNDLER_PROVIDER == "generic":
249
- resp = w3.provider.make_request(
250
- "eth_estimateUserOperationGas", [user_operation, AA_BUNDLER_ENTRYPOINT]
251
- )
252
- if "error" in resp:
253
- next_nonce = check_nonce_error(resp, retry_nonce)
254
- return build_user_operation(w3, tx, retry_nonce=next_nonce)
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()
255
392
 
256
- user_operation.update(resp["result"])
393
+ return GasPrice(max_priority_fee_per_gas=max_priority_fee_per_gas, max_fee_per_gas=max_fee_per_gas)
257
394
 
258
- user_operation["verificationGasLimit"] = apply_factor(
259
- user_operation["verificationGasLimit"], AA_BUNDLER_VERIFICATION_GAS_FACTOR
260
- )
261
- if "maxPriorityFeePerGas" in user_operation:
262
- user_operation["maxPriorityFeePerGas"] = apply_factor(
263
- user_operation["maxPriorityFeePerGas"], AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR
264
- )
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)
265
399
 
266
- if "callGasLimit" in user_operation:
267
- user_operation["callGasLimit"] = apply_factor(
268
- user_operation["callGasLimit"], AA_BUNDLER_GAS_LIMIT_FACTOR
269
- )
400
+ user_operation = UserOperation.from_tx(tx, make_nonce(nonce_key, nonce))
401
+ estimation = self.estimate_user_operation_gas(user_operation)
270
402
 
271
- else:
272
- warn(f"Unknown AA_BUNDLER_PROVIDER: {AA_BUNDLER_PROVIDER}")
403
+ user_operation = user_operation.add_estimation(estimation)
273
404
 
274
- # Remove paymaster related fields
275
- user_operation.pop("paymaster", None)
276
- user_operation.pop("paymasterData", None)
277
- user_operation.pop("paymasterVerificationGasLimit", None)
278
- 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)
279
408
 
280
- # Consume the nonce, even if the userop may fail later
281
- consume_nonce(nonce_key, nonce)
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}")
282
415
 
283
- return user_operation
416
+ return user_operation
284
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
+ )
285
422
 
286
- def send_transaction(w3, tx, retry_nonce=None):
287
- user_operation = build_user_operation(w3, tx, retry_nonce)
288
- user_operation["signature"] = add_0x_prefix(
289
- sign_user_operation(
290
- AA_BUNDLER_EXECUTOR_PK, user_operation, tx["chainId"], AA_BUNDLER_ENTRYPOINT
291
- ).hex()
292
- )
293
- resp = w3.provider.make_request("eth_sendUserOperation", [user_operation, AA_BUNDLER_ENTRYPOINT])
294
- if "error" in resp:
295
- next_nonce = check_nonce_error(resp, retry_nonce)
296
- 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)
297
429
 
298
- return {"userOpHash": resp["result"]}
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.rawTransaction)
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 type(name) == str and name.startswith("0x"):
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="owner", *init_params, **kwargs):
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=k3mUdBXFkD_0NGJkbQeQRGgn0xPvIS6G7mMp5t3VsEA,11104
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.1b1.dist-info/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
10
- eth_prototype-1.2.1b1.dist-info/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
11
- eth_prototype-1.2.1b1.dist-info/METADATA,sha256=Tgo1SAw-_6zQBkqK59iLGI9sYMpNg91vBtP6ofWpicc,2484
12
- eth_prototype-1.2.1b1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
13
- eth_prototype-1.2.1b1.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
14
- eth_prototype-1.2.1b1.dist-info/RECORD,,