Skip to content

Commit

Permalink
feat: add transactions (#38)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
fubuloubu authored Jun 30, 2021
1 parent 64da298 commit 93eb5bc
Show file tree
Hide file tree
Showing 16 changed files with 842 additions and 108 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ markers = "fuzzing: Run Hypothesis fuzz test suite"
line_length = 100
force_grid_wrap = 0
include_trailing_comma = true
known_third_party = ["IPython", "click", "dataclassy", "eth_account", "eth_utils", "github", "hypothesis", "hypothesis_jsonschema", "importlib_metadata", "pluggy", "pytest", "requests", "setuptools", "web3", "yaml"]
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"]
known_first_party = ["ape_accounts", "ape"]
multi_line_output = 3
use_parentheses = true
13 changes: 6 additions & 7 deletions src/ape/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import sys as _sys
from functools import partial as _partial
from pathlib import Path as _Path

from .api.contracts import _Contract
from .managers.accounts import AccountManager as _AccountManager
from .managers.compilers import CompilerManager as _CompilerManager
from .managers.config import ConfigManager as _ConfigManager
Expand Down Expand Up @@ -37,20 +39,17 @@
networks = _NetworkManager(config, plugin_manager) # type: ignore
accounts = _AccountManager(config, plugin_manager, networks) # type: ignore


def Project(path):
if isinstance(path, str):
path = _Path(path)
return _ProjectManager(path=path, config=config, compilers=compilers)


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

Contract = _partial(_Contract, networks=networks)


__all__ = [
"accounts",
"compilers",
"config",
"Contract",
"networks",
"project",
"Project", # So you can load other projects
Expand Down
11 changes: 10 additions & 1 deletion src/ape/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
from .accounts import AccountAPI, AccountContainerAPI
from .address import Address, AddressAPI
from .contracts import ContractLog
from .explorers import ExplorerAPI
from .networks import EcosystemAPI, NetworkAPI, ProviderContextManager, create_network_type
from .providers import ProviderAPI
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI, TransactionStatusEnum

__all__ = [
"AccountAPI",
"AccountContainerAPI",
"Address",
"AddressAPI",
"ContractInstance",
"ContractLog",
"EcosystemAPI",
"ExplorerAPI",
"ProviderAPI",
"ProviderContextManager",
"NetworkAPI",
"ReceiptAPI",
"TransactionAPI",
"TransactionStatusEnum",
"create_network_type",
]
126 changes: 74 additions & 52 deletions src/ape/api/accounts.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,103 @@
from pathlib import Path
from typing import TYPE_CHECKING, Iterator, Optional, Type
from typing import Iterator, List, Optional, Type, Union

from eth_account.datastructures import SignedMessage # type: ignore
from eth_account.datastructures import SignedTransaction
from eth_account.messages import SignableMessage # type: ignore

from ape.types import ContractType

from .address import AddressAPI
from .base import abstractdataclass, abstractmethod
from .contracts import ContractContainer, ContractInstance
from .providers import ReceiptAPI, TransactionAPI

if TYPE_CHECKING:
from ape.managers.networks import NetworkManager

# NOTE: AddressAPI is a dataclass already
class AccountAPI(AddressAPI):
container: "AccountContainerAPI"

@abstractdataclass
class AddressAPI:
network_manager: Optional["NetworkManager"] = None
def __dir__(self) -> List[str]:
# This displays methods to IPython on `a.[TAB]` tab completion
return list(super(AddressAPI, self).__dir__()) + [
"alias",
"sign_message",
"sign_transaction",
"call",
"transfer",
"deploy",
]

@property
def _provider(self):
if not self.network_manager:
raise Exception("Not wired correctly")

if not self.network_manager.active_provider:
raise Exception("Not connected to any network!")

return self.network_manager.active_provider
def alias(self) -> Optional[str]:
"""
Override with whatever alias might want to use, if applicable
"""
return None

@property
@abstractmethod
def address(self) -> str:
def sign_message(self, msg: SignableMessage) -> Optional[SignedMessage]:
...

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

def __str__(self) -> str:
return self.address
def call(self, txn: TransactionAPI) -> ReceiptAPI:
txn.nonce = self.nonce
txn.gas_limit = self.provider.estimate_gas_cost(txn)
txn.gas_price = self.provider.gas_price

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

@property
def balance(self) -> int:
return self._provider.get_balance(self.address)
signed_txn = self.sign_transaction(txn)

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

@property
def codesize(self) -> int:
return len(self.code)
return self.provider.send_transaction(signed_txn)

@property
def is_contract(self) -> bool:
return len(self.code) > 0
def transfer(
self,
account: Union[str, "AddressAPI"],
value: int = None,
data: bytes = None,
) -> ReceiptAPI:
txn = self._transaction_class( # type: ignore
sender=self.address,
receiver=account.address if isinstance(account, AddressAPI) else account,
)

if data:
txn.data = data

# NOTE: AddressAPI is a dataclass already
class AccountAPI(AddressAPI):
container: "AccountContainerAPI"
if value:
txn.value = value

@property
def alias(self) -> Optional[str]:
"""
Override with whatever alias might want to use, if applicable
"""
return None
else:
# NOTE: If `value` is `None`, send everything
txn.value = self.balance - txn.gas_limit * txn.gas_price

@abstractmethod
def sign_message(self, msg: SignableMessage) -> Optional[SignedMessage]:
...
return self.call(txn)

@abstractmethod
def sign_transaction(self, txn: dict) -> Optional[SignedTransaction]:
...
def deploy(self, contract_type: ContractType, *args, **kwargs) -> ContractInstance:
c = ContractContainer( # type: ignore
_provider=self.provider,
_contract_type=contract_type,
)

txn = c(*args, **kwargs)
txn.sender = self.address
receipt = self.call(txn)

if not receipt.contract_address:
raise Exception(f"{receipt.txn_hash} did not create a contract")

return ContractInstance( # type: ignore
_provider=self.provider,
_address=receipt.contract_address,
_contract_type=contract_type,
)


@abstractdataclass
Expand Down
76 changes: 76 additions & 0 deletions src/ape/api/address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from typing import List, Optional, Type

from .base import abstractdataclass, abstractmethod
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI


@abstractdataclass
class AddressAPI:
_provider: Optional[ProviderAPI] = None

@property
def provider(self) -> ProviderAPI:
if not self._provider:
raise Exception("Wired incorrectly")

return self._provider

@property
def _receipt_class(self) -> Type[ReceiptAPI]:
return self.provider.network.ecosystem.receipt_class

@property
def _transaction_class(self) -> Type[TransactionAPI]:
return self.provider.network.ecosystem.transaction_class

@property
@abstractmethod
def address(self) -> str:
...

def __dir__(self) -> List[str]:
# This displays methods to IPython on `a.[TAB]` tab completion
return [
"address",
"balance",
"code",
"codesize",
"nonce",
"is_contract",
"provider",
]

def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.address}>"

def __str__(self) -> str:
return self.address

@property
def nonce(self) -> int:
return self.provider.get_nonce(self.address)

@property
def balance(self) -> int:
return self.provider.get_balance(self.address)

@property
def code(self) -> bytes:
# TODO: Explore caching this (based on `self.provider.network` and examining code)
return self.provider.get_code(self.address)

@property
def codesize(self) -> int:
return len(self.code)

@property
def is_contract(self) -> bool:
return len(self.code) > 0


class Address(AddressAPI):
_address: str

@property
def address(self) -> str:
return self._address
3 changes: 2 additions & 1 deletion src/ape/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class AbstractDataClassMeta(DataClassMeta, ABCMeta):
pass


abstractdataclass = partial(dataclass, meta=AbstractDataClassMeta)
abstractdataclass = partial(dataclass, kwargs=True, meta=AbstractDataClassMeta)


__all__ = [
"abstractdataclass",
Expand Down
Loading

0 comments on commit 93eb5bc

Please sign in to comment.