Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install package
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade setuptools
python -m pip install --upgrade build pip setuptools twine
python -m pip install .
- name: Run test
run: |
python tests/test_mnemonic.py
- name: Build wheel and sdist
run: |
python -m build
- name: Check long description
run: |
twine check dist/*
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_, and this project adheres to
`Semantic Versioning`_.

`0.22`_ - Unreleased
--------------------

.. _0.22: https://github.com/trezor/python-mnemonic/compare/v0.21...HEAD

Added
~~~~~

- Command-line interface with ``create``, ``check``, and ``to-seed`` commands
- Click as a runtime dependency

`0.21`_ - 2024-01-05
--------------------

Expand Down
51 changes: 47 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,31 @@ python-mnemonic
Reference implementation of BIP-0039: Mnemonic code for generating
deterministic keys

Maintained by `Trezor <https://trezor.io>`_. See the `GitHub repository <https://github.com/trezor/python-mnemonic>`_ for source code and issue tracking.

Abstract
--------

This BIP describes the implementation of a mnemonic code or mnemonic sentence --
a group of easy to remember words -- for the generation of deterministic wallets.

It consists of two parts: generating the mnenomic, and converting it into a
It consists of two parts: generating the mnemonic, and converting it into a
binary seed. This seed can be later used to generate deterministic wallets using
BIP-0032 or similar methods.

BIP Paper
---------

See https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
for full specification
See `BIP-0039`_ for the full specification.

Installation
------------

To install this library and its dependencies use:

``pip install mnemonic``
.. code-block:: sh

$ pip install mnemonic

Usage examples
--------------
Expand Down Expand Up @@ -75,3 +78,43 @@ Given the word list, calculate original entropy:
.. code-block:: python

entropy = mnemo.to_entropy(words)

Command-line interface
----------------------

The ``mnemonic`` command provides CLI access to the library:

.. code-block:: sh

$ mnemonic create --help
$ mnemonic check --help
$ mnemonic to-seed --help

Generate a new mnemonic phrase:

.. code-block:: sh

$ mnemonic create
$ mnemonic create -s 256 -l english -p "my passphrase"
$ mnemonic create -s 256 -l english
$ mnemonic create -P # prompt for passphrase (hidden input)
$ mnemonic create --hide-seed # only output mnemonic, not the seed
$ MNEMONIC_PASSPHRASE="secret" mnemonic create

Validate a mnemonic phrase:

.. code-block:: sh

$ mnemonic check abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
$ echo "abandon abandon ..." | mnemonic check

Derive seed from a mnemonic phrase:

.. code-block:: sh

$ mnemonic to-seed abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
$ mnemonic to-seed -p "my passphrase" word1 word2 ...
$ mnemonic to-seed -P word1 word2 ... # prompt for passphrase (hidden input)
$ MNEMONIC_PASSPHRASE="secret" mnemonic to-seed word1 word2 ...

.. _BIP-0039: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ include = [

[tool.poetry.dependencies]
python = ">=3.8.1"
click = "^8.0"

[tool.poetry.scripts]
mnemonic = "mnemonic.cli:cli"

[tool.poetry.group.dev.dependencies]
isort = "^5.13.2"
Expand Down
168 changes: 168 additions & 0 deletions src/mnemonic/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import sys

import click

from mnemonic import Mnemonic
from mnemonic.mnemonic import ConfigurationError


@click.group()
def cli() -> None:
"""BIP-39 mnemonic phrase generator and validator."""
pass


@cli.command()
@click.option(
"-l",
"--language",
default="english",
type=str,
help="Language for the mnemonic wordlist.",
)
@click.option(
"-s",
"--strength",
default="128",
type=click.Choice(["128", "160", "192", "224", "256"]),
help="Entropy strength in bits.",
)
@click.option(
"-p",
"--passphrase",
default="",
envvar="MNEMONIC_PASSPHRASE",
type=str,
help="Passphrase for seed derivation. Can also be set via MNEMONIC_PASSPHRASE env var.",
)
@click.option(
"-P",
"--prompt-passphrase",
is_flag=True,
default=False,
help="Prompt for passphrase with hidden input (secure).",
)
@click.option(
"--hide-seed",
is_flag=True,
default=False,
help="Do not display the derived seed.",
)
def create(
language: str,
passphrase: str,
prompt_passphrase: bool,
strength: str,
hide_seed: bool,
) -> None:
"""Generate a new mnemonic phrase and its derived seed."""
if prompt_passphrase:
if passphrase:
click.secho(
"Warning: --prompt-passphrase overrides -p/MNEMONIC_PASSPHRASE.",
fg="yellow",
err=True,
)
passphrase = click.prompt("Passphrase", default="", hide_input=True)
try:
mnemo = Mnemonic(language)
words = mnemo.generate(int(strength))
click.echo(f"Mnemonic: {words}")
if not hide_seed:
seed = mnemo.to_seed(words, passphrase)
click.echo(f"Seed: {seed.hex()}")
except ConfigurationError as e:
raise click.ClickException(str(e))


@cli.command()
@click.option(
"-l",
"--language",
default=None,
type=str,
help="Language for the mnemonic wordlist. Auto-detected if not specified.",
)
@click.argument("words", nargs=-1)
def check(language: str | None, words: tuple[str, ...]) -> None:
"""Validate a mnemonic phrase's checksum.

WORDS can be provided as arguments or piped via stdin.
"""
if words:
mnemonic = " ".join(words)
else:
mnemonic = sys.stdin.read().strip()

if not mnemonic:
raise click.ClickException("No mnemonic provided.")

try:
if language is None:
language = Mnemonic.detect_language(mnemonic)
mnemo = Mnemonic(language)
if mnemo.check(mnemonic):
click.secho("Valid mnemonic.", fg="green")
else:
raise click.ClickException("Invalid mnemonic checksum.")
except ConfigurationError as e:
raise click.ClickException(str(e))
except (ValueError, LookupError) as e:
raise click.ClickException(str(e))


@cli.command("to-seed")
@click.option(
"-p",
"--passphrase",
default="",
envvar="MNEMONIC_PASSPHRASE",
type=str,
help="Passphrase for seed derivation. Can also be set via MNEMONIC_PASSPHRASE env var.",
)
@click.option(
"-P",
"--prompt-passphrase",
is_flag=True,
default=False,
help="Prompt for passphrase with hidden input (secure).",
)
@click.argument("words", nargs=-1)
def to_seed(passphrase: str, prompt_passphrase: bool, words: tuple[str, ...]) -> None:
"""Derive a seed from a mnemonic phrase.

WORDS can be provided as arguments or piped via stdin.
Outputs the 64-byte seed in hexadecimal format.
"""
if words:
mnemonic = " ".join(words)
else:
mnemonic = sys.stdin.read().strip()

if not mnemonic:
raise click.ClickException("No mnemonic provided.")

if prompt_passphrase:
if passphrase:
click.secho(
"Warning: --prompt-passphrase overrides -p/MNEMONIC_PASSPHRASE.",
fg="yellow",
err=True,
)
passphrase = click.prompt("Passphrase", default="", hide_input=True)

try:
language = Mnemonic.detect_language(mnemonic)
mnemo = Mnemonic(language)
if not mnemo.check(mnemonic):
raise click.ClickException("Invalid mnemonic checksum.")
seed = mnemo.to_seed(mnemonic, passphrase)
click.echo(seed.hex())
except ConfigurationError as e:
raise click.ClickException(str(e))
except (ValueError, LookupError) as e:
raise click.ClickException(str(e))


if __name__ == "__main__":
cli()
Loading