eth-prototype 0.7.5__py3-none-any.whl → 1.0.1__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.
- {eth_prototype-0.7.5.dist-info → eth_prototype-1.0.1.dist-info}/METADATA +17 -46
- eth_prototype-1.0.1.dist-info/RECORD +13 -0
- {eth_prototype-0.7.5.dist-info → eth_prototype-1.0.1.dist-info}/WHEEL +1 -1
- ethproto/build_artifacts.py +152 -0
- ethproto/contracts.py +8 -6
- ethproto/w3wrappers.py +185 -76
- ethproto/wrappers.py +94 -49
- eth_prototype-0.7.5.dist-info/RECORD +0 -13
- ethproto/brwrappers.py +0 -221
- {eth_prototype-0.7.5.dist-info → eth_prototype-1.0.1.dist-info}/AUTHORS.rst +0 -0
- {eth_prototype-0.7.5.dist-info → eth_prototype-1.0.1.dist-info}/LICENSE.txt +0 -0
- {eth_prototype-0.7.5.dist-info → eth_prototype-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: eth-prototype
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.1
|
4
4
|
Summary: Prototype Ethereum Smart Contracts in Python
|
5
5
|
Home-page: https://github.com/gnarvaja/eth-prototype
|
6
6
|
Author: Guillermo M. Narvaja
|
@@ -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/
|
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
|
-
Requires-Dist: web3[tester] ; extra == 'testing-w3'
|
32
|
+
Requires-Dist: web3[tester] >=6 ; 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
|
-
Requires-Dist: web3 ; extra == 'web3'
|
46
|
-
Requires-Dist: eth-event ; extra == 'web3'
|
38
|
+
Requires-Dist: web3 >=6 ; 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
|
-
|
69
|
-
|
57
|
+
```
|
58
|
+
warrant @ git+https://github.com/gnarvaja/warrant.git#egg=warrant
|
59
|
+
```
|
70
60
|
|
71
|
-
|
61
|
+
Note that using the `warrant` package from pypi will not work because of incompatibilities with newer python versions.
|
72
62
|
|
73
|
-
|
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
|
-
|
65
|
+
The tox tests run in two variants:
|
83
66
|
|
84
|
-
|
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=MrRf7GLFgDhKYKiTctjNONZHQNTo6nVzqPzprv0BU2w,20607
|
6
|
+
ethproto/wadray.py,sha256=JBsu5KcyU9k70bDK03T2IY6qPVFO30WbYPhwrAHdXao,8262
|
7
|
+
ethproto/wrappers.py,sha256=9qDwRDOXw3wquzvGfIsub-VPWm98GBWP7dHLFOUPWzg,17307
|
8
|
+
eth_prototype-1.0.1.dist-info/AUTHORS.rst,sha256=Ui-05yYXtDZxna6o1yNcfdm8Jt68UIDQ01osiLxlYlU,95
|
9
|
+
eth_prototype-1.0.1.dist-info/LICENSE.txt,sha256=U_Q6_nDYDwZPIuhttHi37hXZ2qU2-HlV2geo9hzHXFw,1087
|
10
|
+
eth_prototype-1.0.1.dist-info/METADATA,sha256=TE-gm2NZKjLg7mgbihmuddG0XwORT7DrAM8UqtLPMtg,2669
|
11
|
+
eth_prototype-1.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
12
|
+
eth_prototype-1.0.1.dist-info/top_level.txt,sha256=Dl0X7m6N1hxeo4JpGpSNqWC2gtsN0731g-DL1J0mpjc,9
|
13
|
+
eth_prototype-1.0.1.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
|
-
|
6
|
+
|
6
7
|
from m9g import Model
|
7
|
-
from m9g.fields import IntField,
|
8
|
-
|
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:
|
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:
|
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:
|
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
|
3
|
-
from
|
4
|
-
|
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
|
10
|
-
from
|
8
|
+
from eth_utils.abi import event_abi_to_log_topic
|
9
|
+
from hexbytes import HexBytes
|
10
|
+
from web3.contract import Contract
|
11
|
+
from web3.exceptions import ContractLogicError, ExtraDataLengthError
|
11
12
|
from web3.middleware import geth_poa_middleware
|
12
|
-
|
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
|
-
|
29
|
-
|
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
|
@@ -80,15 +97,19 @@ def transact(provider, function, tx_kwargs):
|
|
80
97
|
else: # it's a string, I try to get the PK from the environment
|
81
98
|
from_ = provider.address_book.get_signer_account(from_)
|
82
99
|
tx_kwargs["from"] = from_.address
|
83
|
-
tx = function.
|
84
|
-
{
|
100
|
+
tx = function.build_transaction(
|
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
|
-
tx = function.
|
112
|
+
tx = function.build_transaction(tx_kwargs)
|
92
113
|
return send_transaction(tx)
|
93
114
|
|
94
115
|
return provider.w3.eth.wait_for_transaction_receipt(tx_hash)
|
@@ -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,
|
132
|
+
if isinstance(name, BaseAccount):
|
112
133
|
return name
|
113
134
|
if name is None:
|
114
135
|
return self.ZERO
|
115
|
-
if
|
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
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
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,16 @@ 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
|
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
|
-
if
|
263
|
-
raise RevertError(
|
371
|
+
if isinstance(err, ContractLogicError):
|
372
|
+
raise RevertError(
|
373
|
+
err.message[len("execution reverted: ") :]
|
374
|
+
if err.message and err.message.startswith("execution reverted: ")
|
375
|
+
else err.message
|
376
|
+
)
|
264
377
|
super()._handle_exception(err)
|
265
378
|
|
266
379
|
@classmethod
|
@@ -268,16 +381,14 @@ class W3ETHCall(ETHCall):
|
|
268
381
|
if value_type.startswith("(") and value_type.endswith(")"):
|
269
382
|
# It's a tuple / struct
|
270
383
|
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
|
-
)
|
384
|
+
return tuple(cls.parse(wrapper, vt, value[i]) for i, vt in enumerate(value_types))
|
274
385
|
if value_type == "address":
|
275
|
-
if isinstance(value,
|
276
|
-
return value
|
277
|
-
# elif isinstance(value, (Contract, ProjectContract)):
|
278
|
-
# return value.address
|
386
|
+
if isinstance(value, BaseAccount):
|
387
|
+
return value.address
|
279
388
|
elif isinstance(value, ETHWrapper):
|
280
389
|
return value.contract.address
|
390
|
+
elif isinstance(value, Contract):
|
391
|
+
return value.address
|
281
392
|
elif isinstance(value, str) and value.startswith("0x"):
|
282
393
|
return value
|
283
394
|
return wrapper.provider.address_book.get_account(value)
|
@@ -299,30 +410,19 @@ class W3Provider(BaseProvider):
|
|
299
410
|
|
300
411
|
def __init__(self, w3, address_book=None, contracts_path=None, tx_kwargs=None):
|
301
412
|
self.w3 = w3
|
302
|
-
self.
|
303
|
-
|
413
|
+
self.artifact_library = ArtifactLibrary(
|
414
|
+
*(contracts_path if contracts_path is not None else CONTRACT_JSON_PATH)
|
415
|
+
)
|
304
416
|
self.address_book = address_book or W3AddressBook(w3)
|
305
417
|
self.time_control = W3TimeControl(w3)
|
306
418
|
self.tx_kwargs = tx_kwargs or {}
|
307
419
|
|
308
420
|
def get_contract_def(self, eth_contract):
|
309
|
-
|
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]
|
421
|
+
return self.artifact_library.get_artifact_by_name(eth_contract)
|
322
422
|
|
323
423
|
def get_contract_factory(self, eth_contract):
|
324
424
|
contract_def = self.get_contract_def(eth_contract)
|
325
|
-
return self.w3.eth.contract(abi=contract_def
|
425
|
+
return self.w3.eth.contract(abi=contract_def.abi, bytecode=contract_def.bytecode)
|
326
426
|
|
327
427
|
def deploy(self, eth_contract, init_params, from_, **kwargs):
|
328
428
|
factory = self.get_contract_factory(eth_contract)
|
@@ -358,49 +458,58 @@ class W3Provider(BaseProvider):
|
|
358
458
|
|
359
459
|
def init_eth_wrapper(self, eth_wrapper, owner, init_params, kwargs):
|
360
460
|
eth_wrapper.owner = self.address_book.get_account(owner)
|
361
|
-
assert not eth_wrapper.libraries_required, "Not supported"
|
362
461
|
|
363
|
-
|
462
|
+
contract_def = self.get_contract_def(eth_wrapper.eth_contract)
|
463
|
+
libraries = {}
|
464
|
+
for lib, _ in contract_def.libraries():
|
465
|
+
if lib not in libraries:
|
466
|
+
library_def = self.get_contract_factory(lib)
|
467
|
+
library = self.construct(library_def)
|
468
|
+
libraries[lib] = library.address
|
469
|
+
|
470
|
+
if libraries:
|
471
|
+
contract_def = contract_def.link(libraries)
|
472
|
+
|
473
|
+
eth_contract = self.w3.eth.contract(abi=contract_def.abi, bytecode=contract_def.bytecode)
|
474
|
+
|
364
475
|
if eth_wrapper.proxy_kind is None:
|
365
476
|
eth_wrapper.contract = self.construct(eth_contract, init_params, {"from": eth_wrapper.owner})
|
366
477
|
elif eth_wrapper.proxy_kind == "uups" and not SKIP_PROXY:
|
367
478
|
constructor_params, init_params = init_params
|
368
479
|
real_contract = self.construct(eth_contract, constructor_params, {"from": eth_wrapper.owner})
|
369
480
|
ERC1967Proxy = self.get_contract_factory("ERC1967Proxy")
|
370
|
-
init_data =
|
371
|
-
*init_params
|
372
|
-
).build_transaction({**self.tx_kwargs, **{"from": eth_wrapper.owner}})["data"]
|
481
|
+
init_data = eth_contract.encodeABI(fn_name="initialize", args=init_params)
|
373
482
|
proxy_contract = self.construct(
|
374
483
|
ERC1967Proxy,
|
375
484
|
(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
|
485
|
+
{**self.tx_kwargs, **{"from": eth_wrapper.owner}},
|
381
486
|
)
|
487
|
+
eth_wrapper.contract = self.w3.eth.contract(abi=eth_contract.abi, address=proxy_contract.address)
|
382
488
|
elif eth_wrapper.proxy_kind == "uups" and SKIP_PROXY:
|
383
489
|
constructor_params, init_params = init_params
|
384
|
-
eth_wrapper.contract = self.construct(
|
385
|
-
|
490
|
+
eth_wrapper.contract = self.construct(
|
491
|
+
eth_contract, constructor_params, {"from": eth_wrapper.owner}
|
492
|
+
)
|
386
493
|
transact(
|
387
494
|
self,
|
388
495
|
eth_wrapper.contract.functions.initialize(*init_params),
|
389
|
-
{"from": eth_wrapper.owner}
|
496
|
+
{"from": eth_wrapper.owner},
|
390
497
|
)
|
391
498
|
|
499
|
+
_contract_map[eth_wrapper.contract.address] = eth_wrapper.contract
|
500
|
+
|
392
501
|
def construct(self, contract_factory, constructor_args=(), transact_kwargs={}):
|
393
502
|
try:
|
394
|
-
receipt = transact(
|
395
|
-
self,
|
396
|
-
contract_factory.constructor(*constructor_args),
|
397
|
-
transact_kwargs
|
398
|
-
)
|
503
|
+
receipt = transact(self, contract_factory.constructor(*constructor_args), transact_kwargs)
|
399
504
|
except Exception as err:
|
400
505
|
if str(err).startswith("execution reverted: "):
|
401
|
-
raise RevertError(str(err)[len("execution reverted: "):])
|
506
|
+
raise RevertError(str(err)[len("execution reverted: ") :])
|
402
507
|
raise
|
403
508
|
return self.w3.eth.contract(abi=contract_factory.abi, address=receipt.contractAddress)
|
404
509
|
|
405
510
|
def build_contract(self, contract_address, contract_factory, contract_name=None):
|
406
511
|
return self.w3.eth.contract(abi=contract_factory.abi, address=contract_address)
|
512
|
+
|
513
|
+
def unlock_account(self, address):
|
514
|
+
"""Unlocks an account on the node. Assumes hardhat network API."""
|
515
|
+
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
|
-
|
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,10 +47,8 @@ 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
|
-
elif provider_key == "brownie":
|
48
|
-
from .brwrappers import BrownieProvider
|
49
|
-
register_provider("brownie", BrownieProvider())
|
50
52
|
else:
|
51
53
|
raise RuntimeError(f"Unknown provider {provider_key}")
|
52
54
|
|
@@ -57,8 +59,16 @@ def register_provider(provider_key, provider):
|
|
57
59
|
|
58
60
|
|
59
61
|
class MethodAdapter:
|
60
|
-
def __init__(
|
61
|
-
|
62
|
+
def __init__(
|
63
|
+
self,
|
64
|
+
args=(),
|
65
|
+
return_type="",
|
66
|
+
eth_method=None,
|
67
|
+
adapt_args=None,
|
68
|
+
is_property=False,
|
69
|
+
set_eth_method=None,
|
70
|
+
eth_variant=None,
|
71
|
+
):
|
62
72
|
self.eth_method = eth_method
|
63
73
|
self.set_eth_method = set_eth_method
|
64
74
|
self.return_type = return_type
|
@@ -76,8 +86,8 @@ class MethodAdapter:
|
|
76
86
|
|
77
87
|
@staticmethod
|
78
88
|
def snake_to_camel(name):
|
79
|
-
components = name.split(
|
80
|
-
return components[0] +
|
89
|
+
components = name.split("_")
|
90
|
+
return components[0] + "".join(x.title() for x in components[1:])
|
81
91
|
|
82
92
|
@property
|
83
93
|
def method_name(self):
|
@@ -85,8 +95,7 @@ class MethodAdapter:
|
|
85
95
|
|
86
96
|
def __get__(self, instance, owner=None):
|
87
97
|
eth_call = instance.eth_call(
|
88
|
-
self.eth_method, self.args, self.return_type, self.adapt_args,
|
89
|
-
eth_variant=self.eth_variant
|
98
|
+
self.eth_method, self.args, self.return_type, self.adapt_args, eth_variant=self.eth_variant
|
90
99
|
)
|
91
100
|
if self.is_property:
|
92
101
|
return eth_call(instance)
|
@@ -95,7 +104,7 @@ class MethodAdapter:
|
|
95
104
|
def __set__(self, instance, value):
|
96
105
|
if not self.is_property:
|
97
106
|
raise NotImplementedError()
|
98
|
-
eth_call = instance.eth_call(self.set_eth_method, (("new_value", self.return_type),
|
107
|
+
eth_call = instance.eth_call(self.set_eth_method, (("new_value", self.return_type),))
|
99
108
|
return eth_call(instance, value)
|
100
109
|
|
101
110
|
|
@@ -147,7 +156,7 @@ class ETHCall(ABC):
|
|
147
156
|
|
148
157
|
def __call__(self, wrapper, *args, **kwargs):
|
149
158
|
call_args = []
|
150
|
-
msg_args =
|
159
|
+
msg_args = wrapper.provider.tx_kwargs.copy()
|
151
160
|
|
152
161
|
if self.adapt_args:
|
153
162
|
args, kwargs = self.adapt_args(args, kwargs)
|
@@ -203,22 +212,26 @@ class ETHCall(ABC):
|
|
203
212
|
return call_args, msg_args
|
204
213
|
|
205
214
|
@classmethod
|
206
|
-
def _parse_keccak256(cls, value):
|
207
|
-
from Crypto.Hash import
|
208
|
-
|
215
|
+
def _parse_keccak256(cls, value) -> HexBytes:
|
216
|
+
from Crypto.Hash import (
|
217
|
+
keccak, # To avoid import of wrappers breaks if keccak not installed
|
218
|
+
)
|
219
|
+
|
220
|
+
if isinstance(value, HexBytes):
|
209
221
|
return value
|
222
|
+
|
223
|
+
if value.startswith("0x"):
|
224
|
+
return HexBytes(value)
|
210
225
|
k = keccak.new(digest_bits=256)
|
211
226
|
k.update(value.encode("utf-8"))
|
212
|
-
return k.hexdigest()
|
227
|
+
return HexBytes(k.hexdigest())
|
213
228
|
|
214
229
|
@classmethod
|
215
230
|
def unparse(cls, wrapper, value_type, value):
|
216
231
|
if value_type.startswith("(") and value_type.endswith(")"):
|
217
232
|
# It's a tuple / struct
|
218
233
|
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
|
-
)
|
234
|
+
return tuple(cls.unparse(wrapper, vt, value[i]) for i, vt in enumerate(value_types))
|
222
235
|
if value_type == "amount":
|
223
236
|
return AMOUNT_CLASS(value)
|
224
237
|
if value_type == "ray":
|
@@ -240,6 +253,8 @@ class ETHCall(ABC):
|
|
240
253
|
|
241
254
|
|
242
255
|
class BaseProvider(ABC):
|
256
|
+
tx_kwargs = {}
|
257
|
+
|
243
258
|
@abstractmethod
|
244
259
|
def get_contract_factory(self, eth_contract):
|
245
260
|
raise NotImplementedError()
|
@@ -259,8 +274,9 @@ class BaseProvider(ABC):
|
|
259
274
|
return 0
|
260
275
|
address = self.get_contract_address(eth_wrapper)
|
261
276
|
url = (
|
262
|
-
etherscan_url
|
263
|
-
"
|
277
|
+
etherscan_url
|
278
|
+
+ f"&module=account&action=txlist&address={address}&startblock=0&"
|
279
|
+
+ "endblock=99999999&page=1&offset=10&sort=asc"
|
264
280
|
)
|
265
281
|
resp = requests.get(url)
|
266
282
|
resp.raise_for_status()
|
@@ -305,9 +321,9 @@ class ETHWrapper:
|
|
305
321
|
else:
|
306
322
|
if self.constructor_args:
|
307
323
|
constructor_params, transaction_kwargs = self.eth_call.parse_args(
|
308
|
-
self, self.constructor_args, *init_params[:len(self.constructor_args)], **kwargs
|
324
|
+
self, self.constructor_args, *init_params[: len(self.constructor_args)], **kwargs
|
309
325
|
)
|
310
|
-
init_params = init_params[len(self.constructor_args):]
|
326
|
+
init_params = init_params[len(self.constructor_args) :]
|
311
327
|
if transaction_kwargs:
|
312
328
|
kwargs.update(transaction_kwargs)
|
313
329
|
else:
|
@@ -357,6 +373,31 @@ class ETHWrapper:
|
|
357
373
|
obj._auto_from = obj.owner
|
358
374
|
return obj
|
359
375
|
|
376
|
+
@classmethod
|
377
|
+
def build_from_def(cls, contract_def, extra_properties=None):
|
378
|
+
extra_properties = extra_properties or {}
|
379
|
+
attributes = {}
|
380
|
+
for item in contract_def.abi:
|
381
|
+
if item["type"] == "constructor":
|
382
|
+
attributes["constructor_args"] = tuple((arg["name"], arg["type"]) for arg in item["inputs"])
|
383
|
+
elif item["type"] == "function":
|
384
|
+
attributes[item["name"]] = MethodAdapter(
|
385
|
+
args=tuple((arg["name"], arg["type"]) for arg in item["inputs"]),
|
386
|
+
return_type=item["outputs"][0]["type"] if item["outputs"] else "",
|
387
|
+
eth_method=item["name"],
|
388
|
+
eth_variant=item["inputs"],
|
389
|
+
)
|
390
|
+
wrapper_class = type(
|
391
|
+
contract_def.contract_name,
|
392
|
+
(cls,),
|
393
|
+
{
|
394
|
+
"eth_contract": contract_def.contract_name,
|
395
|
+
**attributes,
|
396
|
+
**extra_properties,
|
397
|
+
},
|
398
|
+
)
|
399
|
+
return wrapper_class
|
400
|
+
|
360
401
|
@property
|
361
402
|
def contract_id(self):
|
362
403
|
return self.contract.address
|
@@ -371,8 +412,8 @@ class ETHWrapper:
|
|
371
412
|
revoke_role = MethodAdapter((("role", "keccak256"), ("user", "address")))
|
372
413
|
renounce_role = MethodAdapter((("role", "keccak256"), ("user", "address")))
|
373
414
|
has_role = MethodAdapter((("role", "keccak256"), ("user", "address")), "bool")
|
374
|
-
get_role_admin = MethodAdapter((("role", "keccak256"),
|
375
|
-
get_role_admin = MethodAdapter((("role", "keccak256"),
|
415
|
+
get_role_admin = MethodAdapter((("role", "keccak256"),), "address")
|
416
|
+
get_role_admin = MethodAdapter((("role", "keccak256"),), "bytes32")
|
376
417
|
|
377
418
|
@contextmanager
|
378
419
|
def as_(self, user):
|
@@ -397,14 +438,15 @@ class IERC20(ETHWrapper):
|
|
397
438
|
symbol = MethodAdapter((), "string", is_property=True)
|
398
439
|
decimals = MethodAdapter((), "int", is_property=True)
|
399
440
|
total_supply = MethodAdapter((), "amount")
|
400
|
-
balance_of = MethodAdapter((("account", "address"),
|
401
|
-
transfer = MethodAdapter(
|
402
|
-
("sender", "msg.sender"), ("recipient", "address"), ("amount", "amount")
|
403
|
-
)
|
441
|
+
balance_of = MethodAdapter((("account", "address"),), "amount")
|
442
|
+
transfer = MethodAdapter(
|
443
|
+
(("sender", "msg.sender"), ("recipient", "address"), ("amount", "amount")), "receipt"
|
444
|
+
)
|
404
445
|
|
405
446
|
allowance = MethodAdapter((("owner", "address"), ("spender", "address")), "amount")
|
406
|
-
approve = MethodAdapter(
|
407
|
-
|
447
|
+
approve = MethodAdapter(
|
448
|
+
(("owner", "msg.sender"), ("spender", "address"), ("amount", "amount")), "receipt"
|
449
|
+
)
|
408
450
|
increase_allowance = MethodAdapter(
|
409
451
|
(("owner", "msg.sender"), ("spender", "address"), ("amount", "amount"))
|
410
452
|
)
|
@@ -412,9 +454,10 @@ class IERC20(ETHWrapper):
|
|
412
454
|
(("owner", "msg.sender"), ("spender", "address"), ("amount", "amount"))
|
413
455
|
)
|
414
456
|
|
415
|
-
transfer_from = MethodAdapter(
|
416
|
-
("spender", "msg.sender"), ("sender", "address"), ("recipient", "address"), ("amount", "amount")
|
417
|
-
|
457
|
+
transfer_from = MethodAdapter(
|
458
|
+
(("spender", "msg.sender"), ("sender", "address"), ("recipient", "address"), ("amount", "amount")),
|
459
|
+
"receipt",
|
460
|
+
)
|
418
461
|
|
419
462
|
|
420
463
|
class IERC721(ETHWrapper):
|
@@ -423,19 +466,21 @@ class IERC721(ETHWrapper):
|
|
423
466
|
name = MethodAdapter((), "string", is_property=True)
|
424
467
|
symbol = MethodAdapter((), "string", is_property=True)
|
425
468
|
total_supply = MethodAdapter((), "int")
|
426
|
-
balance_of = MethodAdapter((("account", "address"),
|
427
|
-
owner_of = MethodAdapter((("token_id", "int"),
|
428
|
-
approve = MethodAdapter(
|
429
|
-
("sender", "msg.sender"), ("spender", "address"), ("token_id", "int")
|
430
|
-
)
|
431
|
-
get_approved = MethodAdapter((("token_id", "int"),
|
432
|
-
set_approval_for_all = MethodAdapter(
|
433
|
-
("sender", "msg.sender"), ("operator", "address"), ("approved", "bool")
|
434
|
-
)
|
469
|
+
balance_of = MethodAdapter((("account", "address"),), "int")
|
470
|
+
owner_of = MethodAdapter((("token_id", "int"),), "address")
|
471
|
+
approve = MethodAdapter(
|
472
|
+
(("sender", "msg.sender"), ("spender", "address"), ("token_id", "int")), "receipt"
|
473
|
+
)
|
474
|
+
get_approved = MethodAdapter((("token_id", "int"),), "address")
|
475
|
+
set_approval_for_all = MethodAdapter(
|
476
|
+
(("sender", "msg.sender"), ("operator", "address"), ("approved", "bool"))
|
477
|
+
)
|
435
478
|
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
|
-
)
|
439
|
-
safe_transfer_from = MethodAdapter(
|
440
|
-
("spender", "msg.sender"), ("from", "address"), ("to", "address"), ("token_id", "int")
|
441
|
-
|
479
|
+
transfer_from = MethodAdapter(
|
480
|
+
(("spender", "msg.sender"), ("from", "address"), ("to", "address"), ("token_id", "int")), "receipt"
|
481
|
+
)
|
482
|
+
safe_transfer_from = MethodAdapter(
|
483
|
+
(("spender", "msg.sender"), ("from", "address"), ("to", "address"), ("token_id", "int")),
|
484
|
+
"receipt",
|
485
|
+
eth_variant="address, address, uint256",
|
486
|
+
)
|
@@ -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)
|
File without changes
|
File without changes
|
File without changes
|