eth-prototype 1.2.1__py3-none-any.whl → 1.3.0b1__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.1
3
+ Version: 1.3.0b1
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,6 +19,8 @@ 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
@@ -30,8 +32,8 @@ Requires-Dist: gmpy2; extra == "testing"
30
32
  Requires-Dist: pytest-cov; extra == "testing"
31
33
  Requires-Dist: web3[tester]==7.*; extra == "testing"
32
34
  Requires-Dist: boto3; extra == "testing"
33
- Provides-Extra: web3
34
- Requires-Dist: web3==7.*; extra == "web3"
35
+ Requires-Dist: pytest-recording; extra == "testing"
36
+ Requires-Dist: factory-boy; extra == "testing"
35
37
 
36
38
  # eth-prototype
37
39
 
@@ -0,0 +1,18 @@
1
+ ethproto/__init__.py,sha256=YWkAFysBp4tZjLWWB2FFmp5yG23pUYhQvgQW9b3soXs,579
2
+ ethproto/aa_bundler.py,sha256=vVEiorWPpLq7ev8-wKUQ8fpgDZpaOz9LGlKGNFH0faU,14987
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
+ 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.0b1.dist-info/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
14
+ eth_prototype-1.3.0b1.dist-info/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
15
+ eth_prototype-1.3.0b1.dist-info/METADATA,sha256=Ipe-jiFcOwqo89hto7kwoZ52UetWAmwC7i8y_5km0_g,2583
16
+ eth_prototype-1.3.0b1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
17
+ eth_prototype-1.3.0b1.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
18
+ eth_prototype-1.3.0b1.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,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,7 +9,8 @@ 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 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
12
14
  from hexbytes import HexBytes
13
15
  from web3 import Web3
14
16
  from web3.constants import ADDRESS_ZERO
@@ -17,6 +19,7 @@ from .contracts import RevertError
17
19
 
18
20
  env = Env()
19
21
 
22
+ AA_BUNDLER_URL = env.str("AA_BUNDLER_URL", env.str("WEB3_PROVIDER_URI", "http://localhost:8545"))
20
23
  AA_BUNDLER_SENDER = env.str("AA_BUNDLER_SENDER", None)
21
24
  AA_BUNDLER_ENTRYPOINT = env.str("AA_BUNDLER_ENTRYPOINT", "0x0000000071727De22E5E9d8BAf0edAc6f37da032")
22
25
  AA_BUNDLER_EXECUTOR_PK = env.str("AA_BUNDLER_EXECUTOR_PK", None)
@@ -30,6 +33,7 @@ NonceMode = Enum(
30
33
  "NonceMode",
31
34
  [
32
35
  "RANDOM_KEY", # first time initializes a random key and increments nonce locally with calling the blockchain
36
+ "RANDOM_KEY_EVERYTIME", # initializes a random key every time and increments nonce locally
33
37
  "FIXED_KEY_LOCAL_NONCE", # uses a fixed key, keeps nonce locally and fetches the nonce when receiving
34
38
  # 'AA25 invalid account nonce'
35
39
  "FIXED_KEY_FETCH_ALWAYS", # uses a fixed key, always fetches unless received as parameter
@@ -57,6 +61,179 @@ GET_NONCE_ABI = [
57
61
  NONCE_CACHE = defaultdict(lambda: 0)
58
62
  RANDOM_NONCE_KEY = local()
59
63
 
64
+ DUMMY_SIGNATURE = HexBytes(
65
+ "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007"
66
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"
67
+ )
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class UserOpEstimation:
72
+ """eth_estimateUserOperationGas response"""
73
+
74
+ pre_verification_gas: int
75
+ verification_gas_limit: int
76
+ call_gas_limit: int
77
+ paymaster_verification_gas_limit: int
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class GasPrice:
82
+ max_priority_fee_per_gas: int
83
+ max_fee_per_gas: int
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class Tx:
88
+ target: HexAddress
89
+ data: HexBytes
90
+ value: int
91
+
92
+ nonce_key: HexBytes = None
93
+ nonce: int = None
94
+ from_: HexAddress = ADDRESS_ZERO
95
+ chain_id: int = None
96
+
97
+ def as_execute_args(self):
98
+ return [self.target, self.value, self.data]
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class UserOperation:
103
+ EXECUTE_ARG_TYPES = ["address", "uint256", "bytes"]
104
+ EXECUTE_SELECTOR = function_signature_to_4byte_selector(f"execute({','.join(EXECUTE_ARG_TYPES)})")
105
+
106
+ sender: HexBytes
107
+ nonce: int
108
+ call_data: HexBytes
109
+
110
+ max_fee_per_gas: int = 0
111
+ max_priority_fee_per_gas: int = 0
112
+
113
+ call_gas_limit: int = 0
114
+ verification_gas_limit: int = 0
115
+ pre_verification_gas: int = 0
116
+
117
+ signature: HexBytes = DUMMY_SIGNATURE
118
+
119
+ init_code: HexBytes = HexBytes("0x")
120
+ paymaster_and_data: HexBytes = HexBytes("0x")
121
+
122
+ @classmethod
123
+ def from_tx(cls, tx: Tx, nonce):
124
+ return cls(
125
+ sender=get_sender(tx),
126
+ nonce=nonce,
127
+ call_data=add_0x_prefix(
128
+ (cls.EXECUTE_SELECTOR + encode(cls.EXECUTE_ARG_TYPES, tx.as_execute_args())).hex()
129
+ ),
130
+ )
131
+
132
+ def as_reduced_dict(self):
133
+ return {
134
+ "sender": self.sender,
135
+ "nonce": "0x%x" % self.nonce,
136
+ "callData": self.call_data,
137
+ "signature": add_0x_prefix(self.signature.hex()),
138
+ }
139
+
140
+ def as_dict(self):
141
+ return {
142
+ "sender": self.sender,
143
+ "nonce": "0x%x" % self.nonce,
144
+ "callData": self.call_data,
145
+ "callGasLimit": "0x%x" % self.call_gas_limit,
146
+ "verificationGasLimit": "0x%x" % self.verification_gas_limit,
147
+ "preVerificationGas": "0x%x" % self.pre_verification_gas,
148
+ "maxPriorityFeePerGas": "0x%x" % self.max_priority_fee_per_gas,
149
+ "maxFeePerGas": "0x%x" % self.max_fee_per_gas,
150
+ "signature": add_0x_prefix(self.signature.hex()),
151
+ }
152
+
153
+ def add_estimation(self, estimation: UserOpEstimation) -> "UserOperation":
154
+ return replace(
155
+ self,
156
+ call_gas_limit=estimation.call_gas_limit,
157
+ verification_gas_limit=estimation.verification_gas_limit,
158
+ pre_verification_gas=estimation.pre_verification_gas,
159
+ )
160
+
161
+ def add_gas_price(self, gas_price: GasPrice) -> "UserOperation":
162
+ return replace(
163
+ self,
164
+ max_priority_fee_per_gas=gas_price.max_priority_fee_per_gas,
165
+ max_fee_per_gas=gas_price.max_fee_per_gas,
166
+ )
167
+
168
+ def sign(self, private_key: HexBytes, chain_id, entrypoint) -> "UserOperation":
169
+ signature = Account.sign_message(
170
+ encode_defunct(
171
+ hexstr=PackedUserOperation.from_user_operation(self)
172
+ .hash_full(chain_id=chain_id, entrypoint=entrypoint)
173
+ .hex()
174
+ ),
175
+ private_key,
176
+ )
177
+ return replace(self, signature=signature.signature)
178
+
179
+
180
+ @dataclass(frozen=True)
181
+ class PackedUserOperation:
182
+ sender: HexBytes
183
+ nonce: int
184
+ call_data: HexBytes
185
+
186
+ account_gas_limits: HexBytes
187
+ pre_verification_gas: int
188
+ gas_fees: HexBytes
189
+
190
+ init_code: HexBytes = HexBytes("0x")
191
+ paymaster_and_data: HexBytes = HexBytes("0x")
192
+ signature: HexBytes = HexBytes("0x")
193
+
194
+ @classmethod
195
+ def from_user_operation(cls, user_operation: UserOperation):
196
+ return cls(
197
+ sender=user_operation.sender,
198
+ nonce=user_operation.nonce,
199
+ call_data=user_operation.call_data,
200
+ account_gas_limits=pack_two(user_operation.verification_gas_limit, user_operation.call_gas_limit),
201
+ pre_verification_gas=user_operation.pre_verification_gas,
202
+ gas_fees=pack_two(user_operation.max_priority_fee_per_gas, user_operation.max_fee_per_gas),
203
+ init_code=user_operation.init_code,
204
+ paymaster_and_data=user_operation.paymaster_and_data,
205
+ signature=user_operation.signature,
206
+ )
207
+
208
+ def hash(self):
209
+ # https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/UserOperationLib.sol#L54
210
+ hash_init_code = Web3.solidity_keccak(["bytes"], [self.init_code])
211
+ hash_call_data = Web3.solidity_keccak(["bytes"], [self.call_data])
212
+ hash_paymaster_and_data = Web3.solidity_keccak(["bytes"], [self.paymaster_and_data])
213
+ return Web3.keccak(
214
+ hexstr=encode(
215
+ ["address", "uint256", "bytes32", "bytes32", "bytes32", "uint256", "bytes32", "bytes32"],
216
+ [
217
+ self.sender,
218
+ self.nonce,
219
+ hash_init_code,
220
+ hash_call_data,
221
+ HexBytes(self.account_gas_limits),
222
+ self.pre_verification_gas,
223
+ HexBytes(self.gas_fees),
224
+ hash_paymaster_and_data,
225
+ ],
226
+ ).hex()
227
+ )
228
+
229
+ def hash_full(self, chain_id, entrypoint):
230
+ return Web3.keccak(
231
+ hexstr=encode(
232
+ ["bytes32", "address", "uint256"],
233
+ [self.hash(), entrypoint, chain_id],
234
+ ).hex()
235
+ )
236
+
60
237
 
61
238
  def pack_two(a, b):
62
239
  a = HexBytes(a).hex()
@@ -72,98 +249,23 @@ def _to_uint(x):
72
249
  raise RuntimeError(f"Invalid int value {x}")
73
250
 
74
251
 
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
252
  def make_nonce(nonce_key, nonce):
133
253
  nonce_key = _to_uint(nonce_key)
134
254
  nonce = _to_uint(nonce)
135
255
  return (nonce_key << 64) | nonce
136
256
 
137
257
 
138
- def fetch_nonce(w3, account, entry_point, nonce_key):
139
- ep = w3.eth.contract(abi=GET_NONCE_ABI, address=entry_point)
258
+ def fetch_nonce(w3, account, entrypoint, nonce_key):
259
+ ep = w3.eth.contract(abi=GET_NONCE_ABI, address=entrypoint)
140
260
  return ep.functions.getNonce(account, nonce_key).call()
141
261
 
142
262
 
143
- def get_random_nonce_key():
144
- if getattr(RANDOM_NONCE_KEY, "key", None) is None:
263
+ def get_random_nonce_key(force=False):
264
+ if force or getattr(RANDOM_NONCE_KEY, "key", None) is None:
145
265
  RANDOM_NONCE_KEY.key = random.randint(1, 2**192 - 1)
146
266
  return RANDOM_NONCE_KEY.key
147
267
 
148
268
 
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
269
  def consume_nonce(nonce_key, nonce):
168
270
  NONCE_CACHE[nonce_key] = max(NONCE_CACHE[nonce_key], nonce + 1)
169
271
 
@@ -182,104 +284,128 @@ def check_nonce_error(resp, retry_nonce):
182
284
  raise RevertError(resp["error"]["message"])
183
285
 
184
286
 
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
287
  def get_sender(tx):
191
- if "from" not in tx or tx["from"] == ADDRESS_ZERO:
288
+ if tx.from_ == ADDRESS_ZERO:
192
289
  if AA_BUNDLER_SENDER is None:
193
290
  raise RuntimeError("Must define AA_BUNDLER_SENDER or send 'from' in the TX")
194
291
  return AA_BUNDLER_SENDER
195
292
  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
- }
218
-
219
- if AA_BUNDLER_PROVIDER == "alchemy":
220
- resp = w3.provider.make_request(
221
- "eth_estimateUserOperationGas", [user_operation, AA_BUNDLER_ENTRYPOINT]
293
+ return tx.from_
294
+
295
+
296
+ class Bundler:
297
+ def __init__(
298
+ self,
299
+ w3: Web3,
300
+ bundler_url: str = AA_BUNDLER_URL,
301
+ bundler_type: str = AA_BUNDLER_PROVIDER,
302
+ entrypoint: HexAddress = AA_BUNDLER_ENTRYPOINT,
303
+ nonce_mode: NonceMode = AA_BUNDLER_NONCE_MODE,
304
+ fixed_nonce_key: int = AA_BUNDLER_NONCE_KEY,
305
+ verification_gas_factor: float = AA_BUNDLER_VERIFICATION_GAS_FACTOR,
306
+ gas_limit_factor: float = AA_BUNDLER_GAS_LIMIT_FACTOR,
307
+ priority_gas_price_factor: float = AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR,
308
+ base_gas_price_factor: float = AA_BUNDLER_BASE_GAS_PRICE_FACTOR,
309
+ executor_pk: HexBytes = AA_BUNDLER_EXECUTOR_PK,
310
+ ):
311
+ self.w3 = w3
312
+ self.bundler = Web3(Web3.HTTPProvider(bundler_url), middleware=[])
313
+ self.bundler_type = bundler_type
314
+ self.entrypoint = entrypoint
315
+ self.nonce_mode = nonce_mode
316
+ self.fixed_nonce_key = fixed_nonce_key
317
+ self.verification_gas_factor = verification_gas_factor
318
+ self.gas_limit_factor = gas_limit_factor
319
+ self.priority_gas_price_factor = priority_gas_price_factor
320
+ self.base_gas_price_factor = base_gas_price_factor
321
+ self.executor_pk = executor_pk
322
+
323
+ def get_nonce_and_key(self, tx: Tx, fetch=False):
324
+ nonce_key = tx.nonce_key
325
+ nonce = tx.nonce
326
+
327
+ if nonce_key is None:
328
+ if self.nonce_mode == NonceMode.RANDOM_KEY:
329
+ nonce_key = get_random_nonce_key()
330
+ elif self.nonce_mode == NonceMode.RANDOM_KEY_EVERYTIME:
331
+ nonce_key = get_random_nonce_key(force=True)
332
+ else:
333
+ nonce_key = self.fixed_nonce_key
334
+
335
+ if nonce is None:
336
+ if fetch or self.nonce_mode == NonceMode.FIXED_KEY_FETCH_ALWAYS:
337
+ nonce = fetch_nonce(self.w3, get_sender(tx), self.entrypoint, nonce_key)
338
+ else:
339
+ nonce = NONCE_CACHE[nonce_key]
340
+ return nonce_key, nonce
341
+
342
+ def get_base_fee(self):
343
+ blk = self.w3.eth.get_block("latest")
344
+ return int(_to_uint(blk["baseFeePerGas"]) * self.base_gas_price_factor)
345
+
346
+ def estimate_user_operation_gas(self, user_operation: UserOperation) -> UserOpEstimation:
347
+ resp = self.bundler.provider.make_request(
348
+ "eth_estimateUserOperationGas", [user_operation.as_reduced_dict(), self.entrypoint]
222
349
  )
223
350
  if "error" in resp:
224
- next_nonce = check_nonce_error(resp, retry_nonce)
225
- return build_user_operation(w3, tx, retry_nonce=next_nonce)
351
+ raise RevertError(resp["error"]["message"])
226
352
 
227
- user_operation.update(resp["result"])
353
+ paymaster_verification_gas_limit = resp["result"].get("paymasterVerificationGasLimit", "0x00")
354
+ return UserOpEstimation(
355
+ pre_verification_gas=int(resp["result"].get("preVerificationGas", "0x00"), 16),
356
+ verification_gas_limit=int(
357
+ int(resp["result"].get("verificationGasLimit", "0x00"), 16) * self.verification_gas_factor
358
+ ),
359
+ call_gas_limit=int(int(resp["result"].get("callGasLimit", "0x00"), 16) * self.gas_limit_factor),
360
+ paymaster_verification_gas_limit=(
361
+ int(paymaster_verification_gas_limit, 16)
362
+ if paymaster_verification_gas_limit is not None
363
+ else 0
364
+ ),
365
+ )
228
366
 
229
- resp = w3.provider.make_request("rundler_maxPriorityFeePerGas", [])
367
+ def alchemy_gas_price(self):
368
+ resp = self.bundler.provider.make_request("rundler_maxPriorityFeePerGas", [])
230
369
  if "error" in resp:
231
370
  raise RevertError(resp["error"]["message"])
232
- user_operation["maxPriorityFeePerGas"] = resp["result"]
233
- user_operation["maxFeePerGas"] = hex(int(resp["result"], 16) + get_base_fee(w3))
371
+ max_priority_fee_per_gas = int(int(resp["result"], 16) * self.priority_gas_price_factor)
372
+ max_fee_per_gas = max_priority_fee_per_gas + self.get_base_fee()
234
373
 
235
- elif AA_BUNDLER_PROVIDER == "generic":
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)
374
+ return GasPrice(max_priority_fee_per_gas=max_priority_fee_per_gas, max_fee_per_gas=max_fee_per_gas)
242
375
 
243
- user_operation.update(resp["result"])
376
+ def build_user_operation(self, tx: Tx, retry_nonce=None) -> UserOperation:
377
+ nonce_key, nonce = self.get_nonce_and_key(tx, fetch=retry_nonce is not None)
378
+ # Consume the nonce, even if the userop may fail later
379
+ consume_nonce(nonce_key, nonce)
244
380
 
245
- else:
246
- warn(f"Unknown AA_BUNDLER_PROVIDER: {AA_BUNDLER_PROVIDER}")
247
-
248
- # Apply increase factors
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
- )
381
+ user_operation = UserOperation.from_tx(tx, make_nonce(nonce_key, nonce))
382
+ estimation = self.estimate_user_operation_gas(user_operation)
383
+
384
+ user_operation = user_operation.add_estimation(estimation)
260
385
 
261
- # Remove paymaster related fields
262
- user_operation.pop("paymaster", None)
263
- user_operation.pop("paymasterData", None)
264
- user_operation.pop("paymasterVerificationGasLimit", None)
265
- user_operation.pop("paymasterPostOpGasLimit", None)
386
+ if self.bundler_type == "alchemy":
387
+ gas_price = self.alchemy_gas_price()
388
+ user_operation = user_operation.add_gas_price(gas_price)
266
389
 
267
- # Consume the nonce, even if the userop may fail later
268
- consume_nonce(nonce_key, nonce)
390
+ elif self.bundler_type == "generic":
391
+ # At the moment generic just prices the gas at 0
392
+ pass
393
+
394
+ else:
395
+ warn(f"Unknown bundler_type: {self.bundler_type}")
269
396
 
270
- return user_operation
397
+ return user_operation
271
398
 
399
+ def send_transaction(self, tx: Tx, retry_nonce=None):
400
+ user_operation = self.build_user_operation(tx, retry_nonce).sign(
401
+ self.executor_pk, tx.chain_id, self.entrypoint
402
+ )
272
403
 
273
- def send_transaction(w3, tx, retry_nonce=None):
274
- user_operation = build_user_operation(w3, tx, retry_nonce)
275
- user_operation["signature"] = add_0x_prefix(
276
- sign_user_operation(
277
- AA_BUNDLER_EXECUTOR_PK, user_operation, tx["chainId"], AA_BUNDLER_ENTRYPOINT
278
- ).hex()
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)
404
+ resp = self.bundler.provider.make_request(
405
+ "eth_sendUserOperation", [user_operation.as_dict(), self.entrypoint]
406
+ )
407
+ if "error" in resp:
408
+ next_nonce = check_nonce_error(resp, retry_nonce)
409
+ return self.send_transaction(tx, retry_nonce=next_nonce)
284
410
 
285
- return {"userOpHash": resp["result"]}
411
+ 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"]
@@ -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,,