Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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", "ape_ethereum", "ape_plugins", "click", "dataclassy", "eth_abi", "eth_account", "eth_typing", "eth_utils", "github", "hexbytes", "hypothesis", "hypothesis_jsonschema", "importlib_metadata", "pluggy", "pytest", "requests", "setuptools", "web3", "yaml"]
known_third_party = ["IPython", "ape_ethereum", "ape_http", "ape_plugins", "click", "dataclassy", "eth_abi", "eth_account", "eth_typing", "eth_utils", "github", "hexbytes", "hypothesis", "hypothesis_jsonschema", "importlib_metadata", "pluggy", "pytest", "requests", "setuptools", "web3", "yaml"]
known_first_party = ["ape_accounts", "ape_console", "ape"]
multi_line_output = 3
use_parentheses = true
2 changes: 1 addition & 1 deletion src/ape/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def invoke(self, ctx) -> Any:
except click.UsageError as err:
self._suggest_cmd(err)
except ApeException as err:
raise Abort(str(err)) from err
raise Abort(f"({type(err).__name__}) {err}") from err

@staticmethod
def _suggest_cmd(usage_error):
Expand Down
2 changes: 1 addition & 1 deletion src/ape/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .accounts import AccountAPI, AccountContainerAPI
from .address import Address, AddressAPI
from .contracts import ContractLog
from .contracts import ContractInstance, ContractLog
from .convert import ConverterAPI
from .explorers import ExplorerAPI
from .networks import EcosystemAPI, NetworkAPI, ProviderContextManager, create_network_type
Expand Down
38 changes: 15 additions & 23 deletions src/ape/api/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)
from ape.utils import cached_property

from ..exceptions import AccountsError, AliasAlreadyInUseError, TransactionError
from ..exceptions import AccountsError, AliasAlreadyInUseError, SignatureError
from .address import AddressAPI
from .base import abstractdataclass, abstractmethod
from .contracts import ContractContainer, ContractInstance
Expand Down Expand Up @@ -55,7 +55,7 @@ def call(self, txn: TransactionAPI, send_everything: bool = False) -> ReceiptAPI
if txn.nonce is None:
txn.nonce = self.nonce
elif txn.nonce < self.nonce:
raise AccountsError("Invalid nonce, will not publish")
raise AccountsError("Invalid nonce, will not publish.")

# TODO: Add `GasEstimationAPI`
if txn.gas_price is None:
Expand All @@ -64,19 +64,21 @@ def call(self, txn: TransactionAPI, send_everything: bool = False) -> ReceiptAPI

# NOTE: Allow overriding gas limit
if txn.gas_limit is None:
txn.gas_limit = self._estimate_gas(txn)
txn.gas_limit = self.provider.estimate_gas_cost(txn)
# else: assume user specified the correct amount or txn will fail and waste gas

if send_everything:
txn.value = self.balance - txn.gas_limit * txn.gas_price

if txn.gas_limit * txn.gas_price + txn.value > self.balance:
raise AccountsError("Transfer value meets or exceeds account balance")
if txn.total_transfer_value > self.balance:
raise AccountsError(
"Transfer value meets or exceeds account balance. "
f"(transfer_value={txn.total_transfer_value}, balance={self.balance})."
)

txn.signature = self.sign_transaction(txn)

if not txn.signature:
raise AccountsError("The transaction was not signed")
raise SignatureError("The transaction was not signed.")

return self.provider.send_transaction(txn)

Expand All @@ -87,16 +89,6 @@ def _convert(self) -> Callable:

return convert

def _estimate_gas(self, txn: TransactionAPI) -> int:
try:
return self.provider.estimate_gas_cost(txn)
except ValueError as err:
message = (
f"Gas estimation failed: '{err}'. This transaction will likely revert. "
"If you wish to broadcast, you must set the gas limit manually."
)
raise TransactionError(message) from err

def transfer(
self,
account: Union[str, AddressType, "AddressAPI"],
Expand Down Expand Up @@ -129,7 +121,7 @@ def deploy(self, contract_type: ContractType, *args, **kwargs) -> ContractInstan
receipt = self.call(txn)

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

return ContractInstance( # type: ignore
_provider=self.provider,
Expand Down Expand Up @@ -167,25 +159,25 @@ def append(self, account: AccountAPI):
self._verify_account_type(account)

if account.address in self:
raise AccountsError(f"Account '{account.address}' already in container")
raise AccountsError(f"Account '{account.address}' already in container.")

self._verify_unused_alias(account)

self.__setitem__(account.address, account)

def __setitem__(self, address: AddressType, account: AccountAPI):
raise NotImplementedError("Must define this method to use `container.append(acct)`")
raise NotImplementedError("Must define this method to use `container.append(acct)`.")

def remove(self, account: AccountAPI):
self._verify_account_type(account)

if account.address not in self:
raise AccountsError(f"Account '{account.address}' not known")
raise AccountsError(f"Account '{account.address}' not known.")

self.__delitem__(account.address)

def __delitem__(self, address: AddressType):
raise NotImplementedError("Must define this method to use `container.remove(acct)`")
raise NotImplementedError("Must define this method to use `container.remove(acct)`.")

def __contains__(self, address: AddressType) -> bool:
try:
Expand All @@ -199,7 +191,7 @@ def _verify_account_type(self, account):
if not isinstance(account, self.account_type):
message = (
f"Container '{type(account).__name__}' is an incorrect "
f"type for container '{type(self.account_type).__name__}'"
f"type for container '{type(self.account_type).__name__}'."
)
raise AccountsError(message)

Expand Down
12 changes: 6 additions & 6 deletions src/ape/api/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from ape.logging import logger
from ape.types import ABI, AddressType, ContractType

from ..exceptions import ContractCallError, ContractDeployError
from ..exceptions import ArgumentsLengthError, ContractDeployError, TransactionError
from .address import Address, AddressAPI
from .base import dataclass
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI
Expand All @@ -23,7 +23,7 @@ class ContractConstructor:

def __post_init__(self):
if len(self.deployment_bytecode) == 0:
raise ContractDeployError("No bytecode to deploy")
raise ContractDeployError("No bytecode to deploy.")

def __repr__(self) -> str:
return self.abi.signature if self.abi else "constructor()"
Expand Down Expand Up @@ -95,7 +95,7 @@ def __repr__(self) -> str:
def __call__(self, *args, **kwargs) -> Any:
selected_abi = _select_abi(self.abis, args)
if not selected_abi:
raise ContractCallError()
raise ArgumentsLengthError()

return ContractCall( # type: ignore
abi=selected_abi,
Expand Down Expand Up @@ -137,7 +137,7 @@ def __call__(self, *args, **kwargs) -> ReceiptAPI:
return sender.call(txn)

else:
raise ContractCallError("Must specify a `sender`")
raise TransactionError(message="Must specify a `sender`.")


@dataclass
Expand All @@ -153,7 +153,7 @@ def __repr__(self) -> str:
def __call__(self, *args, **kwargs) -> ReceiptAPI:
selected_abi = _select_abi(self.abis, args)
if not selected_abi:
raise ContractCallError()
raise ArgumentsLengthError()

return ContractTransaction( # type: ignore
abi=selected_abi,
Expand Down Expand Up @@ -219,7 +219,7 @@ def get_handler(abi_type: str) -> Any:

except Exception as e:
# NOTE: Just a hack, because `__getattr__` *must* raise `AttributeError`
raise AttributeError from e
raise AttributeError(str(e)) from e

# Reverse search for the proper handler for this ABI name, if one exists
for abi_type in handlers:
Expand Down
2 changes: 1 addition & 1 deletion src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def __exit__(self, *args, **kwargs):
# Put our providers back the way it was
provider = self._connected_providers.pop()
if self.provider != provider:
raise ValueError("Previous provider value unknown")
raise ValueError("Previous provider value unknown.")

provider.disconnect()

Expand Down
16 changes: 13 additions & 3 deletions src/ape/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,17 @@ class TransactionAPI:

def __post_init__(self):
if not self.is_valid:
raise ProviderError("Transaction is not valid")
raise ProviderError("Transaction is not valid.")

@property
def total_transfer_value(self) -> int:
"""
The total amount of WEI that a transaction could use.
Useful for determining if an account balance can afford
to submit the transaction.
"""
# TODO Support EIP-1559
return (self.gas_limit or 0) * (self.gas_price or 0) + self.value

@property
@abstractmethod
Expand Down Expand Up @@ -63,8 +73,8 @@ def __str__(self) -> str:


class TransactionStatusEnum(IntEnum):
failing = 0
no_error = 1
FAILING = 0
NO_ERROR = 1


@abstractdataclass
Expand Down
2 changes: 1 addition & 1 deletion src/ape/cli/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def convert(
choice = self.choices[self.choice_index]
return choice
except Exception:
self.fail("Invalid choice", param=param)
return self.fail("Invalid choice", param=param)


class NetworkChoice(click.Choice):
Expand Down
2 changes: 1 addition & 1 deletion src/ape/cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def decorator(f):
def _set_level(ctx, param, value):
log_level = getattr(LogLevel, value.upper(), None)
if log_level is None:
raise click.BadParameter(f"Must be one of {names_str}, not {value}")
raise click.BadParameter(f"Must be one of {names_str}, not {value}.")

cli_logger.set_level(log_level.name)

Expand Down
93 changes: 79 additions & 14 deletions src/ape/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from typing import Optional


class ApeException(Exception):
"""
An exception raised by ape.
"""

def __init__(self, message):
if not message.endswith("."):
message = f"{message}."
super().__init__(message)


class AccountsError(ApeException):
"""
Expand All @@ -18,7 +26,13 @@ class AliasAlreadyInUseError(AccountsError):

def __init__(self, alias: str):
self.alias = alias
super().__init__(f"Account with alias '{alias}' already in use")
super().__init__(f"Account with alias '{alias}' already in use.")


class SignatureError(AccountsError):
"""
Raised when there are issues with signing.
"""


class ContractError(ApeException):
Expand All @@ -28,33 +42,84 @@ class ContractError(ApeException):
"""


class ContractCallError(ContractError):
class ArgumentsLengthError(ContractError):
"""
Raised when issues occur when making a contract call.
Raised when calling a contract method with the wrong number of arguments.
"""

def __init__(self, message=None):
message = message or "Number of args does not match"
def __init__(self):
message = "The number of the given arguments do not match what is defined in the ABI."
super().__init__(message)


class TransactionError(AccountsError, ContractError):
"""
Raised when issues occur while making contract transactions.
"""


class DecodingError(ContractError):
"""
Raised when issues occur while decoding data from
a contract call or transaction.
"""

def __init__(self):
super().__init__("Output corrupted")
super().__init__("Output corrupted.")


class TransactionError(ContractError):
"""
Raised when issues occur related to transactions.
"""

def __init__(
self,
base_err: Optional[Exception] = None,
message: Optional[str] = None,
code: Optional[int] = None,
):
self.base_err = base_err
if not message:
message = str(base_err) if base_err else "Tranaction Failed"

self.message = message
self.code = code

ex_message = f"({code}) {message}" if code else message
super().__init__(ex_message)


class VirtualMachineError(TransactionError):
"""
Raised when there is either an internal fault in a virtual machine
or a contract-defined revert, such as from an assert/require statement.
"""

def __init__(self, revert_message: str):
super().__init__(message=revert_message)

@property
def revert_message(self):
return self.message

@classmethod
def from_error(cls, err: Exception):
"""
Creates this class from the error message of the given
error.

This should be overridden whenever possible to handle
provider-specific use-cases for raising this error.
"""
return cls(str(err))


class OutOfGasError(TransactionError):
"""
Raised when detecting a transaction failed because it ran
out of gas.
"""

def __init__(self, code: Optional[int] = None):
super().__init__(message="The transaction ran out of gas.", code=code)


class ContractDeployError(ApeException):
class ContractDeployError(TransactionError):
"""
Raised when a problem occurs when deploying a contract.
"""
Expand All @@ -73,7 +138,7 @@ class NetworkNotFoundError(NetworkError):

def __init__(self, network: str):
self.network = network
message = f"No network named '{network}'"
message = f"No network named '{network}'."
super().__init__(message)


Expand Down
2 changes: 1 addition & 1 deletion src/ape/managers/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def load(self, alias: str) -> AccountAPI:

@singledispatchmethod
def __getitem__(self, account_id) -> AccountAPI:
raise NotImplementedError("Cannot use " + type(account_id) + " as account id")
raise NotImplementedError(f"Cannot use {type(account_id)} as account ID.")

@__getitem__.register
def __getitem_int(self, account_id: int) -> AccountAPI:
Expand Down
Loading