diff --git a/docs/userguides/accounts.md b/docs/userguides/accounts.md index b7cb01287e..022e0494b7 100644 --- a/docs/userguides/accounts.md +++ b/docs/userguides/accounts.md @@ -55,7 +55,45 @@ For example, you can [generate](../commands/accounts.html#accounts-generate) an ape accounts generate ``` -It will prompt you for a passphrase. +Ape will prompt you for entropy which is used to increase randomness when creating your account. + +Ape will then prompt you whether you want to show your mnemonic. + +If you do not want to see your mnemonic you can select `n`. + +Alternatively you can use the `--hide-mnemonic` option to skip the prompt. + +```bash +ape accounts generate --hide-mnemonic +``` + +If you elected to show your mnemonic Ape will then show you your newly generated mnemonic. + +Ape will then prompt you for a passphrase which you will need to enter twice to confirm. + +This passphrase is used to encrypt your account on disk, for extra security. + +You will be prompted for it each time you load your account, so make sure to remember it. + +After entering the passphrase Ape will then show you your new account address, HDPath, and account alias. + +If you want to use a custom HDPath, use the `--hd-path` option: + +```bash +ape accounts generate --hd-path +``` + +If you do not use the `--hd-path` option, Ape will use the default HDPath of (Ethereum network, first account). + +If you want to use a custom mnemonic phrase word length, use the `--word-count` option: + +```bash +ape accounts generate --word-count +``` + +If you do not use the `--word-count` option, Ape will use the default word count of 12. + +You can use all of these together or separately to control the way Ape creates and displays your account information. If you already have an account and you wish to import it into Ape (say, from Metamask), you can use the [import command](../commands/accounts.html#accounts-import): @@ -64,6 +102,7 @@ ape accounts import ``` It will prompt you for the private key. + If you need help exporting your private key from Metamask, see [this guide](https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-export-an-account-s-private-key). You can also import accounts from mnemonic seed by using the `--use-mnemonic` flag: @@ -73,6 +112,7 @@ ape accounts import --use-mnemonic ``` It will then prompt you for the [mnemonic seed](https://en.bitcoin.it/wiki/Seed_phrase). + If you need help finding your mnemonic seed (Secret Recovery Phrase) in Metamask, see [this guide](https://metamask.zendesk.com/hc/en-us/articles/360015290032-How-to-reveal-your-Secret-Recovery-Phrase). In addition, you can also use a custom HDPath by using the `--hd-path` option: @@ -82,6 +122,7 @@ ape accounts import --use-mnemonic --hd-path ``` If you use the `--hd-path` option, you will need to pass the [HDPath](https://help.myetherwallet.com/en/articles/5867305-hd-wallets-and-derivation-paths) you'd like to use as an argument in the command. + If you do not use the `--hd-path` option, Ape will use the default HDPath of (Ethereum network, first account). Then, in your scripts, you can [load](../methoddocs/managers.html#ape.managers.accounts.AccountManager.load) an account: diff --git a/src/ape_accounts/_cli.py b/src/ape_accounts/_cli.py index b5c70a844f..bdde868811 100644 --- a/src/ape_accounts/_cli.py +++ b/src/ape_accounts/_cli.py @@ -64,25 +64,51 @@ def _list(cli_ctx, show_all_plugins): click.echo() -@cli.command(short_help="Create a new keyfile account with a random private key") +@cli.command(short_help="Create a new keyfile account with a random mnemonic seed phrase") +@click.option( + "--hide-mnemonic", + help="Hide the newly generated mnemonic from the terminal", + is_flag=True, +) +@click.option( + "--word-count", + help="Number of words to use to generate seed phrase", + default=12, + show_default=True, +) +@click.option( + "--hd-path", + "custom_hd_path", + help="Specify an HD path for deriving seed phrase", + default=ETHEREUM_DEFAULT_PATH, + show_default=True, +) @non_existing_alias_argument() @ape_cli_context() -def generate(cli_ctx, alias): +def generate(cli_ctx, alias, hide_mnemonic, word_count, custom_hd_path): path = _get_container().data_folder.joinpath(f"{alias}.json") - extra_entropy = click.prompt( + EthAccount.enable_unaudited_hdwallet_features() + # os.urandom (used internally for this method) requries a certain amount of entropy + # Adding entropy increases os.urandom randomness output + # Despite not being used in create_with_mnemonic + click.prompt( "Add extra entropy for key generation...", hide_input=True, ) - - account = EthAccount.create(extra_entropy) + account, mnemonic = EthAccount.create_with_mnemonic( + num_words=word_count, account_path=custom_hd_path + ) + if not hide_mnemonic and click.confirm("Show mnemonic?", default=True): + cli_ctx.logger.info(f"Newly generated mnemonic is: {mnemonic}") passphrase = click.prompt( - "Create Passphrase", + "Create Passphrase to encrypt account", hide_input=True, confirmation_prompt=True, ) path.write_text(json.dumps(EthAccount.encrypt(account.key, passphrase))) cli_ctx.logger.success( - f"A new account '{account.address}' has been added with the id '{alias}'" + f"A new account '{account.address}' with " + + f"HDPath {custom_hd_path} has been added with the id '{alias}'" ) @@ -108,7 +134,7 @@ def _import(cli_ctx, alias, import_from_mnemonic, custom_hd_path): mnemonic = click.prompt("Enter mnemonic seed phrase", hide_input=True) EthAccount.enable_unaudited_hdwallet_features() try: - account = EthAccount.from_mnemonic(mnemonic, account_path=custom_hd_path) + account = EthAccount.from_mnemonic(mnemonic=mnemonic, account_path=custom_hd_path) except Exception as error: cli_ctx.abort(f"Seed phrase can't be imported: {error}") return @@ -121,7 +147,7 @@ def _import(cli_ctx, alias, import_from_mnemonic, custom_hd_path): return passphrase = click.prompt( - "Create Passphrase", + "Create Passphrase to encrypt account", hide_input=True, confirmation_prompt=True, ) diff --git a/tests/integration/cli/test_accounts.py b/tests/integration/cli/test_accounts.py index 8af5044002..f41d6b264c 100644 --- a/tests/integration/cli/test_accounts.py +++ b/tests/integration/cli/test_accounts.py @@ -157,15 +157,102 @@ def test_import_invalid_mnemonic(ape_cli, runner): @run_once -def test_generate(ape_cli, runner, temp_keyfile_path): +def test_generate_default(ape_cli, runner, temp_keyfile_path): assert not temp_keyfile_path.is_file() # Generate new private key + show_mnemonic = "" result = runner.invoke( ape_cli, ["accounts", "generate", ALIAS], + input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + ) + assert result.exit_code == 0, result.output + assert "Newly generated mnemonic is" in result.output + assert ETHEREUM_DEFAULT_PATH in result.output + assert ALIAS in result.output + assert temp_keyfile_path.is_file() + + +@run_once +def test_generate_hide_mnemonic_prompt(ape_cli, runner, temp_keyfile_path): + assert not temp_keyfile_path.is_file() + # Generate new private key + show_mnemonic = "n" + result = runner.invoke( + ape_cli, + ["accounts", "generate", ALIAS], + input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + ) + assert result.exit_code == 0, result.output + assert "Newly generated mnemonic is" not in result.output + assert ETHEREUM_DEFAULT_PATH in result.output + assert ALIAS in result.output + assert temp_keyfile_path.is_file() + + +@run_once +def test_generate_hide_mnemonic_option(ape_cli, runner, temp_keyfile_path): + assert not temp_keyfile_path.is_file() + # Generate new private key + result = runner.invoke( + ape_cli, + ["accounts", "generate", ALIAS, "--hide-mnemonic"], input="\n".join(["random entropy", PASSWORD, PASSWORD]), ) assert result.exit_code == 0, result.output + assert "Newly generated mnemonic is" not in result.output + assert ETHEREUM_DEFAULT_PATH in result.output + assert ALIAS in result.output + assert temp_keyfile_path.is_file() + + +@run_once +def test_generate_24_words(ape_cli, runner, temp_keyfile_path): + assert not temp_keyfile_path.is_file() + # Generate new private key + show_mnemonic = "" + result = runner.invoke( + ape_cli, + ["accounts", "generate", ALIAS, "--word-count", 24], + input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + ) + assert result.exit_code == 0, result.output + assert "Newly generated mnemonic is" in result.output # should check for 24 words + assert ETHEREUM_DEFAULT_PATH in result.output + assert ALIAS in result.output + assert temp_keyfile_path.is_file() + + +@run_once +def test_generate_custom_hdpath(ape_cli, runner, temp_keyfile_path): + assert not temp_keyfile_path.is_file() + # Generate new private key + show_mnemonic = "" + result = runner.invoke( + ape_cli, + ["accounts", "generate", ALIAS, "--hd-path", CUSTOM_HDPATH], + input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + ) + assert result.exit_code == 0, result.output + assert "Newly generated mnemonic is" in result.output + assert CUSTOM_HDPATH in result.output + assert ALIAS in result.output + assert temp_keyfile_path.is_file() + + +@run_once +def test_generate_24_words_and_custom_hdpath(ape_cli, runner, temp_keyfile_path): + assert not temp_keyfile_path.is_file() + # Generate new private key + show_mnemonic = "" + result = runner.invoke( + ape_cli, + ["accounts", "generate", ALIAS, "--word-count", 24, "--hd-path", CUSTOM_HDPATH], + input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + ) + assert result.exit_code == 0, result.output + assert "Newly generated mnemonic is" in result.output # should check for 24 words + assert CUSTOM_HDPATH in result.output assert ALIAS in result.output assert temp_keyfile_path.is_file() @@ -173,10 +260,11 @@ def test_generate(ape_cli, runner, temp_keyfile_path): @run_once def test_generate_alias_already_in_use(ape_cli, runner): def invoke_generate(): + show_mnemonic = "" return runner.invoke( ape_cli, ["accounts", "generate", ALIAS], - input="\n".join(["random entropy", PASSWORD, PASSWORD]), + input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), ) result = invoke_generate()