eth-prototype 1.2.0__tar.gz → 1.2.1__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 (62) hide show
  1. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/PKG-INFO +1 -1
  2. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/eth_prototype.egg-info/PKG-INFO +1 -1
  3. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/ethproto/aa_bundler.py +66 -34
  4. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/test_aa_bundler.py +45 -2
  5. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/.coveragerc +0 -0
  6. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/.github/workflows/publish.yaml +0 -0
  7. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/.github/workflows/test.yaml +0 -0
  8. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/.gitignore +0 -0
  9. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/.isort.cfg +0 -0
  10. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/.pre-commit-config.yaml +0 -0
  11. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/.readthedocs.yml +0 -0
  12. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/AUTHORS.rst +0 -0
  13. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/CHANGELOG.rst +0 -0
  14. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/LICENSE.txt +0 -0
  15. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/README.md +0 -0
  16. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/docs/Makefile +0 -0
  17. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/docs/_static/.gitignore +0 -0
  18. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/docs/authors.rst +0 -0
  19. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/docs/changelog.rst +0 -0
  20. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/docs/conf.py +0 -0
  21. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/docs/index.rst +0 -0
  22. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/docs/license.rst +0 -0
  23. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/docs/readme.rst +0 -0
  24. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/docs/requirements.txt +0 -0
  25. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/pyproject.toml +0 -0
  26. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/setup.cfg +0 -0
  27. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/setup.py +0 -0
  28. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/eth_prototype.egg-info/SOURCES.txt +0 -0
  29. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/eth_prototype.egg-info/dependency_links.txt +0 -0
  30. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/eth_prototype.egg-info/not-zip-safe +0 -0
  31. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/eth_prototype.egg-info/requires.txt +0 -0
  32. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/eth_prototype.egg-info/top_level.txt +0 -0
  33. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/ethproto/__init__.py +0 -0
  34. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/ethproto/build_artifacts.py +0 -0
  35. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/ethproto/contracts.py +0 -0
  36. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/ethproto/defender_relay.py +0 -0
  37. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/ethproto/w3wrappers.py +0 -0
  38. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/ethproto/wadray.py +0 -0
  39. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/src/ethproto/wrappers.py +0 -0
  40. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/conftest.py +0 -0
  41. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/README.md +0 -0
  42. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/artifacts2/TestCurrency.sol/TestCurrency.json +0 -0
  43. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/Count.sol +0 -0
  44. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/Counter.sol +0 -0
  45. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/CounterUpgradeable.sol +0 -0
  46. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/CounterUpgradeableWithLibrary.sol +0 -0
  47. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/CounterWithLibrary.sol +0 -0
  48. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/Datatypes.sol +0 -0
  49. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/EventLauncher.sol +0 -0
  50. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/TestCurrency.sol +0 -0
  51. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/TestCurrencyUUPS.sol +0 -0
  52. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/contracts/TestNFT.sol +0 -0
  53. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/hardhat.config.js +0 -0
  54. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/package-lock.json +0 -0
  55. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/hardhat-project/package.json +0 -0
  56. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/test_build_artifacts.py +0 -0
  57. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/test_contracts.py +0 -0
  58. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/test_defender.py +0 -0
  59. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/test_time_control.py +0 -0
  60. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/test_w3.py +0 -0
  61. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tests/test_wadray.py +0 -0
  62. {eth_prototype-1.2.0 → eth_prototype-1.2.1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eth-prototype
3
- Version: 1.2.0
3
+ Version: 1.2.1
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eth-prototype
3
- Version: 1.2.0
3
+ Version: 1.2.1
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
@@ -1,5 +1,7 @@
1
1
  import random
2
+ from collections import defaultdict
2
3
  from enum import Enum
4
+ from threading import local
3
5
  from warnings import warn
4
6
 
5
7
  from environs import Env
@@ -22,6 +24,7 @@ AA_BUNDLER_PROVIDER = env.str("AA_BUNDLER_PROVIDER", "alchemy")
22
24
  AA_BUNDLER_GAS_LIMIT_FACTOR = env.float("AA_BUNDLER_GAS_LIMIT_FACTOR", 1)
23
25
  AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR", 1)
24
26
  AA_BUNDLER_BASE_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_BASE_GAS_PRICE_FACTOR", 1)
27
+ AA_BUNDLER_VERIFICATION_GAS_FACTOR = env.float("AA_BUNDLER_VERIFICATION_GAS_FACTOR", 1)
25
28
 
26
29
  NonceMode = Enum(
27
30
  "NonceMode",
@@ -51,8 +54,8 @@ GET_NONCE_ABI = [
51
54
  }
52
55
  ]
53
56
 
54
- NONCE_CACHE = {}
55
- RANDOM_NONCE_KEY = None
57
+ NONCE_CACHE = defaultdict(lambda: 0)
58
+ RANDOM_NONCE_KEY = local()
56
59
 
57
60
 
58
61
  def pack_two(a, b):
@@ -69,6 +72,10 @@ def _to_uint(x):
69
72
  raise RuntimeError(f"Invalid int value {x}")
70
73
 
71
74
 
75
+ def apply_factor(x, factor):
76
+ return int(_to_uint(x) * factor)
77
+
78
+
72
79
  def pack_user_operation(user_operation):
73
80
  # https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/interfaces/PackedUserOperation.sol
74
81
  return {
@@ -134,10 +141,9 @@ def fetch_nonce(w3, account, entry_point, nonce_key):
134
141
 
135
142
 
136
143
  def get_random_nonce_key():
137
- global RANDOM_NONCE_KEY
138
- if RANDOM_NONCE_KEY is None:
139
- RANDOM_NONCE_KEY = random.randint(1, 2**192 - 1)
140
- return RANDOM_NONCE_KEY
144
+ if getattr(RANDOM_NONCE_KEY, "key", None) is None:
145
+ RANDOM_NONCE_KEY.key = random.randint(1, 2**192 - 1)
146
+ return RANDOM_NONCE_KEY.key
141
147
 
142
148
 
143
149
  def get_nonce_and_key(w3, tx, nonce_mode, entry_point=AA_BUNDLER_ENTRYPOINT, fetch=False):
@@ -153,20 +159,25 @@ def get_nonce_and_key(w3, tx, nonce_mode, entry_point=AA_BUNDLER_ENTRYPOINT, fet
153
159
  if nonce is None:
154
160
  if fetch or nonce_mode == NonceMode.FIXED_KEY_FETCH_ALWAYS:
155
161
  nonce = fetch_nonce(w3, get_sender(tx), entry_point, nonce_key)
156
- elif nonce_key not in NONCE_CACHE:
157
- nonce = 0
158
162
  else:
159
163
  nonce = NONCE_CACHE[nonce_key]
160
164
  return nonce_key, nonce
161
165
 
162
166
 
163
- def handle_response_error(resp, w3, tx, retry_nonce):
167
+ def consume_nonce(nonce_key, nonce):
168
+ NONCE_CACHE[nonce_key] = max(NONCE_CACHE[nonce_key], nonce + 1)
169
+
170
+
171
+ def check_nonce_error(resp, retry_nonce):
172
+ """Returns the next nonce if resp contains a nonce error and retries weren't exhausted
173
+ Raises RevertError otherwise
174
+ """
164
175
  if "AA25" in resp["error"]["message"] and AA_BUNDLER_MAX_GETNONCE_RETRIES > 0:
165
176
  # Retry fetching the nonce
166
177
  if retry_nonce == AA_BUNDLER_MAX_GETNONCE_RETRIES:
167
178
  raise RevertError(resp["error"]["message"])
168
179
  warn(f'{resp["error"]["message"]} error, I will retry fetching the nonce')
169
- return send_transaction(w3, tx, retry_nonce=(retry_nonce or 0) + 1)
180
+ return (retry_nonce or 0) + 1
170
181
  else:
171
182
  raise RevertError(resp["error"]["message"])
172
183
 
@@ -185,7 +196,7 @@ def get_sender(tx):
185
196
  return tx["from"]
186
197
 
187
198
 
188
- def send_transaction(w3, tx, retry_nonce=None):
199
+ def build_user_operation(w3, tx, retry_nonce=None):
189
200
  nonce_key, nonce = get_nonce_and_key(
190
201
  w3, tx, AA_BUNDLER_NONCE_MODE, entry_point=AA_BUNDLER_ENTRYPOINT, fetch=retry_nonce is not None
191
202
  )
@@ -210,44 +221,65 @@ def send_transaction(w3, tx, retry_nonce=None):
210
221
  "eth_estimateUserOperationGas", [user_operation, AA_BUNDLER_ENTRYPOINT]
211
222
  )
212
223
  if "error" in resp:
213
- return handle_response_error(resp, w3, tx, retry_nonce)
224
+ next_nonce = check_nonce_error(resp, retry_nonce)
225
+ return build_user_operation(w3, tx, retry_nonce=next_nonce)
214
226
 
215
227
  user_operation.update(resp["result"])
216
228
 
217
229
  resp = w3.provider.make_request("rundler_maxPriorityFeePerGas", [])
218
230
  if "error" in resp:
219
231
  raise RevertError(resp["error"]["message"])
220
- max_priority_fee_per_gas = int(_to_uint(resp["result"]) * AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR)
221
- user_operation["maxPriorityFeePerGas"] = hex(max_priority_fee_per_gas)
222
- user_operation["maxFeePerGas"] = hex(max_priority_fee_per_gas + get_base_fee(w3))
223
- user_operation["callGasLimit"] = hex(
224
- int(_to_uint(user_operation["callGasLimit"]) * AA_BUNDLER_GAS_LIMIT_FACTOR)
225
- )
226
- elif AA_BUNDLER_PROVIDER == "gelato":
227
- user_operation.update(
228
- {
229
- "preVerificationGas": "0x00",
230
- "callGasLimit": "0x00",
231
- "verificationGasLimit": "0x00",
232
- "maxFeePerGas": "0x00",
233
- "maxPriorityFeePerGas": "0x00",
234
- }
232
+ user_operation["maxPriorityFeePerGas"] = resp["result"]
233
+ user_operation["maxFeePerGas"] = hex(int(resp["result"], 16) + get_base_fee(w3))
234
+
235
+ elif AA_BUNDLER_PROVIDER == "generic":
236
+ resp = w3.provider.make_request(
237
+ "eth_estimateUserOperationGas", [user_operation, AA_BUNDLER_ENTRYPOINT]
235
238
  )
236
- user_operation["signature"] = add_0x_prefix(
237
- sign_user_operation(
238
- AA_BUNDLER_EXECUTOR_PK, user_operation, tx["chainId"], AA_BUNDLER_ENTRYPOINT
239
- ).hex()
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)
242
+
243
+ user_operation.update(resp["result"])
244
+
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)
240
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
+ )
260
+
241
261
  # Remove paymaster related fields
242
262
  user_operation.pop("paymaster", None)
243
263
  user_operation.pop("paymasterData", None)
244
264
  user_operation.pop("paymasterVerificationGasLimit", None)
245
265
  user_operation.pop("paymasterPostOpGasLimit", None)
246
266
 
267
+ # Consume the nonce, even if the userop may fail later
268
+ consume_nonce(nonce_key, nonce)
269
+
270
+ return user_operation
271
+
272
+
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
+ )
247
280
  resp = w3.provider.make_request("eth_sendUserOperation", [user_operation, AA_BUNDLER_ENTRYPOINT])
248
281
  if "error" in resp:
249
- return handle_response_error(resp, w3, tx, retry_nonce)
282
+ next_nonce = check_nonce_error(resp, retry_nonce)
283
+ return send_transaction(w3, tx, retry_nonce=next_nonce)
250
284
 
251
- # Store nonce in the cache, so next time uses a new nonce
252
- NONCE_CACHE[nonce_key] = nonce + 1
253
285
  return {"userOpHash": resp["result"]}
@@ -1,3 +1,5 @@
1
+ from queue import Queue
2
+ from threading import Event, Thread
1
3
  from unittest.mock import MagicMock, patch
2
4
 
3
5
  from hexbytes import HexBytes
@@ -155,8 +157,8 @@ def test_get_nonce_random_key_mode(fetch_nonce_mock, randint_mock):
155
157
  fetch_nonce_mock.assert_not_called()
156
158
  randint_mock.assert_called_with(1, 2**192 - 1)
157
159
  randint_mock.reset_mock()
158
- assert aa_bundler.RANDOM_NONCE_KEY == 444
159
- aa_bundler.RANDOM_NONCE_KEY = None # cleanup
160
+ assert aa_bundler.RANDOM_NONCE_KEY.key == 444
161
+ aa_bundler.RANDOM_NONCE_KEY.key = None # cleanup
160
162
 
161
163
 
162
164
  @patch.object(aa_bundler.random, "randint")
@@ -241,3 +243,44 @@ def test_send_transaction(get_base_fee_mock):
241
243
  get_base_fee_mock.assert_called_once_with(w3)
242
244
  assert aa_bundler.NONCE_CACHE[0] == 1
243
245
  assert ret == {"userOpHash": "0xa950a17ca1ed83e974fb1aa227360a007cb65f566518af117ffdbb04d8d2d524"}
246
+
247
+
248
+ def test_random_key_nonces_are_thread_safe():
249
+ queue = Queue()
250
+ event = Event()
251
+
252
+ def worker():
253
+ event.wait() # Get all threads running at the same time
254
+ nonce_key, nonce = aa_bundler.get_nonce_and_key(
255
+ FAIL_IF_USED,
256
+ {"from": TEST_SENDER},
257
+ nonce_mode=aa_bundler.NonceMode.RANDOM_KEY,
258
+ )
259
+ aa_bundler.consume_nonce(nonce_key, nonce)
260
+ queue.put(
261
+ aa_bundler.get_nonce_and_key(
262
+ FAIL_IF_USED,
263
+ {"from": TEST_SENDER},
264
+ nonce_mode=aa_bundler.NonceMode.RANDOM_KEY,
265
+ )
266
+ )
267
+
268
+ threads = [Thread(target=worker) for _ in range(15)]
269
+ for thread in threads:
270
+ thread.start()
271
+
272
+ # Fire all threads at once
273
+ event.set()
274
+ for thread in threads:
275
+ thread.join()
276
+
277
+ nonces = {}
278
+
279
+ while not queue.empty():
280
+ nonce_key, nonce = queue.get_nowait()
281
+ # Each thread got a different key
282
+ assert nonce_key not in nonces
283
+ nonces[nonce_key] = nonce
284
+
285
+ # All nonces are the same
286
+ assert all(nonce == 1 for nonce in nonces.values())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes