eth-prototype 1.0.1__tar.gz → 1.1.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 (61) hide show
  1. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/PKG-INFO +4 -8
  2. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/setup.cfg +2 -6
  3. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/eth_prototype.egg-info/PKG-INFO +4 -8
  4. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/eth_prototype.egg-info/SOURCES.txt +2 -0
  5. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/eth_prototype.egg-info/requires.txt +2 -7
  6. eth_prototype-1.1.1/src/ethproto/aa_bundler.py +250 -0
  7. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/ethproto/build_artifacts.py +7 -18
  8. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/ethproto/w3wrappers.py +13 -1
  9. eth_prototype-1.1.1/tests/test_aa_bundler.py +244 -0
  10. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tox.ini +4 -18
  11. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/.coveragerc +0 -0
  12. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/.github/workflows/publish.yaml +0 -0
  13. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/.github/workflows/test.yaml +0 -0
  14. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/.gitignore +0 -0
  15. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/.isort.cfg +0 -0
  16. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/.pre-commit-config.yaml +0 -0
  17. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/.readthedocs.yml +0 -0
  18. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/AUTHORS.rst +0 -0
  19. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/CHANGELOG.rst +0 -0
  20. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/LICENSE.txt +0 -0
  21. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/README.md +0 -0
  22. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/docs/Makefile +0 -0
  23. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/docs/_static/.gitignore +0 -0
  24. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/docs/authors.rst +0 -0
  25. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/docs/changelog.rst +0 -0
  26. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/docs/conf.py +0 -0
  27. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/docs/index.rst +0 -0
  28. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/docs/license.rst +0 -0
  29. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/docs/readme.rst +0 -0
  30. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/docs/requirements.txt +0 -0
  31. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/pyproject.toml +0 -0
  32. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/setup.py +0 -0
  33. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/eth_prototype.egg-info/dependency_links.txt +0 -0
  34. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/eth_prototype.egg-info/not-zip-safe +0 -0
  35. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/eth_prototype.egg-info/top_level.txt +0 -0
  36. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/ethproto/__init__.py +0 -0
  37. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/ethproto/contracts.py +0 -0
  38. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/ethproto/defender_relay.py +0 -0
  39. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/ethproto/wadray.py +0 -0
  40. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/src/ethproto/wrappers.py +0 -0
  41. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/conftest.py +0 -0
  42. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/README.md +0 -0
  43. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/artifacts2/TestCurrency.sol/TestCurrency.json +0 -0
  44. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/contracts/Count.sol +0 -0
  45. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/contracts/Counter.sol +0 -0
  46. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/contracts/CounterUpgradeable.sol +0 -0
  47. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/contracts/CounterUpgradeableWithLibrary.sol +0 -0
  48. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/contracts/CounterWithLibrary.sol +0 -0
  49. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/contracts/Datatypes.sol +0 -0
  50. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/contracts/TestCurrency.sol +0 -0
  51. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/contracts/TestCurrencyUUPS.sol +0 -0
  52. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/contracts/TestNFT.sol +0 -0
  53. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/hardhat.config.js +0 -0
  54. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/package-lock.json +0 -0
  55. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/hardhat-project/package.json +0 -0
  56. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/test_build_artifacts.py +0 -0
  57. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/test_contracts.py +0 -0
  58. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/test_defender.py +0 -0
  59. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/test_time_control.py +0 -0
  60. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/test_w3.py +0 -0
  61. {eth_prototype-1.0.1 → eth_prototype-1.1.1}/tests/test_wadray.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eth-prototype
3
- Version: 1.0.1
3
+ Version: 1.1.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
@@ -20,7 +20,7 @@ Requires-Dist: requests
20
20
  Requires-Dist: hexbytes
21
21
  Requires-Dist: importlib-metadata; python_version < "3.8"
22
22
  Provides-Extra: web3
23
- Requires-Dist: web3>=6; extra == "web3"
23
+ Requires-Dist: web3==6.*; extra == "web3"
24
24
  Provides-Extra: defender
25
25
  Requires-Dist: boto3; extra == "defender"
26
26
  Provides-Extra: gmpy2
@@ -30,12 +30,8 @@ Requires-Dist: setuptools; extra == "testing"
30
30
  Requires-Dist: pytest; extra == "testing"
31
31
  Requires-Dist: gmpy2; extra == "testing"
32
32
  Requires-Dist: pytest-cov; extra == "testing"
33
- Provides-Extra: testing-w3
34
- Requires-Dist: web3[tester]>=6; extra == "testing-w3"
35
- Requires-Dist: setuptools; extra == "testing-w3"
36
- Requires-Dist: pytest; extra == "testing-w3"
37
- Requires-Dist: pytest-cov; extra == "testing-w3"
38
- Requires-Dist: boto3; extra == "testing-w3"
33
+ Requires-Dist: web3[tester]==6.*; extra == "testing"
34
+ Requires-Dist: boto3; extra == "testing"
39
35
 
40
36
  # eth-prototype
41
37
 
@@ -35,7 +35,7 @@ exclude =
35
35
 
36
36
  [options.extras_require]
37
37
  web3 =
38
- web3>=6
38
+ web3==6.*
39
39
  defender =
40
40
  boto3
41
41
  gmpy2 =
@@ -45,11 +45,7 @@ testing =
45
45
  pytest
46
46
  gmpy2
47
47
  pytest-cov
48
- testing-w3 =
49
- web3[tester]>=6
50
- setuptools
51
- pytest
52
- pytest-cov
48
+ web3[tester]==6.*
53
49
  boto3
54
50
 
55
51
  [options.entry_points]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eth-prototype
3
- Version: 1.0.1
3
+ Version: 1.1.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
@@ -20,7 +20,7 @@ Requires-Dist: requests
20
20
  Requires-Dist: hexbytes
21
21
  Requires-Dist: importlib-metadata; python_version < "3.8"
22
22
  Provides-Extra: web3
23
- Requires-Dist: web3>=6; extra == "web3"
23
+ Requires-Dist: web3==6.*; extra == "web3"
24
24
  Provides-Extra: defender
25
25
  Requires-Dist: boto3; extra == "defender"
26
26
  Provides-Extra: gmpy2
@@ -30,12 +30,8 @@ Requires-Dist: setuptools; extra == "testing"
30
30
  Requires-Dist: pytest; extra == "testing"
31
31
  Requires-Dist: gmpy2; extra == "testing"
32
32
  Requires-Dist: pytest-cov; extra == "testing"
33
- Provides-Extra: testing-w3
34
- Requires-Dist: web3[tester]>=6; extra == "testing-w3"
35
- Requires-Dist: setuptools; extra == "testing-w3"
36
- Requires-Dist: pytest; extra == "testing-w3"
37
- Requires-Dist: pytest-cov; extra == "testing-w3"
38
- Requires-Dist: boto3; extra == "testing-w3"
33
+ Requires-Dist: web3[tester]==6.*; extra == "testing"
34
+ Requires-Dist: boto3; extra == "testing"
39
35
 
40
36
  # eth-prototype
41
37
 
@@ -29,6 +29,7 @@ src/eth_prototype.egg-info/not-zip-safe
29
29
  src/eth_prototype.egg-info/requires.txt
30
30
  src/eth_prototype.egg-info/top_level.txt
31
31
  src/ethproto/__init__.py
32
+ src/ethproto/aa_bundler.py
32
33
  src/ethproto/build_artifacts.py
33
34
  src/ethproto/contracts.py
34
35
  src/ethproto/defender_relay.py
@@ -36,6 +37,7 @@ src/ethproto/w3wrappers.py
36
37
  src/ethproto/wadray.py
37
38
  src/ethproto/wrappers.py
38
39
  tests/conftest.py
40
+ tests/test_aa_bundler.py
39
41
  tests/test_build_artifacts.py
40
42
  tests/test_contracts.py
41
43
  tests/test_defender.py
@@ -17,13 +17,8 @@ setuptools
17
17
  pytest
18
18
  gmpy2
19
19
  pytest-cov
20
-
21
- [testing-w3]
22
- web3[tester]>=6
23
- setuptools
24
- pytest
25
- pytest-cov
20
+ web3[tester]==6.*
26
21
  boto3
27
22
 
28
23
  [web3]
29
- web3>=6
24
+ web3==6.*
@@ -0,0 +1,250 @@
1
+ import random
2
+ from warnings import warn
3
+ from enum import Enum
4
+ import requests
5
+ from environs import Env
6
+ from eth_abi import encode
7
+ from eth_account import Account
8
+ from eth_account.messages import encode_defunct
9
+ from hexbytes import HexBytes
10
+ from web3 import Web3
11
+ from web3.constants import ADDRESS_ZERO
12
+ from .contracts import RevertError
13
+
14
+
15
+ env = Env()
16
+
17
+ AA_BUNDLER_SENDER = env.str("AA_BUNDLER_SENDER", None)
18
+ AA_BUNDLER_ENTRYPOINT = env.str("AA_BUNDLER_ENTRYPOINT", "0x0000000071727De22E5E9d8BAf0edAc6f37da032")
19
+ AA_BUNDLER_EXECUTOR_PK = env.str("AA_BUNDLER_EXECUTOR_PK", None)
20
+ AA_BUNDLER_PROVIDER = env.str("AA_BUNDLER_PROVIDER", "alchemy")
21
+ AA_BUNDLER_GAS_LIMIT_FACTOR = env.float("AA_BUNDLER_GAS_LIMIT_FACTOR", 1)
22
+ AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR", 1)
23
+ AA_BUNDLER_BASE_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_BASE_GAS_PRICE_FACTOR", 1)
24
+
25
+ NonceMode = Enum(
26
+ "NonceMode",
27
+ [
28
+ "RANDOM_KEY", # first time initializes a random key and increments nonce locally with calling the blockchain
29
+ "FIXED_KEY_LOCAL_NONCE", # uses a fixed key, keeps nonce locally and fetches the nonce when receiving
30
+ # 'AA25 invalid account nonce'
31
+ "FIXED_KEY_FETCH_ALWAYS", # uses a fixed key, always fetches unless received as parameter
32
+ ],
33
+ )
34
+
35
+ AA_BUNDLER_NONCE_MODE = env.enum("AA_BUNDLER_NONCE_MODE", default="FIXED_KEY_LOCAL_NONCE", type=NonceMode)
36
+ AA_BUNDLER_NONCE_KEY = env.int("AA_BUNDLER_NONCE_KEY", 0)
37
+ AA_BUNDLER_MAX_GETNONCE_RETRIES = env.int("AA_BUNDLER_MAX_GETNONCE_RETRIES", 3)
38
+
39
+
40
+ GET_NONCE_ABI = [
41
+ {
42
+ "inputs": [
43
+ {"internalType": "address", "name": "sender", "type": "address"},
44
+ {"internalType": "uint192", "name": "key", "type": "uint192"},
45
+ ],
46
+ "name": "getNonce",
47
+ "outputs": [{"internalType": "uint256", "name": "nonce", "type": "uint256"}],
48
+ "stateMutability": "view",
49
+ "type": "function",
50
+ }
51
+ ]
52
+
53
+ NONCE_CACHE = {}
54
+ RANDOM_NONCE_KEY = None
55
+
56
+
57
+ def pack_two(a, b):
58
+ a = HexBytes(a).hex()[2:]
59
+ b = HexBytes(b).hex()[2:]
60
+ return "0x" + a.zfill(32) + b.zfill(32)
61
+
62
+
63
+ def _to_uint(x):
64
+ if isinstance(x, str):
65
+ return int(x, 16)
66
+ elif isinstance(x, int):
67
+ return x
68
+ raise RuntimeError(f"Invalid int value {x}")
69
+
70
+
71
+ def pack_user_operation(user_operation):
72
+ # https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/interfaces/PackedUserOperation.sol
73
+ return {
74
+ "sender": user_operation["sender"],
75
+ "nonce": _to_uint(user_operation["nonce"]),
76
+ "initCode": "0x",
77
+ "callData": user_operation["callData"],
78
+ "accountGasLimits": pack_two(user_operation["verificationGasLimit"], user_operation["callGasLimit"]),
79
+ "preVerificationGas": _to_uint(user_operation["preVerificationGas"]),
80
+ "gasFees": pack_two(user_operation["maxPriorityFeePerGas"], user_operation["maxFeePerGas"]),
81
+ "paymasterAndData": "0x",
82
+ "signature": "0x",
83
+ }
84
+
85
+
86
+ def hash_packed_user_operation_only(packed_user_op):
87
+ # https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/UserOperationLib.sol#L54
88
+ hash_init_code = Web3.solidity_keccak(["bytes"], [packed_user_op["initCode"]])
89
+ hash_call_data = Web3.solidity_keccak(["bytes"], [packed_user_op["callData"]])
90
+ hash_paymaster_and_data = Web3.solidity_keccak(["bytes"], [packed_user_op["paymasterAndData"]])
91
+ return Web3.keccak(
92
+ hexstr=encode(
93
+ ["address", "uint256", "bytes32", "bytes32", "bytes32", "uint256", "bytes32", "bytes32"],
94
+ [
95
+ packed_user_op["sender"],
96
+ packed_user_op["nonce"],
97
+ hash_init_code,
98
+ hash_call_data,
99
+ HexBytes(packed_user_op["accountGasLimits"]),
100
+ packed_user_op["preVerificationGas"],
101
+ HexBytes(packed_user_op["gasFees"]),
102
+ hash_paymaster_and_data,
103
+ ],
104
+ ).hex()
105
+ ).hex()
106
+
107
+
108
+ def hash_packed_user_operation(packed_user_op, chain_id, entry_point):
109
+ return Web3.keccak(
110
+ hexstr=encode(
111
+ ["bytes32", "address", "uint256"],
112
+ [HexBytes(hash_packed_user_operation_only(packed_user_op)), entry_point, chain_id],
113
+ ).hex()
114
+ ).hex()
115
+
116
+
117
+ def sign_user_operation(private_key, user_operation, chain_id, entry_point):
118
+ packed_user_op = pack_user_operation(user_operation)
119
+ hash = hash_packed_user_operation(packed_user_op, chain_id, entry_point)
120
+ signature = Account.sign_message(encode_defunct(hexstr=hash), private_key)
121
+ return signature.signature.hex()
122
+
123
+
124
+ def make_nonce(nonce_key, nonce):
125
+ nonce_key = _to_uint(nonce_key)
126
+ nonce = _to_uint(nonce)
127
+ return (nonce_key << 64) | nonce
128
+
129
+
130
+ def fetch_nonce(w3, account, entry_point, nonce_key):
131
+ ep = w3.eth.contract(abi=GET_NONCE_ABI, address=entry_point)
132
+ return ep.functions.getNonce(account, nonce_key).call()
133
+
134
+
135
+ def get_random_nonce_key():
136
+ global RANDOM_NONCE_KEY
137
+ if RANDOM_NONCE_KEY is None:
138
+ RANDOM_NONCE_KEY = random.randint(1, 2**192 - 1)
139
+ return RANDOM_NONCE_KEY
140
+
141
+
142
+ def get_nonce_and_key(w3, tx, nonce_mode, entry_point=AA_BUNDLER_ENTRYPOINT, fetch=False):
143
+ nonce_key = tx.get("nonceKey", None)
144
+ nonce = tx.get("nonce", None)
145
+
146
+ if nonce_key is None:
147
+ if nonce_mode == NonceMode.RANDOM_KEY:
148
+ nonce_key = get_random_nonce_key()
149
+ else:
150
+ nonce_key = AA_BUNDLER_NONCE_KEY
151
+
152
+ if nonce is None:
153
+ if fetch or nonce_mode == NonceMode.FIXED_KEY_FETCH_ALWAYS:
154
+ nonce = fetch_nonce(w3, get_sender(tx), entry_point, nonce_key)
155
+ elif nonce_key not in NONCE_CACHE:
156
+ nonce = 0
157
+ else:
158
+ nonce = NONCE_CACHE[nonce_key]
159
+ return nonce_key, nonce
160
+
161
+
162
+ def handle_response_error(resp, w3, tx, retry_nonce):
163
+ if "AA25" in resp["error"]["message"] and AA_BUNDLER_MAX_GETNONCE_RETRIES > 0:
164
+ # Retry fetching the nonce
165
+ if retry_nonce == AA_BUNDLER_MAX_GETNONCE_RETRIES:
166
+ raise RevertError(resp["error"]["message"])
167
+ warn(f'{resp["error"]["message"]} error, I will retry fetching the nonce')
168
+ return send_transaction(w3, tx, retry_nonce=(retry_nonce or 0) + 1)
169
+ else:
170
+ raise RevertError(resp["error"]["message"])
171
+
172
+
173
+ def get_base_fee(w3):
174
+ blk = w3.eth.get_block("latest")
175
+ return int(_to_uint(blk["baseFeePerGas"]) * AA_BUNDLER_BASE_GAS_PRICE_FACTOR)
176
+
177
+
178
+ def get_sender(tx):
179
+ if "from" not in tx or tx["from"] == ADDRESS_ZERO:
180
+ if AA_BUNDLER_SENDER is None:
181
+ raise RuntimeError("Must define AA_BUNDLER_SENDER or send 'from' in the TX")
182
+ return AA_BUNDLER_SENDER
183
+ else:
184
+ return tx["from"]
185
+
186
+
187
+ def send_transaction(w3, tx, retry_nonce=None):
188
+ nonce_key, nonce = get_nonce_and_key(
189
+ w3, tx, AA_BUNDLER_NONCE_MODE, entry_point=AA_BUNDLER_ENTRYPOINT, fetch=retry_nonce is not None
190
+ )
191
+ # "0xb61d27f6" = bytes4 hash of execute(address,uint256,bytes)
192
+ call_data = (
193
+ "0xb61d27f6"
194
+ + encode(["address", "uint256", "bytes"], [tx["to"], tx["value"], HexBytes(tx["data"])]).hex()
195
+ )
196
+ dummy_signature = (
197
+ "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007"
198
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"
199
+ )
200
+ user_operation = {
201
+ "sender": get_sender(tx),
202
+ "nonce": hex(make_nonce(nonce_key, nonce)),
203
+ "callData": call_data,
204
+ "signature": dummy_signature,
205
+ }
206
+
207
+ if AA_BUNDLER_PROVIDER == "alchemy":
208
+ resp = w3.provider.make_request(
209
+ "eth_estimateUserOperationGas", [user_operation, AA_BUNDLER_ENTRYPOINT]
210
+ )
211
+ if "error" in resp:
212
+ return handle_response_error(resp, w3, tx, retry_nonce)
213
+
214
+ user_operation.update(resp["result"])
215
+
216
+ resp = w3.provider.make_request("rundler_maxPriorityFeePerGas", [])
217
+ if "error" in resp:
218
+ raise RevertError(resp["error"]["message"])
219
+ max_priority_fee_per_gas = int(_to_uint(resp["result"]) * AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR)
220
+ user_operation["maxPriorityFeePerGas"] = hex(max_priority_fee_per_gas)
221
+ user_operation["maxFeePerGas"] = hex(max_priority_fee_per_gas + get_base_fee(w3))
222
+ user_operation["callGasLimit"] = hex(
223
+ int(_to_uint(user_operation["callGasLimit"]) * AA_BUNDLER_GAS_LIMIT_FACTOR)
224
+ )
225
+ elif AA_BUNDLER_PROVIDER == "gelato":
226
+ user_operation.update(
227
+ {
228
+ "preVerificationGas": "0x00",
229
+ "callGasLimit": "0x00",
230
+ "verificationGasLimit": "0x00",
231
+ "maxFeePerGas": "0x00",
232
+ "maxPriorityFeePerGas": "0x00",
233
+ }
234
+ )
235
+ user_operation["signature"] = sign_user_operation(
236
+ AA_BUNDLER_EXECUTOR_PK, user_operation, tx["chainId"], AA_BUNDLER_ENTRYPOINT
237
+ )
238
+ # Remove paymaster related fields
239
+ user_operation.pop("paymaster", None)
240
+ user_operation.pop("paymasterData", None)
241
+ user_operation.pop("paymasterVerificationGasLimit", None)
242
+ user_operation.pop("paymasterPostOpGasLimit", None)
243
+
244
+ resp = w3.provider.make_request("eth_sendUserOperation", [user_operation, AA_BUNDLER_ENTRYPOINT])
245
+ if "error" in resp:
246
+ return handle_response_error(resp, w3, tx, retry_nonce)
247
+
248
+ # Store nonce in the cache, so next time uses a new nonce
249
+ NONCE_CACHE[nonce_key] = nonce + 1
250
+ return {"userOpHash": resp["result"]}
@@ -1,6 +1,5 @@
1
1
  """Helper classes to use hardhat build artifacts from python"""
2
2
 
3
-
4
3
  import json
5
4
  import os
6
5
  import os.path
@@ -26,17 +25,15 @@ class Artifact:
26
25
  self.abi = kwargs["abi"]
27
26
  self.bytecode = kwargs["bytecode"]
28
27
  self.deployed_bytecode = kwargs["deployedBytecode"]
29
- self.link_references = kwargs["linkReferences"]
30
- self.deployed_link_references = kwargs["deployedLinkReferences"]
28
+ self.link_references = kwargs.get("linkReferences", {})
29
+ self.deployed_link_references = kwargs.get("deployedLinkReferences", {})
31
30
 
32
31
  def link(self, libraries: dict) -> "Artifact":
33
32
  """Returns a new artifact with the external libraries linked
34
33
 
35
34
  Libraries is a dictionary of the form {library_name: address}
36
35
  """
37
- bytecode = self._replace_link_references(
38
- self.bytecode, self.link_references, libraries
39
- )
36
+ bytecode = self._replace_link_references(self.bytecode, self.link_references, libraries)
40
37
  deployed_bytecode = self._replace_link_references(
41
38
  self.deployed_bytecode, self.deployed_link_references, libraries
42
39
  )
@@ -55,9 +52,7 @@ class Artifact:
55
52
  for lib in libs.keys():
56
53
  yield lib, source
57
54
 
58
- def _replace_link_references(
59
- self, bytecode: str, link_references: dict, libraries: dict
60
- ) -> str:
55
+ def _replace_link_references(self, bytecode: str, link_references: dict, libraries: dict) -> str:
61
56
  # remove 0x prefix if present
62
57
  bytecode = bytecode[2:] if bytecode.startswith("0x") else bytecode
63
58
 
@@ -115,17 +110,13 @@ class ArtifactLibrary:
115
110
 
116
111
  if contract not in self._fullpath_cache:
117
112
  for path in self.lookup_paths:
118
- build_artifact_path = (
119
- path / contract / contract.with_suffix(".json").name
120
- )
113
+ build_artifact_path = path / contract / contract.with_suffix(".json").name
121
114
  if build_artifact_path.exists():
122
115
  with open(build_artifact_path) as f:
123
116
  self._fullpath_cache[contract] = Artifact(**json.load(f))
124
117
 
125
118
  if contract not in self._fullpath_cache:
126
- raise FileNotFoundError(
127
- f"Could not find artifact for {contract} on {self.lookup_paths}"
128
- )
119
+ raise FileNotFoundError(f"Could not find artifact for {contract} on {self.lookup_paths}")
129
120
 
130
121
  return self._fullpath_cache[contract]
131
122
 
@@ -145,8 +136,6 @@ class ArtifactLibrary:
145
136
  self._name_cache[contract_name] = Artifact(**json.load(f))
146
137
 
147
138
  if contract_name not in self._name_cache:
148
- raise FileNotFoundError(
149
- f"Could not find artifact for {contract_name} on {self.lookup_paths}"
150
- )
139
+ raise FileNotFoundError(f"Could not find artifact for {contract_name} on {self.lookup_paths}")
151
140
 
152
141
  return self._name_cache[contract_name]
@@ -108,9 +108,19 @@ def transact(provider, function, tx_kwargs):
108
108
  elif W3_TRANSACT_MODE == "defender-async":
109
109
  from .defender_relay import send_transaction
110
110
 
111
- tx_kwargs = {**provider.tx_kwargs, **tx_kwargs}
111
+ tx_kwargs |= provider.tx_kwargs
112
112
  tx = function.build_transaction(tx_kwargs)
113
113
  return send_transaction(tx)
114
+ elif W3_TRANSACT_MODE == "aa-bundler-async":
115
+ from .aa_bundler import send_transaction
116
+
117
+ tx_kwargs |= provider.tx_kwargs
118
+ # To avoid fetching gas and gasPrice in a standard way, when it's not relevant for user ops
119
+ tx_kwargs.update(dict(gas=0, gasPrice=0))
120
+ tx = function.build_transaction(tx_kwargs)
121
+ return send_transaction(provider.w3, tx)
122
+ else:
123
+ raise RuntimeError(f"Unknown W3_TRANSACT_MODE {W3_TRANSACT_MODE}")
114
124
 
115
125
  return provider.w3.eth.wait_for_transaction_receipt(tx_hash)
116
126
 
@@ -365,6 +375,8 @@ class W3ETHCall(ETHCall):
365
375
  def normalize_receipt(self, wrapper, receipt):
366
376
  if W3_TRANSACT_MODE == "defender-async":
367
377
  return receipt # Don't do anything because the receipt is just a dict of not-yet-mined tx
378
+ elif W3_TRANSACT_MODE == "aa-bundler-async":
379
+ return receipt # Don't do anything because the receipt is just a dict of {"userOpHash": "..."}
368
380
  return ReceiptWrapper(receipt, wrapper.contract)
369
381
 
370
382
  def _handle_exception(self, err):
@@ -0,0 +1,244 @@
1
+ import os
2
+ from hexbytes import HexBytes
3
+ from ethproto import aa_bundler
4
+ from web3.constants import HASH_ZERO
5
+ from unittest.mock import MagicMock, patch
6
+
7
+
8
+ def test_pack_two():
9
+ assert aa_bundler.pack_two(0, 0) == HASH_ZERO
10
+ assert aa_bundler.pack_two(1, 2) == "0x0000000000000000000000000000000100000000000000000000000000000002"
11
+ assert (
12
+ aa_bundler.pack_two("0x1", "0x2")
13
+ == "0x0000000000000000000000000000000100000000000000000000000000000002"
14
+ )
15
+ assert aa_bundler.pack_two(HexBytes(2), HexBytes(3) == "0x{:032x}{:032x}".format(2, 3))
16
+
17
+
18
+ TEST_CALL_DATA = "0x47e1da2a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa8417400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c800000000000000000000000000000000000000000000000000000000004c4b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000" # noqa
19
+
20
+ # Private key of index=1 of seed phrase ["test"] * 11 + ["junk"]
21
+ TEST_PRIVATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
22
+ CHAIN_ID = 31337
23
+ ENTRYPOINT = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
24
+
25
+ TEST_SENDER = "0x8961423b54f06bf6D57F8dD3dD1184FA6F3aac3f"
26
+
27
+ user_operation = {
28
+ "sender": TEST_SENDER,
29
+ "nonce": 0,
30
+ "initCode": "0x",
31
+ "callData": TEST_CALL_DATA,
32
+ "callGasLimit": 999999,
33
+ "verificationGasLimit": 999999,
34
+ "preVerificationGas": 999999,
35
+ "maxFeePerGas": 1000000000,
36
+ "maxPriorityFeePerGas": 1000000000,
37
+ "paymaster": "0x0000000000000000000000000000000000000000",
38
+ "paymasterData": "0x",
39
+ "paymasterVerificationGasLimit": 0,
40
+ "paymasterPostOpGasLimit": 0,
41
+ }
42
+
43
+
44
+ def test_pack_user_operation():
45
+ expected = {
46
+ "sender": TEST_SENDER,
47
+ "nonce": 0,
48
+ "initCode": "0x",
49
+ "callData": TEST_CALL_DATA,
50
+ "accountGasLimits": "0x000000000000000000000000000f423f000000000000000000000000000f423f",
51
+ "preVerificationGas": 999999,
52
+ "gasFees": "0x0000000000000000000000003b9aca000000000000000000000000003b9aca00",
53
+ "paymasterAndData": "0x",
54
+ "signature": "0x",
55
+ }
56
+ assert aa_bundler.pack_user_operation(user_operation) == expected
57
+
58
+
59
+ def test_hash_packed_user_operation():
60
+ packed = aa_bundler.pack_user_operation(user_operation)
61
+ hash = aa_bundler.hash_packed_user_operation_only(packed)
62
+ assert hash == "0xa2c19765d18b0d690c05b20061bd23d066201aff1833a51bd28af115fbd4bcd9"
63
+ hash = aa_bundler.hash_packed_user_operation(packed, CHAIN_ID, ENTRYPOINT)
64
+ assert hash == "0xb365ad4d366e9081718e926912da7a78a2faae592286fda0cc11923bd141b7cf"
65
+
66
+
67
+ def test_sign_user_operation():
68
+ signature = aa_bundler.sign_user_operation(TEST_PRIVATE_KEY, user_operation, CHAIN_ID, ENTRYPOINT)
69
+ assert (
70
+ signature
71
+ == "0xb9b872bfe4e90f4628e8ec24879a5b01045f91da8457f3ce2b417d2e5774b508261ec1147a820e75a141cb61b884a78d7e88996ceddafb9a7016cfe7a48a1f4f1b" # noqa
72
+ )
73
+
74
+
75
+ def test_sign_user_operation_gas_diff():
76
+ user_operation_2 = dict(user_operation)
77
+ user_operation_2["maxPriorityFeePerGas"] -= 1
78
+ signature = aa_bundler.sign_user_operation(TEST_PRIVATE_KEY, user_operation_2, CHAIN_ID, ENTRYPOINT)
79
+ assert (
80
+ signature
81
+ == "0x8162479d2dbd18d7fe93a2f51e283021d6e4eae4f57d20cdd553042723a0b0ea690ab3903d45126b0047da08ab53dfdf86656e4f258ac4936ba96a759ccb77f61b" # noqa
82
+ )
83
+
84
+
85
+ def test_make_nonce():
86
+ assert aa_bundler.make_nonce(0, 0) == 0
87
+ assert aa_bundler.make_nonce(0, 1) == 1
88
+ assert aa_bundler.make_nonce(1, 1) == (1 << 64) + 1
89
+
90
+
91
+ FAIL_IF_USED = object()
92
+
93
+
94
+ @patch.object(aa_bundler.random, "randint")
95
+ @patch.object(aa_bundler, "fetch_nonce")
96
+ def test_get_nonce_force_fetch(fetch_nonce_mock, randint_mock):
97
+ # Test fetch=True
98
+ fetch_nonce_mock.return_value = 123
99
+ assert aa_bundler.get_nonce_and_key(
100
+ FAIL_IF_USED,
101
+ {"nonceKey": 12, "from": TEST_SENDER},
102
+ nonce_mode=aa_bundler.NonceMode.FIXED_KEY_LOCAL_NONCE,
103
+ fetch=True,
104
+ ) == (12, 123)
105
+ fetch_nonce_mock.assert_called_once_with(FAIL_IF_USED, TEST_SENDER, ENTRYPOINT, 12)
106
+ randint_mock.assert_not_called()
107
+
108
+
109
+ @patch.object(aa_bundler.random, "randint")
110
+ @patch.object(aa_bundler, "fetch_nonce")
111
+ def test_get_nonce_fetch_always_mode(fetch_nonce_mock, randint_mock):
112
+ # Test nonce_mode=NonceMode.FIXED_KEY_FETCH_ALWAYS
113
+ fetch_nonce_mock.return_value = 111
114
+ assert aa_bundler.get_nonce_and_key(
115
+ FAIL_IF_USED,
116
+ {"nonceKey": 22, "from": TEST_SENDER},
117
+ nonce_mode=aa_bundler.NonceMode.FIXED_KEY_FETCH_ALWAYS,
118
+ ) == (22, 111)
119
+ fetch_nonce_mock.assert_called_once_with(FAIL_IF_USED, TEST_SENDER, ENTRYPOINT, 22)
120
+ randint_mock.assert_not_called()
121
+ fetch_nonce_mock.reset_mock()
122
+
123
+
124
+ @patch.object(aa_bundler.random, "randint")
125
+ @patch.object(aa_bundler, "fetch_nonce")
126
+ def test_get_nonce_nonce_key_in_tx(fetch_nonce_mock, randint_mock):
127
+ # Test nonce_mode=NonceMode.FIXED_KEY_LOCAL_NONCE
128
+ assert aa_bundler.get_nonce_and_key(
129
+ FAIL_IF_USED,
130
+ {"nonceKey": 22, "from": TEST_SENDER},
131
+ nonce_mode=aa_bundler.NonceMode.FIXED_KEY_LOCAL_NONCE,
132
+ ) == (22, 0)
133
+ randint_mock.assert_not_called()
134
+ fetch_nonce_mock.assert_not_called()
135
+
136
+ # Same if nonce_mode=NonceMode.RANDOM_KEY but nonceKey in the tx
137
+ assert aa_bundler.get_nonce_and_key(
138
+ FAIL_IF_USED,
139
+ {"nonceKey": 22, "from": TEST_SENDER},
140
+ nonce_mode=aa_bundler.NonceMode.RANDOM_KEY,
141
+ ) == (22, 0)
142
+ randint_mock.assert_not_called()
143
+ fetch_nonce_mock.assert_not_called()
144
+
145
+
146
+ @patch.object(aa_bundler.random, "randint")
147
+ @patch.object(aa_bundler, "fetch_nonce")
148
+ def test_get_nonce_random_key_mode(fetch_nonce_mock, randint_mock):
149
+ # If nonce_mode=NonceMode.RANDOM_KEY creates a random key and stores it
150
+ randint_mock.return_value = 444
151
+ assert aa_bundler.get_nonce_and_key(
152
+ FAIL_IF_USED,
153
+ {"from": TEST_SENDER},
154
+ nonce_mode=aa_bundler.NonceMode.RANDOM_KEY,
155
+ ) == (444, 0)
156
+ fetch_nonce_mock.assert_not_called()
157
+ randint_mock.assert_called_with(1, 2**192 - 1)
158
+ randint_mock.reset_mock()
159
+ assert aa_bundler.RANDOM_NONCE_KEY == 444
160
+ aa_bundler.RANDOM_NONCE_KEY = None # cleanup
161
+
162
+
163
+ @patch.object(aa_bundler.random, "randint")
164
+ @patch.object(aa_bundler, "fetch_nonce")
165
+ def test_get_nonce_with_local_cache(fetch_nonce_mock, randint_mock):
166
+ with patch.object(aa_bundler, "AA_BUNDLER_NONCE_KEY", new=55), patch.object(
167
+ aa_bundler, "NONCE_CACHE", new={55: 33}
168
+ ):
169
+ # Test nonce_mode=NonceMode.FIXED_KEY_LOCAL_NONCE
170
+ assert aa_bundler.get_nonce_and_key(
171
+ FAIL_IF_USED,
172
+ {"from": TEST_SENDER},
173
+ nonce_mode=aa_bundler.NonceMode.FIXED_KEY_LOCAL_NONCE,
174
+ ) == (55, 33)
175
+ randint_mock.assert_not_called()
176
+ fetch_nonce_mock.assert_not_called()
177
+
178
+
179
+ @patch.object(aa_bundler, "AA_BUNDLER_NONCE_MODE", new=aa_bundler.NonceMode.FIXED_KEY_LOCAL_NONCE)
180
+ @patch.object(aa_bundler, "get_base_fee")
181
+ def test_send_transaction(get_base_fee_mock):
182
+ get_base_fee_mock.return_value = 0
183
+ w3 = MagicMock()
184
+ w3.eth.chain_id = CHAIN_ID
185
+
186
+ aa_bundler.AA_BUNDLER_EXECUTOR_PK = TEST_PRIVATE_KEY
187
+
188
+ tx = {
189
+ "value": 0,
190
+ "chainId": 137,
191
+ "from": "0xE8B412158c205B0F605e0FC09dCdA27d3F140FE9",
192
+ "to": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
193
+ "data": "0x095ea7b30000000000000000000000007ace242f32208d836a2245df957c08547059bf45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", # noqa
194
+ }
195
+
196
+ def make_request(method, params):
197
+ if method == "eth_estimateUserOperationGas":
198
+ assert len(params) == 2
199
+ assert params[1] == ENTRYPOINT
200
+ assert params[0] == {
201
+ "sender": "0xE8B412158c205B0F605e0FC09dCdA27d3F140FE9",
202
+ "nonce": "0x0",
203
+ "callData": "0xb61d27f60000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000007ace242f32208d836a2245df957c08547059bf45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000", # noqa
204
+ "signature": "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c", # noqa
205
+ }
206
+ return {
207
+ "jsonrpc": "2.0",
208
+ "id": 1,
209
+ "result": {
210
+ "preVerificationGas": "0xb430",
211
+ "callGasLimit": "0xcbb8",
212
+ "verificationGasLimit": "0x13664",
213
+ "paymasterVerificationGasLimit": None,
214
+ },
215
+ }
216
+ elif method == "rundler_maxPriorityFeePerGas":
217
+ assert len(params) == 0
218
+ return {"jsonrpc": "2.0", "id": 1, "result": "0x7ffffffff"}
219
+ elif method == "eth_sendUserOperation":
220
+ assert len(params) == 2
221
+ assert params[1] == ENTRYPOINT
222
+ assert params[0] == {
223
+ "sender": "0xE8B412158c205B0F605e0FC09dCdA27d3F140FE9",
224
+ "nonce": "0x0",
225
+ "callData": "0xb61d27f60000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000007ace242f32208d836a2245df957c08547059bf45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000", # noqa
226
+ "callGasLimit": "0xcbb8",
227
+ "verificationGasLimit": "0x13664",
228
+ "preVerificationGas": "0xb430",
229
+ "maxFeePerGas": "0x7ffffffff",
230
+ "maxPriorityFeePerGas": "0x7ffffffff",
231
+ "signature": "0x7980544d044bc1202fed7edec96f2fa795ab8670b439935e6bbb5104e95d84ea32af8bff187913ff7eb2b442baab06d0c300273942e312332659ab0a194bbbe81c", # noqa
232
+ }
233
+ return {
234
+ "jsonrpc": "2.0",
235
+ "id": 1,
236
+ "result": "0xa950a17ca1ed83e974fb1aa227360a007cb65f566518af117ffdbb04d8d2d524",
237
+ }
238
+
239
+ w3.provider.make_request.side_effect = make_request
240
+
241
+ ret = aa_bundler.send_transaction(w3, tx)
242
+ get_base_fee_mock.assert_called_once_with(w3)
243
+ assert aa_bundler.NONCE_CACHE[0] == 1
244
+ assert ret == {"userOpHash": "0xa950a17ca1ed83e974fb1aa227360a007cb65f566518af117ffdbb04d8d2d524"}
@@ -4,12 +4,12 @@
4
4
 
5
5
  [tox]
6
6
  minversion = 3.15
7
- envlist = {py39,py310}, {py39,py310}-w3
7
+ envlist = {py39,py310}
8
8
 
9
9
  [gh-actions]
10
10
  python =
11
- 3.9: py39, py39-w3
12
- 3.10: py310, py310-w3
11
+ 3.9: py39
12
+ 3.10: py310
13
13
 
14
14
  [testenv]
15
15
  description = invoke pytest to run automated tests
@@ -20,26 +20,12 @@ setenv =
20
20
  passenv =
21
21
  HOME
22
22
  WADRAY_USE_GMPY2
23
- extras =
24
- testing
25
- commands =
26
- pytest {posargs}
27
-
28
-
29
- [testenv:{py39,py310}-w3]
30
- description = invoke pytest to run automated tests
31
- isolated_build = True
32
- setenv =
33
- TOXINIDIR = {toxinidir}
34
- TEST_ENV = web3py
35
- passenv =
36
- HOME
37
23
  W3_*
38
24
  TRANSACT_MODE
39
25
  WEB3_PROVIDER_URI
40
26
  WEB3_*
41
27
  extras =
42
- testing-w3
28
+ testing
43
29
  deps =
44
30
  warrant @ git+https://github.com/gnarvaja/warrant.git#egg=warrant
45
31
  commands =
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes