Skip to content

Commit

Permalink
feat: generate with mnemonic (#1165)
Browse files Browse the repository at this point in the history
* feat: generate with mnemonic

* chore: removed breakpoint

* test: added test for generate_mnemonic and update test for generate

* refactor: changed mnemonic report to cli_ctx.logger.success

* test: added test for hidden mnemonic

* test: re arranged text input for generate test cases

* test: added tests for custom words and custom hdpath - todo: check for 24 words output

* chore: linting

* docs: cleaned up spacing in docs, updated short help for generate

* chore: changes from pyupgrade (#1163)

* refactor: move word count and hd path to optional arguments

* refactor: remove use-mnemonic flag and solely use nmemonics

* docs: update short help for ape accounts generate

* bug: click options were not included in generate function call

* refactor: removed --hide-mnemonic

* chore: linting

* bug: removed hide_mnemonic from generate function call

* refactor: clarified calls to eth account and remove passphrase for seed gen

* feat: added HDPath to logger message

* test: updated tests for post refactor

* refactor: use ethereum default path

* refactor: skip setting language in create_with_mnemonic

* refactor: move passphrase input next to write_test

* test: removed entropy from tests - not used with mnemonic generation

* chore: linting

* refactor: --hdpath => --hd-path

* docs: updated passphrase prompt to be more clear

* bug: forgot f string when linting to shorten line

* refactor: added back hide-mnemonic

* refactor: cleaned word count click argument

* test: refactored and added tests for hide mnemonic as a flag or prompt

* test: refactored show_mnemonic as a variable for legibility

* test: added checks for ETHEREUM_DEFAULT_PATH in result.output

* feat: add random entropy back for os.urandom randomness output

* refactor: output mnemonic as info log instead of success

* refactor: suggestion from peer review

* docs: updated docs for ape accounts generate

* docs: reworded some docs for ape accounts generate

* docs: improved documentation wording

* docs: added details around hide mnemonic and passphrase

Co-authored-by: Juliya Smith <[email protected]>
Co-authored-by: El De-dog-lo <[email protected]>
  • Loading branch information
3 people authored Nov 30, 2022
1 parent a57f1dc commit c536511
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 12 deletions.
43 changes: 42 additions & 1 deletion docs/userguides/accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,45 @@ For example, you can [generate](../commands/accounts.html#accounts-generate) an
ape accounts generate <ALIAS>
```

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 <ALIAS> --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 <ALIAS> --hd-path <HDPATH>
```

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 <ALIAS> --word-count <WORDCOUNT>
```

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):

Expand All @@ -64,6 +102,7 @@ ape accounts import <ALIAS>
```

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:
Expand All @@ -73,6 +112,7 @@ ape accounts import <ALIAS> --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:
Expand All @@ -82,6 +122,7 @@ ape accounts import <ALIAS> --use-mnemonic --hd-path <HDPATH>
```

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:
Expand Down
44 changes: 35 additions & 9 deletions src/ape_accounts/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'"
)


Expand All @@ -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
Expand All @@ -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,
)
Expand Down
92 changes: 90 additions & 2 deletions tests/integration/cli/test_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,26 +157,114 @@ 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()


@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()
Expand Down

0 comments on commit c536511

Please sign in to comment.