Skip to content

Commit 93eb5bc

Browse files
authored
feat: add transactions (#38)
* feat: add AccountAPI.transfer * feat: support for contract deployment and calls * feat: add ability to load generic contracts * feat: add selector and signature properties to ABI dataclasses * feat: add support for contract interactions * fix: Ensure we don't try to deploy empty bytecode * fix: need to pass address to create transaction; other fixes * fix: handle ABI encoding correctly * lint: ignore mypy error in import * feat: add tab completion for contracts and accounts/addresses * fix: display signature properly * fix: unclosed file handle * refactor: display prettier transaction to sign * refactor: have signing a transaction also return Optional * fix: display strings and generic Exceptions to show full stack * feat: display signature of longest function when showing call handlers * refactor: simplify handler code * refactor: don't export `Contract` helper function * fix: raise internal exception on password mismatch
1 parent 64da298 commit 93eb5bc

File tree

16 files changed

+842
-108
lines changed

16 files changed

+842
-108
lines changed

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_account", "eth_utils", "github", "hypothesis", "hypothesis_jsonschema", "importlib_metadata", "pluggy", "pytest", "requests", "setuptools", "web3", "yaml"]
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"]
2626
known_first_party = ["ape_accounts", "ape"]
2727
multi_line_output = 3
2828
use_parentheses = true

src/ape/__init__.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import sys as _sys
2+
from functools import partial as _partial
23
from pathlib import Path as _Path
34

5+
from .api.contracts import _Contract
46
from .managers.accounts import AccountManager as _AccountManager
57
from .managers.compilers import CompilerManager as _CompilerManager
68
from .managers.config import ConfigManager as _ConfigManager
@@ -37,20 +39,17 @@
3739
networks = _NetworkManager(config, plugin_manager) # type: ignore
3840
accounts = _AccountManager(config, plugin_manager, networks) # type: ignore
3941

40-
41-
def Project(path):
42-
if isinstance(path, str):
43-
path = _Path(path)
44-
return _ProjectManager(path=path, config=config, compilers=compilers)
45-
46-
42+
Project = _partial(_ProjectManager, config=config, compilers=compilers)
4743
project = Project(config.PROJECT_FOLDER)
4844

45+
Contract = _partial(_Contract, networks=networks)
46+
4947

5048
__all__ = [
5149
"accounts",
5250
"compilers",
5351
"config",
52+
"Contract",
5453
"networks",
5554
"project",
5655
"Project", # So you can load other projects

src/ape/api/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
from .accounts import AccountAPI, AccountContainerAPI
2+
from .address import Address, AddressAPI
3+
from .contracts import ContractLog
24
from .explorers import ExplorerAPI
35
from .networks import EcosystemAPI, NetworkAPI, ProviderContextManager, create_network_type
4-
from .providers import ProviderAPI
6+
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI, TransactionStatusEnum
57

68
__all__ = [
79
"AccountAPI",
810
"AccountContainerAPI",
11+
"Address",
12+
"AddressAPI",
13+
"ContractInstance",
14+
"ContractLog",
915
"EcosystemAPI",
1016
"ExplorerAPI",
1117
"ProviderAPI",
1218
"ProviderContextManager",
1319
"NetworkAPI",
20+
"ReceiptAPI",
21+
"TransactionAPI",
22+
"TransactionStatusEnum",
1423
"create_network_type",
1524
]

src/ape/api/accounts.py

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,103 @@
11
from pathlib import Path
2-
from typing import TYPE_CHECKING, Iterator, Optional, Type
2+
from typing import Iterator, List, Optional, Type, Union
33

44
from eth_account.datastructures import SignedMessage # type: ignore
5-
from eth_account.datastructures import SignedTransaction
65
from eth_account.messages import SignableMessage # type: ignore
76

7+
from ape.types import ContractType
8+
9+
from .address import AddressAPI
810
from .base import abstractdataclass, abstractmethod
11+
from .contracts import ContractContainer, ContractInstance
12+
from .providers import ReceiptAPI, TransactionAPI
913

10-
if TYPE_CHECKING:
11-
from ape.managers.networks import NetworkManager
1214

15+
# NOTE: AddressAPI is a dataclass already
16+
class AccountAPI(AddressAPI):
17+
container: "AccountContainerAPI"
1318

14-
@abstractdataclass
15-
class AddressAPI:
16-
network_manager: Optional["NetworkManager"] = None
19+
def __dir__(self) -> List[str]:
20+
# This displays methods to IPython on `a.[TAB]` tab completion
21+
return list(super(AddressAPI, self).__dir__()) + [
22+
"alias",
23+
"sign_message",
24+
"sign_transaction",
25+
"call",
26+
"transfer",
27+
"deploy",
28+
]
1729

1830
@property
19-
def _provider(self):
20-
if not self.network_manager:
21-
raise Exception("Not wired correctly")
22-
23-
if not self.network_manager.active_provider:
24-
raise Exception("Not connected to any network!")
25-
26-
return self.network_manager.active_provider
31+
def alias(self) -> Optional[str]:
32+
"""
33+
Override with whatever alias might want to use, if applicable
34+
"""
35+
return None
2736

28-
@property
2937
@abstractmethod
30-
def address(self) -> str:
38+
def sign_message(self, msg: SignableMessage) -> Optional[SignedMessage]:
3139
...
3240

33-
def __repr__(self) -> str:
34-
return f"<{self.__class__.__name__} {self.address}>"
41+
def sign_transaction(self, txn: TransactionAPI) -> Optional[TransactionAPI]:
42+
# NOTE: Some accounts may not offer signing things
43+
return txn
3544

36-
def __str__(self) -> str:
37-
return self.address
45+
def call(self, txn: TransactionAPI) -> ReceiptAPI:
46+
txn.nonce = self.nonce
47+
txn.gas_limit = self.provider.estimate_gas_cost(txn)
48+
txn.gas_price = self.provider.gas_price
3849

39-
@property
40-
def nonce(self) -> int:
41-
return self._provider.get_nonce(self.address)
50+
if txn.gas_limit * txn.gas_price + txn.value > self.balance:
51+
raise Exception("Transfer value meets or exceeds account balance")
4252

43-
@property
44-
def balance(self) -> int:
45-
return self._provider.get_balance(self.address)
53+
signed_txn = self.sign_transaction(txn)
4654

47-
@property
48-
def code(self) -> bytes:
49-
# TODO: Explore caching this (based on `self.provider.network` and examining code)
50-
return self._provider.get_code(self.address)
55+
if not signed_txn:
56+
raise Exception("User didn't sign!")
5157

52-
@property
53-
def codesize(self) -> int:
54-
return len(self.code)
58+
return self.provider.send_transaction(signed_txn)
5559

56-
@property
57-
def is_contract(self) -> bool:
58-
return len(self.code) > 0
60+
def transfer(
61+
self,
62+
account: Union[str, "AddressAPI"],
63+
value: int = None,
64+
data: bytes = None,
65+
) -> ReceiptAPI:
66+
txn = self._transaction_class( # type: ignore
67+
sender=self.address,
68+
receiver=account.address if isinstance(account, AddressAPI) else account,
69+
)
5970

71+
if data:
72+
txn.data = data
6073

61-
# NOTE: AddressAPI is a dataclass already
62-
class AccountAPI(AddressAPI):
63-
container: "AccountContainerAPI"
74+
if value:
75+
txn.value = value
6476

65-
@property
66-
def alias(self) -> Optional[str]:
67-
"""
68-
Override with whatever alias might want to use, if applicable
69-
"""
70-
return None
77+
else:
78+
# NOTE: If `value` is `None`, send everything
79+
txn.value = self.balance - txn.gas_limit * txn.gas_price
7180

72-
@abstractmethod
73-
def sign_message(self, msg: SignableMessage) -> Optional[SignedMessage]:
74-
...
81+
return self.call(txn)
7582

76-
@abstractmethod
77-
def sign_transaction(self, txn: dict) -> Optional[SignedTransaction]:
78-
...
83+
def deploy(self, contract_type: ContractType, *args, **kwargs) -> ContractInstance:
84+
c = ContractContainer( # type: ignore
85+
_provider=self.provider,
86+
_contract_type=contract_type,
87+
)
88+
89+
txn = c(*args, **kwargs)
90+
txn.sender = self.address
91+
receipt = self.call(txn)
92+
93+
if not receipt.contract_address:
94+
raise Exception(f"{receipt.txn_hash} did not create a contract")
95+
96+
return ContractInstance( # type: ignore
97+
_provider=self.provider,
98+
_address=receipt.contract_address,
99+
_contract_type=contract_type,
100+
)
79101

80102

81103
@abstractdataclass

src/ape/api/address.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from typing import List, Optional, Type
2+
3+
from .base import abstractdataclass, abstractmethod
4+
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI
5+
6+
7+
@abstractdataclass
8+
class AddressAPI:
9+
_provider: Optional[ProviderAPI] = None
10+
11+
@property
12+
def provider(self) -> ProviderAPI:
13+
if not self._provider:
14+
raise Exception("Wired incorrectly")
15+
16+
return self._provider
17+
18+
@property
19+
def _receipt_class(self) -> Type[ReceiptAPI]:
20+
return self.provider.network.ecosystem.receipt_class
21+
22+
@property
23+
def _transaction_class(self) -> Type[TransactionAPI]:
24+
return self.provider.network.ecosystem.transaction_class
25+
26+
@property
27+
@abstractmethod
28+
def address(self) -> str:
29+
...
30+
31+
def __dir__(self) -> List[str]:
32+
# This displays methods to IPython on `a.[TAB]` tab completion
33+
return [
34+
"address",
35+
"balance",
36+
"code",
37+
"codesize",
38+
"nonce",
39+
"is_contract",
40+
"provider",
41+
]
42+
43+
def __repr__(self) -> str:
44+
return f"<{self.__class__.__name__} {self.address}>"
45+
46+
def __str__(self) -> str:
47+
return self.address
48+
49+
@property
50+
def nonce(self) -> int:
51+
return self.provider.get_nonce(self.address)
52+
53+
@property
54+
def balance(self) -> int:
55+
return self.provider.get_balance(self.address)
56+
57+
@property
58+
def code(self) -> bytes:
59+
# TODO: Explore caching this (based on `self.provider.network` and examining code)
60+
return self.provider.get_code(self.address)
61+
62+
@property
63+
def codesize(self) -> int:
64+
return len(self.code)
65+
66+
@property
67+
def is_contract(self) -> bool:
68+
return len(self.code) > 0
69+
70+
71+
class Address(AddressAPI):
72+
_address: str
73+
74+
@property
75+
def address(self) -> str:
76+
return self._address

src/ape/api/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ class AbstractDataClassMeta(DataClassMeta, ABCMeta):
99
pass
1010

1111

12-
abstractdataclass = partial(dataclass, meta=AbstractDataClassMeta)
12+
abstractdataclass = partial(dataclass, kwargs=True, meta=AbstractDataClassMeta)
13+
1314

1415
__all__ = [
1516
"abstractdataclass",

0 commit comments

Comments
 (0)