eth-prototype 1.3.5__tar.gz → 1.4.0__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 (71) hide show
  1. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/.github/workflows/publish.yaml +1 -1
  2. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/.github/workflows/test.yaml +1 -1
  3. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/.pre-commit-config.yaml +4 -4
  4. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/PKG-INFO +1 -1
  5. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/eth_prototype.egg-info/PKG-INFO +1 -1
  6. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/build_artifacts.py +4 -0
  7. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/contracts.py +132 -33
  8. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/test_build_artifacts.py +8 -0
  9. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/test_contracts.py +20 -4
  10. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tox.ini +2 -2
  11. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/.coveragerc +0 -0
  12. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/.gitignore +0 -0
  13. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/.isort.cfg +0 -0
  14. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/.readthedocs.yml +0 -0
  15. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/AUTHORS.rst +0 -0
  16. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/CHANGELOG.rst +0 -0
  17. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/LICENSE.txt +0 -0
  18. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/README.md +0 -0
  19. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/docs/Makefile +0 -0
  20. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/docs/_static/.gitignore +0 -0
  21. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/docs/authors.rst +0 -0
  22. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/docs/changelog.rst +0 -0
  23. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/docs/conf.py +0 -0
  24. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/docs/index.rst +0 -0
  25. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/docs/license.rst +0 -0
  26. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/docs/readme.rst +0 -0
  27. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/docs/requirements.txt +0 -0
  28. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/pyproject.toml +0 -0
  29. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/setup.cfg +0 -0
  30. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/setup.py +0 -0
  31. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/eth_prototype.egg-info/SOURCES.txt +0 -0
  32. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/eth_prototype.egg-info/dependency_links.txt +0 -0
  33. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/eth_prototype.egg-info/not-zip-safe +0 -0
  34. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/eth_prototype.egg-info/requires.txt +0 -0
  35. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/eth_prototype.egg-info/top_level.txt +0 -0
  36. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/__init__.py +0 -0
  37. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/aa_bundler.py +0 -0
  38. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/defender_relay.py +0 -0
  39. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/test_utils/__init__.py +0 -0
  40. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/test_utils/factories.py +0 -0
  41. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/test_utils/hardhat.py +0 -0
  42. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/test_utils/vcr_utils.py +0 -0
  43. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/w3wrappers.py +0 -0
  44. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/wadray.py +0 -0
  45. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/src/ethproto/wrappers.py +0 -0
  46. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/__init__.py +0 -0
  47. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/cassettes/test_aa_bundler/test_build_user_operation.yaml +0 -0
  48. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/conftest.py +0 -0
  49. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/README.md +0 -0
  50. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/artifacts2/TestCurrency.sol/TestCurrency.json +0 -0
  51. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/Count.sol +0 -0
  52. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/Counter.sol +0 -0
  53. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/CounterUpgradeable.sol +0 -0
  54. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/CounterUpgradeableWithLibrary.sol +0 -0
  55. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/CounterWithLibrary.sol +0 -0
  56. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/Datatypes.sol +0 -0
  57. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/EventLauncher.sol +0 -0
  58. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/TestCurrency.sol +0 -0
  59. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/TestCurrencyUUPS.sol +0 -0
  60. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/contracts/TestNFT.sol +0 -0
  61. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/hardhat.config.js +0 -0
  62. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/package-lock.json +0 -0
  63. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/package.json +0 -0
  64. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/verifiable-binaries/@anotherOrg/aPkg/1.0.2/build/contracts/TestCurrency.sol/TestCurrency.json +0 -0
  65. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/verifiable-binaries/@org/pkg/0.2.1/build/contracts/TestCurrency.json +0 -0
  66. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/hardhat-project/verifiable-binaries/@org/pkg/0.3.0/build/contracts/TestCurrency.json +0 -0
  67. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/test_aa_bundler.py +0 -0
  68. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/test_defender.py +0 -0
  69. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/test_time_control.py +0 -0
  70. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/test_w3.py +0 -0
  71. {eth_prototype-1.3.5 → eth_prototype-1.4.0}/tests/test_wadray.py +0 -0
@@ -24,7 +24,7 @@ jobs:
24
24
  - name: Set up Python
25
25
  uses: actions/setup-python@v3
26
26
  with:
27
- python-version: "3.9"
27
+ python-version: "3.12"
28
28
  - name: Install dependencies
29
29
  run: |
30
30
  python -m pip install --upgrade pip
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- python-version: ["3.9", "3.10"]
14
+ python-version: ["3.10", "3.12"]
15
15
 
16
16
  steps:
17
17
  - uses: actions/checkout@v3
@@ -2,7 +2,7 @@ exclude: "^docs/conf.py"
2
2
 
3
3
  repos:
4
4
  - repo: https://github.com/pre-commit/pre-commit-hooks
5
- rev: v4.1.0
5
+ rev: v4.4.0
6
6
  hooks:
7
7
  - id: trailing-whitespace
8
8
  - id: check-added-large-files
@@ -23,19 +23,19 @@ repos:
23
23
  - id: isort
24
24
 
25
25
  - repo: https://github.com/psf/black
26
- rev: stable
26
+ rev: 23.3.0
27
27
  hooks:
28
28
  - id: black
29
29
  language_version: python3
30
30
 
31
31
  - repo: https://github.com/PyCQA/flake8
32
- rev: 4.0.1
32
+ rev: 6.0.0
33
33
  hooks:
34
34
  - id: flake8
35
35
  ## You can add flake8 plugins via `additional_dependencies`:
36
36
  # additional_dependencies: [flake8-bugbear]
37
37
 
38
38
  - repo: https://github.com/zricethezav/gitleaks
39
- rev: v8.12.0
39
+ rev: v8.16.3
40
40
  hooks:
41
41
  - id: gitleaks-docker
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eth-prototype
3
- Version: 1.3.5
3
+ Version: 1.4.0
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.4
2
2
  Name: eth-prototype
3
- Version: 1.3.5
3
+ Version: 1.4.0
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
@@ -250,6 +250,10 @@ class ArtifactLibrary:
250
250
  Calling with contract_ref <ContractClass>@local is the same as calling get_artifact_by_name(<ContractClass>).
251
251
  """
252
252
  ref = self._find_ref(contract_ref)
253
+ if ref is None:
254
+ by_name = self.get_artifact_by_name(contract_ref)
255
+ if by_name is not None:
256
+ return by_name
253
257
 
254
258
  if ref is None:
255
259
  raise FileNotFoundError(f"Could not find artifact for {contract_ref} on {self.lookup_paths}")
@@ -4,6 +4,7 @@ from contextlib import contextmanager
4
4
  from decimal import Decimal
5
5
  from functools import wraps
6
6
 
7
+ from environs import Env
7
8
  from m9g import Model
8
9
  from m9g.fields import DictField, IntField, ListField, StringField, TupleField
9
10
 
@@ -13,11 +14,24 @@ __author__ = "Guillermo M. Narvaja"
13
14
  __copyright__ = "Guillermo M. Narvaja"
14
15
  __license__ = "MIT"
15
16
 
17
+ env = Env()
18
+
19
+ USE_CUSTOM_ERRORS = env.bool("USE_CUSTOM_ERRORS", False)
20
+
16
21
 
17
22
  class RevertError(Exception):
18
23
  pass
19
24
 
20
25
 
26
+ class RevertCustomError(RevertError):
27
+ def __init__(self, error, *args):
28
+ self.error = error
29
+ self.args = args
30
+
31
+ def __str__(self):
32
+ return f"{self.error}({', '.join(map(str, self.args))})"
33
+
34
+
21
35
  class WadField(IntField):
22
36
  FIELD_TYPE = Wad
23
37
 
@@ -144,9 +158,9 @@ class ROTransaction:
144
158
  def _on_end(self):
145
159
  while self.modified_contracts:
146
160
  contract = self.modified_contracts.pop()
147
- assert contract.serialize("pydict") == self.serialized_contracts[
148
- contract.contract_id
149
- ], f"Contract {contract.contract_id} modified in view"
161
+ assert (
162
+ contract.serialize("pydict") == self.serialized_contracts[contract.contract_id]
163
+ ), f"Contract {contract.contract_id} modified in view"
150
164
  del self.serialized_contracts[contract.contract_id]
151
165
 
152
166
  def archive(self):
@@ -198,10 +212,11 @@ def only_role(*roles):
198
212
  if self.has_role(role, self.running_as):
199
213
  break
200
214
  else:
201
- raise RevertError(f"AccessControl: account {self.running_as} is missing role {role}")
215
+ self._error("AccessControlUnauthorizedAccount", self.running_as, role)
202
216
  return method(self, *args, **kwargs)
203
217
 
204
218
  return inner
219
+
205
220
  return decorator
206
221
 
207
222
 
@@ -229,10 +244,14 @@ class Contract(Model):
229
244
  def __init__(self, contract_id=None, **kwargs):
230
245
  if contract_id is None:
231
246
  contract_id = f"{self.__class__.__name__}-{id(self)}"
247
+ self.use_custom_errors = kwargs.pop("use_custom_errors", USE_CUSTOM_ERRORS)
232
248
  super().__init__(contract_id=contract_id, **kwargs)
233
249
  self._versions = []
234
250
  self.manager.add_contract(self.contract_id, self)
235
251
 
252
+ def _error(self, error_class, *args) -> RevertError:
253
+ return RevertCustomError(error_class, *args)
254
+
236
255
  @contextmanager
237
256
  def as_(self, user):
238
257
  "Dummy as method to do the same with the wrapper"
@@ -272,11 +291,7 @@ class Contract(Model):
272
291
 
273
292
  class AccessControlContract(Contract):
274
293
  owner = AddressField(default="owner")
275
- roles = DictField(
276
- StringField(),
277
- TupleField((ListField(AddressField()), StringField())),
278
- default={}
279
- )
294
+ roles = DictField(StringField(), TupleField((ListField(AddressField()), StringField())), default={})
280
295
 
281
296
  set_attr_roles = {}
282
297
 
@@ -298,6 +313,14 @@ class AccessControlContract(Contract):
298
313
  self._running_as = self.owner
299
314
  self.roles[""] = ([self.owner], "") # Add owner as default_admin
300
315
 
316
+ def _error(self, error_class, *args) -> RevertError:
317
+ if error_class == "AccessControlUnauthorizedAccount":
318
+ if self.use_custom_errors:
319
+ return RevertCustomError(error_class, args[0], args[1])
320
+ else:
321
+ return RevertError(f"AccessControl: account {args[0]} is missing role {args[1]}")
322
+ return super()._error(error_class, *args)
323
+
301
324
  @contextmanager
302
325
  def _disable_role_validation(self):
303
326
  self._role_validation_disabled = True
@@ -320,8 +343,10 @@ class AccessControlContract(Contract):
320
343
  members, admin_role = self.roles[role]
321
344
  else:
322
345
  members, admin_role = [], ""
323
- require(self.has_role(admin_role, self._running_as),
324
- f"AccessControl: AccessControl: account {self._running_as} is missing role '{admin_role}'")
346
+ require(
347
+ self.has_role(admin_role, self._running_as),
348
+ self._error("AccessControlUnauthorizedAccount", self._running_as, admin_role),
349
+ )
325
350
 
326
351
  if user not in members:
327
352
  members.append(user)
@@ -337,13 +362,16 @@ class AccessControlContract(Contract):
337
362
  if attr_name in self.set_attr_roles:
338
363
  require(
339
364
  self.has_role(self.set_attr_roles[attr_name], self._running_as),
340
- f"AccessControl: AccessControl: account {self._running_as} is missing role "
341
- f"'{self.set_attr_roles[attr_name]}'"
365
+ self._error(
366
+ "AccessControlUnauthorizedAccount", self._running_as, self.set_attr_roles[attr_name]
367
+ ),
342
368
  )
343
369
 
344
370
 
345
371
  def require(condition, message=None):
346
372
  if not condition:
373
+ if isinstance(message, RevertError):
374
+ raise message
347
375
  raise RevertError(message or "required condition not met")
348
376
 
349
377
 
@@ -354,14 +382,43 @@ class ERC20Token(AccessControlContract):
354
382
  symbol = StringField(default="")
355
383
  decimals = IntField(default=18)
356
384
  balances = DictField(AddressField(), WadField(), default={})
357
- allowances = DictField(
358
- TupleField((AddressField(), AddressField())),
359
- WadField(),
360
- default={}
361
- )
385
+ allowances = DictField(TupleField((AddressField(), AddressField())), WadField(), default={})
362
386
 
363
387
  _total_supply = WadField(default=ZERO)
364
388
 
389
+ _arg_count_by_error = {
390
+ "ERC20InsufficientBalance": 3,
391
+ "ERC20InvalidSender": 1,
392
+ "ERC20InvalidReceiver": 1,
393
+ "ERC20InsufficientAllowance": 3,
394
+ "ERC20InvalidApprover": 1,
395
+ "ERC20InvalidSpender": 1,
396
+ }
397
+
398
+ _message_by_error = {
399
+ "ERC20InsufficientBalance": "ERC20: transfer amount exceeds balance",
400
+ "ERC20InvalidSender": "ERC20: transfer from the zero address",
401
+ "ERC20InvalidReceiver": "ERC20: transfer to the zero address",
402
+ "ERC20InsufficientAllowance": "ERC20: insufficient allowance",
403
+ "ERC20InvalidApprover": "ERC20: approve from the zero address",
404
+ "ERC20InvalidSpender": "ERC20: approve to the zero address",
405
+ }
406
+
407
+ def _error(self, error_class, *args) -> RevertError:
408
+ if self.use_custom_errors:
409
+ arg_count = self._arg_count_by_error.get(error_class, None)
410
+ if arg_count == 1:
411
+ return RevertCustomError(
412
+ error_class, args[0] if args else "0x0000000000000000000000000000000000000000"
413
+ )
414
+ elif arg_count is not None:
415
+ return RevertCustomError(error_class, *args[:arg_count])
416
+ else:
417
+ message = self._message_by_error.get(error_class, None)
418
+ if message is not None:
419
+ return RevertError(message)
420
+ return super()._error(error_class, *args)
421
+
365
422
  def __init__(self, **kwargs):
366
423
  if "initial_supply" in kwargs:
367
424
  initial_supply = kwargs.pop("initial_supply")
@@ -410,7 +467,7 @@ class ERC20Token(AccessControlContract):
410
467
  def _transfer(self, sender, recipient, amount):
411
468
  sender, recipient = self._parse_accounts(sender, recipient)
412
469
  if self.balance_of(sender) < amount:
413
- raise RevertError("ERC20: transfer amount exceeds balance")
470
+ raise self._error("ERC20InsufficientBalance", sender, self.balance_of(sender), amount)
414
471
  elif self.balances[sender] == amount:
415
472
  del self.balances[sender]
416
473
  else:
@@ -425,8 +482,8 @@ class ERC20Token(AccessControlContract):
425
482
 
426
483
  def _approve(self, owner, spender, amount):
427
484
  owner, spender = self._parse_accounts(owner, spender)
428
- require(owner is not None, "ERC20: approve from the zero address")
429
- require(spender is not None, "ERC20: approve to the zero address")
485
+ require(owner is not None, self._error("ERC20InvalidApprover"))
486
+ require(spender is not None, self._error("ERC20InvalidSpender", spender))
430
487
  if amount == self.ZERO:
431
488
  try:
432
489
  del self.allowances[(owner, spender)]
@@ -447,7 +504,7 @@ class ERC20Token(AccessControlContract):
447
504
  def decrease_allowance(self, sender, spender, amount):
448
505
  sender, spender = self._parse_accounts(sender, spender)
449
506
  allowance = self.allowances.get((sender, spender), self.ZERO)
450
- require(allowance >= amount, "ERC20: decreased allowance below zero")
507
+ require(allowance >= amount, self._error("ERC20InsufficientAllowance", spender, allowance, amount))
451
508
  self._approve(sender, spender, allowance - amount)
452
509
 
453
510
  @external
@@ -455,7 +512,7 @@ class ERC20Token(AccessControlContract):
455
512
  spender, sender, recipient = self._parse_accounts(spender, sender, recipient)
456
513
  allowance = self.allowances.get((sender, spender), self.ZERO)
457
514
  if allowance < amount:
458
- raise RevertError("ERC20: transfer amount exceeds allowance")
515
+ raise self._error("ERC20InsufficientAllowance", spender, allowance, amount)
459
516
  self._transfer(sender, recipient, amount)
460
517
  self._approve(sender, spender, allowance - amount)
461
518
  return True
@@ -464,7 +521,7 @@ class ERC20Token(AccessControlContract):
464
521
  return self._total_supply
465
522
 
466
523
 
467
- class ERC721Token(AccessControlContract): # NFT
524
+ class ERC721Token(AccessControlContract): # NFT
468
525
  ZERO = Wad(0)
469
526
 
470
527
  name = StringField()
@@ -477,13 +534,49 @@ class ERC721Token(AccessControlContract): # NFT
477
534
 
478
535
  _token_count = IntField(default=0)
479
536
 
537
+ _arg_count_by_error = {
538
+ "ERC721InvalidOwner": 1,
539
+ "ERC721NonexistentToken": 1,
540
+ "ERC721IncorrectOwner": 3,
541
+ "ERC721InvalidSender": 1,
542
+ "ERC721InvalidReceiver": 1,
543
+ "ERC721InsufficientApproval": 2,
544
+ }
545
+
546
+ _message_by_error = {
547
+ "ERC721InvalidOwner": "ERC721: address zero is not a valid owner",
548
+ "ERC721NonexistentToken": "ERC721: invalid token ID",
549
+ "ERC721IncorrectOwner": "ERC721: transfer from incorrect owner",
550
+ "ERC721InvalidSender": "ERC721: transfer from incorrect owner",
551
+ "ERC721InvalidReceiver": "ERC721: transfer to the zero address",
552
+ "ERC721InsufficientApproval": "ERC721: caller is not token owner nor approved",
553
+ }
554
+
555
+ def _error(self, error_class, *args) -> RevertError:
556
+ if self.use_custom_errors:
557
+ arg_count = self._arg_count_by_error.get(error_class, None)
558
+ if arg_count == 1:
559
+ return RevertCustomError(
560
+ error_class, args[0] if args else "0x0000000000000000000000000000000000000000"
561
+ )
562
+ elif arg_count is not None:
563
+ return RevertCustomError(error_class, *args[:arg_count])
564
+ else:
565
+ message = self._message_by_error.get(error_class, None)
566
+ if message is not None:
567
+ return RevertError(message)
568
+ return super()._error(error_class, *args)
569
+
480
570
  @external
481
571
  def mint(self, to, token_id):
482
572
  if token_id is None:
483
573
  self._token_count += 1
484
574
  token_id = self._token_count
485
575
  if token_id in self.owners:
486
- raise RevertError("ERC721: token already minted")
576
+ if self.use_custom_errors:
577
+ raise RevertError("ERC721: token already minted")
578
+ else:
579
+ raise self._error("ERC721InvalidSender")
487
580
  self.balances[to] = self.balances.get(to, 0) + 1
488
581
  self.owners[token_id] = to
489
582
 
@@ -503,7 +596,7 @@ class ERC721Token(AccessControlContract): # NFT
503
596
  @view
504
597
  def owner_of(self, token_id):
505
598
  if token_id not in self.owners:
506
- raise RevertError("ERC721: invalid token ID")
599
+ raise self._error("ERC721NonexistentToken", token_id)
507
600
  return self.owners[token_id]
508
601
 
509
602
  # def token_uri
@@ -536,23 +629,29 @@ class ERC721Token(AccessControlContract): # NFT
536
629
  @external
537
630
  def transfer_from(self, sender, from_, to, token_id):
538
631
  owner = self.owners[token_id]
539
- if sender != owner and self.token_approvals.get(token_id, None) != sender and \
540
- sender not in self.operator_approvals.get(owner, []):
541
- raise RevertError("ERC721: caller is not token owner or approved")
632
+ if (
633
+ sender != owner
634
+ and self.token_approvals.get(token_id, None) != sender
635
+ and sender not in self.operator_approvals.get(owner, [])
636
+ ):
637
+ raise self._error("ERC721InsufficientApproval", sender, token_id)
542
638
  return self._transfer(from_, to, token_id)
543
639
 
544
640
  @external
545
641
  def safe_transfer_from(self, sender, from_, to, token_id):
546
642
  owner = self.owners[token_id]
547
- if sender != owner and self.token_approvals.get(token_id, None) != sender and \
548
- sender not in self.operator_approvals.get(owner, []):
549
- raise RevertError("ERC721: caller is not token owner or approved")
643
+ if (
644
+ sender != owner
645
+ and self.token_approvals.get(token_id, None) != sender
646
+ and sender not in self.operator_approvals.get(owner, [])
647
+ ):
648
+ raise self._error("ERC721InsufficientApproval", sender, token_id)
550
649
  # TODO: if `to` is contract, call onERC721Received
551
650
  return self._transfer(from_, to, token_id)
552
651
 
553
652
  def _transfer(self, from_, to, token_id):
554
653
  if self.owners[token_id] != from_:
555
- raise RevertError("ERC721: transfer of token that is not own:")
654
+ raise self._error("ERC721InvalidOwner", from_)
556
655
  if token_id in self.token_approvals:
557
656
  del self.token_approvals[token_id]
558
657
  self.balances[from_] -= 1
@@ -125,6 +125,14 @@ def test_artifact_libraries_generator():
125
125
  assert libraries == []
126
126
 
127
127
 
128
+ def test_artifact_library_ref_lookup_fallback():
129
+ library = ArtifactLibrary(
130
+ os.path.join(HARDHAT_PROJECT, "artifacts", "contracts", "Counter.sol"),
131
+ )
132
+
133
+ assert library.get_artifact_by_ref("Counter") == library.get_artifact_by_name("Counter")
134
+
135
+
128
136
  def test_artifact_library_ref_lookup():
129
137
  library = ArtifactLibrary(
130
138
  os.path.join(HARDHAT_PROJECT, "artifacts"),
@@ -140,6 +140,8 @@ def _connected_contract_address(eth_wrapper_class, *args, **kwargs):
140
140
 
141
141
 
142
142
  ERC20TokenAlternatives = [ERC20Token]
143
+ ERC20TokenAlternatives.append(partial(ERC20Token, use_custom_errors=False))
144
+ ERC20TokenAlternatives.append(partial(ERC20Token, use_custom_errors=True))
143
145
 
144
146
  if "web3py" in TEST_ENV:
145
147
  ERC20TokenAlternatives.append(partial(TestCurrency, provider_key="w3"))
@@ -215,8 +217,13 @@ class TestERC20Token:
215
217
  token.transfer_from("Spender", "owner", "Giacomo", _W(300))
216
218
  assert token.allowance("owner", "Spender") == _W(0)
217
219
 
218
- with pytest.raises(RevertError):
219
- token.transfer_from("Spender", "owner", "Luca", _W(1))
220
+ with pytest.raises(RevertError, match="allowance|ERC20InsufficientAllowance"):
221
+ try:
222
+ token.transfer_from("Spender", "owner", "Luca", _W(1))
223
+ except RevertError as err:
224
+ if getattr(token, "use_custom_errors", False):
225
+ assert str(err).startswith("ERC20InsufficientAllowance(")
226
+ raise
220
227
 
221
228
  assert token.balance_of("Guillo") == _W(200)
222
229
  assert token.balance_of("owner") == _W(1500)
@@ -225,6 +232,8 @@ class TestERC20Token:
225
232
 
226
233
 
227
234
  ERC721TokenAlternatives = [ERC721Token]
235
+ ERC721TokenAlternatives.append(partial(ERC721Token, use_custom_errors=False))
236
+ ERC721TokenAlternatives.append(partial(ERC721Token, use_custom_errors=True))
228
237
  if "web3py" in TEST_ENV:
229
238
  ERC721TokenAlternatives.append(partial(TestNFT, provider_key="w3"))
230
239
 
@@ -242,7 +251,7 @@ class TestERC721Token:
242
251
  assert nft.owner_of(1235) == "CUST1"
243
252
  nft.burn("CUST1", 1235)
244
253
  assert nft.balance_of("CUST1") == 1
245
- with pytest.raises(RevertError, match="ERC721: invalid token ID"):
254
+ with pytest.raises(RevertError, match="ERC721: invalid token ID|ERC721NonexistentToken"):
246
255
  nft.owner_of(1235)
247
256
  nft.burn("CUST1", 1234)
248
257
  assert nft.balance_of("CUST1") == 0
@@ -297,5 +306,12 @@ class TestERC721Token:
297
306
  assert nft.balance_of("CUST2") == 2
298
307
  nft.set_approval_for_all("CUST1", "SPEND", False)
299
308
 
300
- with pytest.raises(RevertError, match="ERC721: caller is not token owner or approved"):
309
+ with pytest.raises(
310
+ RevertError,
311
+ match=(
312
+ "ERC721InsufficientApproval"
313
+ if getattr(nft, "use_custom_errors", False)
314
+ else "ERC721: caller is not token owner"
315
+ ),
316
+ ):
301
317
  nft.transfer_from("SPEND", "CUST1", "CUST2", 1235)
@@ -4,12 +4,12 @@
4
4
 
5
5
  [tox]
6
6
  minversion = 3.15
7
- envlist = {py39,py310}
7
+ envlist = {py310,py312}
8
8
 
9
9
  [gh-actions]
10
10
  python =
11
- 3.9: py39
12
11
  3.10: py310
12
+ 3.12: py312
13
13
 
14
14
  [testenv]
15
15
  description = invoke pytest to run automated tests
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes