Skip to content

Commit

Permalink
feat: add unlock_account function to ProviderAPI (#594)
Browse files Browse the repository at this point in the history
  • Loading branch information
fubuloubu authored Mar 23, 2022
1 parent 377a502 commit c9ffc88
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 212 deletions.
8 changes: 8 additions & 0 deletions docs/userguides/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ test:
number_of_accounts: 5
```

If you are using a fork-provider, such as [Hardhat](https://github.com/ApeWorX/ape-hardhat), you can use impersonated accounts by accessing random addresses off the fixture:

```python
@pytest.fixture
def vitalik(accounts):
return accounts["0xab5801a7d398351b8be11c439e05c5b3259aec9b"]
```

#### project fixture

You also have access to the `project` you are testing. You will need this to deploy your contracts in your tests.
Expand Down
9 changes: 8 additions & 1 deletion src/ape/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from .accounts import AccountAPI, AccountContainerAPI, TestAccountAPI, TestAccountContainerAPI
from .accounts import (
AccountAPI,
AccountContainerAPI,
ImpersonatedAccount,
TestAccountAPI,
TestAccountContainerAPI,
)
from .address import Address
from .compiler import CompilerAPI
from .config import ConfigDict, ConfigEnum, PluginConfig
Expand Down Expand Up @@ -37,6 +43,7 @@
"DependencyAPI",
"EcosystemAPI",
"ExplorerAPI",
"ImpersonatedAccount",
"PluginConfig",
"ProjectAPI",
"ProviderAPI",
Expand Down
92 changes: 57 additions & 35 deletions src/ape/api/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ape.utils import BaseInterfaceModel, abstractmethod, cached_property

from .address import BaseAddress
from .providers import ReceiptAPI, TransactionAPI, TransactionType
from .providers import ReceiptAPI, TransactionAPI

if TYPE_CHECKING:
from ape.contracts import ContractContainer, ContractInstance
Expand Down Expand Up @@ -89,48 +89,15 @@ def call(self, txn: TransactionAPI, send_everything: bool = False) -> ReceiptAPI
Returns:
:class:`~ape.api.providers.ReceiptAPI`
"""
# NOTE: Use "expected value" for Chain ID, so if it doesn't match actual, we raise
txn.chain_id = self.provider.network.chain_id

# NOTE: Allow overriding nonce, assume user understand what this does
if txn.nonce is None:
txn.nonce = self.nonce
elif txn.nonce < self.nonce:
raise AccountsError("Invalid nonce, will not publish.")

txn_type = TransactionType(txn.type)
if txn_type == TransactionType.STATIC and txn.gas_price is None: # type: ignore
txn.gas_price = self.provider.gas_price # type: ignore
elif txn_type == TransactionType.DYNAMIC:
if txn.max_priority_fee is None: # type: ignore
txn.max_priority_fee = self.provider.priority_fee # type: ignore

if txn.max_fee is None:
txn.max_fee = self.provider.base_fee + txn.max_priority_fee
# else: Assume user specified the correct amount or txn will fail and waste gas

if txn.gas_limit is None:
txn.gas_limit = self.provider.estimate_gas_cost(txn)
# else: Assume user specified the correct amount or txn will fail and waste gas
txn = self.prepare_transaction(txn)

if send_everything:
if txn.max_fee is None:
raise TransactionError(message="Max fee must not be None.")

txn.value = self.balance - txn.max_fee

if txn.total_transfer_value > self.balance:
raise AccountsError(
"Transfer value meets or exceeds account balance.\n"
"Are you using the correct provider/account combination?\n"
f"(transfer_value={txn.total_transfer_value}, balance={self.balance})."
)

if txn.required_confirmations is None:
txn.required_confirmations = self.provider.network.required_confirmations
elif not isinstance(txn.required_confirmations, int) or txn.required_confirmations < 0:
raise TransactionError(message="'required_confirmations' must be a positive integer.")

txn.signature = self.sign_transaction(txn)
if not txn.signature:
raise SignatureError("The transaction was not signed.")
Expand Down Expand Up @@ -230,6 +197,39 @@ def check_signature(
else:
raise ValueError(f"Unsupported Message type: {type(data)}.")

def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI:
"""
Set default values on a transaction.
Raises:
:class:`~ape.exceptions.AccountsError`: When the account cannot afford the transaction
or the nonce is invalid.
:class:`~ape.exceptions.TransactionError`: When given negative required confirmations.
Args:
txn (:class:`~ape.api.providers.TransactionAPI`): The transaction to prepare.
Returns:
:class:`~ape.api.providers.TransactionAPI`
"""

# NOTE: Allow overriding nonce, assume user understand what this does
if txn.nonce is None:
txn.nonce = self.nonce
elif txn.nonce < self.nonce:
raise AccountsError("Invalid nonce, will not publish.")

txn = self.provider.prepare_transaction(txn)

if txn.total_transfer_value > self.balance:
raise AccountsError(
"Transfer value meets or exceeds account balance.\n"
"Are you using the correct provider/account combination?\n"
f"(transfer_value={txn.total_transfer_value}, balance={self.balance})."
)

return txn


class AccountContainerAPI(BaseInterfaceModel):
"""
Expand Down Expand Up @@ -384,3 +384,25 @@ class TestAccountAPI(AccountAPI):
:class:`~ape.utils.GeneratedDevAccounts`) should implement this API
instead of ``AccountAPI`` directly. This is how they show up in the ``accounts`` test fixture.
"""


class ImpersonatedAccount(AccountAPI):
"""
An account to use that does not require signing.
"""

raw_address: AddressType

@property
def address(self) -> AddressType:
return self.raw_address

def sign_message(self, msg: SignableMessage) -> Optional[MessageSignature]:
raise NotImplementedError("This account cannot sign messages")

def sign_transaction(self, txn: TransactionAPI) -> Optional[TransactionSignature]:
return None

def call(self, txn: TransactionAPI, send_everything: bool = False) -> ReceiptAPI:
txn = self.prepare_transaction(txn)
return self.provider.send_transaction(txn)
58 changes: 57 additions & 1 deletion src/ape/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
TransactionError,
)
from ape.logging import logger
from ape.types import BlockID, SnapshotID, TransactionSignature
from ape.types import AddressType, BlockID, SnapshotID, TransactionSignature
from ape.utils import BaseInterfaceModel, abstractmethod, cached_property

if TYPE_CHECKING:
Expand Down Expand Up @@ -574,6 +574,62 @@ def mine(self, num_blocks: int = 1):
NotImplementedError: Unless overridden.
"""

@raises_not_implemented
def unlock_account(self, address: AddressType) -> bool:
"""
Ask the provider to allow an address to submit transactions without validating
signatures. This feature is intended to be subclassed by a
:class:`~ape.api.providers.TestProviderAPI` so that during a fork-mode test,
a transaction can be submitted by an arbitrary account or contract without a private key.
Raises:
NotImplementedError: When this provider does not support unlocking an account.
Args:
address (``AddressType``): The address to unlock.
Returns:
bool: ``True`` if successfully unlocked account and ``False`` otherwise.
"""

def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI:
"""
Set default values on the transaction.
Raises:
:class:`~ape.exceptions.TransactionError`: When given negative required confirmations.
Args:
txn (:class:`~ape.api.providers.TransactionAPI`): The transaction to prepare.
Returns:
:class:`~ape.api.providers.TransactionAPI`
"""

# NOTE: Use "expected value" for Chain ID, so if it doesn't match actual, we raise
txn.chain_id = self.network.chain_id

txn_type = TransactionType(txn.type)
if txn_type == TransactionType.STATIC and txn.gas_price is None: # type: ignore
txn.gas_price = self.gas_price # type: ignore
elif txn_type == TransactionType.DYNAMIC:
if txn.max_priority_fee is None: # type: ignore
txn.max_priority_fee = self.priority_fee # type: ignore

if txn.max_fee is None:
txn.max_fee = self.base_fee + txn.max_priority_fee
# else: Assume user specified the correct amount or txn will fail and waste gas

if txn.gas_limit is None:
txn.gas_limit = self.estimate_gas_cost(txn)

if txn.required_confirmations is None:
txn.required_confirmations = self.network.required_confirmations
elif not isinstance(txn.required_confirmations, int) or txn.required_confirmations < 0:
raise TransactionError(message="'required_confirmations' must be a positive integer.")

return txn

def _try_track_receipt(self, receipt: ReceiptAPI):
if self.chain_manager:
self.chain_manager.account_history.append(receipt)
Expand Down
25 changes: 10 additions & 15 deletions src/ape/contracts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@
from hexbytes import HexBytes
from pydantic.dataclasses import dataclass

from ape.api import Address, ReceiptAPI, TransactionAPI
from ape.api import AccountAPI, Address, ReceiptAPI, TransactionAPI
from ape.api.address import BaseAddress
from ape.exceptions import (
ArgumentsLengthError,
ContractError,
ProviderNotConnectedError,
TransactionError,
)
from ape.exceptions import ArgumentsLengthError, ContractError, ProviderNotConnectedError
from ape.logging import logger
from ape.types import AddressType
from ape.utils import ManagerAccessMixin, cached_property
Expand Down Expand Up @@ -56,12 +51,12 @@ def serialize_transaction(self, *args, **kwargs) -> TransactionAPI:
)

def __call__(self, *args, **kwargs) -> ReceiptAPI:
if "sender" in kwargs:
txn = self.serialize_transaction(*args, **kwargs)

if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI):
sender = kwargs["sender"]
txn = self.serialize_transaction(*args, **kwargs)
return sender.call(txn)

txn = self.serialize_transaction(*args, **kwargs)
return self.provider.send_transaction(txn)


Expand Down Expand Up @@ -180,12 +175,12 @@ def serialize_transaction(self, *args, **kwargs) -> TransactionAPI:
)

def __call__(self, *args, **kwargs) -> ReceiptAPI:
if "sender" in kwargs:
sender = kwargs["sender"]
txn = self.serialize_transaction(*args, **kwargs)
return sender.call(txn)
txn = self.serialize_transaction(*args, **kwargs)

raise TransactionError(message="Must specify a `sender`.")
if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI):
return kwargs["sender"].call(txn)

return self.provider.send_transaction(txn)


class ContractTransactionHandler(ManagerAccessMixin):
Expand Down
Loading

0 comments on commit c9ffc88

Please sign in to comment.