eth-prototype 1.2.1b1__tar.gz → 1.3.0b1__tar.gz

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.
Files changed (70) hide show
  1. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/.github/workflows/test.yaml +0 -1
  2. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/PKG-INFO +3 -1
  3. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/setup.cfg +2 -0
  4. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/eth_prototype.egg-info/PKG-INFO +3 -1
  5. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/eth_prototype.egg-info/SOURCES.txt +6 -0
  6. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/eth_prototype.egg-info/requires.txt +2 -0
  7. eth_prototype-1.3.0b1/src/ethproto/aa_bundler.py +411 -0
  8. eth_prototype-1.3.0b1/src/ethproto/test_utils/__init__.py +0 -0
  9. eth_prototype-1.3.0b1/src/ethproto/test_utils/factories.py +70 -0
  10. eth_prototype-1.3.0b1/src/ethproto/test_utils/hardhat.py +71 -0
  11. eth_prototype-1.3.0b1/src/ethproto/test_utils/vcr_utils.py +23 -0
  12. eth_prototype-1.3.0b1/tests/__init__.py +0 -0
  13. eth_prototype-1.3.0b1/tests/cassettes/test_aa_bundler/test_build_user_operation.yaml +89 -0
  14. eth_prototype-1.3.0b1/tests/conftest.py +61 -0
  15. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/test_aa_bundler.py +161 -87
  16. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/test_contracts.py +2 -0
  17. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/test_time_control.py +2 -0
  18. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/test_w3.py +4 -1
  19. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tox.ini +3 -3
  20. eth_prototype-1.2.1b1/src/ethproto/aa_bundler.py +0 -298
  21. eth_prototype-1.2.1b1/tests/conftest.py +0 -42
  22. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/.coveragerc +0 -0
  23. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/.github/workflows/publish.yaml +0 -0
  24. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/.gitignore +0 -0
  25. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/.isort.cfg +0 -0
  26. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/.pre-commit-config.yaml +0 -0
  27. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/.readthedocs.yml +0 -0
  28. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/AUTHORS.rst +0 -0
  29. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/CHANGELOG.rst +0 -0
  30. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/LICENSE.txt +0 -0
  31. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/README.md +0 -0
  32. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/docs/Makefile +0 -0
  33. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/docs/_static/.gitignore +0 -0
  34. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/docs/authors.rst +0 -0
  35. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/docs/changelog.rst +0 -0
  36. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/docs/conf.py +0 -0
  37. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/docs/index.rst +0 -0
  38. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/docs/license.rst +0 -0
  39. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/docs/readme.rst +0 -0
  40. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/docs/requirements.txt +0 -0
  41. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/pyproject.toml +0 -0
  42. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/setup.py +0 -0
  43. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/eth_prototype.egg-info/dependency_links.txt +0 -0
  44. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/eth_prototype.egg-info/not-zip-safe +0 -0
  45. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/eth_prototype.egg-info/top_level.txt +0 -0
  46. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/ethproto/__init__.py +0 -0
  47. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/ethproto/build_artifacts.py +0 -0
  48. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/ethproto/contracts.py +0 -0
  49. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/ethproto/defender_relay.py +0 -0
  50. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/ethproto/w3wrappers.py +0 -0
  51. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/ethproto/wadray.py +0 -0
  52. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/src/ethproto/wrappers.py +0 -0
  53. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/README.md +0 -0
  54. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/artifacts2/TestCurrency.sol/TestCurrency.json +0 -0
  55. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/Count.sol +0 -0
  56. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/Counter.sol +0 -0
  57. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/CounterUpgradeable.sol +0 -0
  58. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/CounterUpgradeableWithLibrary.sol +0 -0
  59. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/CounterWithLibrary.sol +0 -0
  60. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/Datatypes.sol +0 -0
  61. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/EventLauncher.sol +0 -0
  62. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/TestCurrency.sol +0 -0
  63. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/TestCurrencyUUPS.sol +0 -0
  64. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/contracts/TestNFT.sol +0 -0
  65. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/hardhat.config.js +0 -0
  66. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/package-lock.json +0 -0
  67. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/hardhat-project/package.json +0 -0
  68. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/test_build_artifacts.py +0 -0
  69. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/test_defender.py +0 -0
  70. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b1}/tests/test_wadray.py +0 -0
@@ -28,6 +28,5 @@ jobs:
28
28
  cd tests/hardhat-project
29
29
  npm ci
30
30
  npx hardhat compile
31
- npx hardhat node &
32
31
  - name: Test with tox
33
32
  run: tox
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eth-prototype
3
- Version: 1.2.1b1
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
@@ -32,6 +32,8 @@ Requires-Dist: gmpy2; extra == "testing"
32
32
  Requires-Dist: pytest-cov; extra == "testing"
33
33
  Requires-Dist: web3[tester]==7.*; extra == "testing"
34
34
  Requires-Dist: boto3; extra == "testing"
35
+ Requires-Dist: pytest-recording; extra == "testing"
36
+ Requires-Dist: factory-boy; extra == "testing"
35
37
 
36
38
  # eth-prototype
37
39
 
@@ -47,6 +47,8 @@ testing =
47
47
  pytest-cov
48
48
  web3[tester]==7.*
49
49
  boto3
50
+ pytest-recording
51
+ factory-boy
50
52
 
51
53
  [options.entry_points]
52
54
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eth-prototype
3
- Version: 1.2.1b1
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
@@ -32,6 +32,8 @@ Requires-Dist: gmpy2; extra == "testing"
32
32
  Requires-Dist: pytest-cov; extra == "testing"
33
33
  Requires-Dist: web3[tester]==7.*; extra == "testing"
34
34
  Requires-Dist: boto3; extra == "testing"
35
+ Requires-Dist: pytest-recording; extra == "testing"
36
+ Requires-Dist: factory-boy; extra == "testing"
35
37
 
36
38
  # eth-prototype
37
39
 
@@ -36,6 +36,11 @@ src/ethproto/defender_relay.py
36
36
  src/ethproto/w3wrappers.py
37
37
  src/ethproto/wadray.py
38
38
  src/ethproto/wrappers.py
39
+ src/ethproto/test_utils/__init__.py
40
+ src/ethproto/test_utils/factories.py
41
+ src/ethproto/test_utils/hardhat.py
42
+ src/ethproto/test_utils/vcr_utils.py
43
+ tests/__init__.py
39
44
  tests/conftest.py
40
45
  tests/test_aa_bundler.py
41
46
  tests/test_build_artifacts.py
@@ -44,6 +49,7 @@ tests/test_defender.py
44
49
  tests/test_time_control.py
45
50
  tests/test_w3.py
46
51
  tests/test_wadray.py
52
+ tests/cassettes/test_aa_bundler/test_build_user_operation.yaml
47
53
  tests/hardhat-project/README.md
48
54
  tests/hardhat-project/hardhat.config.js
49
55
  tests/hardhat-project/package-lock.json
@@ -19,6 +19,8 @@ gmpy2
19
19
  pytest-cov
20
20
  web3[tester]==7.*
21
21
  boto3
22
+ pytest-recording
23
+ factory-boy
22
24
 
23
25
  [web3]
24
26
  web3==7.*
@@ -0,0 +1,411 @@
1
+ import random
2
+ from collections import defaultdict
3
+ from dataclasses import dataclass, replace
4
+ from enum import Enum
5
+ from threading import local
6
+ from warnings import warn
7
+
8
+ from environs import Env
9
+ from eth_abi import encode
10
+ from eth_account import Account
11
+ from eth_account.messages import encode_defunct
12
+ from eth_typing import HexAddress
13
+ from eth_utils import add_0x_prefix, function_signature_to_4byte_selector
14
+ from hexbytes import HexBytes
15
+ from web3 import Web3
16
+ from web3.constants import ADDRESS_ZERO
17
+
18
+ from .contracts import RevertError
19
+
20
+ env = Env()
21
+
22
+ AA_BUNDLER_URL = env.str("AA_BUNDLER_URL", env.str("WEB3_PROVIDER_URI", "http://localhost:8545"))
23
+ AA_BUNDLER_SENDER = env.str("AA_BUNDLER_SENDER", None)
24
+ AA_BUNDLER_ENTRYPOINT = env.str("AA_BUNDLER_ENTRYPOINT", "0x0000000071727De22E5E9d8BAf0edAc6f37da032")
25
+ AA_BUNDLER_EXECUTOR_PK = env.str("AA_BUNDLER_EXECUTOR_PK", None)
26
+ AA_BUNDLER_PROVIDER = env.str("AA_BUNDLER_PROVIDER", "alchemy")
27
+ AA_BUNDLER_GAS_LIMIT_FACTOR = env.float("AA_BUNDLER_GAS_LIMIT_FACTOR", 1)
28
+ AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR", 1)
29
+ AA_BUNDLER_BASE_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_BASE_GAS_PRICE_FACTOR", 1)
30
+ AA_BUNDLER_VERIFICATION_GAS_FACTOR = env.float("AA_BUNDLER_VERIFICATION_GAS_FACTOR", 1)
31
+
32
+ NonceMode = Enum(
33
+ "NonceMode",
34
+ [
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
37
+ "FIXED_KEY_LOCAL_NONCE", # uses a fixed key, keeps nonce locally and fetches the nonce when receiving
38
+ # 'AA25 invalid account nonce'
39
+ "FIXED_KEY_FETCH_ALWAYS", # uses a fixed key, always fetches unless received as parameter
40
+ ],
41
+ )
42
+
43
+ AA_BUNDLER_NONCE_MODE = env.enum("AA_BUNDLER_NONCE_MODE", default="FIXED_KEY_LOCAL_NONCE", type=NonceMode)
44
+ AA_BUNDLER_NONCE_KEY = env.int("AA_BUNDLER_NONCE_KEY", 0)
45
+ AA_BUNDLER_MAX_GETNONCE_RETRIES = env.int("AA_BUNDLER_MAX_GETNONCE_RETRIES", 3)
46
+
47
+
48
+ GET_NONCE_ABI = [
49
+ {
50
+ "inputs": [
51
+ {"internalType": "address", "name": "sender", "type": "address"},
52
+ {"internalType": "uint192", "name": "key", "type": "uint192"},
53
+ ],
54
+ "name": "getNonce",
55
+ "outputs": [{"internalType": "uint256", "name": "nonce", "type": "uint256"}],
56
+ "stateMutability": "view",
57
+ "type": "function",
58
+ }
59
+ ]
60
+
61
+ NONCE_CACHE = defaultdict(lambda: 0)
62
+ RANDOM_NONCE_KEY = local()
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
+
237
+
238
+ def pack_two(a, b):
239
+ a = HexBytes(a).hex()
240
+ b = HexBytes(b).hex()
241
+ return "0x" + a.zfill(32) + b.zfill(32)
242
+
243
+
244
+ def _to_uint(x):
245
+ if isinstance(x, str):
246
+ return int(x, 16)
247
+ elif isinstance(x, int):
248
+ return x
249
+ raise RuntimeError(f"Invalid int value {x}")
250
+
251
+
252
+ def make_nonce(nonce_key, nonce):
253
+ nonce_key = _to_uint(nonce_key)
254
+ nonce = _to_uint(nonce)
255
+ return (nonce_key << 64) | nonce
256
+
257
+
258
+ def fetch_nonce(w3, account, entrypoint, nonce_key):
259
+ ep = w3.eth.contract(abi=GET_NONCE_ABI, address=entrypoint)
260
+ return ep.functions.getNonce(account, nonce_key).call()
261
+
262
+
263
+ def get_random_nonce_key(force=False):
264
+ if force or getattr(RANDOM_NONCE_KEY, "key", None) is None:
265
+ RANDOM_NONCE_KEY.key = random.randint(1, 2**192 - 1)
266
+ return RANDOM_NONCE_KEY.key
267
+
268
+
269
+ def consume_nonce(nonce_key, nonce):
270
+ NONCE_CACHE[nonce_key] = max(NONCE_CACHE[nonce_key], nonce + 1)
271
+
272
+
273
+ def check_nonce_error(resp, retry_nonce):
274
+ """Returns the next nonce if resp contains a nonce error and retries weren't exhausted
275
+ Raises RevertError otherwise
276
+ """
277
+ if "AA25" in resp["error"]["message"] and AA_BUNDLER_MAX_GETNONCE_RETRIES > 0:
278
+ # Retry fetching the nonce
279
+ if retry_nonce == AA_BUNDLER_MAX_GETNONCE_RETRIES:
280
+ raise RevertError(resp["error"]["message"])
281
+ warn(f'{resp["error"]["message"]} error, I will retry fetching the nonce')
282
+ return (retry_nonce or 0) + 1
283
+ else:
284
+ raise RevertError(resp["error"]["message"])
285
+
286
+
287
+ def get_sender(tx):
288
+ if tx.from_ == ADDRESS_ZERO:
289
+ if AA_BUNDLER_SENDER is None:
290
+ raise RuntimeError("Must define AA_BUNDLER_SENDER or send 'from' in the TX")
291
+ return AA_BUNDLER_SENDER
292
+ else:
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]
349
+ )
350
+ if "error" in resp:
351
+ raise RevertError(resp["error"]["message"])
352
+
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
+ )
366
+
367
+ def alchemy_gas_price(self):
368
+ resp = self.bundler.provider.make_request("rundler_maxPriorityFeePerGas", [])
369
+ if "error" in resp:
370
+ raise RevertError(resp["error"]["message"])
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()
373
+
374
+ return GasPrice(max_priority_fee_per_gas=max_priority_fee_per_gas, max_fee_per_gas=max_fee_per_gas)
375
+
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)
380
+
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)
385
+
386
+ if self.bundler_type == "alchemy":
387
+ gas_price = self.alchemy_gas_price()
388
+ user_operation = user_operation.add_gas_price(gas_price)
389
+
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}")
396
+
397
+ return user_operation
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
+ )
403
+
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)
410
+
411
+ return {"userOpHash": resp["result"]}
@@ -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"]
File without changes