diff --git a/docs/userguides/accounts.md b/docs/userguides/accounts.md index 18870a6dd1..ca498fa6f6 100644 --- a/docs/userguides/accounts.md +++ b/docs/userguides/accounts.md @@ -46,6 +46,16 @@ test: number_of_accounts: 5 ``` +You can also change settings at run-time, such as the mnemonic: + +```python +from ape import accounts + +accounts.test_accounts.mnemonic = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat" +print(accounts.test_accounts[0]) +# 0x627306090abaB3A6e1400e9345bC60c78a8BEf57 +``` + ```{warning} NEVER put a seed phrase with real funds here. ``` diff --git a/src/ape/api/accounts.py b/src/ape/api/accounts.py index c908faf26d..7be1d39fd2 100644 --- a/src/ape/api/accounts.py +++ b/src/ape/api/accounts.py @@ -29,6 +29,11 @@ from ape.types.signatures import MessageSignature, SignableMessage from ape.utils.basemodel import BaseInterfaceModel from ape.utils.misc import raises_not_implemented +from ape.utils.testing import ( + DEFAULT_NUMBER_OF_TEST_ACCOUNTS, + DEFAULT_TEST_HD_PATH, + DEFAULT_TEST_MNEMONIC, +) if TYPE_CHECKING: from eth_pydantic_types import HexBytes @@ -626,6 +631,30 @@ class TestAccountContainerAPI(AccountContainerAPI): ``AccountContainerAPI`` directly. Then, they show up in the ``accounts`` test fixture. """ + @property + def mnemonic(self) -> str: + return self.config_manager.test.get("mnemonic", DEFAULT_TEST_MNEMONIC) + + @mnemonic.setter + def mnemonic(self, value: str): + self.config_manager.test.mnemonic = value + + @property + def number_of_accounts(self) -> int: + return self.config_manager.test.get("number_of_accounts", DEFAULT_NUMBER_OF_TEST_ACCOUNTS) + + @number_of_accounts.setter + def number_of_accounts(self, value: int): + self.config_manager.test.number_of_accounts = value + + @property + def hd_path(self) -> str: + return self.config_manager.test.get("hd_path", DEFAULT_TEST_HD_PATH) + + @hd_path.setter + def hd_path(self, value: str): + self.config_manager.test.hd_path = value + @cached_property def data_folder(self) -> Path: """ diff --git a/src/ape/managers/accounts.py b/src/ape/managers/accounts.py index 69ebc31906..2439b9de86 100644 --- a/src/ape/managers/accounts.py +++ b/src/ape/managers/accounts.py @@ -6,7 +6,7 @@ from eth_utils import is_hex -from ape.api.accounts import ( +from ape.api import ( AccountAPI, AccountContainerAPI, ImpersonatedAccount, @@ -53,6 +53,35 @@ def containers(self) -> dict[str, TestAccountContainerAPI]: for plugin_name, (container_type, account_type) in account_types } + @property + def mnemonic(self) -> str: + """ + The seed phrase for generated test accounts. + """ + return self.config_manager.get_config("test").mnemonic + + @mnemonic.setter + def mnemonic(self, value: str): + """ + The seed phrase for generated test accounts. + **WARNING**: Changing the test-mnemonic mid-session + re-starts the provider (if connected to one). + """ + self.config_manager.test.mnemonic = value + self.containers["test"].mnemonic = value + + if provider := self.network_manager.active_provider: + provider.update_settings({"mnemonic": value}) + + self._accounts_by_index = {} + + @property + def number_of_accounts(self) -> int: + """ + The number of test accounts to generate and fund by default. + """ + return self.config_manager.test.number_of_accounts + @property def hd_path(self) -> str: """ diff --git a/src/ape/managers/project.py b/src/ape/managers/project.py index 4d0cc35128..05fede30ad 100644 --- a/src/ape/managers/project.py +++ b/src/ape/managers/project.py @@ -1952,7 +1952,7 @@ def reconfigure(self, **overrides): Change a project's config. Args: - **overrides: Config key-value pairs. Completely overridesfe + **overrides: Config key-value pairs. Completely overrides existing. """ diff --git a/src/ape_accounts/accounts.py b/src/ape_accounts/accounts.py index cec087b505..3fdd41ace1 100644 --- a/src/ape_accounts/accounts.py +++ b/src/ape_accounts/accounts.py @@ -319,7 +319,7 @@ def generate_account( Args: alias (str): The alias name of the account. passphrase (str): Passphrase used to encrypt the account storage file. - hd_path (str): The hierarchal deterministic path to use when generating the account. + hd_path (str): The hierarchical deterministic path to use when generating the account. Defaults to `m/44'/60'/0'/0/0`. word_count (int): The amount of words to use in the generated mnemonic. @@ -347,7 +347,7 @@ def import_account_from_mnemonic( alias (str): The alias name of the account. passphrase (str): Passphrase used to encrypt the account storage file. mnemonic (str): List of space-separated words representing the mnemonic seed phrase. - hd_path (str): The hierarchal deterministic path to use when generating the account. + hd_path (str): The hierarchical deterministic path to use when generating the account. Defaults to `m/44'/60'/0'/0/0`. Returns: diff --git a/src/ape_test/accounts.py b/src/ape_test/accounts.py index 6f41dbe615..867b5f73ea 100644 --- a/src/ape_test/accounts.py +++ b/src/ape_test/accounts.py @@ -15,12 +15,7 @@ from ape.types.signatures import MessageSignature, TransactionSignature from ape.utils._web3_compat import sign_hash from ape.utils.misc import log_instead_of_fail -from ape.utils.testing import ( - DEFAULT_NUMBER_OF_TEST_ACCOUNTS, - DEFAULT_TEST_HD_PATH, - DEFAULT_TEST_MNEMONIC, - generate_dev_accounts, -) +from ape.utils.testing import generate_dev_accounts if TYPE_CHECKING: from ape.api.transactions import TransactionAPI @@ -36,21 +31,25 @@ def __init__(self, *args, **kwargs): def __len__(self) -> int: return self.number_of_accounts + len(self.generated_accounts) - @property - def config(self): - return self.config_manager.get_config("test") - @property def mnemonic(self) -> str: - return self.config.get("mnemonic", DEFAULT_TEST_MNEMONIC) + # Overridden so we can overload the setter. + return self.config_manager.test.mnemonic - @property - def number_of_accounts(self) -> int: - return self.config.get("number_of_accounts", DEFAULT_NUMBER_OF_TEST_ACCOUNTS) + @mnemonic.setter + def mnemonic(self, mnemonic: str) -> None: + # Overridden so we can also clear out generated accounts cache. + self.config_manager.test.mnemonic = mnemonic + self.generated_accounts = [] + + @TestAccountContainerAPI.mnemonic.setter + def mnemonic(self, mnemonic: str) -> None: + self.config_manager.test.mnemonic = mnemonic + self.generated_accounts = [] @property - def hd_path(self) -> str: - return self.config.get("hd_path", DEFAULT_TEST_HD_PATH) + def config(self): + return self.config_manager.get_config("test") @property def aliases(self) -> Iterator[str]: diff --git a/tests/functional/test_accounts.py b/tests/functional/test_accounts.py index 35fcd55765..92279897fc 100644 --- a/tests/functional/test_accounts.py +++ b/tests/functional/test_accounts.py @@ -20,6 +20,7 @@ ) from ape.types.gas import AutoGasLimit from ape.types.signatures import recover_signer +from ape.utils.testing import DEFAULT_TEST_MNEMONIC from ape_accounts.accounts import ( KeyfileAccount, generate_account, @@ -696,18 +697,26 @@ def test_using_different_hd_path(accounts, project, eth_tester_provider): assert old_address != new_address -def test_using_random_mnemonic(accounts, project, eth_tester_provider): - mnemonic = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat" - test_config = {"test": {"mnemonic": mnemonic}} +def test_mnemonic(accounts): + actual = accounts.mnemonic + expected = DEFAULT_TEST_MNEMONIC + assert actual == expected - old_address = accounts[0].address - original_settings = eth_tester_provider.settings.model_dump(by_alias=True) - with project.temp_config(**test_config): - eth_tester_provider.update_settings(test_config["test"]) - new_address = accounts[0].address - eth_tester_provider.update_settings(original_settings) - assert old_address != new_address +def test_mnemonic_setter(accounts): + original_mnemonic = accounts.mnemonic + new_mnemonic = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat" + original_address = accounts[0].address + + # Change. + accounts.mnemonic = new_mnemonic + new_address = accounts[0].address + + # Put back. + accounts.mnemonic = original_mnemonic + + # Assert. + assert new_address != original_address def test_iter_test_accounts(accounts):