Skip to content

Commit ca3f1e5

Browse files
feat: add conversion api (#89)
* feat: add non-checksummed address converter (with warning) * refactor: use internal AddressType instead of ChecksumAddress * feat: add AddressAPI converter to address * refactor: add conversion manager dep to account manager * refactor: display accounts in repr * feat: added support for ether types in account calls * test: add tests for ether conversion fn * feat: add conversion for Ether units * feat: add ConversionAPI * fix: updates eth-account requirement in setup.py to fix breaking change from pull 103 (#107) Co-authored-by: Doggo <[email protected]>
1 parent 94c5330 commit ca3f1e5

File tree

19 files changed

+295
-35
lines changed

19 files changed

+295
-35
lines changed

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
python-version: 3.8
3939

4040
- name: Install Dependencies
41-
run: pip install .[lint]
41+
run: pip install .[lint,test]
4242

4343
- name: Run MyPy
4444
run: mypy .

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ markers = "fuzzing: Run Hypothesis fuzz test suite"
2222
line_length = 100
2323
force_grid_wrap = 0
2424
include_trailing_comma = true
25-
known_third_party = ["IPython", "click", "dataclassy", "eth_abi", "eth_account", "eth_utils", "github", "hexbytes", "hypothesis", "hypothesis_jsonschema", "importlib_metadata", "pluggy", "pytest", "requests", "setuptools", "web3", "yaml"]
25+
known_third_party = ["IPython", "ape_ethereum", "click", "dataclassy", "eth_abi", "eth_account", "eth_typing", "eth_utils", "github", "hexbytes", "hypothesis", "hypothesis_jsonschema", "importlib_metadata", "pluggy", "pytest", "requests", "setuptools", "web3", "yaml"]
2626
known_first_party = ["ape_accounts", "ape_console", "ape"]
2727
multi_line_output = 3
2828
use_parentheses = true

src/ape/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .managers.accounts import AccountManager as _AccountManager
77
from .managers.compilers import CompilerManager as _CompilerManager
88
from .managers.config import ConfigManager as _ConfigManager
9+
from .managers.converters import ConversionManager as _ConversionManager
910
from .managers.networks import NetworkManager as _NetworkManager
1011
from .managers.project import ProjectManager as _ProjectManager
1112
from .plugins import PluginManager as _PluginManager
@@ -37,18 +38,21 @@
3738
# Main types we export for the user
3839
compilers = _CompilerManager(config, plugin_manager) # type: ignore
3940
networks = _NetworkManager(config, plugin_manager) # type: ignore
40-
accounts = _AccountManager(config, plugin_manager, networks) # type: ignore
41+
_converters = _ConversionManager(config, plugin_manager, networks) # type: ignore
42+
accounts = _AccountManager(config, _converters, plugin_manager, networks) # type: ignore
4143

4244
Project = _partial(_ProjectManager, config=config, compilers=compilers)
4345
project = Project(config.PROJECT_FOLDER)
4446

45-
Contract = _partial(_Contract, networks=networks)
47+
Contract = _partial(_Contract, networks=networks, converters=_converters)
4648

49+
convert = _converters.convert
4750

4851
__all__ = [
4952
"accounts",
5053
"compilers",
5154
"config",
55+
"convert",
5256
"Contract",
5357
"networks",
5458
"project",

src/ape/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .accounts import AccountAPI, AccountContainerAPI
22
from .address import Address, AddressAPI
33
from .contracts import ContractLog
4+
from .convert import ConverterAPI
45
from .explorers import ExplorerAPI
56
from .networks import EcosystemAPI, NetworkAPI, ProviderContextManager, create_network_type
67
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI, TransactionStatusEnum
@@ -12,6 +13,7 @@
1213
"AddressAPI",
1314
"ContractInstance",
1415
"ContractLog",
16+
"ConverterAPI",
1517
"EcosystemAPI",
1618
"ExplorerAPI",
1719
"ProviderAPI",

src/ape/api/accounts.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from pathlib import Path
2-
from typing import Iterator, List, Optional, Type, Union
2+
from typing import Callable, Iterator, List, Optional, Type, Union
33

44
from eth_account.datastructures import SignedMessage # type: ignore
55
from eth_account.messages import SignableMessage # type: ignore
66

7-
from ape.types import ContractType
7+
from ape.types import AddressType, ContractType
8+
from ape.utils import cached_property
89

910
from .address import AddressAPI
1011
from .base import abstractdataclass, abstractmethod
@@ -57,20 +58,31 @@ def call(self, txn: TransactionAPI) -> ReceiptAPI:
5758

5859
return self.provider.send_transaction(signed_txn)
5960

61+
@cached_property
62+
def _convert(self) -> Callable:
63+
# NOTE: Need to differ loading this property
64+
from ape import convert
65+
66+
return convert
67+
6068
def transfer(
6169
self,
62-
account: Union[str, "AddressAPI"],
63-
value: int = None,
70+
account: Union[str, AddressType, "AddressAPI"],
71+
value: Union[str, int, None] = None,
72+
data: Union[bytes, str, None] = None,
6473
**kwargs,
6574
) -> ReceiptAPI:
6675
txn = self._transaction_class( # type: ignore
6776
sender=self.address,
68-
receiver=account.address if isinstance(account, AddressAPI) else account,
77+
receiver=self._convert(account, AddressType),
6978
**kwargs,
7079
)
7180

81+
if data:
82+
txn.data = self._convert(data, bytes)
83+
7284
if value:
73-
txn.value = value
85+
txn.value = self._convert(value, int)
7486

7587
else:
7688
# NOTE: If `value` is `None`, send everything
@@ -116,7 +128,7 @@ def __len__(self) -> int:
116128
def __iter__(self) -> Iterator[AccountAPI]:
117129
...
118130

119-
def __getitem__(self, address: str) -> AccountAPI:
131+
def __getitem__(self, address: AddressType) -> AccountAPI:
120132
for account in self.__iter__():
121133
if account.address == address:
122134
return account
@@ -135,7 +147,7 @@ def append(self, account: AccountAPI):
135147

136148
self.__setitem__(account.address, account)
137149

138-
def __setitem__(self, address: str, account: AccountAPI):
150+
def __setitem__(self, address: AddressType, account: AccountAPI):
139151
raise NotImplementedError("Must define this method to use `container.append(acct)`")
140152

141153
def remove(self, account: AccountAPI):
@@ -150,10 +162,10 @@ def remove(self, account: AccountAPI):
150162

151163
self.__delitem__(account.address)
152164

153-
def __delitem__(self, address: str):
165+
def __delitem__(self, address: AddressType):
154166
raise NotImplementedError("Must define this method to use `container.remove(acct)`")
155167

156-
def __contains__(self, address: str) -> bool:
168+
def __contains__(self, address: AddressType) -> bool:
157169
try:
158170
self.__getitem__(address)
159171
return True

src/ape/api/address.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import List, Optional, Type
22

3+
from ape.types import AddressType
4+
35
from .base import abstractdataclass, abstractmethod
46
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI
57

@@ -25,7 +27,7 @@ def _transaction_class(self) -> Type[TransactionAPI]:
2527

2628
@property
2729
@abstractmethod
28-
def address(self) -> str:
30+
def address(self) -> AddressType:
2931
...
3032

3133
def __dir__(self) -> List[str]:
@@ -69,8 +71,8 @@ def is_contract(self) -> bool:
6971

7072

7173
class Address(AddressAPI):
72-
_address: str
74+
_address: AddressType
7375

7476
@property
75-
def address(self) -> str:
77+
def address(self) -> AddressType:
7678
return self._address

src/ape/api/contracts.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
from typing import TYPE_CHECKING, Any, Dict, List, Optional
1+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
22

33
from eth_utils import to_bytes
44

5-
from ape.types import ABI, ContractType
5+
from ape.types import ABI, AddressType, ContractType
66
from ape.utils import notify
77

88
from .address import Address, AddressAPI
99
from .base import dataclass
1010
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI
1111

1212
if TYPE_CHECKING:
13+
from ape.managers.converters import ConversionManager
1314
from ape.managers.networks import NetworkManager
1415

1516

@@ -47,7 +48,7 @@ def __call__(self, *args, **kwargs) -> ReceiptAPI:
4748
@dataclass
4849
class ContractCall:
4950
abi: ABI
50-
address: str
51+
address: AddressType
5152
provider: ProviderAPI
5253

5354
def __repr__(self) -> str:
@@ -82,7 +83,7 @@ def __call__(self, *args, **kwargs) -> Any:
8283
@dataclass
8384
class ContractCallHandler:
8485
provider: ProviderAPI
85-
address: str
86+
address: AddressType
8687
abis: List[ABI]
8788

8889
def __repr__(self) -> str:
@@ -108,7 +109,7 @@ def __call__(self, *args, **kwargs) -> Any:
108109
@dataclass
109110
class ContractTransaction:
110111
abi: ABI
111-
address: str
112+
address: AddressType
112113
provider: ProviderAPI
113114

114115
def __repr__(self) -> str:
@@ -135,7 +136,7 @@ def __call__(self, *args, **kwargs) -> ReceiptAPI:
135136
@dataclass
136137
class ContractTransactionHandler:
137138
provider: ProviderAPI
138-
address: str
139+
address: AddressType
139140
abis: List[ABI]
140141

141142
def __repr__(self) -> str:
@@ -173,14 +174,14 @@ class ContractEvent:
173174

174175

175176
class ContractInstance(AddressAPI):
176-
_address: str
177+
_address: AddressType
177178
_contract_type: ContractType
178179

179180
def __repr__(self) -> str:
180181
return f"<{self._contract_type.contractName} {self.address}>"
181182

182183
@property
183-
def address(self) -> str:
184+
def address(self) -> AddressType:
184185
return self._address
185186

186187
def __dir__(self) -> List[str]:
@@ -272,8 +273,9 @@ def __call__(self, *args, **kwargs) -> TransactionAPI:
272273

273274

274275
def _Contract(
275-
address: str,
276+
address: Union[str, AddressAPI, AddressType],
276277
networks: "NetworkManager",
278+
converters: "ConversionManager",
277279
contract_type: Optional[ContractType] = None,
278280
) -> AddressAPI:
279281
"""
@@ -301,7 +303,7 @@ def _Contract(
301303
# 3) from explorer
302304
if contract_type:
303305
return ContractInstance( # type: ignore
304-
_address=address,
306+
_address=converters.convert(address, AddressType),
305307
_provider=networks.active_provider,
306308
_contract_type=contract_type,
307309
)
@@ -310,6 +312,6 @@ def _Contract(
310312
# We don't have a contract type from any source, provide raw address instead
311313
notify("WARNING", f"No contract type found for {address}")
312314
return Address( # type: ignore
313-
_address=address,
315+
_address=converters.convert(address, AddressType),
314316
_provider=networks.active_provider,
315317
)

src/ape/api/convert.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
2+
3+
from .base import abstractdataclass, abstractmethod
4+
from .config import ConfigItem
5+
6+
if TYPE_CHECKING:
7+
from ape.managers.networks import NetworkManager
8+
9+
ConvertedType = TypeVar("ConvertedType")
10+
11+
12+
@abstractdataclass
13+
class ConverterAPI(Generic[ConvertedType]):
14+
# NOTE: In case we need to store info e.g. tokenlists
15+
config: ConfigItem
16+
17+
# NOTE: In case we need access to a network e.g. ENS
18+
networks: "NetworkManager"
19+
20+
@abstractmethod
21+
def is_convertible(self, value: Any) -> bool:
22+
"""
23+
Returns `True` if string value provided by `value` is convertible using
24+
`self.convert(value)`
25+
"""
26+
27+
@abstractmethod
28+
def convert(self, value: Any) -> ConvertedType:
29+
"""
30+
Implements any conversion logic on `value` to produce `ABIType`.
31+
32+
Must throw if not convertible.
33+
"""

src/ape/api/networks.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from pluggy import PluginManager # type: ignore
66

7-
from ape.types import ABI
7+
from ape.types import ABI, AddressType
88
from ape.utils import cached_property
99

1010
from .base import abstractdataclass, abstractmethod
@@ -114,7 +114,9 @@ def encode_deployment(
114114
...
115115

116116
@abstractmethod
117-
def encode_transaction(self, address: str, abi: ABI, *args, **kwargs) -> "TransactionAPI":
117+
def encode_transaction(
118+
self, address: AddressType, abi: ABI, *args, **kwargs
119+
) -> "TransactionAPI":
118120
...
119121

120122
@abstractmethod

src/ape/managers/accounts.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from pluggy import PluginManager # type: ignore
55

66
from ape.api.accounts import AccountAPI, AccountContainerAPI
7+
from ape.types import AddressType
78
from ape.utils import cached_property, singledispatchmethod
89

910
from .config import ConfigManager
11+
from .converters import ConversionManager
1012
from .networks import NetworkManager
1113

1214

@@ -18,6 +20,7 @@ class AccountManager:
1820
"""
1921

2022
config: ConfigManager
23+
converters: ConversionManager
2124
plugin_manager: PluginManager
2225
network_manager: NetworkManager
2326

@@ -49,6 +52,9 @@ def __iter__(self) -> Iterator[AccountAPI]:
4952
account._provider = self.network_manager.active_provider
5053
yield account
5154

55+
def __repr__(self) -> str:
56+
return "[" + ", ".join(repr(a) for a in self) + "]"
57+
5258
def load(self, alias: str) -> AccountAPI:
5359
if alias == "":
5460
raise ValueError("Cannot use empty string as alias!")
@@ -76,7 +82,9 @@ def __getitem_int(self, account_id: int) -> AccountAPI:
7682
raise IndexError(f"No account at index `{account_id}`.")
7783

7884
@__getitem__.register
79-
def __getitem_str(self, account_id: str) -> AccountAPI:
85+
def __getitem_str(self, account_str: str) -> AccountAPI:
86+
account_id = self.converters.convert(account_str, AddressType)
87+
8088
for container in self.containers.values():
8189
if account_id in container:
8290
account = container[account_id]
@@ -86,5 +94,5 @@ def __getitem_str(self, account_id: str) -> AccountAPI:
8694

8795
raise IndexError(f"No account with address `{account_id}`.")
8896

89-
def __contains__(self, address: str) -> bool:
97+
def __contains__(self, address: AddressType) -> bool:
9098
return any(address in container for container in self.containers.values())

0 commit comments

Comments
 (0)