eth-prototype 0.7.5__py3-none-any.whl → 1.0.0__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
1
  Metadata-Version: 2.1
2
2
  Name: eth-prototype
3
- Version: 0.7.5
3
+ Version: 1.0.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
@@ -11,15 +11,14 @@ Platform: any
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Programming Language :: Python
13
13
  Requires-Python: >=3.7
14
- Description-Content-Type: text/x-rst; charset=UTF-8
14
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
15
  License-File: LICENSE.txt
16
16
  License-File: AUTHORS.rst
17
17
  Requires-Dist: m9g
18
18
  Requires-Dist: environs
19
19
  Requires-Dist: requests
20
+ Requires-Dist: hexbytes
20
21
  Requires-Dist: importlib-metadata ; python_version < "3.8"
21
- Provides-Extra: brownie
22
- Requires-Dist: eth-brownie ; extra == 'brownie'
23
22
  Provides-Extra: defender
24
23
  Requires-Dist: boto3 ; extra == 'defender'
25
24
  Provides-Extra: gmpy2
@@ -29,74 +28,46 @@ Requires-Dist: setuptools ; extra == 'testing'
29
28
  Requires-Dist: pytest ; extra == 'testing'
30
29
  Requires-Dist: gmpy2 ; extra == 'testing'
31
30
  Requires-Dist: pytest-cov ; extra == 'testing'
32
- Provides-Extra: testing-br
33
- Requires-Dist: eth-brownie ; extra == 'testing-br'
34
- Requires-Dist: setuptools ; extra == 'testing-br'
35
- Requires-Dist: pytest ; extra == 'testing-br'
36
- Requires-Dist: pytest-cov ; extra == 'testing-br'
37
31
  Provides-Extra: testing-w3
38
32
  Requires-Dist: web3[tester] ; extra == 'testing-w3'
39
33
  Requires-Dist: setuptools ; extra == 'testing-w3'
40
34
  Requires-Dist: pytest ; extra == 'testing-w3'
41
- Requires-Dist: eth-event ; extra == 'testing-w3'
42
35
  Requires-Dist: pytest-cov ; extra == 'testing-w3'
43
36
  Requires-Dist: boto3 ; extra == 'testing-w3'
44
37
  Provides-Extra: web3
45
38
  Requires-Dist: web3 ; extra == 'web3'
46
- Requires-Dist: eth-event ; extra == 'web3'
47
39
 
48
- =============
49
- eth-prototype
50
- =============
40
+ # eth-prototype
51
41
 
52
42
 
53
43
  Prototype Ethereum Smart Contracts in Python
54
44
 
55
45
 
56
- Description
57
- ===========
46
+ ## Description
58
47
 
59
48
  Library with base classes to prototype Ethereum Smart Contracts in Python. This includes:
60
49
 
61
50
  - wadray: classes for fixed number of decimals math implemented with integers.
62
51
  - contracts: classes to simulate contracts in Python with features like *rollback* on exception, external
63
52
  methods, views. Also classes for common contracts like ERC20 (tokens), ERC721 (NFTs) and AccessControl.
64
- - brwrappers: classes to wrap ethereum contracts called thru [brownie](https://github.com/eth-brownie/brownie/) but with a pythonic interface
65
53
  - w3wrappers: classes to wrap ethereum contracts called thru [web3py](https://web3py.readthedocs.io/) but with a pythonic interface
66
54
 
55
+ To use the `defender_relay` module you need to have the `warrant` package from this repo: https://github.com/gnarvaja/warrant. Add it to your requirements.txt as:
67
56
 
68
- Tox Tests
69
- =========
57
+ ```
58
+ warrant @ git+https://github.com/gnarvaja/warrant.git#egg=warrant
59
+ ```
70
60
 
71
- The tox tests run in three variants:
61
+ Note that using the `warrant` package from pypi will not work because of incompatibilities with newer python versions.
72
62
 
73
- - `default`: only uses and tests the prototype libraries, no blockchain.
74
- - `default-w3`: users and tests two variants: prototype and w3wrappers (wrappers using web3py).
75
- - `default-br`: users and tests two variants: prototype and brwrappers (wrappers using brownie).
76
-
77
- It's not possible for now running all the tests together because of incompatibilities between brownie and web3[tester].
78
-
79
-
80
- To run the tox `default-br` tests, you need an environment with Brownie, SOLC and other requirements.
63
+ ## Tox Tests
81
64
 
82
- You can do it using a Docker image an a few commands
65
+ The tox tests run in two variants:
83
66
 
84
- .. code-block:: bash
85
-
86
- docker run -it -v $PWD:/code -w /code gnarvaja/eth-dev:1.0.0 bash
87
- gnarvaja/eth-dev:eth-proto-brownie
88
- pip install tox
89
- brownie pm install OpenZeppelin/openzeppelin-contracts@4.3.2
90
- brownie pm install OpenZeppelin/openzeppelin-contracts-upgradeable@4.3.2
91
- tox -e py39-br
92
-
93
- docker run -it -v $PWD:/code -w /code gnarvaja/eth-dev:eth-proto-brownie bash
94
- tox -e py39-br
95
-
96
- .. _pyscaffold-notes:
67
+ - `default`: only uses and tests the prototype libraries, no blockchain.
68
+ - `default-w3`: users and tests two variants: prototype and w3wrappers (wrappers using web3py).
97
69
 
98
- Note
99
- ====
70
+ # Note
100
71
 
101
72
  This project has been set up using PyScaffold 4.0.2. For details and usage
102
73
  information on PyScaffold see https://pyscaffold.org/.
@@ -0,0 +1,13 @@
1
+ ethproto/__init__.py,sha256=YWkAFysBp4tZjLWWB2FFmp5yG23pUYhQvgQW9b3soXs,579
2
+ ethproto/build_artifacts.py,sha256=FzgUO6ierhCVBKHRxTkgrRx4AaAL6G5ql5Yzx_oTTmg,5578
3
+ ethproto/contracts.py,sha256=rNVbCK1hURy7lWKhzSdXgVWo3wx9O_Ghk-6PfgOsRNk,18662
4
+ ethproto/defender_relay.py,sha256=05A8TfRZwiBhCpo924Pf9CjfKSir2Wvgg1p_asFxJbw,1777
5
+ ethproto/w3wrappers.py,sha256=Z4REzEE56pv2IYbV1cNIh6xfYj24oE1llmI9Nyh0wgs,20445
6
+ ethproto/wadray.py,sha256=JBsu5KcyU9k70bDK03T2IY6qPVFO30WbYPhwrAHdXao,8262
7
+ ethproto/wrappers.py,sha256=gUB4ml9jFZNXbmOnUJH2OdGz7G7qxHS9si7a64DpCTE,17448
8
+ eth_prototype-1.0.0.dist-info/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
9
+ eth_prototype-1.0.0.dist-info/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
10
+ eth_prototype-1.0.0.dist-info/METADATA,sha256=cW-aeuxBJW1QpvCQ6tZgAUN_Qm9oXAqrADsfC3pT7TU,2661
11
+ eth_prototype-1.0.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
12
+ eth_prototype-1.0.0.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
13
+ eth_prototype-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,152 @@
1
+ """Helper classes to use hardhat build artifacts from python"""
2
+
3
+
4
+ import json
5
+ import os
6
+ import os.path
7
+ import re
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Union, Tuple
11
+
12
+ LIBRARY_PLACEHOLDER_MATCHER = re.compile(r"__\$[0-9a-f]{34}\$__")
13
+
14
+
15
+ @dataclass
16
+ class Artifact:
17
+ contract_name: str
18
+ abi: list
19
+ bytecode: str
20
+ deployed_bytecode: str
21
+ link_references: dict
22
+ deployed_link_references: dict
23
+
24
+ def __init__(self, **kwargs):
25
+ self.contract_name = kwargs["contractName"]
26
+ self.abi = kwargs["abi"]
27
+ self.bytecode = kwargs["bytecode"]
28
+ self.deployed_bytecode = kwargs["deployedBytecode"]
29
+ self.link_references = kwargs["linkReferences"]
30
+ self.deployed_link_references = kwargs["deployedLinkReferences"]
31
+
32
+ def link(self, libraries: dict) -> "Artifact":
33
+ """Returns a new artifact with the external libraries linked
34
+
35
+ Libraries is a dictionary of the form {library_name: address}
36
+ """
37
+ bytecode = self._replace_link_references(
38
+ self.bytecode, self.link_references, libraries
39
+ )
40
+ deployed_bytecode = self._replace_link_references(
41
+ self.deployed_bytecode, self.deployed_link_references, libraries
42
+ )
43
+ return Artifact(
44
+ contractName=self.contract_name,
45
+ abi=self.abi,
46
+ bytecode=bytecode,
47
+ deployedBytecode=deployed_bytecode,
48
+ linkReferences=self.link_references,
49
+ deployedLinkReferences=self.deployed_link_references,
50
+ )
51
+
52
+ def libraries(self) -> Tuple[str, str]:
53
+ """Generates a tuple of (library, source) for each library reference in the artifact"""
54
+ for source, libs in self.link_references.items():
55
+ for lib in libs.keys():
56
+ yield lib, source
57
+
58
+ def _replace_link_references(
59
+ self, bytecode: str, link_references: dict, libraries: dict
60
+ ) -> str:
61
+ # remove 0x prefix if present
62
+ bytecode = bytecode[2:] if bytecode.startswith("0x") else bytecode
63
+
64
+ for libs in link_references.values():
65
+ for lib, lib_refs in libs.items():
66
+ try:
67
+ address = libraries[lib]
68
+ except KeyError:
69
+ raise ValueError(f"Missing library address for {lib}")
70
+ address = address[2:] if address.startswith("0x") else address
71
+
72
+ assert len(address) == 40 # Sanity check
73
+
74
+ for ref in lib_refs:
75
+ # 2 nibbles -> 1 byte
76
+ start = ref["start"] * 2
77
+ length = ref["length"] * 2
78
+
79
+ # Sanity check
80
+ assert LIBRARY_PLACEHOLDER_MATCHER.match(
81
+ bytecode[start : start + length]
82
+ ), f"Unexpected placeholder at position {start}: {bytecode[start:start + length]}"
83
+
84
+ # Replace the placeholder with the actual address
85
+ bytecode = bytecode[:start] + address + bytecode[start + length :]
86
+
87
+ # Return value always has 0x prefix
88
+ return "0x" + bytecode
89
+
90
+ def __str__(self):
91
+ return f"Artifact({self.contract_name})"
92
+
93
+ def __repr__(self):
94
+ return f"Artifact({self.contract_name})"
95
+
96
+
97
+ class ArtifactLibrary:
98
+ def __init__(self, *paths: Tuple[Union[str, Path]]):
99
+ self.lookup_paths = [Path(p).absolute() for p in paths]
100
+ self._fullpath_cache = {}
101
+ self._name_cache = {}
102
+
103
+ def get_artifact(self, contract: str) -> Artifact:
104
+ """Returns a build artifact by full contract path
105
+
106
+ This method is compatible with hardhat's artifact structure.
107
+
108
+ Examples:
109
+
110
+ >>> library = ArtifactLibrary("./artifacts")
111
+ >>> counter_build = library.get_artifact("contracts/Counter.sol")
112
+ >>> proxy_build = library.get_artifact("@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol")
113
+ """
114
+ contract = Path(os.path.normpath(contract))
115
+
116
+ if contract not in self._fullpath_cache:
117
+ for path in self.lookup_paths:
118
+ build_artifact_path = (
119
+ path / contract / contract.with_suffix(".json").name
120
+ )
121
+ if build_artifact_path.exists():
122
+ with open(build_artifact_path) as f:
123
+ self._fullpath_cache[contract] = Artifact(**json.load(f))
124
+
125
+ if contract not in self._fullpath_cache:
126
+ raise FileNotFoundError(
127
+ f"Could not find artifact for {contract} on {self.lookup_paths}"
128
+ )
129
+
130
+ return self._fullpath_cache[contract]
131
+
132
+ def get_artifact_by_name(self, contract_name: str) -> Artifact:
133
+ """Returns a build artifact by looking for a matching contract name
134
+
135
+ Example:
136
+
137
+ >>> library = ArtifactLibrary("./artifacts")
138
+ >>> counter_build = library.get_artifact_by_name("Counter")
139
+ """
140
+ if contract_name not in self._name_cache:
141
+ for path in self.lookup_paths:
142
+ for dirpath, _, filenames in os.walk(path):
143
+ if f"{contract_name}.json" in filenames:
144
+ with open(Path(dirpath) / f"{contract_name}.json") as f:
145
+ self._name_cache[contract_name] = Artifact(**json.load(f))
146
+
147
+ 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
+ )
151
+
152
+ return self._name_cache[contract_name]
ethproto/contracts.py CHANGED
@@ -1,11 +1,13 @@
1
1
  import os
2
2
  import time
3
+ from contextlib import contextmanager
3
4
  from decimal import Decimal
4
5
  from functools import wraps
5
- from contextlib import contextmanager
6
+
6
7
  from m9g import Model
7
- from m9g.fields import IntField, DictField, StringField, TupleField, ListField
8
- from .wadray import Wad, Ray
8
+ from m9g.fields import DictField, IntField, ListField, StringField, TupleField
9
+
10
+ from .wadray import Ray, Wad
9
11
 
10
12
  __author__ = "Guillermo M. Narvaja"
11
13
  __copyright__ = "Guillermo M. Narvaja"
@@ -501,7 +503,7 @@ class ERC721Token(AccessControlContract): # NFT
501
503
  @view
502
504
  def owner_of(self, token_id):
503
505
  if token_id not in self.owners:
504
- raise RevertError("ERC721: owner query for nonexistent token")
506
+ raise RevertError("ERC721: invalid token ID")
505
507
  return self.owners[token_id]
506
508
 
507
509
  # def token_uri
@@ -536,7 +538,7 @@ class ERC721Token(AccessControlContract): # NFT
536
538
  owner = self.owners[token_id]
537
539
  if sender != owner and self.token_approvals.get(token_id, None) != sender and \
538
540
  sender not in self.operator_approvals.get(owner, []):
539
- raise RevertError("ERC721: transfer caller is not owner nor approved")
541
+ raise RevertError("ERC721: caller is not token owner or approved")
540
542
  return self._transfer(from_, to, token_id)
541
543
 
542
544
  @external
@@ -544,7 +546,7 @@ class ERC721Token(AccessControlContract): # NFT
544
546
  owner = self.owners[token_id]
545
547
  if sender != owner and self.token_approvals.get(token_id, None) != sender and \
546
548
  sender not in self.operator_approvals.get(owner, []):
547
- raise RevertError("ERC721: transfer caller is not owner nor approved")
549
+ raise RevertError("ERC721: caller is not token owner or approved")
548
550
  # TODO: if `to` is contract, call onERC721Received
549
551
  return self._transfer(from_, to, token_id)
550
552
 
ethproto/w3wrappers.py CHANGED
@@ -1,15 +1,27 @@
1
1
  import os
2
- import json
3
- from .contracts import RevertError
4
- from .wrappers import (
5
- ETHCall, AddressBook, MAXUINT256, ETHWrapper, SKIP_PROXY, register_provider, BaseProvider
6
- )
2
+ from collections import defaultdict
3
+ from typing import Iterator, List, Union
4
+
7
5
  from environs import Env
8
6
  from eth_account.account import Account, LocalAccount
9
7
  from eth_account.signers.base import BaseAccount
8
+ from eth_utils.abi import event_abi_to_log_topic
9
+ from hexbytes import HexBytes
10
+ from web3.contract import Contract
10
11
  from web3.exceptions import ExtraDataLengthError
11
12
  from web3.middleware import geth_poa_middleware
12
- from eth_event import get_topic_map, decode_logs
13
+
14
+ from .build_artifacts import ArtifactLibrary
15
+ from .contracts import RevertError
16
+ from .wrappers import (
17
+ MAXUINT256,
18
+ SKIP_PROXY,
19
+ AddressBook,
20
+ BaseProvider,
21
+ ETHCall,
22
+ ETHWrapper,
23
+ register_provider,
24
+ )
13
25
 
14
26
  env = Env()
15
27
 
@@ -20,17 +32,21 @@ W3_ADDRESS_BOOK_CREATE_UNKNOWN = env.str("W3_ADDRESS_BOOK_CREATE_UNKNOWN", "")
20
32
  W3_POA = env.str("W3_POA", "auto")
21
33
 
22
34
 
35
+ # TODO: This should probably be part of the AddressBook
36
+ _contract_map = {} # Address -> Contract
37
+
38
+
23
39
  class W3TimeControl:
24
40
  def __init__(self, w3):
25
41
  self.w3 = w3
26
42
 
27
43
  def fast_forward(self, secs):
28
- self.w3.provider.make_request("evm_increaseTime", [secs])
29
- # Not tested!
44
+ """Mines a new block whose timestamp is secs after the latest block's timestamp."""
45
+ self.w3.provider.make_request("evm_mine", [self.now + secs])
30
46
 
31
47
  @property
32
48
  def now(self):
33
- return self.w3.get_block("latest").timestamp
49
+ return self.w3.eth.get_block("latest").timestamp
34
50
 
35
51
 
36
52
  def register_w3_provider(provider_key="w3", tester=None, provider_kwargs={}):
@@ -42,6 +58,7 @@ def register_w3_provider(provider_key="w3", tester=None, provider_kwargs={}):
42
58
 
43
59
  if tester:
44
60
  from web3 import Web3
61
+
45
62
  w3 = Web3(Web3.EthereumTesterProvider())
46
63
  else:
47
64
  from web3.auto import w3
@@ -81,12 +98,16 @@ def transact(provider, function, tx_kwargs):
81
98
  from_ = provider.address_book.get_signer_account(from_)
82
99
  tx_kwargs["from"] = from_.address
83
100
  tx = function.buildTransaction(
84
- {**tx_kwargs, **{"nonce": provider.w3.eth.get_transaction_count(from_.address)}}
101
+ {
102
+ **tx_kwargs,
103
+ **{"nonce": provider.w3.eth.get_transaction_count(from_.address)},
104
+ }
85
105
  )
86
106
  signed_tx = from_.sign_transaction(tx)
87
107
  tx_hash = provider.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
88
108
  elif W3_TRANSACT_MODE == "defender-async":
89
109
  from .defender_relay import send_transaction
110
+
90
111
  tx_kwargs = {**provider.tx_kwargs, **tx_kwargs}
91
112
  tx = function.buildTransaction(tx_kwargs)
92
113
  return send_transaction(tx)
@@ -108,11 +129,11 @@ class W3AddressBook(AddressBook):
108
129
  return self._eth_accounts
109
130
 
110
131
  def get_account(self, name):
111
- if isinstance(name, (Account, LocalAccount)):
132
+ if isinstance(name, BaseAccount):
112
133
  return name
113
134
  if name is None:
114
135
  return self.ZERO
115
- if type(name) == str and name.startswith("0x"):
136
+ if isinstance(name, str) and name.startswith("0x"):
116
137
  return name
117
138
  if name not in self.name_to_address:
118
139
  self.last_account_used += 1
@@ -120,10 +141,15 @@ class W3AddressBook(AddressBook):
120
141
  self.name_to_address[name] = self.eth_accounts[self.last_account_used]
121
142
  except IndexError:
122
143
  self.name_to_address[name] = self.w3.eth.account.create().address
144
+ # TODO: This branch only generates a random account and discards the private key. Unlike the accounts in
145
+ # self.eth_accounts this one is not unlocked on the node. Something like this is necessary for this to
146
+ # be useful (and consistent with other branches):
147
+ # self.provider.register_account(address, private_key)
148
+
123
149
  return self.name_to_address[name]
124
150
 
125
151
  def get_name(self, account_or_address):
126
- if isinstance(account_or_address, (LocalAccount, )):
152
+ if isinstance(account_or_address, (LocalAccount,)):
127
153
  account_or_address = account_or_address.address
128
154
 
129
155
  for name, addr in self.name_to_address.items():
@@ -151,7 +177,7 @@ class W3EnvAddressBook(AddressBook):
151
177
  continue
152
178
  if k.endswith("_ADDR"):
153
179
  continue # Addresses linked to names
154
- addr = k[len(env_prefix):]
180
+ addr = k[len(env_prefix) :]
155
181
  if addr.startswith("0x"):
156
182
  account = w3.account.from_key(value)
157
183
  assert account.address == addr
@@ -183,7 +209,7 @@ class W3EnvAddressBook(AddressBook):
183
209
  raise RuntimeError(f"No account found for name {name}")
184
210
 
185
211
  def get_name(self, account_or_address):
186
- if isinstance(account_or_address, (LocalAccount, )):
212
+ if isinstance(account_or_address, (LocalAccount,)):
187
213
  account_or_address = account_or_address.address
188
214
 
189
215
  for name, addr in self.name_to_address.items():
@@ -205,25 +231,105 @@ class ReceiptWrapper:
205
231
  @property
206
232
  def events(self):
207
233
  if not hasattr(self, "_events"):
208
- topic_map = get_topic_map(self._contract.abi)
209
- logs = decode_logs(self._receipt.logs, topic_map, allow_undecoded=True)
210
- evts = {}
211
- for evt in logs:
212
- evt_name = evt["name"]
213
- evt_params = dict((d["name"], d["value"]) for d in evt["data"])
214
- if evt_name not in evts:
215
- evts[evt_name] = evt_params
216
- elif type(evts[evt_name]) == dict:
217
- evts[evt_name] = [evts[evt_name], evt_params] # start a list
218
- else: # it's already a list
219
- evts[evt_name].append(evt_params)
220
- self._events = evts
234
+ # Lookup the events in all known contracts
235
+ addresses = [log.address for log in self._receipt.logs]
236
+ contracts = {addr: _contract_map[addr] for addr in addresses if addr in _contract_map}
237
+ topic_map = {
238
+ HexBytes(event_abi_to_log_topic(event().abi)): event()
239
+ for contract in contracts.values()
240
+ for event in contract.events
241
+ }
242
+
243
+ parsed_logs = []
244
+ for log in self._receipt.logs:
245
+ for topic in log.topics:
246
+ if topic in topic_map:
247
+ parsed_logs += topic_map[topic].process_receipt(self._receipt)
248
+
249
+ evts = defaultdict(list)
250
+ for evt in parsed_logs:
251
+ evt_name = evt.event
252
+ evt_params = evt.args
253
+ evts[evt_name].append(evt_params)
254
+ self._events = {k: EventItem(k, v) for k, v in evts.items()}
221
255
  return self._events
222
256
 
223
257
  def __getattr__(self, attr_name):
224
258
  return getattr(self._receipt, attr_name)
225
259
 
226
260
 
261
+ class EventItem:
262
+ """
263
+ Dict/list hybrid container, represents one or more events with the same name
264
+ that were fired in a transaction.
265
+
266
+ Inspired on eth-brownies brownie.network.event._EventItem class
267
+ """
268
+
269
+ def __init__(self, name: str, events: List) -> None:
270
+ self.name = name
271
+ self._events = events
272
+
273
+ def __getitem__(self, key: Union[int, str]) -> List:
274
+ """if key is int: returns the n'th event that was fired with this name
275
+ if key is str: returns the value of data field 'key' from the 1st event
276
+ within the container"""
277
+ if not isinstance(key, (int, str)):
278
+ raise TypeError(f"Invalid key type '{type(key)}' - can only use strings or integers")
279
+ if isinstance(key, int):
280
+ try:
281
+ return self._events[key]
282
+ except IndexError:
283
+ raise IndexError(
284
+ f"Index {key} out of range - only {len(self._ordered)} '{self.name}' events fired"
285
+ )
286
+ if key in self._events[0]:
287
+ return self._events[0][key]
288
+ if f"{key} (indexed)" in self._events[0]:
289
+ return self._events[0][f"{key} (indexed)"]
290
+ valid_keys = ", ".join(self.keys())
291
+ raise KeyError(f"Unknown key '{key}' - the '{self.name}' event includes these keys: {valid_keys}")
292
+
293
+ def __contains__(self, name: str) -> bool:
294
+ """returns True if this event contains a value with the given name."""
295
+ return name in self._events[0]
296
+
297
+ def __len__(self) -> int:
298
+ """returns the number of events held in this container."""
299
+ return len(self._events)
300
+
301
+ def __repr__(self) -> str:
302
+ return str(self)
303
+
304
+ def __str__(self) -> str:
305
+ if len(self._events) == 1:
306
+ return str(self._events[0])
307
+ return str([i[0] for i in self._events])
308
+
309
+ def __iter__(self) -> Iterator:
310
+ return iter(self._events)
311
+
312
+ def __eq__(self, other: object) -> bool:
313
+ if len(self._events) == 1:
314
+ if isinstance(other, (tuple, list)):
315
+ # sequences compare directly against the event values
316
+ return self._events[0].values() == other
317
+ return other == self._events[0]
318
+ return other == self._events
319
+
320
+ def items(self) -> List:
321
+ """EventItem.items() -> a list object providing a view on EventItem[0]'s items"""
322
+ return [(i, self[i]) for i in self.keys()]
323
+
324
+ def keys(self) -> List:
325
+ """EventItem.keys() -> a list object providing a view on EventItem[0]'s keys"""
326
+ return [i.replace(" (indexed)", "") for i in self._events[0].keys()]
327
+
328
+ def values(self) -> List:
329
+ """EventItem.values() -> a list object providing a view on EventItem[0]'s values"""
330
+ return self._events[0].values()
331
+
332
+
227
333
  class W3ETHCall(ETHCall):
228
334
  @classmethod
229
335
  def find_function_abi(cls, contract, eth_method, eth_variant):
@@ -238,11 +344,14 @@ class W3ETHCall(ETHCall):
238
344
  function = getattr(wrapper.contract.functions, eth_method) # TODO: eth_variant
239
345
  function_abi = cls.find_function_abi(wrapper.contract, eth_method, eth_variant)
240
346
  if function_abi["stateMutability"] in ("pure", "view"):
347
+
241
348
  def eth_function(*args):
242
349
  if args and type(args[-1]) == dict:
243
350
  args = args[:-1] # remove dict with {from: ...}
244
351
  return function(*args).call()
352
+
245
353
  else: # Mutable function, need to send and wait transaction
354
+
246
355
  def eth_function(*args):
247
356
  if args and type(args[-1]) == dict:
248
357
  transact_args = args[-1]
@@ -255,12 +364,12 @@ class W3ETHCall(ETHCall):
255
364
 
256
365
  def normalize_receipt(self, wrapper, receipt):
257
366
  if W3_TRANSACT_MODE == "defender-async":
258
- return receipt # Don't do anything because the receipt it's just a dict of not-yet-mined tx
367
+ return receipt # Don't do anything because the receipt is just a dict of not-yet-mined tx
259
368
  return ReceiptWrapper(receipt, wrapper.contract)
260
369
 
261
370
  def _handle_exception(self, err):
262
371
  if str(err).startswith("execution reverted: "):
263
- raise RevertError(str(err)[len("execution reverted: "):])
372
+ raise RevertError(str(err)[len("execution reverted: ") :])
264
373
  super()._handle_exception(err)
265
374
 
266
375
  @classmethod
@@ -268,16 +377,14 @@ class W3ETHCall(ETHCall):
268
377
  if value_type.startswith("(") and value_type.endswith(")"):
269
378
  # It's a tuple / struct
270
379
  value_types = [t.strip() for t in value_type.split(",")]
271
- return tuple(
272
- cls.parse(wrapper, vt, value[i]) for i, vt in enumerate(value_types)
273
- )
380
+ return tuple(cls.parse(wrapper, vt, value[i]) for i, vt in enumerate(value_types))
274
381
  if value_type == "address":
275
- if isinstance(value, (LocalAccount, Account)):
276
- return value
277
- # elif isinstance(value, (Contract, ProjectContract)):
278
- # return value.address
382
+ if isinstance(value, BaseAccount):
383
+ return value.address
279
384
  elif isinstance(value, ETHWrapper):
280
385
  return value.contract.address
386
+ elif isinstance(value, Contract):
387
+ return value.address
281
388
  elif isinstance(value, str) and value.startswith("0x"):
282
389
  return value
283
390
  return wrapper.provider.address_book.get_account(value)
@@ -299,30 +406,19 @@ class W3Provider(BaseProvider):
299
406
 
300
407
  def __init__(self, w3, address_book=None, contracts_path=None, tx_kwargs=None):
301
408
  self.w3 = w3
302
- self.contracts_path = contracts_path or CONTRACT_JSON_PATH
303
- self.contract_def_cache = {}
409
+ self.artifact_library = ArtifactLibrary(
410
+ *(contracts_path if contracts_path is not None else CONTRACT_JSON_PATH)
411
+ )
304
412
  self.address_book = address_book or W3AddressBook(w3)
305
413
  self.time_control = W3TimeControl(w3)
306
414
  self.tx_kwargs = tx_kwargs or {}
307
415
 
308
416
  def get_contract_def(self, eth_contract):
309
- if eth_contract not in self.contract_def_cache:
310
- json_file = None
311
- for contract_path in self.contracts_path:
312
- for sub_path, _, files in os.walk(contract_path):
313
- if f"{eth_contract}.json" in files:
314
- json_file = os.path.join(sub_path, f"{eth_contract}.json")
315
- break
316
- if json_file is not None:
317
- break
318
- else:
319
- raise RuntimeError(f"{eth_contract} JSON definition not found in {self.contracts_path}")
320
- self.contract_def_cache[eth_contract] = json.load(open(json_file))
321
- return self.contract_def_cache[eth_contract]
417
+ return self.artifact_library.get_artifact_by_name(eth_contract)
322
418
 
323
419
  def get_contract_factory(self, eth_contract):
324
420
  contract_def = self.get_contract_def(eth_contract)
325
- return self.w3.eth.contract(abi=contract_def["abi"], bytecode=contract_def.get("bytecode", None))
421
+ return self.w3.eth.contract(abi=contract_def.abi, bytecode=contract_def.bytecode)
326
422
 
327
423
  def deploy(self, eth_contract, init_params, from_, **kwargs):
328
424
  factory = self.get_contract_factory(eth_contract)
@@ -358,49 +454,58 @@ class W3Provider(BaseProvider):
358
454
 
359
455
  def init_eth_wrapper(self, eth_wrapper, owner, init_params, kwargs):
360
456
  eth_wrapper.owner = self.address_book.get_account(owner)
361
- assert not eth_wrapper.libraries_required, "Not supported"
362
457
 
363
- eth_contract = self.get_contract_factory(eth_wrapper.eth_contract)
458
+ contract_def = self.get_contract_def(eth_wrapper.eth_contract)
459
+ libraries = {}
460
+ for lib, _ in contract_def.libraries():
461
+ if lib not in libraries:
462
+ library_def = self.get_contract_factory(lib)
463
+ library = self.construct(library_def)
464
+ libraries[lib] = library.address
465
+
466
+ if libraries:
467
+ contract_def = contract_def.link(libraries)
468
+
469
+ eth_contract = self.w3.eth.contract(abi=contract_def.abi, bytecode=contract_def.bytecode)
470
+
364
471
  if eth_wrapper.proxy_kind is None:
365
472
  eth_wrapper.contract = self.construct(eth_contract, init_params, {"from": eth_wrapper.owner})
366
473
  elif eth_wrapper.proxy_kind == "uups" and not SKIP_PROXY:
367
474
  constructor_params, init_params = init_params
368
475
  real_contract = self.construct(eth_contract, constructor_params, {"from": eth_wrapper.owner})
369
476
  ERC1967Proxy = self.get_contract_factory("ERC1967Proxy")
370
- init_data = real_contract.functions.initialize(
371
- *init_params
372
- ).build_transaction({**self.tx_kwargs, **{"from": eth_wrapper.owner}})["data"]
477
+ init_data = eth_contract.encodeABI(fn_name="initialize", args=init_params)
373
478
  proxy_contract = self.construct(
374
479
  ERC1967Proxy,
375
480
  (real_contract.address, init_data),
376
- {**self.tx_kwargs, **{"from": eth_wrapper.owner}}
377
- )
378
- eth_wrapper.contract = self.w3.eth.contract(
379
- abi=eth_contract.abi,
380
- address=proxy_contract.address
481
+ {**self.tx_kwargs, **{"from": eth_wrapper.owner}},
381
482
  )
483
+ eth_wrapper.contract = self.w3.eth.contract(abi=eth_contract.abi, address=proxy_contract.address)
382
484
  elif eth_wrapper.proxy_kind == "uups" and SKIP_PROXY:
383
485
  constructor_params, init_params = init_params
384
- eth_wrapper.contract = self.construct(eth_contract, constructor_params,
385
- {"from": eth_wrapper.owner})
486
+ eth_wrapper.contract = self.construct(
487
+ eth_contract, constructor_params, {"from": eth_wrapper.owner}
488
+ )
386
489
  transact(
387
490
  self,
388
491
  eth_wrapper.contract.functions.initialize(*init_params),
389
- {"from": eth_wrapper.owner}
492
+ {"from": eth_wrapper.owner},
390
493
  )
391
494
 
495
+ _contract_map[eth_wrapper.contract.address] = eth_wrapper.contract
496
+
392
497
  def construct(self, contract_factory, constructor_args=(), transact_kwargs={}):
393
498
  try:
394
- receipt = transact(
395
- self,
396
- contract_factory.constructor(*constructor_args),
397
- transact_kwargs
398
- )
499
+ receipt = transact(self, contract_factory.constructor(*constructor_args), transact_kwargs)
399
500
  except Exception as err:
400
501
  if str(err).startswith("execution reverted: "):
401
- raise RevertError(str(err)[len("execution reverted: "):])
502
+ raise RevertError(str(err)[len("execution reverted: ") :])
402
503
  raise
403
504
  return self.w3.eth.contract(abi=contract_factory.abi, address=receipt.contractAddress)
404
505
 
405
506
  def build_contract(self, contract_address, contract_factory, contract_name=None):
406
507
  return self.w3.eth.contract(abi=contract_factory.abi, address=contract_address)
508
+
509
+ def unlock_account(self, address):
510
+ """Unlocks an account on the node. Assumes hardhat network API."""
511
+ self.w3.provider.make_request("hardhat_impersonateAccount", [address])
ethproto/wrappers.py CHANGED
@@ -1,10 +1,14 @@
1
1
  """Base module for wrappers"""
2
+
2
3
  from abc import ABC, abstractmethod
3
4
  from contextlib import contextmanager
4
5
  from functools import partial
5
- from .wadray import Wad, Ray, make_integer_float
6
+
6
7
  import requests
7
8
  from environs import Env
9
+ from hexbytes import HexBytes
10
+
11
+ from .wadray import Ray, Wad, make_integer_float
8
12
 
9
13
  env = Env()
10
14
 
@@ -43,9 +47,11 @@ def get_provider(provider_key=None):
43
47
  def auto_register_provider(provider_key):
44
48
  if provider_key == "w3":
45
49
  from .w3wrappers import register_w3_provider
50
+
46
51
  register_w3_provider()
47
52
  elif provider_key == "brownie":
48
53
  from .brwrappers import BrownieProvider
54
+
49
55
  register_provider("brownie", BrownieProvider())
50
56
  else:
51
57
  raise RuntimeError(f"Unknown provider {provider_key}")
@@ -57,8 +63,16 @@ def register_provider(provider_key, provider):
57
63
 
58
64
 
59
65
  class MethodAdapter:
60
- def __init__(self, args=(), return_type="", eth_method=None, adapt_args=None, is_property=False,
61
- set_eth_method=None, eth_variant=None):
66
+ def __init__(
67
+ self,
68
+ args=(),
69
+ return_type="",
70
+ eth_method=None,
71
+ adapt_args=None,
72
+ is_property=False,
73
+ set_eth_method=None,
74
+ eth_variant=None,
75
+ ):
62
76
  self.eth_method = eth_method
63
77
  self.set_eth_method = set_eth_method
64
78
  self.return_type = return_type
@@ -76,8 +90,8 @@ class MethodAdapter:
76
90
 
77
91
  @staticmethod
78
92
  def snake_to_camel(name):
79
- components = name.split('_')
80
- return components[0] + ''.join(x.title() for x in components[1:])
93
+ components = name.split("_")
94
+ return components[0] + "".join(x.title() for x in components[1:])
81
95
 
82
96
  @property
83
97
  def method_name(self):
@@ -85,8 +99,7 @@ class MethodAdapter:
85
99
 
86
100
  def __get__(self, instance, owner=None):
87
101
  eth_call = instance.eth_call(
88
- self.eth_method, self.args, self.return_type, self.adapt_args,
89
- eth_variant=self.eth_variant
102
+ self.eth_method, self.args, self.return_type, self.adapt_args, eth_variant=self.eth_variant
90
103
  )
91
104
  if self.is_property:
92
105
  return eth_call(instance)
@@ -95,7 +108,7 @@ class MethodAdapter:
95
108
  def __set__(self, instance, value):
96
109
  if not self.is_property:
97
110
  raise NotImplementedError()
98
- eth_call = instance.eth_call(self.set_eth_method, (("new_value", self.return_type), ))
111
+ eth_call = instance.eth_call(self.set_eth_method, (("new_value", self.return_type),))
99
112
  return eth_call(instance, value)
100
113
 
101
114
 
@@ -147,7 +160,7 @@ class ETHCall(ABC):
147
160
 
148
161
  def __call__(self, wrapper, *args, **kwargs):
149
162
  call_args = []
150
- msg_args = {}
163
+ msg_args = wrapper.provider.tx_kwargs.copy()
151
164
 
152
165
  if self.adapt_args:
153
166
  args, kwargs = self.adapt_args(args, kwargs)
@@ -203,22 +216,26 @@ class ETHCall(ABC):
203
216
  return call_args, msg_args
204
217
 
205
218
  @classmethod
206
- def _parse_keccak256(cls, value):
207
- from Crypto.Hash import keccak # To avoid import of wrappers breaks if keccak not installed
208
- if value.startswith("0x"):
219
+ def _parse_keccak256(cls, value) -> HexBytes:
220
+ from Crypto.Hash import (
221
+ keccak, # To avoid import of wrappers breaks if keccak not installed
222
+ )
223
+
224
+ if isinstance(value, HexBytes):
209
225
  return value
226
+
227
+ if value.startswith("0x"):
228
+ return HexBytes(value)
210
229
  k = keccak.new(digest_bits=256)
211
230
  k.update(value.encode("utf-8"))
212
- return k.hexdigest()
231
+ return HexBytes(k.hexdigest())
213
232
 
214
233
  @classmethod
215
234
  def unparse(cls, wrapper, value_type, value):
216
235
  if value_type.startswith("(") and value_type.endswith(")"):
217
236
  # It's a tuple / struct
218
237
  value_types = [t.strip() for t in value_type.strip("()").split(",")]
219
- return tuple(
220
- cls.unparse(wrapper, vt, value[i]) for i, vt in enumerate(value_types)
221
- )
238
+ return tuple(cls.unparse(wrapper, vt, value[i]) for i, vt in enumerate(value_types))
222
239
  if value_type == "amount":
223
240
  return AMOUNT_CLASS(value)
224
241
  if value_type == "ray":
@@ -240,6 +257,8 @@ class ETHCall(ABC):
240
257
 
241
258
 
242
259
  class BaseProvider(ABC):
260
+ tx_kwargs = {}
261
+
243
262
  @abstractmethod
244
263
  def get_contract_factory(self, eth_contract):
245
264
  raise NotImplementedError()
@@ -259,8 +278,9 @@ class BaseProvider(ABC):
259
278
  return 0
260
279
  address = self.get_contract_address(eth_wrapper)
261
280
  url = (
262
- etherscan_url + f"&module=account&action=txlist&address={address}&startblock=0&" +
263
- "endblock=99999999&page=1&offset=10&sort=asc"
281
+ etherscan_url
282
+ + f"&module=account&action=txlist&address={address}&startblock=0&"
283
+ + "endblock=99999999&page=1&offset=10&sort=asc"
264
284
  )
265
285
  resp = requests.get(url)
266
286
  resp.raise_for_status()
@@ -305,9 +325,9 @@ class ETHWrapper:
305
325
  else:
306
326
  if self.constructor_args:
307
327
  constructor_params, transaction_kwargs = self.eth_call.parse_args(
308
- self, self.constructor_args, *init_params[:len(self.constructor_args)], **kwargs
328
+ self, self.constructor_args, *init_params[: len(self.constructor_args)], **kwargs
309
329
  )
310
- init_params = init_params[len(self.constructor_args):]
330
+ init_params = init_params[len(self.constructor_args) :]
311
331
  if transaction_kwargs:
312
332
  kwargs.update(transaction_kwargs)
313
333
  else:
@@ -357,6 +377,31 @@ class ETHWrapper:
357
377
  obj._auto_from = obj.owner
358
378
  return obj
359
379
 
380
+ @classmethod
381
+ def build_from_def(cls, contract_def, extra_properties=None):
382
+ extra_properties = extra_properties or {}
383
+ attributes = {}
384
+ for item in contract_def.abi:
385
+ if item["type"] == "constructor":
386
+ attributes["constructor_args"] = tuple((arg["name"], arg["type"]) for arg in item["inputs"])
387
+ elif item["type"] == "function":
388
+ attributes[item["name"]] = MethodAdapter(
389
+ args=tuple((arg["name"], arg["type"]) for arg in item["inputs"]),
390
+ return_type=item["outputs"][0]["type"] if item["outputs"] else "",
391
+ eth_method=item["name"],
392
+ eth_variant=item["inputs"],
393
+ )
394
+ wrapper_class = type(
395
+ contract_def.contract_name,
396
+ (cls,),
397
+ {
398
+ "eth_contract": contract_def.contract_name,
399
+ **attributes,
400
+ **extra_properties,
401
+ },
402
+ )
403
+ return wrapper_class
404
+
360
405
  @property
361
406
  def contract_id(self):
362
407
  return self.contract.address
@@ -371,8 +416,8 @@ class ETHWrapper:
371
416
  revoke_role = MethodAdapter((("role", "keccak256"), ("user", "address")))
372
417
  renounce_role = MethodAdapter((("role", "keccak256"), ("user", "address")))
373
418
  has_role = MethodAdapter((("role", "keccak256"), ("user", "address")), "bool")
374
- get_role_admin = MethodAdapter((("role", "keccak256"), ), "address")
375
- get_role_admin = MethodAdapter((("role", "keccak256"), ), "bytes32")
419
+ get_role_admin = MethodAdapter((("role", "keccak256"),), "address")
420
+ get_role_admin = MethodAdapter((("role", "keccak256"),), "bytes32")
376
421
 
377
422
  @contextmanager
378
423
  def as_(self, user):
@@ -397,14 +442,15 @@ class IERC20(ETHWrapper):
397
442
  symbol = MethodAdapter((), "string", is_property=True)
398
443
  decimals = MethodAdapter((), "int", is_property=True)
399
444
  total_supply = MethodAdapter((), "amount")
400
- balance_of = MethodAdapter((("account", "address"), ), "amount")
401
- transfer = MethodAdapter((
402
- ("sender", "msg.sender"), ("recipient", "address"), ("amount", "amount")
403
- ), "receipt")
445
+ balance_of = MethodAdapter((("account", "address"),), "amount")
446
+ transfer = MethodAdapter(
447
+ (("sender", "msg.sender"), ("recipient", "address"), ("amount", "amount")), "receipt"
448
+ )
404
449
 
405
450
  allowance = MethodAdapter((("owner", "address"), ("spender", "address")), "amount")
406
- approve = MethodAdapter((("owner", "msg.sender"), ("spender", "address"), ("amount", "amount")),
407
- "receipt")
451
+ approve = MethodAdapter(
452
+ (("owner", "msg.sender"), ("spender", "address"), ("amount", "amount")), "receipt"
453
+ )
408
454
  increase_allowance = MethodAdapter(
409
455
  (("owner", "msg.sender"), ("spender", "address"), ("amount", "amount"))
410
456
  )
@@ -412,9 +458,10 @@ class IERC20(ETHWrapper):
412
458
  (("owner", "msg.sender"), ("spender", "address"), ("amount", "amount"))
413
459
  )
414
460
 
415
- transfer_from = MethodAdapter((
416
- ("spender", "msg.sender"), ("sender", "address"), ("recipient", "address"), ("amount", "amount")
417
- ), "receipt")
461
+ transfer_from = MethodAdapter(
462
+ (("spender", "msg.sender"), ("sender", "address"), ("recipient", "address"), ("amount", "amount")),
463
+ "receipt",
464
+ )
418
465
 
419
466
 
420
467
  class IERC721(ETHWrapper):
@@ -423,19 +470,21 @@ class IERC721(ETHWrapper):
423
470
  name = MethodAdapter((), "string", is_property=True)
424
471
  symbol = MethodAdapter((), "string", is_property=True)
425
472
  total_supply = MethodAdapter((), "int")
426
- balance_of = MethodAdapter((("account", "address"), ), "int")
427
- owner_of = MethodAdapter((("token_id", "int"), ), "address")
428
- approve = MethodAdapter((
429
- ("sender", "msg.sender"), ("spender", "address"), ("token_id", "int")
430
- ), "receipt")
431
- get_approved = MethodAdapter((("token_id", "int"), ), "address")
432
- set_approval_for_all = MethodAdapter((
433
- ("sender", "msg.sender"), ("operator", "address"), ("approved", "bool")
434
- ))
473
+ balance_of = MethodAdapter((("account", "address"),), "int")
474
+ owner_of = MethodAdapter((("token_id", "int"),), "address")
475
+ approve = MethodAdapter(
476
+ (("sender", "msg.sender"), ("spender", "address"), ("token_id", "int")), "receipt"
477
+ )
478
+ get_approved = MethodAdapter((("token_id", "int"),), "address")
479
+ set_approval_for_all = MethodAdapter(
480
+ (("sender", "msg.sender"), ("operator", "address"), ("approved", "bool"))
481
+ )
435
482
  is_approved_for_all = MethodAdapter((("owner", "address"), ("operator", "address")), "bool")
436
- transfer_from = MethodAdapter((
437
- ("spender", "msg.sender"), ("from", "address"), ("to", "address"), ("token_id", "int")
438
- ), "receipt")
439
- safe_transfer_from = MethodAdapter((
440
- ("spender", "msg.sender"), ("from", "address"), ("to", "address"), ("token_id", "int")
441
- ), "receipt", eth_variant="address, address, uint256")
483
+ transfer_from = MethodAdapter(
484
+ (("spender", "msg.sender"), ("from", "address"), ("to", "address"), ("token_id", "int")), "receipt"
485
+ )
486
+ safe_transfer_from = MethodAdapter(
487
+ (("spender", "msg.sender"), ("from", "address"), ("to", "address"), ("token_id", "int")),
488
+ "receipt",
489
+ eth_variant="address, address, uint256",
490
+ )
@@ -1,13 +0,0 @@
1
- ethproto/__init__.py,sha256=YWkAFysBp4tZjLWWB2FFmp5yG23pUYhQvgQW9b3soXs,579
2
- ethproto/brwrappers.py,sha256=sC6N7kzmly5M2s-ewvambbbA-8NbYJZP_MO_UC7o_Z4,9173
3
- ethproto/contracts.py,sha256=44oEIyCyX2mH-PftG1ZpQ7aXRPJZy95t1tgL29UsqP4,18685
4
- ethproto/defender_relay.py,sha256=05A8TfRZwiBhCpo924Pf9CjfKSir2Wvgg1p_asFxJbw,1777
5
- ethproto/w3wrappers.py,sha256=D8NJPvacZb60c5ukFtJyysmQi9H5y72VBnwMKli5DwA,16810
6
- ethproto/wadray.py,sha256=JBsu5KcyU9k70bDK03T2IY6qPVFO30WbYPhwrAHdXao,8262
7
- ethproto/wrappers.py,sha256=hlqmmJOzNo2V4BUpMJ4hVzdLYtIb0IqLMU3Up1o-RPU,16185
8
- eth_prototype-0.7.5.dist-info/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
9
- eth_prototype-0.7.5.dist-info/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
10
- eth_prototype-0.7.5.dist-info/METADATA,sha256=wLQ8c-2UPhMC0H87QoW-nfWQC3MEFf6aBp8UFFA68hk,3646
11
- eth_prototype-0.7.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
12
- eth_prototype-0.7.5.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
13
- eth_prototype-0.7.5.dist-info/RECORD,,
ethproto/brwrappers.py DELETED
@@ -1,221 +0,0 @@
1
- import re
2
- from .contracts import RevertError
3
- from .wrappers import ETHCall, AddressBook, MAXUINT256, SKIP_PROXY, ETHWrapper, BaseProvider
4
- import eth_utils
5
- from brownie import accounts
6
- import brownie
7
- from brownie.network.account import Account, LocalAccount
8
- from brownie.exceptions import VirtualMachineError
9
- from brownie.network.state import Chain
10
- from brownie.network.contract import Contract, ProjectContract
11
-
12
-
13
- class BrownieTimeControl:
14
- def __init__(self, chain=None):
15
- self.chain = chain or Chain()
16
-
17
- def fast_forward(self, secs):
18
- self.chain.sleep(secs)
19
- self.chain.mine()
20
-
21
- @property
22
- def now(self):
23
- if len(self.chain) > 0:
24
- return self.chain[-1].timestamp
25
- return self.chain.time()
26
-
27
-
28
- class BrownieAddressBook(AddressBook):
29
-
30
- def __init__(self, eth_accounts):
31
- self.eth_accounts = eth_accounts # brownie.network.account.Accounts
32
- self.name_to_address = {}
33
- self.last_account_used = -1
34
-
35
- def get_account(self, name):
36
- if isinstance(name, (Account, LocalAccount)):
37
- return name
38
- if isinstance(name, (Contract, ProjectContract)):
39
- return name
40
- if name is None:
41
- return self.ZERO
42
- if name not in self.name_to_address:
43
- self.last_account_used += 1
44
- if (len(self.eth_accounts) - 1) > self.last_account_used:
45
- self.eth_accounts.add()
46
- self.name_to_address[name] = self.eth_accounts[self.last_account_used].address
47
- return self.eth_accounts.at(self.name_to_address[name])
48
-
49
- def get_name(self, account_or_address):
50
- if isinstance(account_or_address, Account):
51
- account_or_address = account_or_address.address
52
-
53
- for name, addr in self.name_to_address.items():
54
- if addr == account_or_address:
55
- return name
56
- return None
57
-
58
-
59
- AddressBook.set_instance(BrownieAddressBook(accounts))
60
-
61
-
62
- def encode_function_data(initializer=None, *args):
63
- """Encodes the function call so we can work with an initializer.
64
- Args:
65
- initializer ([brownie.network.contract.ContractTx], optional):
66
- The initializer function we want to call. Example: `box.store`.
67
- Defaults to None.
68
- args (Any, optional):
69
- The arguments to pass to the initializer function
70
- Returns:
71
- [bytes]: Return the encoded bytes.
72
- """
73
- if not initializer:
74
- return eth_utils.to_bytes(hexstr="0x")
75
- else:
76
- return initializer.encode_input(*args)
77
-
78
-
79
- class BrownieETHCall(ETHCall):
80
- error_match = [
81
- re.compile('VM Exception while processing transaction: revert with reason "([^"]+)"')
82
- ]
83
-
84
- def _handle_exception(self, err):
85
- if isinstance(err, VirtualMachineError) and err.revert_type == "revert":
86
- raise RevertError(err.revert_msg)
87
- # Tries to match error by regex
88
- for err_regex in self.error_match:
89
- m = err_regex.match(str(err))
90
- if m:
91
- raise RevertError(m.group(1))
92
- super()._handle_exception(err)
93
-
94
- @classmethod
95
- def get_eth_function(cls, wrapper, eth_method, eth_variant=None):
96
- if eth_variant:
97
- return getattr(wrapper.contract, eth_method)[eth_variant]
98
- else:
99
- return getattr(wrapper.contract, eth_method)
100
-
101
- @classmethod
102
- def get_eth_function_and_mutability(cls, wrapper, eth_method, eth_variant=None):
103
- function = cls.get_eth_function(wrapper, eth_method, eth_variant)
104
- return function, function.abi["stateMutability"]
105
-
106
- @classmethod
107
- def parse(cls, wrapper, value_type, value):
108
- if value_type.startswith("(") and value_type.endswith(")"):
109
- # It's a tuple / struct
110
- value_types = [t.strip() for t in value_type.strip("()").split(",")]
111
- return tuple(
112
- cls.parse(wrapper, vt, value[i]) for i, vt in enumerate(value_types)
113
- )
114
- if value_type == "address":
115
- if isinstance(value, (LocalAccount, Account)):
116
- return value
117
- elif isinstance(value, (Contract, ProjectContract)):
118
- return value.address
119
- elif isinstance(value, ETHWrapper):
120
- return value.contract.address
121
- elif isinstance(value, str) and value.startswith("0x"):
122
- return value
123
- return wrapper.provider.address_book.get_account(value)
124
- if value_type == "keccak256":
125
- return cls._parse_keccak256(value)
126
- if value_type == "contract":
127
- if isinstance(value, ETHWrapper):
128
- return value.contract.address
129
- elif value is None:
130
- return AddressBook.ZERO
131
- raise RuntimeError(f"Invalid contract: {value}")
132
- if value_type == "amount" and value is None:
133
- return MAXUINT256
134
- return value
135
-
136
-
137
- class BrownieProvider(BaseProvider):
138
- eth_call = BrownieETHCall
139
-
140
- def __init__(self, time_control=None, address_book=None):
141
- self.time_control = time_control or BrownieTimeControl()
142
- self.address_book = address_book or BrownieAddressBook(accounts)
143
-
144
- def get_contract_factory(self, eth_contract):
145
- ret = getattr(brownie, eth_contract, None)
146
- if ret is not None:
147
- return ret
148
- # Might be a manually loaded project or an interface
149
- project = brownie.project.get_loaded_projects()[0]
150
- ret = getattr(project, eth_contract, None)
151
- if ret is not None:
152
- return ret
153
- return getattr(project.interface, eth_contract)
154
-
155
- def get_events(self, eth_wrapper, event_name, filter_kwargs={}):
156
- """Returns a list of events given a filter, like this:
157
-
158
- >>> provider.get_events(currencywrapper, "Transfer", dict(fromBlock=0))
159
- [AttributeDict({
160
- 'args': AttributeDict(
161
- {'from': '0x0000000000000000000000000000000000000000',
162
- 'to': '0x56Cd397bAA08F2339F0ae470DEA99D944Ac064bB',
163
- 'value': 6000000000000000000000}),
164
- 'event': 'Transfer',
165
- 'logIndex': 0,
166
- 'transactionIndex': 0,
167
- 'transactionHash': HexBytes(
168
- '0x406b2cf8de2f12f4d0958e9f0568dc0919f337ed399f8d8d78ddbc648c01f806'
169
- ),
170
- 'address': '0xf8BedC7458fb8cAbD616B5e90F57c34c392e7168',
171
- 'blockHash': HexBytes('0x7b23c6ea49759bcee769b1a357dec7f63f03bdb1dd13f1ee19868925954134b3'),
172
- 'blockNumber': 23
173
- })]
174
- """
175
- w3 = brownie.network.web3
176
- w3_contract = w3.eth.contract(abi=eth_wrapper.contract.abi, address=eth_wrapper.contract.address)
177
- event = getattr(w3_contract.events, event_name)
178
- if "fromBlock" not in filter_kwargs:
179
- filter_kwargs["fromBlock"] = self.get_first_block(eth_wrapper)
180
- event_filter = event.create_filter(**filter_kwargs)
181
- return event_filter.get_all_entries()
182
-
183
- def deploy(self, eth_contract, init_params, from_, **kwargs):
184
- factory = self.get_contract_factory(eth_contract)
185
- kwargs["from"] = from_
186
- return factory.deploy(*init_params, kwargs)
187
-
188
- def init_eth_wrapper(self, eth_wrapper, owner, init_params, kwargs):
189
- eth_wrapper.owner = self.address_book.get_account(owner)
190
- for library in eth_wrapper.libraries_required:
191
- self.get_contract_factory(library).deploy({"from": eth_wrapper.owner})
192
- eth_contract = self.get_contract_factory(eth_wrapper.eth_contract)
193
- if eth_wrapper.proxy_kind is None:
194
- eth_wrapper.contract = eth_contract.deploy(*init_params, {"from": eth_wrapper.owner})
195
- elif eth_wrapper.proxy_kind == "uups" and not SKIP_PROXY:
196
- constructor_params, init_params = init_params
197
- real_contract = eth_contract.deploy(*constructor_params, {"from": eth_wrapper.owner})
198
- proxy_factory = self.get_contract_factory("ERC1967Proxy")
199
- try:
200
- proxy_contract = proxy_factory.deploy(
201
- real_contract,
202
- encode_function_data(getattr(real_contract, "initialize", None), *init_params),
203
- {"from": eth_wrapper.owner}
204
- )
205
- except VirtualMachineError as err:
206
- if err.revert_type == "revert":
207
- raise RevertError(err.revert_msg)
208
- raise
209
-
210
- # Replace the proxy contract with the implementation in brownie's state
211
- # This makes the gas reports work properly for uups contracts
212
- self.get_contract_factory("ERC1967Proxy").remove(proxy_contract)
213
- eth_wrapper.contract = eth_contract.at(proxy_contract.address)
214
-
215
- elif eth_wrapper.proxy_kind == "uups" and SKIP_PROXY:
216
- constructor_params, init_params = init_params
217
- eth_wrapper.contract = eth_contract.deploy(*constructor_params, {"from": eth_wrapper.owner})
218
- eth_wrapper.contract.initialize(*init_params, {"from": eth_wrapper.owner})
219
-
220
- def build_contract(self, contract_address, contract_factory, contract_name=None):
221
- return Contract.from_abi(contract_name or "Contract", contract_address, contract_factory.abi)