eth-prototype 1.2.1b1__tar.gz → 1.3.0b2__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.0b2}/.github/workflows/test.yaml +0 -1
  2. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/PKG-INFO +7 -4
  3. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/setup.cfg +6 -3
  4. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/eth_prototype.egg-info/PKG-INFO +7 -4
  5. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/eth_prototype.egg-info/SOURCES.txt +6 -0
  6. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/eth_prototype.egg-info/requires.txt +6 -3
  7. eth_prototype-1.3.0b2/src/ethproto/aa_bundler.py +430 -0
  8. eth_prototype-1.3.0b2/src/ethproto/test_utils/__init__.py +0 -0
  9. eth_prototype-1.3.0b2/src/ethproto/test_utils/factories.py +70 -0
  10. eth_prototype-1.3.0b2/src/ethproto/test_utils/hardhat.py +71 -0
  11. eth_prototype-1.3.0b2/src/ethproto/test_utils/vcr_utils.py +23 -0
  12. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/ethproto/w3wrappers.py +4 -4
  13. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/ethproto/wrappers.py +1 -1
  14. eth_prototype-1.3.0b2/tests/__init__.py +0 -0
  15. eth_prototype-1.3.0b2/tests/cassettes/test_aa_bundler/test_build_user_operation.yaml +89 -0
  16. eth_prototype-1.3.0b2/tests/conftest.py +61 -0
  17. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/test_aa_bundler.py +161 -87
  18. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/test_contracts.py +2 -0
  19. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/test_time_control.py +2 -0
  20. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/test_w3.py +52 -6
  21. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tox.ini +4 -7
  22. eth_prototype-1.2.1b1/src/ethproto/aa_bundler.py +0 -298
  23. eth_prototype-1.2.1b1/tests/conftest.py +0 -42
  24. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/.coveragerc +0 -0
  25. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/.github/workflows/publish.yaml +0 -0
  26. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/.gitignore +0 -0
  27. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/.isort.cfg +0 -0
  28. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/.pre-commit-config.yaml +0 -0
  29. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/.readthedocs.yml +0 -0
  30. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/AUTHORS.rst +0 -0
  31. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/CHANGELOG.rst +0 -0
  32. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/LICENSE.txt +0 -0
  33. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/README.md +0 -0
  34. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/docs/Makefile +0 -0
  35. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/docs/_static/.gitignore +0 -0
  36. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/docs/authors.rst +0 -0
  37. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/docs/changelog.rst +0 -0
  38. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/docs/conf.py +0 -0
  39. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/docs/index.rst +0 -0
  40. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/docs/license.rst +0 -0
  41. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/docs/readme.rst +0 -0
  42. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/docs/requirements.txt +0 -0
  43. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/pyproject.toml +0 -0
  44. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/setup.py +0 -0
  45. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/eth_prototype.egg-info/dependency_links.txt +0 -0
  46. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/eth_prototype.egg-info/not-zip-safe +0 -0
  47. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/eth_prototype.egg-info/top_level.txt +0 -0
  48. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/ethproto/__init__.py +0 -0
  49. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/ethproto/build_artifacts.py +0 -0
  50. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/ethproto/contracts.py +0 -0
  51. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/ethproto/defender_relay.py +0 -0
  52. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/src/ethproto/wadray.py +0 -0
  53. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/README.md +0 -0
  54. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/artifacts2/TestCurrency.sol/TestCurrency.json +0 -0
  55. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/Count.sol +0 -0
  56. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/Counter.sol +0 -0
  57. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/CounterUpgradeable.sol +0 -0
  58. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/CounterUpgradeableWithLibrary.sol +0 -0
  59. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/CounterWithLibrary.sol +0 -0
  60. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/Datatypes.sol +0 -0
  61. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/EventLauncher.sol +0 -0
  62. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/TestCurrency.sol +0 -0
  63. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/TestCurrencyUUPS.sol +0 -0
  64. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/contracts/TestNFT.sol +0 -0
  65. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/hardhat.config.js +0 -0
  66. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/package-lock.json +0 -0
  67. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/hardhat-project/package.json +0 -0
  68. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/test_build_artifacts.py +0 -0
  69. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/tests/test_defender.py +0 -0
  70. {eth_prototype-1.2.1b1 → eth_prototype-1.3.0b2}/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.0b2
4
4
  Summary: Prototype Ethereum Smart Contracts in Python
5
5
  Home-page: https://github.com/gnarvaja/eth-prototype
6
6
  Author: Guillermo M. Narvaja
@@ -26,12 +26,15 @@ Requires-Dist: boto3; extra == "defender"
26
26
  Provides-Extra: gmpy2
27
27
  Requires-Dist: gmpy2; extra == "gmpy2"
28
28
  Provides-Extra: testing
29
- Requires-Dist: setuptools; extra == "testing"
30
- Requires-Dist: pytest; extra == "testing"
29
+ Requires-Dist: boto3; extra == "testing"
30
+ Requires-Dist: factory-boy; extra == "testing"
31
31
  Requires-Dist: gmpy2; extra == "testing"
32
+ Requires-Dist: pytest; extra == "testing"
32
33
  Requires-Dist: pytest-cov; extra == "testing"
34
+ Requires-Dist: pytest-mock; extra == "testing"
35
+ Requires-Dist: pytest-recording; extra == "testing"
36
+ Requires-Dist: setuptools; extra == "testing"
33
37
  Requires-Dist: web3[tester]==7.*; extra == "testing"
34
- Requires-Dist: boto3; extra == "testing"
35
38
 
36
39
  # eth-prototype
37
40
 
@@ -41,12 +41,15 @@ defender =
41
41
  gmpy2 =
42
42
  gmpy2
43
43
  testing =
44
- setuptools
45
- pytest
44
+ boto3
45
+ factory-boy
46
46
  gmpy2
47
+ pytest
47
48
  pytest-cov
49
+ pytest-mock
50
+ pytest-recording
51
+ setuptools
48
52
  web3[tester]==7.*
49
- boto3
50
53
 
51
54
  [options.entry_points]
52
55
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eth-prototype
3
- Version: 1.2.1b1
3
+ Version: 1.3.0b2
4
4
  Summary: Prototype Ethereum Smart Contracts in Python
5
5
  Home-page: https://github.com/gnarvaja/eth-prototype
6
6
  Author: Guillermo M. Narvaja
@@ -26,12 +26,15 @@ Requires-Dist: boto3; extra == "defender"
26
26
  Provides-Extra: gmpy2
27
27
  Requires-Dist: gmpy2; extra == "gmpy2"
28
28
  Provides-Extra: testing
29
- Requires-Dist: setuptools; extra == "testing"
30
- Requires-Dist: pytest; extra == "testing"
29
+ Requires-Dist: boto3; extra == "testing"
30
+ Requires-Dist: factory-boy; extra == "testing"
31
31
  Requires-Dist: gmpy2; extra == "testing"
32
+ Requires-Dist: pytest; extra == "testing"
32
33
  Requires-Dist: pytest-cov; extra == "testing"
34
+ Requires-Dist: pytest-mock; extra == "testing"
35
+ Requires-Dist: pytest-recording; extra == "testing"
36
+ Requires-Dist: setuptools; extra == "testing"
33
37
  Requires-Dist: web3[tester]==7.*; extra == "testing"
34
- Requires-Dist: boto3; extra == "testing"
35
38
 
36
39
  # eth-prototype
37
40
 
@@ -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
@@ -13,12 +13,15 @@ boto3
13
13
  gmpy2
14
14
 
15
15
  [testing]
16
- setuptools
17
- pytest
16
+ boto3
17
+ factory-boy
18
18
  gmpy2
19
+ pytest
19
20
  pytest-cov
21
+ pytest-mock
22
+ pytest-recording
23
+ setuptools
20
24
  web3[tester]==7.*
21
- boto3
22
25
 
23
26
  [web3]
24
27
  web3==7.*
@@ -0,0 +1,430 @@
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
+ from web3.types import TxParams
18
+
19
+ from .contracts import RevertError
20
+
21
+ env = Env()
22
+
23
+ AA_BUNDLER_URL = env.str("AA_BUNDLER_URL", env.str("WEB3_PROVIDER_URI", "http://localhost:8545"))
24
+ AA_BUNDLER_SENDER = env.str("AA_BUNDLER_SENDER", None)
25
+ AA_BUNDLER_ENTRYPOINT = env.str("AA_BUNDLER_ENTRYPOINT", "0x0000000071727De22E5E9d8BAf0edAc6f37da032")
26
+ AA_BUNDLER_EXECUTOR_PK = env.str("AA_BUNDLER_EXECUTOR_PK", None)
27
+ AA_BUNDLER_PROVIDER = env.str("AA_BUNDLER_PROVIDER", "alchemy")
28
+ AA_BUNDLER_GAS_LIMIT_FACTOR = env.float("AA_BUNDLER_GAS_LIMIT_FACTOR", 1)
29
+ AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR", 1)
30
+ AA_BUNDLER_BASE_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_BASE_GAS_PRICE_FACTOR", 1)
31
+ AA_BUNDLER_VERIFICATION_GAS_FACTOR = env.float("AA_BUNDLER_VERIFICATION_GAS_FACTOR", 1)
32
+
33
+ NonceMode = Enum(
34
+ "NonceMode",
35
+ [
36
+ "RANDOM_KEY", # first time initializes a random key and increments nonce locally with calling the blockchain
37
+ "RANDOM_KEY_EVERYTIME", # initializes a random key every time and increments nonce locally
38
+ "FIXED_KEY_LOCAL_NONCE", # uses a fixed key, keeps nonce locally and fetches the nonce when receiving
39
+ # 'AA25 invalid account nonce'
40
+ "FIXED_KEY_FETCH_ALWAYS", # uses a fixed key, always fetches unless received as parameter
41
+ ],
42
+ )
43
+
44
+ AA_BUNDLER_NONCE_MODE = env.enum("AA_BUNDLER_NONCE_MODE", default="FIXED_KEY_LOCAL_NONCE", type=NonceMode)
45
+ AA_BUNDLER_NONCE_KEY = env.int("AA_BUNDLER_NONCE_KEY", 0)
46
+ AA_BUNDLER_MAX_GETNONCE_RETRIES = env.int("AA_BUNDLER_MAX_GETNONCE_RETRIES", 3)
47
+
48
+
49
+ GET_NONCE_ABI = [
50
+ {
51
+ "inputs": [
52
+ {"internalType": "address", "name": "sender", "type": "address"},
53
+ {"internalType": "uint192", "name": "key", "type": "uint192"},
54
+ ],
55
+ "name": "getNonce",
56
+ "outputs": [{"internalType": "uint256", "name": "nonce", "type": "uint256"}],
57
+ "stateMutability": "view",
58
+ "type": "function",
59
+ }
60
+ ]
61
+
62
+ NONCE_CACHE = defaultdict(lambda: 0)
63
+ RANDOM_NONCE_KEY = local()
64
+
65
+ DUMMY_SIGNATURE = HexBytes(
66
+ "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007"
67
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"
68
+ )
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class UserOpEstimation:
73
+ """eth_estimateUserOperationGas response"""
74
+
75
+ pre_verification_gas: int
76
+ verification_gas_limit: int
77
+ call_gas_limit: int
78
+ paymaster_verification_gas_limit: int
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class GasPrice:
83
+ max_priority_fee_per_gas: int
84
+ max_fee_per_gas: int
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class Tx:
89
+ target: HexAddress
90
+ data: HexBytes
91
+ value: int
92
+
93
+ nonce_key: HexBytes = None
94
+ nonce: int = None
95
+ from_: HexAddress = ADDRESS_ZERO
96
+ chain_id: int = None
97
+
98
+ @classmethod
99
+ def from_tx_params(cls, params: TxParams) -> "Tx":
100
+ return cls(
101
+ target=params["to"],
102
+ data=HexBytes(params["data"]),
103
+ value=params["value"],
104
+ from_=params.get("from", ADDRESS_ZERO),
105
+ chain_id=params.get("chainId", None),
106
+ )
107
+
108
+ def as_execute_args(self):
109
+ return [self.target, self.value, self.data]
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class UserOperation:
114
+ EXECUTE_ARG_TYPES = ["address", "uint256", "bytes"]
115
+ EXECUTE_SELECTOR = function_signature_to_4byte_selector(f"execute({','.join(EXECUTE_ARG_TYPES)})")
116
+
117
+ sender: HexBytes
118
+ nonce: int
119
+ call_data: HexBytes
120
+
121
+ max_fee_per_gas: int = 0
122
+ max_priority_fee_per_gas: int = 0
123
+
124
+ call_gas_limit: int = 0
125
+ verification_gas_limit: int = 0
126
+ pre_verification_gas: int = 0
127
+
128
+ signature: HexBytes = DUMMY_SIGNATURE
129
+
130
+ init_code: HexBytes = HexBytes("0x")
131
+ paymaster_and_data: HexBytes = HexBytes("0x")
132
+
133
+ @classmethod
134
+ def from_tx(cls, tx: Tx, nonce):
135
+ return cls(
136
+ sender=get_sender(tx),
137
+ nonce=nonce,
138
+ call_data=add_0x_prefix(
139
+ (cls.EXECUTE_SELECTOR + encode(cls.EXECUTE_ARG_TYPES, tx.as_execute_args())).hex()
140
+ ),
141
+ )
142
+
143
+ def as_reduced_dict(self):
144
+ return {
145
+ "sender": self.sender,
146
+ "nonce": "0x%x" % self.nonce,
147
+ "callData": self.call_data,
148
+ "signature": add_0x_prefix(self.signature.hex()),
149
+ }
150
+
151
+ def as_dict(self):
152
+ return {
153
+ "sender": self.sender,
154
+ "nonce": "0x%x" % self.nonce,
155
+ "callData": self.call_data,
156
+ "callGasLimit": "0x%x" % self.call_gas_limit,
157
+ "verificationGasLimit": "0x%x" % self.verification_gas_limit,
158
+ "preVerificationGas": "0x%x" % self.pre_verification_gas,
159
+ "maxPriorityFeePerGas": "0x%x" % self.max_priority_fee_per_gas,
160
+ "maxFeePerGas": "0x%x" % self.max_fee_per_gas,
161
+ "signature": add_0x_prefix(self.signature.hex()),
162
+ }
163
+
164
+ def add_estimation(self, estimation: UserOpEstimation) -> "UserOperation":
165
+ return replace(
166
+ self,
167
+ call_gas_limit=estimation.call_gas_limit,
168
+ verification_gas_limit=estimation.verification_gas_limit,
169
+ pre_verification_gas=estimation.pre_verification_gas,
170
+ )
171
+
172
+ def add_gas_price(self, gas_price: GasPrice) -> "UserOperation":
173
+ return replace(
174
+ self,
175
+ max_priority_fee_per_gas=gas_price.max_priority_fee_per_gas,
176
+ max_fee_per_gas=gas_price.max_fee_per_gas,
177
+ )
178
+
179
+ def sign(self, private_key: HexBytes, chain_id, entrypoint) -> "UserOperation":
180
+ signature = Account.sign_message(
181
+ encode_defunct(
182
+ hexstr=PackedUserOperation.from_user_operation(self)
183
+ .hash_full(chain_id=chain_id, entrypoint=entrypoint)
184
+ .hex()
185
+ ),
186
+ private_key,
187
+ )
188
+ return replace(self, signature=signature.signature)
189
+
190
+
191
+ @dataclass(frozen=True)
192
+ class PackedUserOperation:
193
+ sender: HexBytes
194
+ nonce: int
195
+ call_data: HexBytes
196
+
197
+ account_gas_limits: HexBytes
198
+ pre_verification_gas: int
199
+ gas_fees: HexBytes
200
+
201
+ init_code: HexBytes = HexBytes("0x")
202
+ paymaster_and_data: HexBytes = HexBytes("0x")
203
+ signature: HexBytes = HexBytes("0x")
204
+
205
+ @classmethod
206
+ def from_user_operation(cls, user_operation: UserOperation):
207
+ return cls(
208
+ sender=user_operation.sender,
209
+ nonce=user_operation.nonce,
210
+ call_data=user_operation.call_data,
211
+ account_gas_limits=pack_two(user_operation.verification_gas_limit, user_operation.call_gas_limit),
212
+ pre_verification_gas=user_operation.pre_verification_gas,
213
+ gas_fees=pack_two(user_operation.max_priority_fee_per_gas, user_operation.max_fee_per_gas),
214
+ init_code=user_operation.init_code,
215
+ paymaster_and_data=user_operation.paymaster_and_data,
216
+ signature=user_operation.signature,
217
+ )
218
+
219
+ def hash(self):
220
+ # https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/UserOperationLib.sol#L54
221
+ hash_init_code = Web3.solidity_keccak(["bytes"], [self.init_code])
222
+ hash_call_data = Web3.solidity_keccak(["bytes"], [self.call_data])
223
+ hash_paymaster_and_data = Web3.solidity_keccak(["bytes"], [self.paymaster_and_data])
224
+ return Web3.keccak(
225
+ hexstr=encode(
226
+ ["address", "uint256", "bytes32", "bytes32", "bytes32", "uint256", "bytes32", "bytes32"],
227
+ [
228
+ self.sender,
229
+ self.nonce,
230
+ hash_init_code,
231
+ hash_call_data,
232
+ HexBytes(self.account_gas_limits),
233
+ self.pre_verification_gas,
234
+ HexBytes(self.gas_fees),
235
+ hash_paymaster_and_data,
236
+ ],
237
+ ).hex()
238
+ )
239
+
240
+ def hash_full(self, chain_id, entrypoint):
241
+ return Web3.keccak(
242
+ hexstr=encode(
243
+ ["bytes32", "address", "uint256"],
244
+ [self.hash(), entrypoint, chain_id],
245
+ ).hex()
246
+ )
247
+
248
+
249
+ def pack_two(a, b):
250
+ a = HexBytes(a).hex()
251
+ b = HexBytes(b).hex()
252
+ return "0x" + a.zfill(32) + b.zfill(32)
253
+
254
+
255
+ def _to_uint(x):
256
+ if isinstance(x, str):
257
+ return int(x, 16)
258
+ elif isinstance(x, int):
259
+ return x
260
+ raise RuntimeError(f"Invalid int value {x}")
261
+
262
+
263
+ def make_nonce(nonce_key, nonce):
264
+ nonce_key = _to_uint(nonce_key)
265
+ nonce = _to_uint(nonce)
266
+ return (nonce_key << 64) | nonce
267
+
268
+
269
+ def fetch_nonce(w3, account, entrypoint, nonce_key):
270
+ ep = w3.eth.contract(abi=GET_NONCE_ABI, address=entrypoint)
271
+ return ep.functions.getNonce(account, nonce_key).call()
272
+
273
+
274
+ def get_random_nonce_key(force=False):
275
+ if force or getattr(RANDOM_NONCE_KEY, "key", None) is None:
276
+ RANDOM_NONCE_KEY.key = random.randint(1, 2**192 - 1)
277
+ return RANDOM_NONCE_KEY.key
278
+
279
+
280
+ def consume_nonce(nonce_key, nonce):
281
+ NONCE_CACHE[nonce_key] = max(NONCE_CACHE[nonce_key], nonce + 1)
282
+
283
+
284
+ def check_nonce_error(resp, retry_nonce):
285
+ """Returns the next nonce if resp contains a nonce error and retries weren't exhausted
286
+ Raises RevertError otherwise
287
+ """
288
+ if "AA25" in resp["error"]["message"] and AA_BUNDLER_MAX_GETNONCE_RETRIES > 0:
289
+ # Retry fetching the nonce
290
+ if retry_nonce == AA_BUNDLER_MAX_GETNONCE_RETRIES:
291
+ raise RevertError(resp["error"]["message"])
292
+ warn(f'{resp["error"]["message"]} error, I will retry fetching the nonce')
293
+ return (retry_nonce or 0) + 1
294
+ else:
295
+ raise RevertError(resp["error"]["message"])
296
+
297
+
298
+ def get_sender(tx):
299
+ if tx.from_ == ADDRESS_ZERO:
300
+ if AA_BUNDLER_SENDER is None:
301
+ raise RuntimeError("Must define AA_BUNDLER_SENDER or send 'from' in the TX")
302
+ return AA_BUNDLER_SENDER
303
+ else:
304
+ return tx.from_
305
+
306
+
307
+ class Bundler:
308
+ def __init__(
309
+ self,
310
+ w3: Web3,
311
+ bundler_url: str = AA_BUNDLER_URL,
312
+ bundler_type: str = AA_BUNDLER_PROVIDER,
313
+ entrypoint: HexAddress = AA_BUNDLER_ENTRYPOINT,
314
+ nonce_mode: NonceMode = AA_BUNDLER_NONCE_MODE,
315
+ fixed_nonce_key: int = AA_BUNDLER_NONCE_KEY,
316
+ verification_gas_factor: float = AA_BUNDLER_VERIFICATION_GAS_FACTOR,
317
+ gas_limit_factor: float = AA_BUNDLER_GAS_LIMIT_FACTOR,
318
+ priority_gas_price_factor: float = AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR,
319
+ base_gas_price_factor: float = AA_BUNDLER_BASE_GAS_PRICE_FACTOR,
320
+ executor_pk: HexBytes = AA_BUNDLER_EXECUTOR_PK,
321
+ ):
322
+ self.w3 = w3
323
+ self.bundler = Web3(Web3.HTTPProvider(bundler_url), middleware=[])
324
+ self.bundler_type = bundler_type
325
+ self.entrypoint = entrypoint
326
+ self.nonce_mode = nonce_mode
327
+ self.fixed_nonce_key = fixed_nonce_key
328
+ self.verification_gas_factor = verification_gas_factor
329
+ self.gas_limit_factor = gas_limit_factor
330
+ self.priority_gas_price_factor = priority_gas_price_factor
331
+ self.base_gas_price_factor = base_gas_price_factor
332
+ self.executor_pk = executor_pk
333
+
334
+ def __str__(self):
335
+ return (
336
+ f"Bundler(type={self.bundler_type}, entrypoint={self.entrypoint}, nonce_mode={self.nonce_mode}"
337
+ f"fixed_nonce_key={self.fixed_nonce_key}, verification_gas_factor={self.verification_gas_factor},"
338
+ f"gas_limit_factor={self.gas_limit_factor}, priority_gas_price_factor={self.priority_gas_price_factor},"
339
+ f"base_gas_price_factor={self.base_gas_price_factor})"
340
+ )
341
+
342
+ def get_nonce_and_key(self, tx: Tx, fetch=False):
343
+ nonce_key = tx.nonce_key
344
+ nonce = tx.nonce
345
+
346
+ if nonce_key is None:
347
+ if self.nonce_mode == NonceMode.RANDOM_KEY:
348
+ nonce_key = get_random_nonce_key()
349
+ elif self.nonce_mode == NonceMode.RANDOM_KEY_EVERYTIME:
350
+ nonce_key = get_random_nonce_key(force=True)
351
+ else:
352
+ nonce_key = self.fixed_nonce_key
353
+
354
+ if nonce is None:
355
+ if fetch or self.nonce_mode == NonceMode.FIXED_KEY_FETCH_ALWAYS:
356
+ nonce = fetch_nonce(self.w3, get_sender(tx), self.entrypoint, nonce_key)
357
+ else:
358
+ nonce = NONCE_CACHE[nonce_key]
359
+ return nonce_key, nonce
360
+
361
+ def get_base_fee(self):
362
+ blk = self.w3.eth.get_block("latest")
363
+ return int(_to_uint(blk["baseFeePerGas"]) * self.base_gas_price_factor)
364
+
365
+ def estimate_user_operation_gas(self, user_operation: UserOperation) -> UserOpEstimation:
366
+ resp = self.bundler.provider.make_request(
367
+ "eth_estimateUserOperationGas", [user_operation.as_reduced_dict(), self.entrypoint]
368
+ )
369
+ if "error" in resp:
370
+ raise RevertError(resp["error"]["message"])
371
+
372
+ paymaster_verification_gas_limit = resp["result"].get("paymasterVerificationGasLimit", "0x00")
373
+ return UserOpEstimation(
374
+ pre_verification_gas=int(resp["result"].get("preVerificationGas", "0x00"), 16),
375
+ verification_gas_limit=int(
376
+ int(resp["result"].get("verificationGasLimit", "0x00"), 16) * self.verification_gas_factor
377
+ ),
378
+ call_gas_limit=int(int(resp["result"].get("callGasLimit", "0x00"), 16) * self.gas_limit_factor),
379
+ paymaster_verification_gas_limit=(
380
+ int(paymaster_verification_gas_limit, 16)
381
+ if paymaster_verification_gas_limit is not None
382
+ else 0
383
+ ),
384
+ )
385
+
386
+ def alchemy_gas_price(self):
387
+ resp = self.bundler.provider.make_request("rundler_maxPriorityFeePerGas", [])
388
+ if "error" in resp:
389
+ raise RevertError(resp["error"]["message"])
390
+ max_priority_fee_per_gas = int(int(resp["result"], 16) * self.priority_gas_price_factor)
391
+ max_fee_per_gas = max_priority_fee_per_gas + self.get_base_fee()
392
+
393
+ return GasPrice(max_priority_fee_per_gas=max_priority_fee_per_gas, max_fee_per_gas=max_fee_per_gas)
394
+
395
+ def build_user_operation(self, tx: Tx, retry_nonce=None) -> UserOperation:
396
+ nonce_key, nonce = self.get_nonce_and_key(tx, fetch=retry_nonce is not None)
397
+ # Consume the nonce, even if the userop may fail later
398
+ consume_nonce(nonce_key, nonce)
399
+
400
+ user_operation = UserOperation.from_tx(tx, make_nonce(nonce_key, nonce))
401
+ estimation = self.estimate_user_operation_gas(user_operation)
402
+
403
+ user_operation = user_operation.add_estimation(estimation)
404
+
405
+ if self.bundler_type == "alchemy":
406
+ gas_price = self.alchemy_gas_price()
407
+ user_operation = user_operation.add_gas_price(gas_price)
408
+
409
+ elif self.bundler_type == "generic":
410
+ # At the moment generic just prices the gas at 0
411
+ pass
412
+
413
+ else:
414
+ warn(f"Unknown bundler_type: {self.bundler_type}")
415
+
416
+ return user_operation
417
+
418
+ def send_transaction(self, tx: Tx, retry_nonce=None):
419
+ user_operation = self.build_user_operation(tx, retry_nonce).sign(
420
+ self.executor_pk, tx.chain_id, self.entrypoint
421
+ )
422
+
423
+ resp = self.bundler.provider.make_request(
424
+ "eth_sendUserOperation", [user_operation.as_dict(), self.entrypoint]
425
+ )
426
+ if "error" in resp:
427
+ next_nonce = check_nonce_error(resp, retry_nonce)
428
+ return self.send_transaction(tx, retry_nonce=next_nonce)
429
+
430
+ 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