Skip to content

Commit e905f93

Browse files
committed
fix: address all comments from Copilot
Signed-off-by: Willian Paixao <willian@ufpa.br>
1 parent 7c37cf2 commit e905f93

2 files changed

Lines changed: 148 additions & 16 deletions

File tree

src/mnemonic/cli.py

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import click
44

55
from mnemonic import Mnemonic
6+
from mnemonic.mnemonic import ConfigurationError
67

78

89
@click.group()
@@ -22,28 +23,43 @@ def cli() -> None:
2223
@click.option(
2324
"-s",
2425
"--strength",
25-
default=128,
26-
type=int,
27-
help="Entropy strength in bits (128, 160, 192, 224, or 256).",
26+
default="128",
27+
type=click.Choice(["128", "160", "192", "224", "256"]),
28+
callback=lambda ctx, param, value: int(value),
29+
help="Entropy strength in bits.",
2830
)
2931
@click.option(
3032
"-p",
3133
"--passphrase",
3234
default="",
35+
envvar="MNEMONIC_PASSPHRASE",
3336
type=str,
34-
help="Optional passphrase for seed derivation.",
37+
help="Passphrase for seed derivation. Can also be set via MNEMONIC_PASSPHRASE env var.",
38+
)
39+
@click.option(
40+
"-P",
41+
"--prompt-passphrase",
42+
is_flag=True,
43+
default=False,
44+
help="Prompt for passphrase with hidden input (secure).",
3545
)
3646
def create(
3747
language: str,
3848
passphrase: str,
49+
prompt_passphrase: bool,
3950
strength: int,
4051
) -> None:
4152
"""Generate a new mnemonic phrase and its derived seed."""
42-
mnemo = Mnemonic(language)
43-
words = mnemo.generate(strength)
44-
seed = mnemo.to_seed(words, passphrase)
45-
click.echo(f"Mnemonic: {words}")
46-
click.echo(f"Seed: {seed.hex()}")
53+
if prompt_passphrase:
54+
passphrase = click.prompt("Passphrase", default="", hide_input=True)
55+
try:
56+
mnemo = Mnemonic(language)
57+
words = mnemo.generate(strength)
58+
seed = mnemo.to_seed(words, passphrase)
59+
click.echo(f"Mnemonic: {words}")
60+
click.echo(f"Seed: {seed.hex()}")
61+
except ConfigurationError as e:
62+
raise click.ClickException(str(e))
4763

4864

4965
@cli.command()
@@ -79,21 +95,30 @@ def check(language: str | None, words: tuple[str, ...]) -> None:
7995
else:
8096
click.secho("Invalid mnemonic checksum.", fg="red", err=True)
8197
sys.exit(1)
82-
except Exception as e:
83-
click.secho(f"Error: {e}", fg="red", err=True)
84-
sys.exit(1)
98+
except ConfigurationError as e:
99+
raise click.ClickException(str(e))
100+
except (ValueError, LookupError) as e:
101+
raise click.ClickException(str(e))
85102

86103

87104
@cli.command("to-seed")
88105
@click.option(
89106
"-p",
90107
"--passphrase",
91108
default="",
109+
envvar="MNEMONIC_PASSPHRASE",
92110
type=str,
93-
help="Optional passphrase for seed derivation.",
111+
help="Passphrase for seed derivation. Can also be set via MNEMONIC_PASSPHRASE env var.",
112+
)
113+
@click.option(
114+
"-P",
115+
"--prompt-passphrase",
116+
is_flag=True,
117+
default=False,
118+
help="Prompt for passphrase with hidden input (secure).",
94119
)
95120
@click.argument("words", nargs=-1)
96-
def to_seed(passphrase: str, words: tuple[str, ...]) -> None:
121+
def to_seed(passphrase: str, prompt_passphrase: bool, words: tuple[str, ...]) -> None:
97122
"""Derive a seed from a mnemonic phrase.
98123
99124
WORDS can be provided as arguments or piped via stdin.
@@ -108,8 +133,20 @@ def to_seed(passphrase: str, words: tuple[str, ...]) -> None:
108133
click.secho("Error: No mnemonic provided.", fg="red", err=True)
109134
sys.exit(1)
110135

111-
seed = Mnemonic.to_seed(mnemonic, passphrase)
112-
click.echo(seed.hex())
136+
if prompt_passphrase:
137+
passphrase = click.prompt("Passphrase", default="", hide_input=True)
138+
139+
try:
140+
language = Mnemonic.detect_language(mnemonic)
141+
mnemo = Mnemonic(language)
142+
if not mnemo.check(mnemonic):
143+
raise click.ClickException("Invalid mnemonic checksum.")
144+
seed = mnemo.to_seed(mnemonic, passphrase)
145+
click.echo(seed.hex())
146+
except ConfigurationError as e:
147+
raise click.ClickException(str(e))
148+
except (ValueError, LookupError) as e:
149+
raise click.ClickException(str(e))
113150

114151

115152
if __name__ == "__main__":

tests/test_mnemonic.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
import unittest
2727
from typing import List
2828

29+
from click.testing import CliRunner
30+
2931
from mnemonic import Mnemonic
32+
from mnemonic.cli import cli
3033

3134

3235
class MnemonicTest(unittest.TestCase):
@@ -149,6 +152,98 @@ def test_expand(self) -> None:
149152
)
150153

151154

155+
class CLITest(unittest.TestCase):
156+
def setUp(self) -> None:
157+
self.runner = CliRunner()
158+
159+
def test_create_generates_valid_mnemonic(self) -> None:
160+
result = self.runner.invoke(cli, ["create"])
161+
self.assertEqual(result.exit_code, 0)
162+
self.assertIn("Mnemonic:", result.output)
163+
self.assertIn("Seed:", result.output)
164+
# Extract mnemonic and verify it's valid
165+
mnemonic_line = result.output.split("\n")[0]
166+
mnemonic = mnemonic_line.replace("Mnemonic: ", "")
167+
mnemo = Mnemonic("english")
168+
self.assertTrue(mnemo.check(mnemonic))
169+
170+
def test_create_with_strength(self) -> None:
171+
result = self.runner.invoke(cli, ["create", "-s", "256"])
172+
self.assertEqual(result.exit_code, 0)
173+
mnemonic_line = result.output.split("\n")[0]
174+
mnemonic = mnemonic_line.replace("Mnemonic: ", "")
175+
# 256 bits = 24 words
176+
self.assertEqual(len(mnemonic.split()), 24)
177+
178+
def test_create_invalid_strength(self) -> None:
179+
result = self.runner.invoke(cli, ["create", "-s", "100"])
180+
self.assertNotEqual(result.exit_code, 0)
181+
182+
def test_create_invalid_language(self) -> None:
183+
result = self.runner.invoke(cli, ["create", "-l", "klingon"])
184+
self.assertEqual(result.exit_code, 1)
185+
self.assertIn("Error", result.output)
186+
187+
def test_check_valid_mnemonic(self) -> None:
188+
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
189+
result = self.runner.invoke(cli, ["check"] + mnemonic.split())
190+
self.assertEqual(result.exit_code, 0)
191+
self.assertIn("Valid mnemonic", result.output)
192+
193+
def test_check_invalid_mnemonic(self) -> None:
194+
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon wrong"
195+
result = self.runner.invoke(cli, ["check"] + mnemonic.split())
196+
self.assertEqual(result.exit_code, 1)
197+
self.assertIn("Invalid mnemonic checksum", result.output)
198+
199+
def test_check_stdin(self) -> None:
200+
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
201+
result = self.runner.invoke(cli, ["check"], input=mnemonic)
202+
self.assertEqual(result.exit_code, 0)
203+
self.assertIn("Valid mnemonic", result.output)
204+
205+
def test_check_empty_input(self) -> None:
206+
result = self.runner.invoke(cli, ["check"], input="")
207+
self.assertEqual(result.exit_code, 1)
208+
self.assertIn("No mnemonic provided", result.output)
209+
210+
def test_to_seed_valid_mnemonic(self) -> None:
211+
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
212+
result = self.runner.invoke(cli, ["to-seed"] + mnemonic.split())
213+
self.assertEqual(result.exit_code, 0)
214+
expected_seed = "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
215+
self.assertEqual(result.output.strip(), expected_seed)
216+
217+
def test_to_seed_with_passphrase(self) -> None:
218+
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
219+
result = self.runner.invoke(cli, ["to-seed", "-p", "TREZOR"] + mnemonic.split())
220+
self.assertEqual(result.exit_code, 0)
221+
expected_seed = "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"
222+
self.assertEqual(result.output.strip(), expected_seed)
223+
224+
def test_to_seed_with_env_passphrase(self) -> None:
225+
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
226+
result = self.runner.invoke(
227+
cli, ["to-seed"] + mnemonic.split(), env={"MNEMONIC_PASSPHRASE": "TREZOR"}
228+
)
229+
self.assertEqual(result.exit_code, 0)
230+
expected_seed = "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"
231+
self.assertEqual(result.output.strip(), expected_seed)
232+
233+
def test_to_seed_invalid_mnemonic(self) -> None:
234+
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon wrong"
235+
result = self.runner.invoke(cli, ["to-seed"] + mnemonic.split())
236+
self.assertEqual(result.exit_code, 1)
237+
self.assertIn("Invalid mnemonic checksum", result.output)
238+
239+
def test_to_seed_stdin(self) -> None:
240+
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
241+
result = self.runner.invoke(cli, ["to-seed"], input=mnemonic)
242+
self.assertEqual(result.exit_code, 0)
243+
expected_seed = "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
244+
self.assertEqual(result.output.strip(), expected_seed)
245+
246+
152247
def __main__() -> None:
153248
unittest.main()
154249

0 commit comments

Comments
 (0)