eth-prototype 1.3.0b2__py3-none-any.whl → 1.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: eth-prototype
3
- Version: 1.3.0b2
3
+ Version: 1.3.2
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
@@ -35,6 +35,7 @@ Requires-Dist: pytest-mock; extra == "testing"
35
35
  Requires-Dist: pytest-recording; extra == "testing"
36
36
  Requires-Dist: setuptools; extra == "testing"
37
37
  Requires-Dist: web3[tester]==7.*; extra == "testing"
38
+ Dynamic: license-file
38
39
 
39
40
  # eth-prototype
40
41
 
@@ -0,0 +1,18 @@
1
+ eth_prototype-1.3.2.dist-info/licenses/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
2
+ eth_prototype-1.3.2.dist-info/licenses/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
3
+ ethproto/__init__.py,sha256=YWkAFysBp4tZjLWWB2FFmp5yG23pUYhQvgQW9b3soXs,579
4
+ ethproto/aa_bundler.py,sha256=yl-kgLtjIi4ratPquD0B71QtA29SNAwOiIOXNLvxCOk,16780
5
+ ethproto/build_artifacts.py,sha256=whIXEqnh5f89UYu4Cb3KDigGV7juUCbDnfZkg-SYMKA,9878
6
+ ethproto/contracts.py,sha256=rNVbCK1hURy7lWKhzSdXgVWo3wx9O_Ghk-6PfgOsRNk,18662
7
+ ethproto/defender_relay.py,sha256=05A8TfRZwiBhCpo924Pf9CjfKSir2Wvgg1p_asFxJbw,1777
8
+ ethproto/w3wrappers.py,sha256=aA5yQ25d01s8gtWkGSxSEZlVRdq6JM1ceS3cfvSj4uM,22614
9
+ ethproto/wadray.py,sha256=JBsu5KcyU9k70bDK03T2IY6qPVFO30WbYPhwrAHdXao,8262
10
+ ethproto/wrappers.py,sha256=Mj2sgZmcLVmqsnNab6PqIXtNMMPyRVvUj2_8ButEd4w,17304
11
+ ethproto/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ ethproto/test_utils/factories.py,sha256=G8DnUDG_yThRxMTCkymzcjm9lR_ni0_ZmTsb3sEfIdI,1805
13
+ ethproto/test_utils/hardhat.py,sha256=HzTqIznu6zVd_-doL96ftFJ235ktDCQen1QDQbNuwfM,2361
14
+ ethproto/test_utils/vcr_utils.py,sha256=1FH2sgJlElSjWkJLuO3C7E2J-4HKyFvjAqkCnGRZJyk,797
15
+ eth_prototype-1.3.2.dist-info/METADATA,sha256=vkIpd-qbMF_GtzUe4TW2_rwV9ypTQqxDW3V9uDhtIR8,2650
16
+ eth_prototype-1.3.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
17
+ eth_prototype-1.3.2.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
18
+ eth_prototype-1.3.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
ethproto/aa_bundler.py CHANGED
@@ -14,7 +14,7 @@ from eth_utils import add_0x_prefix, function_signature_to_4byte_selector
14
14
  from hexbytes import HexBytes
15
15
  from web3 import Web3
16
16
  from web3.constants import ADDRESS_ZERO
17
- from web3.types import TxParams
17
+ from web3.types import StateOverride, TxParams
18
18
 
19
19
  from .contracts import RevertError
20
20
 
@@ -30,6 +30,8 @@ AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_PRIORITY_GAS_PRICE_
30
30
  AA_BUNDLER_BASE_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_BASE_GAS_PRICE_FACTOR", 1)
31
31
  AA_BUNDLER_VERIFICATION_GAS_FACTOR = env.float("AA_BUNDLER_VERIFICATION_GAS_FACTOR", 1)
32
32
 
33
+ AA_BUNDLER_STATE_OVERRIDES = env.json("AA_BUNDLER_STATE_OVERRIDES", default={})
34
+
33
35
  NonceMode = Enum(
34
36
  "NonceMode",
35
37
  [
@@ -41,7 +43,7 @@ NonceMode = Enum(
41
43
  ],
42
44
  )
43
45
 
44
- AA_BUNDLER_NONCE_MODE = env.enum("AA_BUNDLER_NONCE_MODE", default="FIXED_KEY_LOCAL_NONCE", type=NonceMode)
46
+ AA_BUNDLER_NONCE_MODE = env.enum("AA_BUNDLER_NONCE_MODE", default="FIXED_KEY_LOCAL_NONCE", enum=NonceMode)
45
47
  AA_BUNDLER_NONCE_KEY = env.int("AA_BUNDLER_NONCE_KEY", 0)
46
48
  AA_BUNDLER_MAX_GETNONCE_RETRIES = env.int("AA_BUNDLER_MAX_GETNONCE_RETRIES", 3)
47
49
 
@@ -68,6 +70,16 @@ DUMMY_SIGNATURE = HexBytes(
68
70
  )
69
71
 
70
72
 
73
+ class BundlerRevertError(RevertError):
74
+ """Bundler specific revert error"""
75
+
76
+ def __init__(self, message, userop=None, response=None):
77
+ super().__init__(message)
78
+ self.message = message
79
+ self.userop = userop
80
+ self.response = response
81
+
82
+
71
83
  @dataclass(frozen=True)
72
84
  class UserOpEstimation:
73
85
  """eth_estimateUserOperationGas response"""
@@ -288,11 +300,11 @@ def check_nonce_error(resp, retry_nonce):
288
300
  if "AA25" in resp["error"]["message"] and AA_BUNDLER_MAX_GETNONCE_RETRIES > 0:
289
301
  # Retry fetching the nonce
290
302
  if retry_nonce == AA_BUNDLER_MAX_GETNONCE_RETRIES:
291
- raise RevertError(resp["error"]["message"])
303
+ raise BundlerRevertError(resp["error"]["message"], response=resp)
292
304
  warn(f'{resp["error"]["message"]} error, I will retry fetching the nonce')
293
305
  return (retry_nonce or 0) + 1
294
306
  else:
295
- raise RevertError(resp["error"]["message"])
307
+ raise BundlerRevertError(resp["error"]["message"], response=resp)
296
308
 
297
309
 
298
310
  def get_sender(tx):
@@ -318,6 +330,7 @@ class Bundler:
318
330
  priority_gas_price_factor: float = AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR,
319
331
  base_gas_price_factor: float = AA_BUNDLER_BASE_GAS_PRICE_FACTOR,
320
332
  executor_pk: HexBytes = AA_BUNDLER_EXECUTOR_PK,
333
+ overrides: StateOverride = AA_BUNDLER_STATE_OVERRIDES,
321
334
  ):
322
335
  self.w3 = w3
323
336
  self.bundler = Web3(Web3.HTTPProvider(bundler_url), middleware=[])
@@ -331,11 +344,15 @@ class Bundler:
331
344
  self.base_gas_price_factor = base_gas_price_factor
332
345
  self.executor_pk = executor_pk
333
346
 
347
+ # stateOverrideSet mapping to use when calling eth_estimateUserOperationGas
348
+ # https://docs.alchemy.com/reference/eth-estimateuseroperationgas
349
+ self.overrides = overrides
350
+
334
351
  def __str__(self):
335
352
  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},"
353
+ f"Bundler(type={self.bundler_type}, entrypoint={self.entrypoint}, nonce_mode={self.nonce_mode}, "
354
+ f"fixed_nonce_key={self.fixed_nonce_key}, verification_gas_factor={self.verification_gas_factor}, "
355
+ f"gas_limit_factor={self.gas_limit_factor}, priority_gas_price_factor={self.priority_gas_price_factor}, "
339
356
  f"base_gas_price_factor={self.base_gas_price_factor})"
340
357
  )
341
358
 
@@ -364,10 +381,11 @@ class Bundler:
364
381
 
365
382
  def estimate_user_operation_gas(self, user_operation: UserOperation) -> UserOpEstimation:
366
383
  resp = self.bundler.provider.make_request(
367
- "eth_estimateUserOperationGas", [user_operation.as_reduced_dict(), self.entrypoint]
384
+ "eth_estimateUserOperationGas",
385
+ [user_operation.as_reduced_dict(), self.entrypoint, self.overrides],
368
386
  )
369
387
  if "error" in resp:
370
- raise RevertError(resp["error"]["message"])
388
+ raise BundlerRevertError(resp["error"]["message"], user_operation, resp)
371
389
 
372
390
  paymaster_verification_gas_limit = resp["result"].get("paymasterVerificationGasLimit", "0x00")
373
391
  return UserOpEstimation(
@@ -386,7 +404,7 @@ class Bundler:
386
404
  def alchemy_gas_price(self):
387
405
  resp = self.bundler.provider.make_request("rundler_maxPriorityFeePerGas", [])
388
406
  if "error" in resp:
389
- raise RevertError(resp["error"]["message"])
407
+ raise BundlerRevertError(resp["error"]["message"], response=resp)
390
408
  max_priority_fee_per_gas = int(int(resp["result"], 16) * self.priority_gas_price_factor)
391
409
  max_fee_per_gas = max_priority_fee_per_gas + self.get_base_fee()
392
410
 
@@ -424,7 +442,14 @@ class Bundler:
424
442
  "eth_sendUserOperation", [user_operation.as_dict(), self.entrypoint]
425
443
  )
426
444
  if "error" in resp:
427
- next_nonce = check_nonce_error(resp, retry_nonce)
445
+ try:
446
+ next_nonce = check_nonce_error(resp, retry_nonce)
447
+ except BundlerRevertError as e:
448
+ raise BundlerRevertError(
449
+ e.message,
450
+ userop=user_operation,
451
+ response=e.response,
452
+ )
428
453
  return self.send_transaction(tx, retry_nonce=next_nonce)
429
454
 
430
455
  return {"userOpHash": resp["result"]}
@@ -6,10 +6,14 @@ import os.path
6
6
  import re
7
7
  from dataclasses import dataclass
8
8
  from pathlib import Path
9
- from typing import Union, Tuple
9
+ from typing import Tuple, Union
10
10
 
11
11
  LIBRARY_PLACEHOLDER_MATCHER = re.compile(r"__\$[0-9a-f]{34}\$__")
12
12
 
13
+ CONTRACT_REF_MATCHER = re.compile(r"^(?:(?P<package>.*)/)?(?P<contract>[^@]+)(?:@(?P<version>.+))?$")
14
+
15
+ VERSION_MATCHER = re.compile(r"^\d+\.\d+\.\d+$")
16
+
13
17
 
14
18
  @dataclass
15
19
  class Artifact:
@@ -94,6 +98,7 @@ class ArtifactLibrary:
94
98
  self.lookup_paths = [Path(p).absolute() for p in paths]
95
99
  self._fullpath_cache = {}
96
100
  self._name_cache = {}
101
+ self._ref_cache = None
97
102
 
98
103
  def get_artifact(self, contract: str) -> Artifact:
99
104
  """Returns a build artifact by full contract path
@@ -139,3 +144,115 @@ class ArtifactLibrary:
139
144
  raise FileNotFoundError(f"Could not find artifact for {contract_name} on {self.lookup_paths}")
140
145
 
141
146
  return self._name_cache[contract_name]
147
+
148
+ def _load_ref_cache(self):
149
+ self._ref_cache = {}
150
+ for path in self.lookup_paths:
151
+ path = Path(path)
152
+ json_files = path.rglob("*.json")
153
+
154
+ for json_file in json_files:
155
+ # Skip build-info and debug files
156
+ if json_file.parent.stem == "build-info" or json_file.stem.endswith(".dbg"):
157
+ continue
158
+
159
+ parts = json_file.parts[len(path.parts) - 1 :]
160
+
161
+ # Find the first parent named either "build" or "artifacts"
162
+ try:
163
+ build_idx = next(i for i, part in enumerate(parts) if part in ("build", "artifacts"))
164
+ except StopIteration:
165
+ continue
166
+
167
+ if build_idx == 0:
168
+ # Our lookup path is already a build directory, we're probably looking at a hardhat build output
169
+ version = "local"
170
+ else:
171
+ # If we have a version number right before the build directory, we're probably looking at a
172
+ # verifiable binaries directory
173
+ version = parts[build_idx - 1]
174
+ if not VERSION_MATCHER.match(version):
175
+ version = "local"
176
+
177
+ if version == "local":
178
+ # For hardhat output dir, package is everything up to "contracts"
179
+ try:
180
+ package = "/".join(parts[build_idx + 1 : parts.index("contracts")])
181
+ except ValueError:
182
+ # There are exceptions, like @openzeppelin/contracts-upgradeable
183
+ # For those the package is everythin up to the contract directory
184
+ package = "/".join(parts[build_idx + 1 : -2])
185
+
186
+ else:
187
+ # For verifiable binaries dir, package is everything up to version
188
+ package = "/".join(parts[1 : build_idx - 1])
189
+
190
+ if json_file.stem not in self._ref_cache:
191
+ self._ref_cache[json_file.stem] = []
192
+ self._ref_cache[json_file.stem].append(
193
+ {
194
+ "path": json_file,
195
+ "package": package,
196
+ "version": version,
197
+ }
198
+ )
199
+
200
+ def _find_ref(self, contract_ref: str):
201
+ if self._ref_cache is None:
202
+ self._load_ref_cache()
203
+
204
+ ref_match = CONTRACT_REF_MATCHER.match(contract_ref)
205
+ if not ref_match:
206
+ raise ValueError(f"Invalid contract reference: {contract_ref}")
207
+
208
+ contract = ref_match.group("contract")
209
+ if contract not in self._ref_cache:
210
+ return None
211
+
212
+ ref = self._ref_cache[contract]
213
+
214
+ package = ref_match.group("package")
215
+ if package:
216
+ ref = [r for r in ref if r["package"] == package]
217
+
218
+ version = ref_match.group("version")
219
+ if version:
220
+ ref = [r for r in ref if r["version"] == version]
221
+
222
+ if not ref:
223
+ return None
224
+
225
+ # Sort by version ascending, forcing local to be last
226
+ ref = sorted(
227
+ ref,
228
+ key=lambda x: (
229
+ x["version"] == "local",
230
+ tuple(map(int, x["version"].split("."))) if x["version"] != "local" else None,
231
+ ),
232
+ )[-1]
233
+
234
+ return ref
235
+
236
+ def get_artifact_by_ref(self, contract_ref: str) -> Artifact:
237
+ """Returns a build artifact by looking for a matching contract reference.
238
+
239
+ This is compatible with the verifiable binaries structure.
240
+
241
+ Accepts the following kind of references:
242
+
243
+ - <ContractClass>
244
+ - <ContractClass>@<version>
245
+ - <package>/<ContractClass>
246
+ - <package>/<ContractClass>@<version>
247
+
248
+ If version is not specified it uses the latest version available.
249
+
250
+ Calling with contract_ref <ContractClass>@local is the same as calling get_artifact_by_name(<ContractClass>).
251
+ """
252
+ ref = self._find_ref(contract_ref)
253
+
254
+ if ref is None:
255
+ raise FileNotFoundError(f"Could not find artifact for {contract_ref} on {self.lookup_paths}")
256
+
257
+ with open(ref["path"]) as f:
258
+ return Artifact(**json.load(f))
ethproto/w3wrappers.py CHANGED
@@ -5,9 +5,10 @@ from typing import Iterator, List, Union
5
5
  from environs import Env
6
6
  from eth_account.account import Account, LocalAccount
7
7
  from eth_account.signers.base import BaseAccount
8
- from eth_utils.abi import event_abi_to_log_topic
8
+ from eth_utils import add_0x_prefix, event_abi_to_log_topic
9
9
  from hexbytes import HexBytes
10
10
  from web3.contract import Contract
11
+ from web3.contract.contract import ContractEvent
11
12
  from web3.exceptions import ContractLogicError, ExtraDataLengthError
12
13
  from web3.middleware import ExtraDataToPOAMiddleware
13
14
 
@@ -437,7 +438,7 @@ class W3Provider(BaseProvider):
437
438
  self.tx_kwargs = tx_kwargs or {}
438
439
 
439
440
  def get_contract_def(self, eth_contract):
440
- return self.artifact_library.get_artifact_by_name(eth_contract)
441
+ return self.artifact_library.get_artifact_by_ref(eth_contract)
441
442
 
442
443
  def get_contract_factory(self, eth_contract):
443
444
  contract_def = self.get_contract_def(eth_contract)
@@ -448,7 +449,9 @@ class W3Provider(BaseProvider):
448
449
  kwargs["from"] = from_
449
450
  return self.construct(factory, init_params, kwargs)
450
451
 
451
- def get_events(self, eth_wrapper, event_name, filter_kwargs={}):
452
+ def get_events(
453
+ self, eth_wrapper, event_names: Union[list[Union[str, ContractEvent]], str], filter_kwargs=None
454
+ ):
452
455
  """Returns a list of events given a filter, like this:
453
456
 
454
457
  >>> provider.get_events(currencywrapper, "Transfer", dict(from_block=0))
@@ -468,12 +471,36 @@ class W3Provider(BaseProvider):
468
471
  'blockNumber': 23
469
472
  })]
470
473
  """
471
- contract = eth_wrapper.contract
472
- event = getattr(contract.events, event_name)
473
- if "from_block" not in filter_kwargs:
474
- filter_kwargs["from_block"] = self.get_first_block(eth_wrapper)
475
- event_filter = event.create_filter(**filter_kwargs)
476
- return event_filter.get_all_entries()
474
+ if filter_kwargs is None:
475
+ filter_kwargs = {}
476
+
477
+ if isinstance(event_names, (str, ContractEvent)):
478
+ # Backwards compatibility, if we don't get a list we're getting a single event name/ref
479
+ event_names = [event_names]
480
+
481
+ topics = {}
482
+
483
+ for name in event_names:
484
+ if isinstance(name, str):
485
+ # We got a plain event name, let's get the event from the contract
486
+ event: ContractEvent = getattr(eth_wrapper.contract.events, name)
487
+ else:
488
+ # Assume we already got an event reference
489
+ event: ContractEvent = name
490
+
491
+ topics[event.topic] = event
492
+
493
+ filter_params = {
494
+ "fromBlock": filter_kwargs.get("from_block", self.get_first_block(eth_wrapper)),
495
+ "toBlock": filter_kwargs.get("to_block", "latest"),
496
+ "address": eth_wrapper.contract.address,
497
+ "topics": [list(topics.keys())],
498
+ }
499
+
500
+ logs = self.w3.eth.get_logs(filter_params)
501
+
502
+ parsed_events = [topics[add_0x_prefix(log["topics"][0].hex())].process_log(log) for log in logs]
503
+ return parsed_events
477
504
 
478
505
  def init_eth_wrapper(self, eth_wrapper, owner, init_params, kwargs):
479
506
  eth_wrapper.owner = self.address_book.get_account(owner)
@@ -1,18 +0,0 @@
1
- ethproto/__init__.py,sha256=YWkAFysBp4tZjLWWB2FFmp5yG23pUYhQvgQW9b3soXs,579
2
- ethproto/aa_bundler.py,sha256=HupCu7fRCwlE556WXDqytRU5GEFLdBoFYzjpqDHBtE4,15789
3
- ethproto/build_artifacts.py,sha256=xwCd5hJUHP82IA-y3sSfX6fV15kjCGtV19RxNRcoor0,5441
4
- ethproto/contracts.py,sha256=rNVbCK1hURy7lWKhzSdXgVWo3wx9O_Ghk-6PfgOsRNk,18662
5
- ethproto/defender_relay.py,sha256=05A8TfRZwiBhCpo924Pf9CjfKSir2Wvgg1p_asFxJbw,1777
6
- ethproto/w3wrappers.py,sha256=lmyfJLhQmPYrclmbzzsthH2cShlQb6LwavKq30jqxFE,21651
7
- ethproto/wadray.py,sha256=JBsu5KcyU9k70bDK03T2IY6qPVFO30WbYPhwrAHdXao,8262
8
- ethproto/wrappers.py,sha256=Mj2sgZmcLVmqsnNab6PqIXtNMMPyRVvUj2_8ButEd4w,17304
9
- ethproto/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- ethproto/test_utils/factories.py,sha256=G8DnUDG_yThRxMTCkymzcjm9lR_ni0_ZmTsb3sEfIdI,1805
11
- ethproto/test_utils/hardhat.py,sha256=HzTqIznu6zVd_-doL96ftFJ235ktDCQen1QDQbNuwfM,2361
12
- ethproto/test_utils/vcr_utils.py,sha256=1FH2sgJlElSjWkJLuO3C7E2J-4HKyFvjAqkCnGRZJyk,797
13
- eth_prototype-1.3.0b2.dist-info/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
14
- eth_prototype-1.3.0b2.dist-info/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
15
- eth_prototype-1.3.0b2.dist-info/METADATA,sha256=VGLInjZc46w_OraflEsO2vG9yXmA_crJL0DVP5dFLvM,2630
16
- eth_prototype-1.3.0b2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
17
- eth_prototype-1.3.0b2.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
18
- eth_prototype-1.3.0b2.dist-info/RECORD,,