diff --git a/.gitignore b/.gitignore index e61d52c9..8dac3a38 100644 --- a/.gitignore +++ b/.gitignore @@ -12,10 +12,15 @@ experiment/ # Setuptools stuff build/ dist/ -hdwallet.egg-info/ +hdwallet*.egg-info/ # Python stuff __pycache__/ # py.test stuff .pytest_cache/ + +# NixOS stuff +flake.lock +CLAUDE.md +.claude diff --git a/CHANGELOG.md b/CHANGELOG.md index 0478a852..d994ad70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [v3.7.0](https://github.com/hdwallet-io/python-hdwallet/tree/v3.7.0) (2025-08-20) + +[Full Changelog](https://github.com/hdwallet-io/python-hdwallet/compare/v3.6.1...v3.7.0) + +**New Additions:** + +- Add: SLIP-39 mnemonic and seed support with multi-language wordlist handling +- Add: Raw entropy support for SLIP-39 seeds + +**Enhancements:** + +- Improve: SLIP-39 mnemonic implementation and multi-mnemonic handling + ## [v3.6.1](https://github.com/hdwallet-io/python-hdwallet/tree/v3.6.1) (2025-08-04) [Full Changelog](https://github.com/hdwallet-io/python-hdwallet/compare/v3.6.0...v3.6.1) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f75d0161..c687dae4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,28 @@ with the owners of this repository before making a change. ## Development -To get started, just fork this repo, clone it locally, and run: +### Using Nix (Recommended) + +The easiest way to get a complete development environment is to use Nix. This approach provides the correct Python version and all dependencies automatically. + +If you have Nix installed, you can get a full development/runtime environment by running: + +``` +make nix-venv +``` + +This will activate an interactive shell with the Nix environment and Python virtual environment set up. + +To run specific commands in the Nix + venv environment, use the pattern `make nix-venv-target`: + +``` +make nix-venv-test # Run tests in Nix + venv environment +make nix-venv-install # Install package in Nix + venv environment +``` + +### Manual Setup + +Alternatively, you can set up the development environment manually. Fork this repo, clone it locally, and run: ``` pip install -e .[cli,tests,docs] diff --git a/MANIFEST.in b/MANIFEST.in index bffbb377..9431c6ee 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,14 @@ include LICENSE include README.md include requirements.txt +include requirements/cli.txt +include requirements/tests.txt +include requirements/docs.txt +include requirements/dev.txt recursive-include hdwallet/mnemonics/algorand/wordlist *.txt recursive-include hdwallet/mnemonics/bip39/wordlist *.txt +recursive-include hdwallet/mnemonics/slip39/wordlist *.txt recursive-include hdwallet/mnemonics/electrum/v1/wordlist *.txt recursive-include hdwallet/mnemonics/electrum/v2/wordlist *.txt recursive-include hdwallet/mnemonics/monero/wordlist *.txt diff --git a/Makefile b/Makefile index b97de95f..c5b88aae 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,106 @@ -# Minimal makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation, Nix and venv # +SHELL := /bin/bash + # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = docs -BUILDDIR = build +export SPHINXOPTS ?= +export SPHINXBUILD ?= sphinx-build +export SOURCEDIR = docs +export BUILDDIR = build + +export PYTHON ?= $(shell python3 --version >/dev/null 2>&1 && echo python3 || echo python ) + +# Ensure $(PYTHON), $(VENV) are re-evaluated at time of expansion, when target 'python' and 'poetry' are known to be available +PYTHON_V = $(shell $(PYTHON) -c "import sys; print('-'.join((('venv' if sys.prefix != sys.base_prefix else next(iter(filter(None,sys.base_prefix.split('/'))))),sys.platform,sys.implementation.cache_tag)))" 2>/dev/null ) + +export PYTEST ?= $(PYTHON) -m pytest +export PYTEST_OPTS ?= # -vv --capture=no + + +VERSION = $(shell $(PYTHON) -c "exec(open('hdwallet/info.py').read()); print(__version__[1:])" ) +WHEEL = dist/hdwallet-$(VERSION)-py3-none-any.whl +VENV = $(CURDIR)-$(VERSION)-$(PYTHON_V) + +# Force export of variables that might be set from command line +export VENV_OPTS ?= +export NIX_OPTS ?= # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: help Makefile +.PHONY: help wheel install test analyze types venv Makefile FORCE + + +wheel: $(WHEEL) + +$(WHEEL): FORCE + $(PYTHON) -m build + @ls -last dist + +# Install from wheel, including all optional extra dependencies (doesn't include dev) +install: $(WHEEL) FORCE + $(PYTHON) -m pip install --force-reinstall $<[cli,tests,docs] + +# Install from requirements/*; eg. install-dev, always getting the latest version +install-%: FORCE + $(PYTHON) -m pip install --upgrade -r requirements/$*.txt + + +unit-%: + $(PYTEST) $(PYTEST_OPTS) -k $* + +test: + $(PYTEST) $(PYTEST_OPTS) tests + +analyze: + $(PYTHON) -m flake8 --color never -j 1 --max-line-length=250 \ + --ignore=W503,W504,E201,E202,E223,E226 \ + hdwallet + +types: + mypy . + +# +# Nix and VirtualEnv build, install and activate +# +# Create, start and run commands in "interactive" shell with a python venv's activate init-file. +# Doesn't allow recursive creation of a venv with a venv-supplied python. Alters the bin/activate +# to include the user's .bashrc (eg. Git prompts, aliases, ...). Use to run Makefile targets in a +# proper context, for example to obtain a Nix environment containing the proper Python version, +# create a python venv with the current Python environment. +# +# make nix-venv-build +# +nix-%: + @if [ -r flake.nix ]; then \ + nix develop $(NIX_OPTS) --command make $*; \ + else \ + nix-shell $(NIX_OPTS) --run "make $*"; \ + fi + +venv-%: $(VENV) + @echo; echo "*** Running in $< VirtualEnv: make $*" + @bash --init-file $#supported-cryptocurrencies | -| Entropies | `Algorand`, `BIP39`, `Electrum-V1`, `Electrum-V2`, `Monero` | -| Mnemonics | `Algorand`, `BIP39`, `Electrum-V1`, `Electrum-V2`, `Monero` | -| Seeds | `Algorand`, `BIP39`, `Cardano`, `Electrum-V1`, `Electrum-V2`, `Monero` | +| Entropies | `Algorand`, `BIP39`, `SLIP39`, `Electrum-V1`, `Electrum-V2`, `Monero` | +| Mnemonics | `Algorand`, `BIP39`, `SLIP39`, `Electrum-V1`, `Electrum-V2`, `Monero` | +| Seeds | `Algorand`, `BIP39`, `SLIP39`, `Cardano`, `Electrum-V1`, `Electrum-V2`, `Monero` | | Elliptic Curve Cryptography's | `Kholaw-Ed25519`, `SLIP10-Ed25519`, `SLIP10-Ed25519-Blake2b`, `SLIP10-Ed25519-Monero`, `SLIP10-Nist256p1`, `SLIP10-Secp256k1` | | Hierarchical Deterministic's | `Algorand`, `BIP32`, `BIP44`, `BIP49`, `BIP84`, `BIP86`, `BIP141`, `Cardano`, `Electrum-V1`, `Electrum-V2`, `Monero` | | Derivations | `BIP44`, `BIP49`, `BIP84`, `BIP86`, `CIP1852`, `Custom`, `Electrum`, `Monero`, `HDW (Our own custom derivation)` | diff --git a/clients/brave_legacy.py b/clients/brave_legacy.py index 13611d66..9baa6c11 100644 --- a/clients/brave_legacy.py +++ b/clients/brave_legacy.py @@ -2,7 +2,7 @@ from hdwallet import HDWallet from hdwallet.mnemonics import BIP39Mnemonic -from hdwallet.seeds import BIP39Seed +from hdwallet.seeds import BIP39Seed, SLIP39Seed from hdwallet.cryptocurrencies import Ethereum as Cryptocurrency from hdwallet.hds import BIP44HD from hdwallet.derivations import ( @@ -15,14 +15,14 @@ MNEMONIC: str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon " \ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" -# Initialize Ethereum HDWallet +# Initialize Ethereum HDWallet using BIP-39 Entropy as Seed hdwallet: HDWallet = HDWallet( cryptocurrency=Cryptocurrency, hd=BIP44HD, network=Cryptocurrency.NETWORKS.MAINNET, passphrase=None ).from_seed( # Get Ethereum HDWallet from seed - seed=BIP39Seed( + seed=SLIP39Seed( seed=BIP39Mnemonic.decode(mnemonic=MNEMONIC) # Use decoded mnemonic (entropy) directly as seed ) ).from_derivation( # Drive from BIP44 derivation @@ -36,4 +36,28 @@ # Same address of Brave crypto wallets extension # print(json.dumps(hdwallet.dump(exclude={"indexes"}), indent=4, ensure_ascii=False)) +print(f"Path: {hdwallet.path()}") print(f"Address: {hdwallet.address()}") + +assert hdwallet.address() == "0xACA6302EcBde40120cb8A08361D8BD461282Bd18" + +# Initialize Ethereum HDWallet using BIP-39 Seed (confirmed current Brave Wallet behavior) +hdwallet: HDWallet = HDWallet( + cryptocurrency=Cryptocurrency, + hd=BIP44HD, + network=Cryptocurrency.NETWORKS.MAINNET, + passphrase=None +).from_seed( # Get Ethereum HDWallet from seed + seed=BIP39Seed.from_mnemonic(mnemonic=MNEMONIC) # Use BIP-39 encoded mnemonic as seed +).from_derivation( # Drive from BIP44 derivation + derivation=BIP44Derivation( + coin_type=Cryptocurrency.COIN_TYPE, + account=0, + change=CHANGES.EXTERNAL_CHAIN, + address=0 + ) +) + +# print(json.dumps(hdwallet.dump(exclude={"indexes"}), indent=4, ensure_ascii=False)) +print(f"Address: {hdwallet.address()}") +assert hdwallet.address() == "0xF278cF59F82eDcf871d630F28EcC8056f25C1cdb" diff --git a/clients/cardano/byron_icarus.py b/clients/cardano/byron_icarus.py index d926d087..1566bb10 100644 --- a/clients/cardano/byron_icarus.py +++ b/clients/cardano/byron_icarus.py @@ -38,7 +38,7 @@ seed: str = CardanoSeed.from_mnemonic( mnemonic=bip39_mnemonic, cardano_type=Cardano.TYPES.BYRON_ICARUS ) -cardano_seed: CardanoSeed = CardanoSeed(seed=seed) +cardano_seed: CardanoSeed = CardanoSeed(seed=seed, cardano_type=Cardano.TYPES.BYRON_ICARUS) # Update Byron-Icarus Cardano HD root keys from seed cardano_hd.from_seed( diff --git a/clients/cardano/byron_ledger.py b/clients/cardano/byron_ledger.py index beec179d..a37ea6b2 100644 --- a/clients/cardano/byron_ledger.py +++ b/clients/cardano/byron_ledger.py @@ -38,7 +38,7 @@ seed: str = CardanoSeed.from_mnemonic( mnemonic=bip39_mnemonic, cardano_type=Cardano.TYPES.BYRON_LEDGER ) -cardano_seed: CardanoSeed = CardanoSeed(seed=seed) +cardano_seed: CardanoSeed = CardanoSeed(seed=seed, cardano_type=Cardano.TYPES.BYRON_LEDGER) # Update Byron-Ledger Cardano HD root keys from seed cardano_hd.from_seed( diff --git a/clients/cardano/byron_legacy.py b/clients/cardano/byron_legacy.py index af370928..382705f8 100644 --- a/clients/cardano/byron_legacy.py +++ b/clients/cardano/byron_legacy.py @@ -38,7 +38,7 @@ seed: str = CardanoSeed.from_mnemonic( mnemonic=bip39_mnemonic, cardano_type=Cardano.TYPES.BYRON_LEGACY ) -cardano_seed: CardanoSeed = CardanoSeed(seed=seed) +cardano_seed: CardanoSeed = CardanoSeed(seed=seed, cardano_type=Cardano.TYPES.BYRON_LEGACY) # Update Byron-Legacy Cardano HD root keys from seed cardano_hd.from_seed( diff --git a/clients/cardano/shelley_icarus.py b/clients/cardano/shelley_icarus.py index 0a4702bb..cceafe64 100644 --- a/clients/cardano/shelley_icarus.py +++ b/clients/cardano/shelley_icarus.py @@ -39,7 +39,7 @@ seed: str = CardanoSeed.from_mnemonic( mnemonic=bip39_mnemonic, cardano_type=Cardano.TYPES.SHELLEY_ICARUS ) -cardano_seed: CardanoSeed = CardanoSeed(seed=seed) +cardano_seed: CardanoSeed = CardanoSeed(seed=seed, cardano_type=Cardano.TYPES.SHELLEY_ICARUS) # Update Shelley-Icarus Cardano HD root keys from seed cardano_hd.from_seed( diff --git a/clients/cardano/shelley_ledger.py b/clients/cardano/shelley_ledger.py index b1248621..a13f31ac 100644 --- a/clients/cardano/shelley_ledger.py +++ b/clients/cardano/shelley_ledger.py @@ -39,7 +39,7 @@ seed: str = CardanoSeed.from_mnemonic( mnemonic=bip39_mnemonic, cardano_type=Cardano.TYPES.SHELLEY_LEDGER ) -cardano_seed: CardanoSeed = CardanoSeed(seed=seed) +cardano_seed: CardanoSeed = CardanoSeed(seed=seed, cardano_type=Cardano.TYPES.SHELLEY_LEDGER) # Update Shelley-Ledger Cardano HD root keys from seed cardano_hd.from_seed( diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..c130bc57 --- /dev/null +++ b/default.nix @@ -0,0 +1,92 @@ +{ pkgs ? import ./nixpkgs.nix {} }: + +with pkgs; + +let +in +{ + py314 = stdenv.mkDerivation rec { + name = "python314-with-poetry"; + + buildInputs = [ + cacert + git + gnumake + openssh + python314 + python314Packages.pytest + python314Packages.coincurve + python314Packages.scikitlearn + python314Packages.pycryptodome + python314Packages.pynacl + ]; + }; + + py313 = stdenv.mkDerivation rec { + name = "python313-with-poetry"; + + buildInputs = [ + cacert + git + gnumake + openssh + python313 + python313Packages.pytest + python313Packages.coincurve + python313Packages.scikitlearn + python313Packages.pycryptodome + python313Packages.pynacl + ]; + }; + + py312 = stdenv.mkDerivation rec { + name = "python312-with-poetry"; + + buildInputs = [ + cacert + git + gnumake + openssh + python312 + python312Packages.pytest + python312Packages.coincurve + python312Packages.scikitlearn + python312Packages.pycryptodome + python312Packages.pynacl + ]; + }; + + py311 = stdenv.mkDerivation rec { + name = "python311-with-poetry"; + + buildInputs = [ + cacert + git + gnumake + openssh + python311 + python311Packages.pytest + python311Packages.coincurve + python311Packages.scikitlearn + python311Packages.pycryptodome + python311Packages.pynacl + ]; + }; + + py310 = stdenv.mkDerivation rec { + name = "python310-with-poetry"; + + buildInputs = [ + cacert + git + gnumake + openssh + python310 + python310Packages.pytest + python310Packages.coincurve + python310Packages.scikitlearn + python310Packages.pycryptodome + python310Packages.pynacl + ]; + }; +} diff --git a/examples/hdwallet/algorand/from_seed.py b/examples/hdwallet/algorand/from_seed.py index 948482db..6f937f12 100644 --- a/examples/hdwallet/algorand/from_seed.py +++ b/examples/hdwallet/algorand/from_seed.py @@ -14,7 +14,7 @@ hd=AlgorandHD ).from_seed( seed=AlgorandSeed( - seed="fca87b68fdffa968895901c894f678f6" + seed="a27436e742dafe27428b84925d4be6a1c40856d14dc73d54431a94bd6b95264b" ) ).from_derivation( derivation=CustomDerivation( diff --git a/examples/hdwallet/bips/from_entropy.py b/examples/hdwallet/bips/from_entropy.py index f7f8263b..80ef1d0c 100644 --- a/examples/hdwallet/bips/from_entropy.py +++ b/examples/hdwallet/bips/from_entropy.py @@ -5,7 +5,7 @@ BIP39Entropy, BIP39_ENTROPY_STRENGTHS ) from hdwallet.mnemonics import BIP39_MNEMONIC_LANGUAGES -from hdwallet.cryptocurrencies import Qtum as Cryptocurrency +from hdwallet.cryptocurrencies import Bitcoin as Cryptocurrency from hdwallet.consts import PUBLIC_KEY_TYPES from hdwallet.derivations import ( BIP44Derivation, CHANGES diff --git a/examples/hdwallet/bips/from_entropy_slip39.py b/examples/hdwallet/bips/from_entropy_slip39.py new file mode 100644 index 00000000..ae9c413c --- /dev/null +++ b/examples/hdwallet/bips/from_entropy_slip39.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 + +from hdwallet import HDWallet +from hdwallet.entropies import ( + IEntropy, SLIP39Entropy, SLIP39_ENTROPY_STRENGTHS, BIP39Entropy, BIP39_ENTROPY_STRENGTHS, +) +from hdwallet.mnemonics import ( + IMnemonic, BIP39_MNEMONIC_LANGUAGES, BIP39Mnemonic, SLIP39Mnemonic +) +from hdwallet.seeds import (ISeed, SLIP39Seed) +from hdwallet.cryptocurrencies import Bitcoin as Cryptocurrency +from hdwallet.consts import PUBLIC_KEY_TYPES +from hdwallet.derivations import ( + BIP84Derivation, CHANGES +) +from hdwallet.hds import BIP84HD + +import json + +entropy_hex = "ffffffffffffffffffffffffffffffff" +entropy_bip39 = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong" + +slip39_entropy: IEntropy = SLIP39Entropy( + entropy=entropy_hex +) +slip39_mnemonic: IMnemonic = SLIP39Mnemonic( SLIP39Mnemonic.from_entropy( + entropy=slip39_entropy, language=BIP39_MNEMONIC_LANGUAGES.ENGLISH +)) +bip39_mnemonic: IMnemonic = BIP39Mnemonic( BIP39Mnemonic.from_entropy( + entropy=entropy_hex, language=BIP39_MNEMONIC_LANGUAGES.ENGLISH +)) +assert bip39_mnemonic.mnemonic() == entropy_bip39 + +slip39_seed: ISeed = SLIP39Seed( + seed=slip39_entropy.entropy() +) + +slip39_seed_recovered = SLIP39Seed.from_mnemonic(slip39_mnemonic) +assert slip39_seed_recovered == slip39_seed.seed() == slip39_entropy.entropy() + +# +# SLIP-39 can transport 128- or 256-bit entropy that is used DIRECTLY as an HD Wallet seed. +# +# This IS the standard way a Trezor SLIP-39 wallet recovery works, and produces the same wallets as +# if the Trezor hardware wallet was recovered using the SLIP-39 Mnemonics. +# +# SLIP-39 Entropy: ffffffffffffffffffffffffffffffff +# == 128-bit HD Wallet Seed +# +hdwallet: HDWallet = HDWallet( + cryptocurrency=Cryptocurrency, + hd=BIP84HD, + network=Cryptocurrency.NETWORKS.MAINNET, + language=BIP39_MNEMONIC_LANGUAGES.ENGLISH, + public_key_type=PUBLIC_KEY_TYPES.COMPRESSED, + passphrase="" +).from_seed( + seed=slip39_seed +) + +assert hdwallet.address() == "bc1q9yscq3l2yfxlvnlk3cszpqefparrv7tk24u6pl" +assert hdwallet.entropy() == None +assert hdwallet.seed() == "ffffffffffffffffffffffffffffffff" + + +# +# A SLIP-39 encoded 128- or 256-bit entropy can also be converted into BIP-39 entropy, and then into +# BIP-39 Mnemonics. This is NOT the normal Trezor SLIP-39 HD Wallet derivation; instead, it uses +# SLIP-39 to remember the original source entropy, and THEN encodes it into BIP-39 Mnemonics, and +# THEN uses mnemonics to recover the wallet via BIP-39 seed derivation. However, this IS the best +# approach to backing up BIP-39 Mnemonics via SLIP-39, as it retains the BIP-39 passphrase, and +# results in exactly the same HD Wallet derivations as if the original BIP-39 Mnemonics had been +# entered directly. +# +# SLIP-39 Entropy: ffffffffffffffffffffffffffffffff +# => BIP-39 Mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong" +# => BIP-39 Seed: b6a6...25b6 +# == 512-bit HD Wallet Seed +# +hdwallet: HDWallet = HDWallet( + cryptocurrency=Cryptocurrency, + hd=BIP84HD, + network=Cryptocurrency.NETWORKS.MAINNET, + language=BIP39_MNEMONIC_LANGUAGES.ENGLISH, + public_key_type=PUBLIC_KEY_TYPES.COMPRESSED, + passphrase="" +).from_entropy( + entropy=slip39_entropy +) +assert hdwallet.address() == "bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2" +assert hdwallet.entropy() == "ffffffffffffffffffffffffffffffff" +assert hdwallet.seed() == "b6a6d8921942dd9806607ebc2750416b289adea669198769f2e15ed926c3aa92bf88ece232317b4ea463e84b0fcd3b53577812ee449ccc448eb45e6f544e25b6" \ + + +# To "back up" an existing BIP-39 Mnemonic phrase into multiple SLIP-39 cards, there are 2 ways: +# +# 1) Back up the BIP-39 Mnemonic's *input* 128- or 256-bit entropy +# a) decode the *input* entropy from the BIP-39 Mnemonic +# b) generate 20- or 33-word SLIP-39 Mnemonic(s) encoding the *input* entropy +# b1) optionally provide a SLIP-39 passphrase (not recommended) +# (later) +# c) recover the 128- or 256-bit entropy from 20- or 33-word SLIP-39 Mnemonic(s) +# c1) enter the SLIP-39 passphrase (not recommended) +# d) re-generate the BIP-39 Mnemonic from the entropy +# e) recover the wallet from BIP-39 Mnemonic generated Seed +# e1) enter the original BIP-39 passphrase +# +# 2) Back up the BIP-39 Mnemonic's *output* 512-bit seed +# a) generate the 512-bit BIP-39 Seed from the Mnemonic +# a1) enter the original BIP-39 passphrase +# b) generate 59-word SLIP-39 Mnemonic(s) encoding the output seed +# b1) optionally provide a SLIP-39 passphrase (not recommended) +# (later) +# c) recover the 512-bit seed from 59-word SLIP-39 Mnemonic(s) +# c1) enter the SLIP-39 passphrase (not recommended) +# d) recover the wallet from the 512-bit data as Seed +# d1) no BIP-39 passphrase required + +hdwallet: HDWallet = HDWallet( + cryptocurrency=Cryptocurrency, + hd=BIP84HD, + network=Cryptocurrency.NETWORKS.MAINNET, + language=BIP39_MNEMONIC_LANGUAGES.ENGLISH, + public_key_type=PUBLIC_KEY_TYPES.COMPRESSED, + passphrase="" +).from_mnemonic( + mnemonic=BIP39Mnemonic(mnemonic=entropy_bip39) +) +assert hdwallet.address() == "bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2" +assert hdwallet.entropy() == "ffffffffffffffffffffffffffffffff" +assert hdwallet.mnemonic() == entropy_bip39 +assert hdwallet.seed() \ + == "b6a6d8921942dd9806607ebc2750416b289adea669198769f2e15ed926c3aa92bf88ece232317b4ea463e84b0fcd3b53577812ee449ccc448eb45e6f544e25b6" + +# 1a-b) Decode and backup *input* entropy to SLIP-39 Mnemonics (size is 20 or 33 words based on input entropy size) +bip39_bu_1a_input_entropy = hdwallet.entropy() +bip39_bu_1b_input_slip39 = SLIP39Mnemonic.encode( + entropy=bip39_bu_1a_input_entropy, + language="Backup 4: One 1/1, Two 1/1, Fam 2/4, Fren 3/6", + passphrase="Don't use this", # 1b1) optional SLIP-39 passphrase - not well supported; leave empty + tabulate=True, +) +#print(f"{bip39_bu_1b_input_slip39}") + +# 1c-d) Recover *input* BIP-39 entropy from SLIP-39 +bip39_bu_1c_input_entropy = SLIP39Mnemonic.decode( + bip39_bu_1b_input_slip39, + passphrase="Don't use this", # 1c1) must match 1b1) - any passphrase is valid, produces different wallets +) +assert bip39_bu_1c_input_entropy == bip39_bu_1a_input_entropy + +# 1e) Recover BIP-39 wallet from SLIP-39 entropy (converts to BIP-39 mnemonic) +hdwallet: HDWallet = HDWallet( + cryptocurrency=Cryptocurrency, + hd=BIP84HD, + network=Cryptocurrency.NETWORKS.MAINNET, + language=BIP39_MNEMONIC_LANGUAGES.ENGLISH, + public_key_type=PUBLIC_KEY_TYPES.COMPRESSED, + passphrase="" +).from_entropy( + entropy=SLIP39Entropy(entropy=bip39_bu_1c_input_entropy) +) +assert hdwallet.address() == "bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2" +assert hdwallet.entropy() == "ffffffffffffffffffffffffffffffff" +assert hdwallet.mnemonic() == entropy_bip39 +assert hdwallet.seed() \ + == "b6a6d8921942dd9806607ebc2750416b289adea669198769f2e15ed926c3aa92bf88ece232317b4ea463e84b0fcd3b53577812ee449ccc448eb45e6f544e25b6" + +# 2a-b Recover *output* BIP-39 seed and backup to SLIP-39 Mnemonics (size is 59 based on 512-bit BIP-39 seed) +bip39_bu_2a_output_seed = hdwallet.seed() +bip39_bu_2b_output_slip39 = SLIP39Mnemonic.encode( + entropy=bip39_bu_2a_output_seed, + language="Backup 4: One 1/1, Two 1/1, Fam 2/4, Fren 3/6", + passphrase="Don't use this", # 2b1) optional SLIP-39 passphrase - not well supported; leave empty + tabulate=True, +) +#print(f"{bip39_bu_2b_output_slip39}") + +# 2c) Recover *output* BIP-39 seed from SLIP-39 +bip39_bu_2c_output_seed = SLIP39Mnemonic.decode( + bip39_bu_2b_output_slip39, + passphrase="Don't use this", # 2c1) must match 2b1) - any passphrase is valid, produces different wallets +) + +# 2d) recover the wallet from the 512-bit data as Seed +hdwallet: HDWallet = HDWallet( + cryptocurrency=Cryptocurrency, + hd=BIP84HD, + network=Cryptocurrency.NETWORKS.MAINNET, + language=BIP39_MNEMONIC_LANGUAGES.ENGLISH, + public_key_type=PUBLIC_KEY_TYPES.COMPRESSED, + passphrase="" +).from_seed( + seed=SLIP39Seed(seed=bip39_bu_2c_output_seed) +) +assert hdwallet.address() == "bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2" +assert hdwallet.entropy() == None +assert hdwallet.mnemonic() == None +assert hdwallet.seed() \ + == "b6a6d8921942dd9806607ebc2750416b289adea669198769f2e15ed926c3aa92bf88ece232317b4ea463e84b0fcd3b53577812ee449ccc448eb45e6f544e25b6" + + +#print(json.dumps(hdwallet.dumps(exclude={"indexes"}), indent=4, ensure_ascii=False)) + +# print("Cryptocurrency:", hdwallet.cryptocurrency()) +# print("Symbol:", hdwallet.symbol()) +# print("Network:", hdwallet.network()) +# print("Coin Type:", hdwallet.coin_type()) +# print("Entropy:", hdwallet.entropy()) +# print("Strength:", hdwallet.strength()) +# print("Mnemonic:", hdwallet.mnemonic()) +# print("Passphrase:", hdwallet.passphrase()) +# print("Language:", hdwallet.language()) +# print("Seed:", hdwallet.seed()) +# print("ECC:", hdwallet.ecc()) +# print("HD:", hdwallet.hd()) +# print("Semantic:", hdwallet.semantic()) +# print("Root XPrivate Key:", hdwallet.root_xprivate_key()) +# print("Root XPublic Key:", hdwallet.root_xpublic_key()) +# print("Root Private Key:", hdwallet.root_private_key()) +# print("Root WIF:", hdwallet.root_wif()) +# print("Root Chain Code:", hdwallet.root_chain_code()) +# print("Root Public Key:", hdwallet.root_public_key()) +# print("Strict:", hdwallet.strict()) +# print("Public Key Type:", hdwallet.public_key_type()) +# print("WIF Type:", hdwallet.wif_type()) +# print("Path:", hdwallet.path()) +# print("Depth:", hdwallet.depth()) +# print("Indexes:", hdwallet.indexes()) +# print("Index:", hdwallet.index()) +# print("XPrivate Key:", hdwallet.xprivate_key()) +# print("XPublic Key:", hdwallet.xpublic_key()) +# print("Private Key:", hdwallet.private_key()) +# print("WIF:", hdwallet.wif()) +# print("Chain Code:", hdwallet.chain_code()) +# print("Public Key:", hdwallet.public_key()) +# print("Uncompressed:", hdwallet.uncompressed()) +# print("Compressed:", hdwallet.compressed()) +# print("Hash:", hdwallet.hash()) +# print("Fingerprint:", hdwallet.fingerprint()) +# print("Parent Fingerprint:", hdwallet.parent_fingerprint()) +# print("Address:", hdwallet.address()) diff --git a/examples/hdwallet/cardano/from_entropy.py b/examples/hdwallet/cardano/from_entropy.py index 54f2fdf3..d786717f 100644 --- a/examples/hdwallet/cardano/from_entropy.py +++ b/examples/hdwallet/cardano/from_entropy.py @@ -24,7 +24,7 @@ ).from_entropy( entropy=BIP39Entropy( entropy=BIP39Entropy.generate( - strength=BIP39_ENTROPY_STRENGTHS.TWO_HUNDRED_TWENTY_FOUR + strength=BIP39_ENTROPY_STRENGTHS.ONE_HUNDRED_TWENTY_EIGHT ) ) ).from_derivation( diff --git a/examples/hdwallet/cardano/from_seed.py b/examples/hdwallet/cardano/from_seed.py index aa919267..f2e034d5 100644 --- a/examples/hdwallet/cardano/from_seed.py +++ b/examples/hdwallet/cardano/from_seed.py @@ -20,7 +20,8 @@ passphrase="talonlab" ).from_seed( seed=CardanoSeed( - seed="fca87b68fdffa968895901c894f678f6" + seed="fca87b68fdffa968895901c894f678f6", + cardano_type=Cryptocurrency.TYPES.SHELLEY_ICARUS, ) ).from_derivation( derivation=CIP1852Derivation( diff --git a/examples/mnemonics/slip39.py b/examples/mnemonics/slip39.py new file mode 100644 index 00000000..02c219bd --- /dev/null +++ b/examples/mnemonics/slip39.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 + +from typing import Type + +from hdwallet.mnemonics import ( + MNEMONICS, IMnemonic, SLIP39Mnemonic, SLIP39_MNEMONIC_LANGUAGES, SLIP39_MNEMONIC_WORDS +) + +data = { + "name": "SLIP39", + "entropy": "b66022fff8b6322f8b8fa444d6d097457b6b9e7bb05add5b75f9c827df7bd3b6", + "mnemonic": ( + "drug cleanup academic academic august branch cage company example duke" + " uncover glen already mortgage ticket emphasis papa agree fitness capacity" + " evening glad trust raspy year sweater hormone database kernel cultural" + " fact angry goat" + ), + "language": SLIP39_MNEMONIC_LANGUAGES.ENGLISH, + "words": SLIP39_MNEMONIC_WORDS.THIRTY_THREE, +} + +SLIP39MnemonicClass: Type[IMnemonic] = MNEMONICS.mnemonic(data["name"]) + +slip39_mnemonic_class = SLIP39MnemonicClass(data["mnemonic"]) +slip39_mnemonic = SLIP39Mnemonic(data["mnemonic"]) + +print( + slip39_mnemonic_class.decode(mnemonic=slip39_mnemonic_class.mnemonic()) + == slip39_mnemonic.decode(mnemonic=slip39_mnemonic.mnemonic()) + == slip39_mnemonic_class.decode(SLIP39MnemonicClass.from_entropy(data["entropy"], data["language"])) + == slip39_mnemonic.decode(SLIP39Mnemonic.from_entropy(data["entropy"], data["language"])) + == SLIP39Mnemonic.decode(mnemonic=data["mnemonic"]), + + slip39_mnemonic_class.language() == slip39_mnemonic.language() == data["language"], + + slip39_mnemonic_class.words() == slip39_mnemonic.words() == data["words"], + + SLIP39MnemonicClass.is_valid(data["mnemonic"]) == SLIP39Mnemonic.is_valid(data["mnemonic"]), + + SLIP39MnemonicClass.is_valid_language(data["language"]) == SLIP39Mnemonic.is_valid_language(data["language"]), + + SLIP39MnemonicClass.is_valid_words(data["words"]) == SLIP39Mnemonic.is_valid_words(data["words"]), + + len(SLIP39MnemonicClass.from_words(data["words"], data["language"]).split(" ")) == + len(SLIP39Mnemonic.from_words(data["words"], data["language"]).split(" ")), "\n" +) + +print("Client:", data["name"]) +print("Mnemonic:", data["mnemonic"]) +print("Language:", data["language"]) +print("Words:", data["words"]) diff --git a/examples/seeds/cardano.py b/examples/seeds/cardano.py index 5a19e777..10436fce 100644 --- a/examples/seeds/cardano.py +++ b/examples/seeds/cardano.py @@ -47,8 +47,8 @@ CardanoSeedClass: Type[ISeed] = SEEDS.seed(data["name"]) for seed in data["seeds"]: - cardano_seed_class = CardanoSeedClass(seed["seed"]) - cardano_seed = CardanoSeed(seed["seed"]) + cardano_seed_class = CardanoSeedClass(seed["seed"], cardano_type=seed['cardano_type']) + cardano_seed = CardanoSeed(seed["seed"], cardano_type=seed['cardano_type']) # Always provide passphrase=None if not present, like TS passphrase = seed.get("passphrase", None) diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..ec442452 --- /dev/null +++ b/flake.nix @@ -0,0 +1,63 @@ +{ + description = "Python HD Wallet development environment with multiple Python versions"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/25.05"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + # Create Python environments with required packages + mkPythonEnv = pythonPkg: pythonPkg.withPackages (ps: with ps; [ + pytest + coincurve + scikit-learn + pycryptodome + pynacl + ]); + + python310Env = mkPythonEnv pkgs.python310; + python311Env = mkPythonEnv pkgs.python311; + python312Env = mkPythonEnv pkgs.python312; + python313Env = mkPythonEnv pkgs.python313; + python314Env = mkPythonEnv pkgs.python314; + + in { + # Single development shell with all Python versions + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + # Common tools + cacert + git + gnumake + openssh + bash + bash-completion + + # All Python versions with packages + #python310Env + python311Env + python312Env + python313Env + #python314Env + ]; + + shellHook = '' + echo "Welcome to the multi-Python development environment!" + echo "Available Python interpreters:" + echo " python (default): $(python --version 2>&1 || echo 'not available')" + #echo " python3.10: $(python3.10 --version 2>&1 || echo 'not available')" + echo " python3.11: $(python3.11 --version 2>&1 || echo 'not available')" + echo " python3.12: $(python3.12 --version 2>&1 || echo 'not available')" + echo " python3.13: $(python3.13 --version 2>&1 || echo 'not available')" + #echo " python3.14: $(python3.14 --version 2>&1 || echo 'not available')" + echo "" + echo "All versions have pytest, coincurve, scikit-learn, pycryptodome, and pynacl installed." + ''; + }; + }); +} diff --git a/hdwallet/addresses/iaddress.py b/hdwallet/addresses/iaddress.py index f5a085ba..ec7079b5 100644 --- a/hdwallet/addresses/iaddress.py +++ b/hdwallet/addresses/iaddress.py @@ -10,7 +10,7 @@ ABC, abstractmethod ) from typing import ( - Union, Optional + Union ) from ..eccs import IPublicKey diff --git a/hdwallet/addresses/monero.py b/hdwallet/addresses/monero.py index e14d7f09..fa2153c2 100644 --- a/hdwallet/addresses/monero.py +++ b/hdwallet/addresses/monero.py @@ -173,10 +173,10 @@ def decode( "Invalid length", expected=(expected_length + cls.payment_id_length), got=len(payload_without_prefix) - ) + ) from ex if payment_id is None or len(payment_id) != cls.payment_id_length: - raise Error("Invalid payment ID") + raise Error("Invalid payment ID") from ex payment_id_got_bytes = payload_without_prefix[-cls.payment_id_length:] if payment_id != payment_id_got_bytes: @@ -184,7 +184,7 @@ def decode( "Invalid payment ID", expected=bytes_to_string(payment_id_got_bytes), got=bytes_to_string(payment_id_got_bytes) - ) + ) from ex length: int = SLIP10Ed25519MoneroPublicKey.compressed_length() diff --git a/hdwallet/addresses/okt_chain.py b/hdwallet/addresses/okt_chain.py index 8a62118c..bb4f063b 100644 --- a/hdwallet/addresses/okt_chain.py +++ b/hdwallet/addresses/okt_chain.py @@ -69,7 +69,7 @@ def decode(cls, address: str, **kwargs: Any) -> str: :return: Decoded OKT-Chain address. :rtype: str """ - + return EthereumAddress.decode( EthereumAddress.address_prefix + bytes_to_string( bech32_decode(cls.hrp, address)[1] diff --git a/hdwallet/addresses/p2pkh.py b/hdwallet/addresses/p2pkh.py index c3944d77..04cad03e 100644 --- a/hdwallet/addresses/p2pkh.py +++ b/hdwallet/addresses/p2pkh.py @@ -24,7 +24,7 @@ class P2PKHAddress(IAddress): - + public_key_address_prefix: int = Bitcoin.NETWORKS.MAINNET.PUBLIC_KEY_ADDRESS_PREFIX alphabet: str = Bitcoin.PARAMS.ALPHABET diff --git a/hdwallet/addresses/p2tr.py b/hdwallet/addresses/p2tr.py index a7a75953..4145438c 100644 --- a/hdwallet/addresses/p2tr.py +++ b/hdwallet/addresses/p2tr.py @@ -23,7 +23,7 @@ class P2TRAddress(IAddress): - + hrp: str = Bitcoin.NETWORKS.MAINNET.HRP field_size: int = Bitcoin.PARAMS.FIELD_SIZE tap_tweak_sha256: bytes = get_bytes(Bitcoin.PARAMS.TAP_TWEAK_SHA256) diff --git a/hdwallet/addresses/p2wpkh.py b/hdwallet/addresses/p2wpkh.py index 6de5da14..33db3999 100644 --- a/hdwallet/addresses/p2wpkh.py +++ b/hdwallet/addresses/p2wpkh.py @@ -23,7 +23,7 @@ class P2WPKHAddress(IAddress): - + hrp: str = Bitcoin.NETWORKS.MAINNET.HRP witness_version: int = Bitcoin.NETWORKS.MAINNET.WITNESS_VERSIONS.P2WPKH diff --git a/hdwallet/addresses/tron.py b/hdwallet/addresses/tron.py index b7828006..18369c69 100644 --- a/hdwallet/addresses/tron.py +++ b/hdwallet/addresses/tron.py @@ -57,7 +57,7 @@ def encode(cls, public_key: Union[bytes, str, IPublicKey], **kwargs: Any) -> str public_key: IPublicKey = validate_and_get_public_key( public_key=public_key, public_key_cls=SLIP10Secp256k1PublicKey ) - + address: str = bytes_to_string( kekkak256(public_key.raw_uncompressed()[1:]) )[24:] @@ -81,7 +81,7 @@ def decode(cls, address: str, **kwargs: Any) -> str: :return: Decoded public key. :rtype: str """ - + address_decode: bytes = check_decode( address, alphabet=kwargs.get( "alphabet", cls.alphabet diff --git a/hdwallet/cli/__init__.py b/hdwallet/cli/__init__.py index 29084982..776a2a4e 100644 --- a/hdwallet/cli/__init__.py +++ b/hdwallet/cli/__init__.py @@ -7,6 +7,6 @@ from bip38 import cryptocurrencies BIP38_CRYPTOCURRENCIES = { - name: cls for name, cls in inspect.getmembers(cryptocurrencies, inspect.isclass) + name: cls for name, cls in inspect.getmembers(cryptocurrencies, inspect.isclass) if issubclass(cls, cryptocurrencies.ICryptocurrency) -} \ No newline at end of file +} diff --git a/hdwallet/cli/__main__.py b/hdwallet/cli/__main__.py index d216ac0d..9760ef60 100644 --- a/hdwallet/cli/__main__.py +++ b/hdwallet/cli/__main__.py @@ -26,6 +26,15 @@ from .list.strengths import list_strengths +def process_kwargs(kwargs): + """Process mnemonic arguments handling both multiple flags and explicit \\n sequences.""" + if kwargs.get("mnemonic"): + # Join multiple mnemonic arguments, then split on any literal raw r"\n" and rejoin + combined = "\n".join(kwargs["mnemonic"]) + kwargs["mnemonic"] = "\n".join(combined.split(r'\n')) + return kwargs + + def current_version( context: click.core.Context, option: click.core.Option, value: bool ) -> None: @@ -95,15 +104,31 @@ def cli_entropy(**kwargs) -> None: @click.option( "-l", "--language", type=str, default=None, help="Set Mnemonic language", show_default=True ) +@click.option( + "-p", "--passphrase", type=str, default=None, help="Set Mnemonic passphrase for SLIP39", show_default=True +) +@click.option( + "-t", "--tabulate", type=int, default=False, help="Set Mnemonic tabulation SLIP39", show_default=True +) +# Sources of entropy for the mnemonic; raw 'entropy', 'words', or another 'mnemonic' of 'mnemonic_type' @click.option( "-e", "--entropy", type=str, default=None, help="Set Mnemonic entropy", show_default=True ) @click.option( "-w", "--words", type=int, default=None, help="Set Mnemonic words", show_default=True ) +@click.option( + "-mc", "--mnemonic-client", type=str, default="BIP39", help="Select entropy Mnemonic client", show_default=True +) +@click.option( + "-m", "--mnemonic", multiple=True, help="Set entropy Mnemonic(s)" +) @click.option( "-mt", "--mnemonic-type", type=str, default="standard", help="Set Mnemonic type for Electrum-V2", show_default=True ) +@click.option( + "-mp", "--mnemonic-passphrase", type=str, default=None, help="Set entropy Mnemonic passphrase", show_default=True +) @click.option( "-max", "--max-attempts", type=int, default=(10 ** 60), help="Set Max attempts for Electrum-V2", show_default=True ) @@ -124,7 +149,10 @@ def cli_mnemonic(**kwargs) -> None: "-c", "--client", type=str, default="BIP39", help="Set Seed client", show_default=True ) @click.option( - "-m", "--mnemonic", type=str, default=None, help="Set Seed mnemonic", show_default=True + "-m", "--mnemonic", multiple=True, help="Set Seed mnemonic(s)" +) +@click.option( + "-l", "--language", type=str, default=None, help="Set Mnemonic language", show_default=True ) @click.option( "-p", "--passphrase", type=str, default=None, help="Set Seed passphrase", show_default=True @@ -136,7 +164,7 @@ def cli_mnemonic(**kwargs) -> None: "-mt", "--mnemonic-type", type=str, default="standard", help="Set Mnemonic type for Electrum-V2", show_default=True ) def cli_seed(**kwargs) -> None: - return generate_seed(**kwargs) + return generate_seed(**process_kwargs(kwargs)) @cli_main.command( @@ -161,7 +189,7 @@ def cli_seed(**kwargs) -> None: "-mc", "--mnemonic-client", type=str, default="BIP39", help="Select Mnemonic client", show_default=True ) @click.option( - "-m", "--mnemonic", type=str, default=None, help="Set Master key from Mnemonic words", show_default=True + "-m", "--mnemonic", multiple=True, help="Set Master key from mnemonic(s)" ) @click.option( "-l", "--language", type=str, default="english", help="Select Language for mnemonic", show_default=True @@ -269,7 +297,7 @@ def cli_seed(**kwargs) -> None: "-ex", "--exclude", type=str, default="", help="Set Exclude keys from dumped", show_default=True ) def cli_dump(**kwargs) -> None: # cli_dumps(max_content_width=120) - return dump(**kwargs) + return dump(**process_kwargs(kwargs)) @cli_main.command( @@ -294,7 +322,7 @@ def cli_dump(**kwargs) -> None: # cli_dumps(max_content_width=120) "-mc", "--mnemonic-client", type=str, default="BIP39", help="Select Mnemonic client", show_default=True ) @click.option( - "-m", "--mnemonic", type=str, default=None, help="Set Master key from Mnemonic words", show_default=True + "-m", "--mnemonic", multiple=True, help="Set Master key from mnemonic(s)" ) @click.option( "-l", "--language", type=str, default="english", help="Select Language for mnemonic", show_default=True @@ -414,7 +442,7 @@ def cli_dump(**kwargs) -> None: # cli_dumps(max_content_width=120) "-de", "--delimiter", type=str, default=" ", help="Set Delimiter for CSV", show_default=True ) def cli_dumps(**kwargs) -> None: # cli_dumps(max_content_width=120) - return dumps(**kwargs) + return dumps(**process_kwargs(kwargs)) @cli_main.group( diff --git a/hdwallet/cli/dump.py b/hdwallet/cli/dump.py index 53bc236c..179b2582 100644 --- a/hdwallet/cli/dump.py +++ b/hdwallet/cli/dump.py @@ -56,7 +56,7 @@ def dump(**kwargs) -> None: semantic = "p2wpkh-in-p2sh" elif kwargs.get("hd") in ["BIP84", "BIP141"]: semantic = "p2wpkh" - + hdwallet: HDWallet = HDWallet( cryptocurrency=cryptocurrency, hd=HDS.hd(name=kwargs.get("hd")), @@ -110,11 +110,20 @@ def dump(**kwargs) -> None: f"Wrong seed client, (expected={SEEDS.names()}, got='{kwargs.get('seed_client')}')" ), err=True) sys.exit() - hdwallet.from_seed( - seed=SEEDS.seed(name=kwargs.get("seed_client")).__call__( - seed=kwargs.get("seed") + if kwargs.get("seed_client") == "Cardano" and kwargs.get("cardano_type"): + # If a specific cardano_type is specified, we must override the CardanoSeed default + hdwallet.from_seed( + seed=SEEDS.seed(name=kwargs.get("seed_client")).__call__( + seed=kwargs.get("seed"), + cardano_type=kwargs.get("cardano_type") + ) + ) + else: + hdwallet.from_seed( + seed=SEEDS.seed(name=kwargs.get("seed_client")).__call__( + seed=kwargs.get("seed") + ) ) - ) elif kwargs.get("xprivate_key"): hdwallet.from_xprivate_key( xprivate_key=kwargs.get("xprivate_key"), @@ -138,7 +147,7 @@ def dump(**kwargs) -> None: if kwargs.get("bip38"): bip38: BIP38 = BIP38( - cryptocurrency=BIP38_CRYPTOCURRENCIES[cryptocurrency.NAME], network=kwargs.get("network") + cryptocurrency=BIP38_CRYPTOCURRENCIES[cryptocurrency.NAME], network=kwargs.get("network") ) _wif = bip38.decrypt(encrypted_wif=_wif, passphrase=kwargs.get("passphrase")) diff --git a/hdwallet/cli/dumps.py b/hdwallet/cli/dumps.py index 73a10a58..f9e3557c 100644 --- a/hdwallet/cli/dumps.py +++ b/hdwallet/cli/dumps.py @@ -116,11 +116,20 @@ def dumps(**kwargs) -> None: f"Wrong seed client, (expected={SEEDS.names()}, got='{kwargs.get('seed_client')}')" ), err=True) sys.exit() - hdwallet.from_seed( - seed=SEEDS.seed(name=kwargs.get("seed_client")).__call__( - seed=kwargs.get("seed") + if kwargs.get("seed_client") == "Cardano" and kwargs.get("cardano_type"): + # If a specific cardano_type is specified, we must override the CardanoSeed default + hdwallet.from_seed( + seed=SEEDS.seed(name=kwargs.get("seed_client")).__call__( + seed=kwargs.get("seed"), + cardano_type=kwargs.get("cardano_type") + ) + ) + else: + hdwallet.from_seed( + seed=SEEDS.seed(name=kwargs.get("seed_client")).__call__( + seed=kwargs.get("seed") + ) ) - ) elif kwargs.get("xprivate_key"): hdwallet.from_xprivate_key( xprivate_key=kwargs.get("xprivate_key"), @@ -143,7 +152,7 @@ def dumps(**kwargs) -> None: if kwargs.get("bip38"): bip38: BIP38 = BIP38( - cryptocurrency=BIP38_CRYPTOCURRENCIES[cryptocurrency.NAME], network=kwargs.get("network") + cryptocurrency=BIP38_CRYPTOCURRENCIES[cryptocurrency.NAME], network=kwargs.get("network") ) _wif = bip38.decrypt(encrypted_wif=_wif, passphrase=kwargs.get("passphrase")) @@ -226,7 +235,6 @@ def dumps(**kwargs) -> None: ) ) - hd_name: str = hdwallet._hd.name() if kwargs.get("include"): _include: str = kwargs.get("include") @@ -262,7 +270,6 @@ def dumps(**kwargs) -> None: elif hdwallet.cryptocurrency() == "Binance": _include: str = "at:path,addresses:chain,public_key,wif" - hdwallet_csv = csv.DictWriter( sys.stdout, fieldnames=_include.split(","), extrasaction="ignore", delimiter=kwargs.get("delimiter") ) @@ -375,7 +382,6 @@ def drive_helper(derivations, current_derivation: List[Tuple[int, bool]] = []) - hdwallet.dump(exclude={'derivation', *excludes}), indent=4, ensure_ascii=False )) - drive(*hdwallet._derivation.derivations()) else: click.echo(click.style( diff --git a/hdwallet/cli/generate/mnemonic.py b/hdwallet/cli/generate/mnemonic.py index e18e61a9..78279425 100644 --- a/hdwallet/cli/generate/mnemonic.py +++ b/hdwallet/cli/generate/mnemonic.py @@ -12,6 +12,7 @@ IMnemonic, AlgorandMnemonic, ALGORAND_MNEMONIC_WORDS, ALGORAND_MNEMONIC_LANGUAGES, BIP39Mnemonic, BIP39_MNEMONIC_WORDS, BIP39_MNEMONIC_LANGUAGES, + SLIP39Mnemonic, SLIP39_MNEMONIC_WORDS, SLIP39_MNEMONIC_LANGUAGES, ElectrumV1Mnemonic, ELECTRUM_V1_MNEMONIC_WORDS, ELECTRUM_V1_MNEMONIC_LANGUAGES, ElectrumV2Mnemonic, ELECTRUM_V2_MNEMONIC_WORDS, ELECTRUM_V2_MNEMONIC_LANGUAGES, MoneroMnemonic, MONERO_MNEMONIC_WORDS, MONERO_MNEMONIC_LANGUAGES, @@ -20,6 +21,10 @@ def generate_mnemonic(**kwargs) -> None: + """Produce a Mnemonic of type 'client' in 'language'. Source from 'entropy' or 'mnemonic', or + produce new entropy appropriate for a certain number of mnemonic 'words'. + + """ try: if not MNEMONICS.is_mnemonic(name=kwargs.get("client")): click.echo(click.style( @@ -32,6 +37,8 @@ def generate_mnemonic(**kwargs) -> None: language: str = ALGORAND_MNEMONIC_LANGUAGES.ENGLISH elif kwargs.get("client") == BIP39Mnemonic.name(): language: str = BIP39_MNEMONIC_LANGUAGES.ENGLISH + elif kwargs.get("client") == SLIP39Mnemonic.name(): + language: str = SLIP39_MNEMONIC_LANGUAGES.ENGLISH elif kwargs.get("client") == ElectrumV1Mnemonic.name(): language: str = ELECTRUM_V1_MNEMONIC_LANGUAGES.ENGLISH elif kwargs.get("client") == ElectrumV2Mnemonic.name(): @@ -46,6 +53,8 @@ def generate_mnemonic(**kwargs) -> None: words: int = ALGORAND_MNEMONIC_WORDS.TWENTY_FIVE elif kwargs.get("client") == BIP39Mnemonic.name(): words: int = BIP39_MNEMONIC_WORDS.TWELVE + elif kwargs.get("client") == SLIP39Mnemonic.name(): + words: int = SLIP39_MNEMONIC_WORDS.TWENTY elif kwargs.get("client") == ElectrumV1Mnemonic.name(): words: int = ELECTRUM_V1_MNEMONIC_WORDS.TWELVE elif kwargs.get("client") == ElectrumV2Mnemonic.name(): @@ -55,7 +64,7 @@ def generate_mnemonic(**kwargs) -> None: else: words: int = kwargs.get("words") - if not MNEMONICS.mnemonic(name=kwargs.get("client")).is_valid_language(language=language): + if not MNEMONICS.mnemonic(name=kwargs.get("client")).is_valid_language(language): click.echo(click.style( f"Wrong {kwargs.get('client')} mnemonic language, " f"(expected={MNEMONICS.mnemonic(name=kwargs.get('client')).languages}, got='{language}')" @@ -69,6 +78,43 @@ def generate_mnemonic(**kwargs) -> None: ), err=True) sys.exit() + if kwargs.get("entropy") and kwargs.get("mnemonic"): + click.echo(click.style( + "Supply either --entropy or --mnemonic, not both" + ), err=True) + sys.exit() + + if kwargs.get("mnemonic"): + # Get source entropy from another mnemonic. Doesn't support those requiring another + # different 'mnemonic_type' from that supplied for the output mnemonic. Recovering the + # original entropy from certain Mnemonics such as SLIP39 requires an optional + # passphrase. For most Mnemonic clients, a passphrase doesn't hide the original entropy + # -- it is used only when deriving wallets. + if not MNEMONICS.is_mnemonic(name=kwargs.get("mnemonic_client")): + click.echo(click.style( + f"Wrong mnemonic client, (expected={MNEMONICS.names()}, got='{kwargs.get('mnemonic_client')}')" + ), err=True) + sys.exit() + if kwargs.get("mnemonic_client") == ElectrumV2Mnemonic.name(): + entropy: str = ElectrumV2Mnemonic.decode( + mnemonic=kwargs.get("mnemonic"), + language=kwargs.get("language"), + mnemonic_type=kwargs.get("mnemonic_type") + ) + elif kwargs.get("mnemonic_client") == SLIP39Mnemonic.name(): + entropy: str = SLIP39Mnemonic.decode( + mnemonic=kwargs.get("mnemonic"), + language=kwargs.get("language"), + passphrase=kwargs.get("mnemonic_passphrase") or "", + ) + else: + entropy: str = MNEMONICS.mnemonic(name=kwargs.get("mnemonic_client")).decode( + mnemonic=kwargs.get("mnemonic"), + language=kwargs.get("language"), + ) + # Now, use the recovered 'entropy' in deriving the new 'client' mnemonic. + kwargs["entropy"] = entropy + if kwargs.get("entropy"): if kwargs.get("client") == ElectrumV2Mnemonic.name(): mnemonic: IMnemonic = ElectrumV2Mnemonic( @@ -78,6 +124,7 @@ def generate_mnemonic(**kwargs) -> None: mnemonic_type=kwargs.get("mnemonic_type"), max_attempts=kwargs.get("max_attempts") ), + language=language, mnemonic_type=kwargs.get("mnemonic_type") ) elif kwargs.get("client") == MoneroMnemonic.name(): @@ -86,13 +133,30 @@ def generate_mnemonic(**kwargs) -> None: entropy=kwargs.get("entropy"), language=language, checksum=kwargs.get("checksum") - ) + ), + language=language, + ) + elif kwargs.get("client") == SLIP39Mnemonic.name(): + # The supplied 'entropy', encoded w/ the SLIP-39 'language', and encrypted w/ + # 'passphrase' (default: ""). We remember the supplied language, because it + # deterministically describes the SLIP-39 secret and group encoding parameters, and + # can also contain specifics like the SLIP-39's overall name and groups' names. Any + # 'tabulate' supplied influences the formatting of the groups of SLIP-39 Mnemonics. + mnemonic: IMnemonic = SLIP39Mnemonic( + mnemonic=SLIP39Mnemonic.from_entropy( + entropy=kwargs.get("entropy"), + language=language, + passphrase=kwargs.get("passphrase") or "", + ), + language=language, + tabulate=kwargs.get("tabulate", False), ) else: mnemonic: IMnemonic = MNEMONICS.mnemonic(name=kwargs.get("client")).__call__( mnemonic=MNEMONICS.mnemonic(name=kwargs.get("client")).from_entropy( entropy=kwargs.get("entropy"), language=language - ) + ), + language=language, ) else: if kwargs.get("client") == ElectrumV2Mnemonic.name(): @@ -103,13 +167,15 @@ def generate_mnemonic(**kwargs) -> None: mnemonic_type=kwargs.get("mnemonic_type"), max_attempts=kwargs.get("max_attempts") ), + language=language, mnemonic_type=kwargs.get("mnemonic_type") ) else: mnemonic: IMnemonic = MNEMONICS.mnemonic(name=kwargs.get("client")).__call__( mnemonic=MNEMONICS.mnemonic(name=kwargs.get("client")).from_words( words=words, language=language - ) + ), + language=language, ) output: dict = { "client": mnemonic.name(), diff --git a/hdwallet/cli/generate/seed.py b/hdwallet/cli/generate/seed.py index e47b7166..9270173c 100644 --- a/hdwallet/cli/generate/seed.py +++ b/hdwallet/cli/generate/seed.py @@ -10,7 +10,7 @@ from ...mnemonics import MNEMONICS from ...seeds import ( - ISeed, BIP39Seed, CardanoSeed, ElectrumV2Seed, SEEDS + ISeed, BIP39Seed, SLIP39Seed, CardanoSeed, ElectrumV2Seed, SEEDS ) @@ -26,24 +26,32 @@ def generate_seed(**kwargs) -> None: ), err=True) sys.exit() - if kwargs.get("client") == "Electrum-V2": + if kwargs.get("client") == ElectrumV2Seed.name(): if not MNEMONICS.mnemonic(name="Electrum-V2").is_valid( mnemonic=kwargs.get("mnemonic"), mnemonic_type=kwargs.get("mnemonic_type") ): - click.echo(click.style(f"Invalid Electrum-V2 mnemonic"), err=True) + click.echo(click.style("Invalid Electrum-V2 mnemonic"), err=True) sys.exit() - else: + elif kwargs.get("client") != SLIP39Seed.name(): # SLIP39 supports any 128-, 256- or 512-bit Mnemonic mnemonic_name: str = "BIP39" if kwargs.get("client") == CardanoSeed.name() else kwargs.get("client") - if not MNEMONICS.mnemonic(name=mnemonic_name).is_valid(mnemonic=kwargs.get("mnemonic")): + if not MNEMONICS.mnemonic(name=mnemonic_name).is_valid(mnemonic=kwargs.get("mnemonic"), language=kwargs.get("language")): click.echo(click.style(f"Invalid {mnemonic_name} mnemonic"), err=True) sys.exit() - if kwargs.get("client") == BIP39Seed.name(): seed: ISeed = BIP39Seed( seed=BIP39Seed.from_mnemonic( mnemonic=kwargs.get("mnemonic"), - passphrase=kwargs.get("passphrase") + passphrase=kwargs.get("passphrase"), + language=kwargs.get("language"), + ) + ) + elif kwargs.get("client") == SLIP39Seed.name(): + seed: ISeed = SLIP39Seed( + seed=SLIP39Seed.from_mnemonic( + mnemonic=kwargs.get("mnemonic"), + passphrase=kwargs.get("passphrase"), + language=kwargs.get("language"), ) ) elif kwargs.get("client") == CardanoSeed.name(): @@ -51,21 +59,25 @@ def generate_seed(**kwargs) -> None: seed=CardanoSeed.from_mnemonic( mnemonic=kwargs.get("mnemonic"), passphrase=kwargs.get("passphrase"), + language=kwargs.get("language"), cardano_type=kwargs.get("cardano_type") - ) + ), + cardano_type=kwargs.get("cardano_type") ) elif kwargs.get("client") == ElectrumV2Seed.name(): seed: ISeed = ElectrumV2Seed( seed=ElectrumV2Seed.from_mnemonic( mnemonic=kwargs.get("mnemonic"), passphrase=kwargs.get("passphrase"), + language=kwargs.get("language"), mnemonic_type=kwargs.get("mnemonic_type") ) ) else: seed: ISeed = SEEDS.seed(name=kwargs.get("client")).__call__( seed=SEEDS.seed(name=kwargs.get("client")).from_mnemonic( - mnemonic=kwargs.get("mnemonic") + mnemonic=kwargs.get("mnemonic"), + language=kwargs.get("language"), ) ) output: dict = { diff --git a/hdwallet/consts.py b/hdwallet/consts.py index 539ec6b8..e802f300 100644 --- a/hdwallet/consts.py +++ b/hdwallet/consts.py @@ -14,9 +14,14 @@ class NestedNamespace(SimpleNamespace): + """Implements a NestedNamespace with support for sub-NestedNamespaces. + Processes the positional data in order, followed by any kwargs in order. As a result, the + __dict__ order reflects the order of the provided data and **kwargs (if they are ordered). + + """ def __init__(self, data: Union[set, tuple, dict], **kwargs): - super().__init__(**kwargs) + super().__init__() if isinstance(data, set): for item in data: self.__setattr__(item, item) @@ -36,12 +41,16 @@ def __init__(self, data: Union[set, tuple, dict], **kwargs): self.__setattr__(key, NestedNamespace(value)) else: self.__setattr__(key, value) - + for key, value in kwargs.items(): + if isinstance(value, dict): + self.__setattr__(key, NestedNamespace(value)) + else: + self.__setattr__(key, value) class SLIP10_ED25519_CONST: """ ``SLIP10-ED25519`` Constants. - + +-------------------------+--------------+ | Name | Value | +=========================+==============+ @@ -61,7 +70,7 @@ class SLIP10_ED25519_CONST: class KHOLAW_ED25519_CONST(SLIP10_ED25519_CONST): """ ``KHOLAW-ED25519`` Constants. - + +-------------------------+--------------+ | Name | Value | +=========================+==============+ @@ -79,7 +88,7 @@ class KHOLAW_ED25519_CONST(SLIP10_ED25519_CONST): class SLIP10_SECP256K1_CONST: """ ``SLIP10-SECP256K1`` Constants. - + +-------------------------------------+-------------+ | Name | Value | +=====================================+=============+ diff --git a/hdwallet/cryptocurrencies/adcoin.py b/hdwallet/cryptocurrencies/adcoin.py index b9feb141..565314da 100644 --- a/hdwallet/cryptocurrencies/adcoin.py +++ b/hdwallet/cryptocurrencies/adcoin.py @@ -55,9 +55,9 @@ class Adcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/akashnetwork.py b/hdwallet/cryptocurrencies/akashnetwork.py index 20d8e184..ac5dcca0 100644 --- a/hdwallet/cryptocurrencies/akashnetwork.py +++ b/hdwallet/cryptocurrencies/akashnetwork.py @@ -50,9 +50,9 @@ class AkashNetwork(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/algorand.py b/hdwallet/cryptocurrencies/algorand.py index e24c10c4..d46252d1 100644 --- a/hdwallet/cryptocurrencies/algorand.py +++ b/hdwallet/cryptocurrencies/algorand.py @@ -51,7 +51,7 @@ class Algorand(ICryptocurrency): {"ALGORAND": "Algorand"}, "BIP39" )) SEEDS = Seeds(( - {"ALGORAND": "Algorand"}, "BIP39" + {"ALGORAND": "Algorand"}, "BIP39", "SLIP39" )) HDS = HDs(( {"ALGORAND": "Algorand"}, "BIP32", "BIP44" diff --git a/hdwallet/cryptocurrencies/anon.py b/hdwallet/cryptocurrencies/anon.py index 465c3aba..27502623 100644 --- a/hdwallet/cryptocurrencies/anon.py +++ b/hdwallet/cryptocurrencies/anon.py @@ -55,9 +55,9 @@ class Anon(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/aptos.py b/hdwallet/cryptocurrencies/aptos.py index 2df4dbcc..1dc49aef 100644 --- a/hdwallet/cryptocurrencies/aptos.py +++ b/hdwallet/cryptocurrencies/aptos.py @@ -49,9 +49,9 @@ class Aptos(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/arbitum.py b/hdwallet/cryptocurrencies/arbitum.py index 51b3816a..1f3a4b66 100644 --- a/hdwallet/cryptocurrencies/arbitum.py +++ b/hdwallet/cryptocurrencies/arbitum.py @@ -50,9 +50,9 @@ class Arbitrum(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/argoneum.py b/hdwallet/cryptocurrencies/argoneum.py index 2700d43f..e022d9d3 100644 --- a/hdwallet/cryptocurrencies/argoneum.py +++ b/hdwallet/cryptocurrencies/argoneum.py @@ -54,9 +54,9 @@ class Argoneum(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/artax.py b/hdwallet/cryptocurrencies/artax.py index a3d33ab1..cd113cb8 100644 --- a/hdwallet/cryptocurrencies/artax.py +++ b/hdwallet/cryptocurrencies/artax.py @@ -54,9 +54,9 @@ class Artax(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/aryacoin.py b/hdwallet/cryptocurrencies/aryacoin.py index dda8b2c3..d7e44834 100644 --- a/hdwallet/cryptocurrencies/aryacoin.py +++ b/hdwallet/cryptocurrencies/aryacoin.py @@ -54,9 +54,9 @@ class Aryacoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/asiacoin.py b/hdwallet/cryptocurrencies/asiacoin.py index 4b02e436..7360792a 100644 --- a/hdwallet/cryptocurrencies/asiacoin.py +++ b/hdwallet/cryptocurrencies/asiacoin.py @@ -53,9 +53,9 @@ class Asiacoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) @@ -69,4 +69,3 @@ class Asiacoin(ICryptocurrency): "p2pkh", "p2sh" ] DEFAULT_SEMANTIC = "p2pkh" - diff --git a/hdwallet/cryptocurrencies/auroracoin.py b/hdwallet/cryptocurrencies/auroracoin.py index 39243d10..17cf4ce5 100644 --- a/hdwallet/cryptocurrencies/auroracoin.py +++ b/hdwallet/cryptocurrencies/auroracoin.py @@ -54,9 +54,9 @@ class Auroracoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) @@ -70,4 +70,3 @@ class Auroracoin(ICryptocurrency): "p2pkh", "p2sh" ] DEFAULT_SEMANTIC = "p2pkh" - diff --git a/hdwallet/cryptocurrencies/avalanche.py b/hdwallet/cryptocurrencies/avalanche.py index 08ab28c5..960456d4 100644 --- a/hdwallet/cryptocurrencies/avalanche.py +++ b/hdwallet/cryptocurrencies/avalanche.py @@ -52,9 +52,9 @@ class Avalanche(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/avian.py b/hdwallet/cryptocurrencies/avian.py index eb014757..fa340359 100644 --- a/hdwallet/cryptocurrencies/avian.py +++ b/hdwallet/cryptocurrencies/avian.py @@ -68,9 +68,9 @@ class Avian(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/axe.py b/hdwallet/cryptocurrencies/axe.py index acd36795..2785c27f 100644 --- a/hdwallet/cryptocurrencies/axe.py +++ b/hdwallet/cryptocurrencies/axe.py @@ -54,9 +54,9 @@ class Axe(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/axelar.py b/hdwallet/cryptocurrencies/axelar.py index feeef7d0..8198ebd6 100644 --- a/hdwallet/cryptocurrencies/axelar.py +++ b/hdwallet/cryptocurrencies/axelar.py @@ -52,9 +52,9 @@ class Axelar(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bandprotocol.py b/hdwallet/cryptocurrencies/bandprotocol.py index 3d534f8c..5c307662 100644 --- a/hdwallet/cryptocurrencies/bandprotocol.py +++ b/hdwallet/cryptocurrencies/bandprotocol.py @@ -51,9 +51,9 @@ class BandProtocol(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/base.py b/hdwallet/cryptocurrencies/base.py index 000092a5..f17d8863 100644 --- a/hdwallet/cryptocurrencies/base.py +++ b/hdwallet/cryptocurrencies/base.py @@ -50,9 +50,9 @@ class Base(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bata.py b/hdwallet/cryptocurrencies/bata.py index 391b1f13..16406345 100644 --- a/hdwallet/cryptocurrencies/bata.py +++ b/hdwallet/cryptocurrencies/bata.py @@ -18,7 +18,7 @@ class Mainnet(INetwork): NAME = "mainnet" PUBLIC_KEY_ADDRESS_PREFIX = 0x19 - SCRIPT_ADDRESS_PREFIX = 0x5 + SCRIPT_ADDRESS_PREFIX = 0x5 XPRIVATE_KEY_VERSIONS = XPrivateKeyVersions({ "P2PKH": 0xa40b91bd, "P2SH": 0xa40b91bd @@ -56,9 +56,9 @@ class Bata(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/beetlecoin.py b/hdwallet/cryptocurrencies/beetlecoin.py index 077603c6..7e1d1a11 100644 --- a/hdwallet/cryptocurrencies/beetlecoin.py +++ b/hdwallet/cryptocurrencies/beetlecoin.py @@ -54,9 +54,9 @@ class BeetleCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/belacoin.py b/hdwallet/cryptocurrencies/belacoin.py index 72a1cb80..c2f6076e 100644 --- a/hdwallet/cryptocurrencies/belacoin.py +++ b/hdwallet/cryptocurrencies/belacoin.py @@ -55,9 +55,9 @@ class BelaCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/binance.py b/hdwallet/cryptocurrencies/binance.py index a5f8ea41..a3157ebc 100644 --- a/hdwallet/cryptocurrencies/binance.py +++ b/hdwallet/cryptocurrencies/binance.py @@ -51,9 +51,9 @@ class Binance(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcloud.py b/hdwallet/cryptocurrencies/bitcloud.py index fd9564de..3e0908b6 100644 --- a/hdwallet/cryptocurrencies/bitcloud.py +++ b/hdwallet/cryptocurrencies/bitcloud.py @@ -18,7 +18,7 @@ class Mainnet(INetwork): NAME = "mainnet" PUBLIC_KEY_ADDRESS_PREFIX = 0x19 - SCRIPT_ADDRESS_PREFIX = 0x5 + SCRIPT_ADDRESS_PREFIX = 0x5 XPRIVATE_KEY_VERSIONS = XPrivateKeyVersions({ "P2PKH": 0x488ade4, "P2SH": 0x488ade4 @@ -55,9 +55,9 @@ class BitCloud(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcoin.py b/hdwallet/cryptocurrencies/bitcoin.py index 4a3d23ae..21eafe96 100644 --- a/hdwallet/cryptocurrencies/bitcoin.py +++ b/hdwallet/cryptocurrencies/bitcoin.py @@ -111,7 +111,7 @@ class Bitcoin(ICryptocurrency): "BIP39", {"ELECTRUM_V1": "Electrum-V1"}, {"ELECTRUM_V2": "Electrum-V2"} )) SEEDS = Seeds(( - "BIP39", {"ELECTRUM_V1": "Electrum-V1"}, {"ELECTRUM_V2": "Electrum-V2"} + "BIP39", {"ELECTRUM_V1": "Electrum-V1"}, {"ELECTRUM_V2": "Electrum-V2"}, "SLIP39" )) HDS = HDs(( "BIP32", "BIP44", "BIP49", "BIP84", "BIP86", "BIP141", {"ELECTRUM_V1": "Electrum-V1"}, {"ELECTRUM_V2": "Electrum-V2"} diff --git a/hdwallet/cryptocurrencies/bitcoinatom.py b/hdwallet/cryptocurrencies/bitcoinatom.py index 24361bee..b4662ba5 100644 --- a/hdwallet/cryptocurrencies/bitcoinatom.py +++ b/hdwallet/cryptocurrencies/bitcoinatom.py @@ -63,9 +63,9 @@ class BitcoinAtom(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcoincash.py b/hdwallet/cryptocurrencies/bitcoincash.py index c5e91c07..8087282a 100644 --- a/hdwallet/cryptocurrencies/bitcoincash.py +++ b/hdwallet/cryptocurrencies/bitcoincash.py @@ -105,9 +105,9 @@ class BitcoinCash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcoincashslp.py b/hdwallet/cryptocurrencies/bitcoincashslp.py index 6a7887a4..ba9ea69c 100644 --- a/hdwallet/cryptocurrencies/bitcoincashslp.py +++ b/hdwallet/cryptocurrencies/bitcoincashslp.py @@ -100,9 +100,9 @@ class BitcoinCashSLP(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcoingold.py b/hdwallet/cryptocurrencies/bitcoingold.py index b95bcada..c6d6f8fd 100644 --- a/hdwallet/cryptocurrencies/bitcoingold.py +++ b/hdwallet/cryptocurrencies/bitcoingold.py @@ -18,7 +18,7 @@ class Mainnet(INetwork): NAME = "mainnet" PUBLIC_KEY_ADDRESS_PREFIX = 0x26 - SCRIPT_ADDRESS_PREFIX = 0x17 + SCRIPT_ADDRESS_PREFIX = 0x17 HRP = "btg" WITNESS_VERSIONS = WitnessVersions({ "P2WPKH": 0x00, @@ -68,9 +68,9 @@ class BitcoinGold(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcoingreen.py b/hdwallet/cryptocurrencies/bitcoingreen.py index 3df76787..b1b4e549 100644 --- a/hdwallet/cryptocurrencies/bitcoingreen.py +++ b/hdwallet/cryptocurrencies/bitcoingreen.py @@ -54,9 +54,9 @@ class BitcoinGreen(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcoinplus.py b/hdwallet/cryptocurrencies/bitcoinplus.py index 9730bad6..aad557cc 100644 --- a/hdwallet/cryptocurrencies/bitcoinplus.py +++ b/hdwallet/cryptocurrencies/bitcoinplus.py @@ -55,9 +55,9 @@ class BitcoinPlus(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcoinprivate.py b/hdwallet/cryptocurrencies/bitcoinprivate.py index 7b76842d..bb0950ea 100644 --- a/hdwallet/cryptocurrencies/bitcoinprivate.py +++ b/hdwallet/cryptocurrencies/bitcoinprivate.py @@ -72,9 +72,9 @@ class BitcoinPrivate(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcoinsv.py b/hdwallet/cryptocurrencies/bitcoinsv.py index a602eb51..3f10f4bf 100644 --- a/hdwallet/cryptocurrencies/bitcoinsv.py +++ b/hdwallet/cryptocurrencies/bitcoinsv.py @@ -54,9 +54,9 @@ class BitcoinSV(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcoinz.py b/hdwallet/cryptocurrencies/bitcoinz.py index f30fcaf8..1eb3f85a 100644 --- a/hdwallet/cryptocurrencies/bitcoinz.py +++ b/hdwallet/cryptocurrencies/bitcoinz.py @@ -55,9 +55,9 @@ class BitcoinZ(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitcore.py b/hdwallet/cryptocurrencies/bitcore.py index ac589fce..70c95a08 100644 --- a/hdwallet/cryptocurrencies/bitcore.py +++ b/hdwallet/cryptocurrencies/bitcore.py @@ -64,9 +64,9 @@ class Bitcore(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bitsend.py b/hdwallet/cryptocurrencies/bitsend.py index ffbddf22..3e475c8d 100644 --- a/hdwallet/cryptocurrencies/bitsend.py +++ b/hdwallet/cryptocurrencies/bitsend.py @@ -56,9 +56,9 @@ class BitSend(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/blackcoin.py b/hdwallet/cryptocurrencies/blackcoin.py index f543b8c5..66a68354 100644 --- a/hdwallet/cryptocurrencies/blackcoin.py +++ b/hdwallet/cryptocurrencies/blackcoin.py @@ -56,9 +56,9 @@ class Blackcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/blocknode.py b/hdwallet/cryptocurrencies/blocknode.py index dfdff4b5..73c326a6 100644 --- a/hdwallet/cryptocurrencies/blocknode.py +++ b/hdwallet/cryptocurrencies/blocknode.py @@ -71,9 +71,9 @@ class Blocknode(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/blockstamp.py b/hdwallet/cryptocurrencies/blockstamp.py index 1a044111..6bd6edcf 100644 --- a/hdwallet/cryptocurrencies/blockstamp.py +++ b/hdwallet/cryptocurrencies/blockstamp.py @@ -64,9 +64,9 @@ class BlockStamp(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/bolivarcoin.py b/hdwallet/cryptocurrencies/bolivarcoin.py index 8bd49dad..fc3142c4 100644 --- a/hdwallet/cryptocurrencies/bolivarcoin.py +++ b/hdwallet/cryptocurrencies/bolivarcoin.py @@ -54,9 +54,9 @@ class Bolivarcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/britcoin.py b/hdwallet/cryptocurrencies/britcoin.py index 40cc1ef0..e98b4b52 100644 --- a/hdwallet/cryptocurrencies/britcoin.py +++ b/hdwallet/cryptocurrencies/britcoin.py @@ -54,9 +54,9 @@ class BritCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/canadaecoin.py b/hdwallet/cryptocurrencies/canadaecoin.py index 269e3112..4303a3eb 100644 --- a/hdwallet/cryptocurrencies/canadaecoin.py +++ b/hdwallet/cryptocurrencies/canadaecoin.py @@ -54,9 +54,9 @@ class CanadaECoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/cannacoin.py b/hdwallet/cryptocurrencies/cannacoin.py index bd6edaf1..db1cc5c4 100644 --- a/hdwallet/cryptocurrencies/cannacoin.py +++ b/hdwallet/cryptocurrencies/cannacoin.py @@ -55,9 +55,9 @@ class Cannacoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/celo.py b/hdwallet/cryptocurrencies/celo.py index 4897917d..240457e5 100644 --- a/hdwallet/cryptocurrencies/celo.py +++ b/hdwallet/cryptocurrencies/celo.py @@ -51,9 +51,9 @@ class Celo(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/chihuahua.py b/hdwallet/cryptocurrencies/chihuahua.py index 24514ff0..cc21c1fe 100644 --- a/hdwallet/cryptocurrencies/chihuahua.py +++ b/hdwallet/cryptocurrencies/chihuahua.py @@ -50,9 +50,9 @@ class Chihuahua(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/clams.py b/hdwallet/cryptocurrencies/clams.py index 2d8d449e..08cdef4d 100644 --- a/hdwallet/cryptocurrencies/clams.py +++ b/hdwallet/cryptocurrencies/clams.py @@ -55,9 +55,9 @@ class Clams(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) @@ -71,4 +71,3 @@ class Clams(ICryptocurrency): "p2pkh", "p2sh" ] DEFAULT_SEMANTIC = "p2pkh" - \ No newline at end of file diff --git a/hdwallet/cryptocurrencies/clubcoin.py b/hdwallet/cryptocurrencies/clubcoin.py index edc4a303..91b6b351 100644 --- a/hdwallet/cryptocurrencies/clubcoin.py +++ b/hdwallet/cryptocurrencies/clubcoin.py @@ -54,9 +54,9 @@ class ClubCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/compcoin.py b/hdwallet/cryptocurrencies/compcoin.py index cf4a4170..67f74be4 100644 --- a/hdwallet/cryptocurrencies/compcoin.py +++ b/hdwallet/cryptocurrencies/compcoin.py @@ -53,9 +53,9 @@ class Compcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) @@ -69,4 +69,3 @@ class Compcoin(ICryptocurrency): "p2pkh", "p2sh" ] DEFAULT_SEMANTIC = "p2pkh" - \ No newline at end of file diff --git a/hdwallet/cryptocurrencies/cosmos.py b/hdwallet/cryptocurrencies/cosmos.py index 91b7fea9..8a7b7d19 100644 --- a/hdwallet/cryptocurrencies/cosmos.py +++ b/hdwallet/cryptocurrencies/cosmos.py @@ -51,9 +51,9 @@ class Cosmos(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/cpuchain.py b/hdwallet/cryptocurrencies/cpuchain.py index 09fa535c..5e1155fb 100644 --- a/hdwallet/cryptocurrencies/cpuchain.py +++ b/hdwallet/cryptocurrencies/cpuchain.py @@ -64,9 +64,9 @@ class CPUChain(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/cranepay.py b/hdwallet/cryptocurrencies/cranepay.py index b4be64d4..41988b34 100644 --- a/hdwallet/cryptocurrencies/cranepay.py +++ b/hdwallet/cryptocurrencies/cranepay.py @@ -63,9 +63,9 @@ class CranePay(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/crave.py b/hdwallet/cryptocurrencies/crave.py index 08b7a1c3..1532c5f8 100644 --- a/hdwallet/cryptocurrencies/crave.py +++ b/hdwallet/cryptocurrencies/crave.py @@ -55,9 +55,9 @@ class Crave(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/dash.py b/hdwallet/cryptocurrencies/dash.py index 3e56ccbc..ea3f139a 100644 --- a/hdwallet/cryptocurrencies/dash.py +++ b/hdwallet/cryptocurrencies/dash.py @@ -73,9 +73,9 @@ class Dash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/deeponion.py b/hdwallet/cryptocurrencies/deeponion.py index ed19902e..af1ecc1c 100644 --- a/hdwallet/cryptocurrencies/deeponion.py +++ b/hdwallet/cryptocurrencies/deeponion.py @@ -64,9 +64,9 @@ class DeepOnion(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/defcoin.py b/hdwallet/cryptocurrencies/defcoin.py index 8744d1b4..7ba309bf 100644 --- a/hdwallet/cryptocurrencies/defcoin.py +++ b/hdwallet/cryptocurrencies/defcoin.py @@ -29,7 +29,7 @@ class Mainnet(INetwork): }) MESSAGE_PREFIX = "\x18defcoin Signed Message:\n" WIF_PREFIX = 0x9e - + class Defcoin(ICryptocurrency): @@ -54,9 +54,9 @@ class Defcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/denarius.py b/hdwallet/cryptocurrencies/denarius.py index d3f2d243..ab6e9e4f 100644 --- a/hdwallet/cryptocurrencies/denarius.py +++ b/hdwallet/cryptocurrencies/denarius.py @@ -55,9 +55,9 @@ class Denarius(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/diamond.py b/hdwallet/cryptocurrencies/diamond.py index 7899e3ea..34cb441a 100644 --- a/hdwallet/cryptocurrencies/diamond.py +++ b/hdwallet/cryptocurrencies/diamond.py @@ -55,9 +55,9 @@ class Diamond(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/digibyte.py b/hdwallet/cryptocurrencies/digibyte.py index 2544b601..8d17af8b 100644 --- a/hdwallet/cryptocurrencies/digibyte.py +++ b/hdwallet/cryptocurrencies/digibyte.py @@ -65,9 +65,9 @@ class DigiByte(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/digitalcoin.py b/hdwallet/cryptocurrencies/digitalcoin.py index ad8ccaa5..0f1909f3 100644 --- a/hdwallet/cryptocurrencies/digitalcoin.py +++ b/hdwallet/cryptocurrencies/digitalcoin.py @@ -56,9 +56,9 @@ class Digitalcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/divi.py b/hdwallet/cryptocurrencies/divi.py index b1eaf2c3..4a47419e 100644 --- a/hdwallet/cryptocurrencies/divi.py +++ b/hdwallet/cryptocurrencies/divi.py @@ -73,9 +73,9 @@ class Divi(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/dogecoin.py b/hdwallet/cryptocurrencies/dogecoin.py index bbd5f50e..18914676 100644 --- a/hdwallet/cryptocurrencies/dogecoin.py +++ b/hdwallet/cryptocurrencies/dogecoin.py @@ -23,7 +23,7 @@ class Mainnet(INetwork): WITNESS_VERSIONS = WitnessVersions({ "P2WPKH": 0x00, "P2WSH": 0x00 - }) + }) XPRIVATE_KEY_VERSIONS = XPrivateKeyVersions({ "DOGECOIN": 0x02fac398, "P2PKH": 0x0488ade4, @@ -94,9 +94,9 @@ class Dogecoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/dydx.py b/hdwallet/cryptocurrencies/dydx.py index 99ce6721..e31dc609 100644 --- a/hdwallet/cryptocurrencies/dydx.py +++ b/hdwallet/cryptocurrencies/dydx.py @@ -50,9 +50,9 @@ class dYdX(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ecash.py b/hdwallet/cryptocurrencies/ecash.py index fdefb1a3..851f3a46 100644 --- a/hdwallet/cryptocurrencies/ecash.py +++ b/hdwallet/cryptocurrencies/ecash.py @@ -100,9 +100,9 @@ class eCash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ecoin.py b/hdwallet/cryptocurrencies/ecoin.py index eb994277..d4f58afd 100644 --- a/hdwallet/cryptocurrencies/ecoin.py +++ b/hdwallet/cryptocurrencies/ecoin.py @@ -54,9 +54,9 @@ class ECoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/edrcoin.py b/hdwallet/cryptocurrencies/edrcoin.py index 4e2ceac7..23292f81 100644 --- a/hdwallet/cryptocurrencies/edrcoin.py +++ b/hdwallet/cryptocurrencies/edrcoin.py @@ -55,9 +55,9 @@ class EDRCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/egulden.py b/hdwallet/cryptocurrencies/egulden.py index 461d1115..8c424c3c 100644 --- a/hdwallet/cryptocurrencies/egulden.py +++ b/hdwallet/cryptocurrencies/egulden.py @@ -54,9 +54,9 @@ class eGulden(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/einsteinium.py b/hdwallet/cryptocurrencies/einsteinium.py index e1381503..162ac5e0 100644 --- a/hdwallet/cryptocurrencies/einsteinium.py +++ b/hdwallet/cryptocurrencies/einsteinium.py @@ -55,9 +55,9 @@ class Einsteinium(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/elastos.py b/hdwallet/cryptocurrencies/elastos.py index 4c602776..06014f57 100644 --- a/hdwallet/cryptocurrencies/elastos.py +++ b/hdwallet/cryptocurrencies/elastos.py @@ -56,9 +56,9 @@ class Elastos(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/energi.py b/hdwallet/cryptocurrencies/energi.py index d1023448..b71ce5ee 100644 --- a/hdwallet/cryptocurrencies/energi.py +++ b/hdwallet/cryptocurrencies/energi.py @@ -56,9 +56,9 @@ class Energi(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/eos.py b/hdwallet/cryptocurrencies/eos.py index c3a90394..ba245a5e 100644 --- a/hdwallet/cryptocurrencies/eos.py +++ b/hdwallet/cryptocurrencies/eos.py @@ -50,9 +50,9 @@ class EOS(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ergo.py b/hdwallet/cryptocurrencies/ergo.py index dae087ac..43560a73 100644 --- a/hdwallet/cryptocurrencies/ergo.py +++ b/hdwallet/cryptocurrencies/ergo.py @@ -64,9 +64,9 @@ class Ergo(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ethereum.py b/hdwallet/cryptocurrencies/ethereum.py index c8a7104a..e947f467 100644 --- a/hdwallet/cryptocurrencies/ethereum.py +++ b/hdwallet/cryptocurrencies/ethereum.py @@ -51,9 +51,9 @@ class Ethereum(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/europecoin.py b/hdwallet/cryptocurrencies/europecoin.py index 4f2f93d6..1b620e45 100644 --- a/hdwallet/cryptocurrencies/europecoin.py +++ b/hdwallet/cryptocurrencies/europecoin.py @@ -55,9 +55,9 @@ class EuropeCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/evrmore.py b/hdwallet/cryptocurrencies/evrmore.py index 6acfb189..afa8f9e8 100644 --- a/hdwallet/cryptocurrencies/evrmore.py +++ b/hdwallet/cryptocurrencies/evrmore.py @@ -98,9 +98,9 @@ class Evrmore(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/exclusivecoin.py b/hdwallet/cryptocurrencies/exclusivecoin.py index efe2215f..ef07c064 100644 --- a/hdwallet/cryptocurrencies/exclusivecoin.py +++ b/hdwallet/cryptocurrencies/exclusivecoin.py @@ -54,9 +54,9 @@ class ExclusiveCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/fantom.py b/hdwallet/cryptocurrencies/fantom.py index b5e1636b..bb20b5b6 100644 --- a/hdwallet/cryptocurrencies/fantom.py +++ b/hdwallet/cryptocurrencies/fantom.py @@ -50,9 +50,9 @@ class Fantom(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/feathercoin.py b/hdwallet/cryptocurrencies/feathercoin.py index 0f6729a6..6d608cc9 100644 --- a/hdwallet/cryptocurrencies/feathercoin.py +++ b/hdwallet/cryptocurrencies/feathercoin.py @@ -55,9 +55,9 @@ class Feathercoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/fetchai.py b/hdwallet/cryptocurrencies/fetchai.py index c7fd68b3..280e273a 100644 --- a/hdwallet/cryptocurrencies/fetchai.py +++ b/hdwallet/cryptocurrencies/fetchai.py @@ -52,9 +52,9 @@ class FetchAI(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/filecoin.py b/hdwallet/cryptocurrencies/filecoin.py index 96f4c8c7..10097b1d 100644 --- a/hdwallet/cryptocurrencies/filecoin.py +++ b/hdwallet/cryptocurrencies/filecoin.py @@ -50,9 +50,9 @@ class Filecoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/firo.py b/hdwallet/cryptocurrencies/firo.py index cc3d1e84..034d03c6 100644 --- a/hdwallet/cryptocurrencies/firo.py +++ b/hdwallet/cryptocurrencies/firo.py @@ -54,9 +54,9 @@ class Firo(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/firstcoin.py b/hdwallet/cryptocurrencies/firstcoin.py index 74b986ab..9eaab7e2 100644 --- a/hdwallet/cryptocurrencies/firstcoin.py +++ b/hdwallet/cryptocurrencies/firstcoin.py @@ -54,9 +54,9 @@ class Firstcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/fix.py b/hdwallet/cryptocurrencies/fix.py index 425e20e4..a35a974d 100644 --- a/hdwallet/cryptocurrencies/fix.py +++ b/hdwallet/cryptocurrencies/fix.py @@ -71,9 +71,9 @@ class FIX(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/flashcoin.py b/hdwallet/cryptocurrencies/flashcoin.py index abf58e30..8fc6e59f 100644 --- a/hdwallet/cryptocurrencies/flashcoin.py +++ b/hdwallet/cryptocurrencies/flashcoin.py @@ -55,9 +55,9 @@ class Flashcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/flux.py b/hdwallet/cryptocurrencies/flux.py index 7ec3a575..66dff6eb 100644 --- a/hdwallet/cryptocurrencies/flux.py +++ b/hdwallet/cryptocurrencies/flux.py @@ -56,9 +56,9 @@ class Flux(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/foxdcoin.py b/hdwallet/cryptocurrencies/foxdcoin.py index 2e170704..b94e27b3 100644 --- a/hdwallet/cryptocurrencies/foxdcoin.py +++ b/hdwallet/cryptocurrencies/foxdcoin.py @@ -97,9 +97,9 @@ class Foxdcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) @@ -113,4 +113,3 @@ class Foxdcoin(ICryptocurrency): "p2pkh", "p2sh", "p2wpkh", "p2wpkh-in-p2sh", "p2wsh", "p2wsh-in-p2sh" ] DEFAULT_SEMANTIC = "p2pkh" - diff --git a/hdwallet/cryptocurrencies/fujicoin.py b/hdwallet/cryptocurrencies/fujicoin.py index fd1a19ce..a1c85a21 100644 --- a/hdwallet/cryptocurrencies/fujicoin.py +++ b/hdwallet/cryptocurrencies/fujicoin.py @@ -64,9 +64,9 @@ class FujiCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/gamecredits.py b/hdwallet/cryptocurrencies/gamecredits.py index 21440ca1..3e00b8b3 100644 --- a/hdwallet/cryptocurrencies/gamecredits.py +++ b/hdwallet/cryptocurrencies/gamecredits.py @@ -54,9 +54,9 @@ class GameCredits(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/gcrcoin.py b/hdwallet/cryptocurrencies/gcrcoin.py index 7549df5c..b0749ff3 100644 --- a/hdwallet/cryptocurrencies/gcrcoin.py +++ b/hdwallet/cryptocurrencies/gcrcoin.py @@ -53,9 +53,9 @@ class GCRCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/gobyte.py b/hdwallet/cryptocurrencies/gobyte.py index beb77d1a..50da93f3 100644 --- a/hdwallet/cryptocurrencies/gobyte.py +++ b/hdwallet/cryptocurrencies/gobyte.py @@ -55,9 +55,9 @@ class GoByte(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/gridcoin.py b/hdwallet/cryptocurrencies/gridcoin.py index 7f44d608..9def6187 100644 --- a/hdwallet/cryptocurrencies/gridcoin.py +++ b/hdwallet/cryptocurrencies/gridcoin.py @@ -55,9 +55,9 @@ class Gridcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/groestlcoin.py b/hdwallet/cryptocurrencies/groestlcoin.py index 12175f06..47d4c0cb 100644 --- a/hdwallet/cryptocurrencies/groestlcoin.py +++ b/hdwallet/cryptocurrencies/groestlcoin.py @@ -90,9 +90,9 @@ class GroestlCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/gulden.py b/hdwallet/cryptocurrencies/gulden.py index 608c4836..2f105780 100644 --- a/hdwallet/cryptocurrencies/gulden.py +++ b/hdwallet/cryptocurrencies/gulden.py @@ -54,9 +54,9 @@ class Gulden(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/harmony.py b/hdwallet/cryptocurrencies/harmony.py index 983a20d4..8947903a 100644 --- a/hdwallet/cryptocurrencies/harmony.py +++ b/hdwallet/cryptocurrencies/harmony.py @@ -52,9 +52,9 @@ class Harmony(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/helleniccoin.py b/hdwallet/cryptocurrencies/helleniccoin.py index 1680d963..f4459c3d 100644 --- a/hdwallet/cryptocurrencies/helleniccoin.py +++ b/hdwallet/cryptocurrencies/helleniccoin.py @@ -56,9 +56,9 @@ class Helleniccoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/hempcoin.py b/hdwallet/cryptocurrencies/hempcoin.py index bd03eb7b..f406bf6c 100644 --- a/hdwallet/cryptocurrencies/hempcoin.py +++ b/hdwallet/cryptocurrencies/hempcoin.py @@ -55,9 +55,9 @@ class Hempcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/horizen.py b/hdwallet/cryptocurrencies/horizen.py index 4b0deaee..5e3cb17e 100644 --- a/hdwallet/cryptocurrencies/horizen.py +++ b/hdwallet/cryptocurrencies/horizen.py @@ -56,9 +56,9 @@ class Horizen(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/huobitoken.py b/hdwallet/cryptocurrencies/huobitoken.py index e3498606..76bf17d9 100644 --- a/hdwallet/cryptocurrencies/huobitoken.py +++ b/hdwallet/cryptocurrencies/huobitoken.py @@ -49,9 +49,9 @@ class HuobiToken(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/hush.py b/hdwallet/cryptocurrencies/hush.py index 271c0f1f..aec36a2f 100644 --- a/hdwallet/cryptocurrencies/hush.py +++ b/hdwallet/cryptocurrencies/hush.py @@ -56,9 +56,9 @@ class Hush(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/icon.py b/hdwallet/cryptocurrencies/icon.py index e8614038..9c8a0e2d 100644 --- a/hdwallet/cryptocurrencies/icon.py +++ b/hdwallet/cryptocurrencies/icon.py @@ -51,9 +51,9 @@ class Icon(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/injective.py b/hdwallet/cryptocurrencies/injective.py index 192f9b89..638d7bbd 100644 --- a/hdwallet/cryptocurrencies/injective.py +++ b/hdwallet/cryptocurrencies/injective.py @@ -51,9 +51,9 @@ class Injective(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/insanecoin.py b/hdwallet/cryptocurrencies/insanecoin.py index 1295efa9..3acbc8e9 100644 --- a/hdwallet/cryptocurrencies/insanecoin.py +++ b/hdwallet/cryptocurrencies/insanecoin.py @@ -55,9 +55,9 @@ class InsaneCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/internetofpeople.py b/hdwallet/cryptocurrencies/internetofpeople.py index b7a5dac9..95de0e39 100644 --- a/hdwallet/cryptocurrencies/internetofpeople.py +++ b/hdwallet/cryptocurrencies/internetofpeople.py @@ -55,9 +55,9 @@ class InternetOfPeople(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/irisnet.py b/hdwallet/cryptocurrencies/irisnet.py index 64926990..afa9492d 100644 --- a/hdwallet/cryptocurrencies/irisnet.py +++ b/hdwallet/cryptocurrencies/irisnet.py @@ -51,9 +51,9 @@ class IRISnet(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ixcoin.py b/hdwallet/cryptocurrencies/ixcoin.py index 021fa006..d088e81c 100644 --- a/hdwallet/cryptocurrencies/ixcoin.py +++ b/hdwallet/cryptocurrencies/ixcoin.py @@ -55,9 +55,9 @@ class IXCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/jumbucks.py b/hdwallet/cryptocurrencies/jumbucks.py index 932bb2c2..c839d8c9 100644 --- a/hdwallet/cryptocurrencies/jumbucks.py +++ b/hdwallet/cryptocurrencies/jumbucks.py @@ -53,9 +53,9 @@ class Jumbucks(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/kava.py b/hdwallet/cryptocurrencies/kava.py index 2778a1b0..8acd2013 100644 --- a/hdwallet/cryptocurrencies/kava.py +++ b/hdwallet/cryptocurrencies/kava.py @@ -52,9 +52,9 @@ class Kava(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/kobocoin.py b/hdwallet/cryptocurrencies/kobocoin.py index dc8c1fc1..f9449cd1 100644 --- a/hdwallet/cryptocurrencies/kobocoin.py +++ b/hdwallet/cryptocurrencies/kobocoin.py @@ -55,9 +55,9 @@ class Kobocoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/komodo.py b/hdwallet/cryptocurrencies/komodo.py index c2d8ba52..de510e82 100644 --- a/hdwallet/cryptocurrencies/komodo.py +++ b/hdwallet/cryptocurrencies/komodo.py @@ -55,9 +55,9 @@ class Komodo(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/landcoin.py b/hdwallet/cryptocurrencies/landcoin.py index a56b6264..45788982 100644 --- a/hdwallet/cryptocurrencies/landcoin.py +++ b/hdwallet/cryptocurrencies/landcoin.py @@ -53,9 +53,9 @@ class Landcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/lbrycredits.py b/hdwallet/cryptocurrencies/lbrycredits.py index ee162ad7..afa8ee34 100644 --- a/hdwallet/cryptocurrencies/lbrycredits.py +++ b/hdwallet/cryptocurrencies/lbrycredits.py @@ -56,9 +56,9 @@ class LBRYCredits(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/linx.py b/hdwallet/cryptocurrencies/linx.py index e5a1986f..6894a336 100644 --- a/hdwallet/cryptocurrencies/linx.py +++ b/hdwallet/cryptocurrencies/linx.py @@ -55,9 +55,9 @@ class Linx(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/litecoin.py b/hdwallet/cryptocurrencies/litecoin.py index 36c758ff..b0b0621e 100644 --- a/hdwallet/cryptocurrencies/litecoin.py +++ b/hdwallet/cryptocurrencies/litecoin.py @@ -97,11 +97,11 @@ class Litecoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ - "BIP32", "BIP44", "BIP84" + "BIP32", "BIP44", "BIP49", "BIP84" }) DEFAULT_HD = HDS.BIP44 DEFAULT_PATH = f"m/44'/{COIN_TYPE}'/0'/0/0" diff --git a/hdwallet/cryptocurrencies/litecoincash.py b/hdwallet/cryptocurrencies/litecoincash.py index 65074bb6..10568658 100644 --- a/hdwallet/cryptocurrencies/litecoincash.py +++ b/hdwallet/cryptocurrencies/litecoincash.py @@ -55,9 +55,9 @@ class LitecoinCash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/litecoinz.py b/hdwallet/cryptocurrencies/litecoinz.py index e91a4f8c..01d0bfc2 100644 --- a/hdwallet/cryptocurrencies/litecoinz.py +++ b/hdwallet/cryptocurrencies/litecoinz.py @@ -29,7 +29,7 @@ class Mainnet(INetwork): }) MESSAGE_PREFIX = "\x18LitecoinZ Signed Message:\n" WIF_PREFIX = 0x80 - + class LitecoinZ(ICryptocurrency): @@ -55,9 +55,9 @@ class LitecoinZ(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/lkrcoin.py b/hdwallet/cryptocurrencies/lkrcoin.py index 1531a750..949b5779 100644 --- a/hdwallet/cryptocurrencies/lkrcoin.py +++ b/hdwallet/cryptocurrencies/lkrcoin.py @@ -55,9 +55,9 @@ class Lkrcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/lynx.py b/hdwallet/cryptocurrencies/lynx.py index 5a0e20f3..b887aeab 100644 --- a/hdwallet/cryptocurrencies/lynx.py +++ b/hdwallet/cryptocurrencies/lynx.py @@ -54,9 +54,9 @@ class Lynx(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/mazacoin.py b/hdwallet/cryptocurrencies/mazacoin.py index 71df9265..ba698f31 100644 --- a/hdwallet/cryptocurrencies/mazacoin.py +++ b/hdwallet/cryptocurrencies/mazacoin.py @@ -54,9 +54,9 @@ class Mazacoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/megacoin.py b/hdwallet/cryptocurrencies/megacoin.py index 03e63cae..50bb5d0d 100644 --- a/hdwallet/cryptocurrencies/megacoin.py +++ b/hdwallet/cryptocurrencies/megacoin.py @@ -55,9 +55,9 @@ class Megacoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/metis.py b/hdwallet/cryptocurrencies/metis.py index fd5ce089..d4f9575d 100644 --- a/hdwallet/cryptocurrencies/metis.py +++ b/hdwallet/cryptocurrencies/metis.py @@ -50,9 +50,9 @@ class Metis(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/minexcoin.py b/hdwallet/cryptocurrencies/minexcoin.py index 82e7b3be..83c5d1a5 100644 --- a/hdwallet/cryptocurrencies/minexcoin.py +++ b/hdwallet/cryptocurrencies/minexcoin.py @@ -55,9 +55,9 @@ class Minexcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/monacoin.py b/hdwallet/cryptocurrencies/monacoin.py index e3cc8056..476cdb72 100644 --- a/hdwallet/cryptocurrencies/monacoin.py +++ b/hdwallet/cryptocurrencies/monacoin.py @@ -64,9 +64,9 @@ class Monacoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/monero.py b/hdwallet/cryptocurrencies/monero.py index bc69afee..90b01c6f 100644 --- a/hdwallet/cryptocurrencies/monero.py +++ b/hdwallet/cryptocurrencies/monero.py @@ -63,7 +63,7 @@ class Monero(ICryptocurrency): {"MONERO": "Monero"}, "BIP39" )) SEEDS = Seeds(( - {"MONERO": "Monero"}, "BIP39" + {"MONERO": "Monero"}, "BIP39", "SLIP39" )) HDS = HDs({ "MONERO": "Monero" diff --git a/hdwallet/cryptocurrencies/monk.py b/hdwallet/cryptocurrencies/monk.py index 0ba504fb..e89150ca 100644 --- a/hdwallet/cryptocurrencies/monk.py +++ b/hdwallet/cryptocurrencies/monk.py @@ -64,9 +64,9 @@ class Monk(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/multiversx.py b/hdwallet/cryptocurrencies/multiversx.py index 4e692399..a3a6dc79 100644 --- a/hdwallet/cryptocurrencies/multiversx.py +++ b/hdwallet/cryptocurrencies/multiversx.py @@ -51,9 +51,9 @@ class MultiversX(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/myriadcoin.py b/hdwallet/cryptocurrencies/myriadcoin.py index ee715796..e993e8c3 100644 --- a/hdwallet/cryptocurrencies/myriadcoin.py +++ b/hdwallet/cryptocurrencies/myriadcoin.py @@ -54,9 +54,9 @@ class Myriadcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/namecoin.py b/hdwallet/cryptocurrencies/namecoin.py index e8c2e1d2..ed07b21b 100644 --- a/hdwallet/cryptocurrencies/namecoin.py +++ b/hdwallet/cryptocurrencies/namecoin.py @@ -55,9 +55,9 @@ class Namecoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/nano.py b/hdwallet/cryptocurrencies/nano.py index 17a3c711..130f2d4c 100644 --- a/hdwallet/cryptocurrencies/nano.py +++ b/hdwallet/cryptocurrencies/nano.py @@ -49,9 +49,9 @@ class Nano(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/navcoin.py b/hdwallet/cryptocurrencies/navcoin.py index 671234e3..b3e8c0fa 100644 --- a/hdwallet/cryptocurrencies/navcoin.py +++ b/hdwallet/cryptocurrencies/navcoin.py @@ -55,9 +55,9 @@ class Navcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/near.py b/hdwallet/cryptocurrencies/near.py index 16694377..d8b4af61 100644 --- a/hdwallet/cryptocurrencies/near.py +++ b/hdwallet/cryptocurrencies/near.py @@ -49,9 +49,9 @@ class Near(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/neblio.py b/hdwallet/cryptocurrencies/neblio.py index bc57fc1c..3d9ed8cb 100644 --- a/hdwallet/cryptocurrencies/neblio.py +++ b/hdwallet/cryptocurrencies/neblio.py @@ -55,9 +55,9 @@ class Neblio(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/neo.py b/hdwallet/cryptocurrencies/neo.py index 77e538ca..05b5c368 100644 --- a/hdwallet/cryptocurrencies/neo.py +++ b/hdwallet/cryptocurrencies/neo.py @@ -49,9 +49,9 @@ class Neo(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/neoscoin.py b/hdwallet/cryptocurrencies/neoscoin.py index bd5c330c..e3359281 100644 --- a/hdwallet/cryptocurrencies/neoscoin.py +++ b/hdwallet/cryptocurrencies/neoscoin.py @@ -54,9 +54,9 @@ class Neoscoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/neurocoin.py b/hdwallet/cryptocurrencies/neurocoin.py index deedb56a..85b1a5c0 100644 --- a/hdwallet/cryptocurrencies/neurocoin.py +++ b/hdwallet/cryptocurrencies/neurocoin.py @@ -54,9 +54,9 @@ class Neurocoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/neutron.py b/hdwallet/cryptocurrencies/neutron.py index 2937d9c5..7689b4f9 100644 --- a/hdwallet/cryptocurrencies/neutron.py +++ b/hdwallet/cryptocurrencies/neutron.py @@ -51,9 +51,9 @@ class Neutron(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/newyorkcoin.py b/hdwallet/cryptocurrencies/newyorkcoin.py index 381d3b34..5e8aa36a 100644 --- a/hdwallet/cryptocurrencies/newyorkcoin.py +++ b/hdwallet/cryptocurrencies/newyorkcoin.py @@ -55,9 +55,9 @@ class NewYorkCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ninechronicles.py b/hdwallet/cryptocurrencies/ninechronicles.py index 1b5670d3..6aad468e 100644 --- a/hdwallet/cryptocurrencies/ninechronicles.py +++ b/hdwallet/cryptocurrencies/ninechronicles.py @@ -50,9 +50,9 @@ class NineChronicles(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/nix.py b/hdwallet/cryptocurrencies/nix.py index 1fca1d7a..4b0bdcfa 100644 --- a/hdwallet/cryptocurrencies/nix.py +++ b/hdwallet/cryptocurrencies/nix.py @@ -65,9 +65,9 @@ class NIX(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/novacoin.py b/hdwallet/cryptocurrencies/novacoin.py index a44910d5..ea35a3dc 100644 --- a/hdwallet/cryptocurrencies/novacoin.py +++ b/hdwallet/cryptocurrencies/novacoin.py @@ -56,9 +56,9 @@ class Novacoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/nubits.py b/hdwallet/cryptocurrencies/nubits.py index 0848f914..768c7258 100644 --- a/hdwallet/cryptocurrencies/nubits.py +++ b/hdwallet/cryptocurrencies/nubits.py @@ -55,9 +55,9 @@ class NuBits(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/nushares.py b/hdwallet/cryptocurrencies/nushares.py index b9602f3c..3167cc5e 100644 --- a/hdwallet/cryptocurrencies/nushares.py +++ b/hdwallet/cryptocurrencies/nushares.py @@ -55,9 +55,9 @@ class NuShares(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/okcash.py b/hdwallet/cryptocurrencies/okcash.py index 75956b37..de2cea66 100644 --- a/hdwallet/cryptocurrencies/okcash.py +++ b/hdwallet/cryptocurrencies/okcash.py @@ -55,9 +55,9 @@ class OKCash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/oktchain.py b/hdwallet/cryptocurrencies/oktchain.py index e3d055db..6171d44b 100644 --- a/hdwallet/cryptocurrencies/oktchain.py +++ b/hdwallet/cryptocurrencies/oktchain.py @@ -51,9 +51,9 @@ class OKTChain(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/omni.py b/hdwallet/cryptocurrencies/omni.py index ffc3e809..096a5856 100644 --- a/hdwallet/cryptocurrencies/omni.py +++ b/hdwallet/cryptocurrencies/omni.py @@ -72,9 +72,9 @@ class Omni(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/onix.py b/hdwallet/cryptocurrencies/onix.py index d0bb527a..71d9d82d 100644 --- a/hdwallet/cryptocurrencies/onix.py +++ b/hdwallet/cryptocurrencies/onix.py @@ -54,9 +54,9 @@ class Onix(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ontology.py b/hdwallet/cryptocurrencies/ontology.py index 1aa22085..2e6635c3 100644 --- a/hdwallet/cryptocurrencies/ontology.py +++ b/hdwallet/cryptocurrencies/ontology.py @@ -49,9 +49,9 @@ class Ontology(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/optimism.py b/hdwallet/cryptocurrencies/optimism.py index 95ca632f..beba9ff9 100644 --- a/hdwallet/cryptocurrencies/optimism.py +++ b/hdwallet/cryptocurrencies/optimism.py @@ -49,9 +49,9 @@ class Optimism(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/osmosis.py b/hdwallet/cryptocurrencies/osmosis.py index e519a897..2fa80d09 100644 --- a/hdwallet/cryptocurrencies/osmosis.py +++ b/hdwallet/cryptocurrencies/osmosis.py @@ -50,9 +50,9 @@ class Osmosis(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/particl.py b/hdwallet/cryptocurrencies/particl.py index 74be9417..158dcfd8 100644 --- a/hdwallet/cryptocurrencies/particl.py +++ b/hdwallet/cryptocurrencies/particl.py @@ -61,9 +61,9 @@ class Particl(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/peercoin.py b/hdwallet/cryptocurrencies/peercoin.py index 884fa191..db5e7c0a 100644 --- a/hdwallet/cryptocurrencies/peercoin.py +++ b/hdwallet/cryptocurrencies/peercoin.py @@ -55,9 +55,9 @@ class Peercoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/pesobit.py b/hdwallet/cryptocurrencies/pesobit.py index 26c8a36f..c7c939d2 100644 --- a/hdwallet/cryptocurrencies/pesobit.py +++ b/hdwallet/cryptocurrencies/pesobit.py @@ -54,9 +54,9 @@ class Pesobit(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/phore.py b/hdwallet/cryptocurrencies/phore.py index 50026096..863b93b5 100644 --- a/hdwallet/cryptocurrencies/phore.py +++ b/hdwallet/cryptocurrencies/phore.py @@ -55,9 +55,9 @@ class Phore(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/pinetwork.py b/hdwallet/cryptocurrencies/pinetwork.py index cee939fb..e5b12605 100644 --- a/hdwallet/cryptocurrencies/pinetwork.py +++ b/hdwallet/cryptocurrencies/pinetwork.py @@ -49,9 +49,9 @@ class PiNetwork(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/pinkcoin.py b/hdwallet/cryptocurrencies/pinkcoin.py index 49609969..4539df99 100644 --- a/hdwallet/cryptocurrencies/pinkcoin.py +++ b/hdwallet/cryptocurrencies/pinkcoin.py @@ -55,9 +55,9 @@ class Pinkcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/pivx.py b/hdwallet/cryptocurrencies/pivx.py index b361b387..1b6bd290 100644 --- a/hdwallet/cryptocurrencies/pivx.py +++ b/hdwallet/cryptocurrencies/pivx.py @@ -72,9 +72,9 @@ class Pivx(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/polygon.py b/hdwallet/cryptocurrencies/polygon.py index e4e408f4..00729dc5 100644 --- a/hdwallet/cryptocurrencies/polygon.py +++ b/hdwallet/cryptocurrencies/polygon.py @@ -50,9 +50,9 @@ class Polygon(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/poswcoin.py b/hdwallet/cryptocurrencies/poswcoin.py index 06fbd817..10ab60d8 100644 --- a/hdwallet/cryptocurrencies/poswcoin.py +++ b/hdwallet/cryptocurrencies/poswcoin.py @@ -53,9 +53,9 @@ class PoSWCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/potcoin.py b/hdwallet/cryptocurrencies/potcoin.py index ea21efff..3471fb38 100644 --- a/hdwallet/cryptocurrencies/potcoin.py +++ b/hdwallet/cryptocurrencies/potcoin.py @@ -29,7 +29,7 @@ class Mainnet(INetwork): }) MESSAGE_PREFIX = "\x18Potcoin Signed Message:\n" WIF_PREFIX = 0xb7 - + class Potcoin(ICryptocurrency): @@ -55,9 +55,9 @@ class Potcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/projectcoin.py b/hdwallet/cryptocurrencies/projectcoin.py index 55713df1..12efd2bb 100644 --- a/hdwallet/cryptocurrencies/projectcoin.py +++ b/hdwallet/cryptocurrencies/projectcoin.py @@ -54,9 +54,9 @@ class ProjectCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/putincoin.py b/hdwallet/cryptocurrencies/putincoin.py index b5c8d819..504dff92 100644 --- a/hdwallet/cryptocurrencies/putincoin.py +++ b/hdwallet/cryptocurrencies/putincoin.py @@ -55,9 +55,9 @@ class Putincoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/qtum.py b/hdwallet/cryptocurrencies/qtum.py index fd8f91df..0ee1d270 100644 --- a/hdwallet/cryptocurrencies/qtum.py +++ b/hdwallet/cryptocurrencies/qtum.py @@ -99,11 +99,11 @@ class Qtum(ICryptocurrency): "BIP39" }) MNEMONICS = Mnemonics({ - "BIP39" - }) - SEEDS = Seeds({ - "BIP39" + "BIP39", }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44", "BIP49", "BIP84", "BIP86", "BIP141" }) diff --git a/hdwallet/cryptocurrencies/rapids.py b/hdwallet/cryptocurrencies/rapids.py index 538102fb..6da56f6e 100644 --- a/hdwallet/cryptocurrencies/rapids.py +++ b/hdwallet/cryptocurrencies/rapids.py @@ -55,9 +55,9 @@ class Rapids(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) @@ -71,4 +71,3 @@ class Rapids(ICryptocurrency): "p2pkh", "p2sh" ] DEFAULT_SEMANTIC = "p2pkh" - diff --git a/hdwallet/cryptocurrencies/ravencoin.py b/hdwallet/cryptocurrencies/ravencoin.py index 6c663832..96b3f525 100644 --- a/hdwallet/cryptocurrencies/ravencoin.py +++ b/hdwallet/cryptocurrencies/ravencoin.py @@ -23,7 +23,7 @@ class Mainnet(INetwork): WITNESS_VERSIONS = WitnessVersions({ "P2WPKH": 0x0c, "P2WSH": 0x0c - }) + }) XPRIVATE_KEY_VERSIONS = XPrivateKeyVersions({ "P2PKH": 0x488ade4, "P2SH": 0x488ade4, @@ -99,9 +99,9 @@ class Ravencoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs(( "BIP32", "BIP44" )) diff --git a/hdwallet/cryptocurrencies/reddcoin.py b/hdwallet/cryptocurrencies/reddcoin.py index 1f9a75e9..050459f9 100644 --- a/hdwallet/cryptocurrencies/reddcoin.py +++ b/hdwallet/cryptocurrencies/reddcoin.py @@ -56,9 +56,9 @@ class Reddcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) @@ -72,4 +72,3 @@ class Reddcoin(ICryptocurrency): "p2pkh", "p2sh" ] DEFAULT_SEMANTIC = "p2pkh" - diff --git a/hdwallet/cryptocurrencies/ripple.py b/hdwallet/cryptocurrencies/ripple.py index 9b104c65..52771bd6 100644 --- a/hdwallet/cryptocurrencies/ripple.py +++ b/hdwallet/cryptocurrencies/ripple.py @@ -55,9 +55,9 @@ class Ripple(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ritocoin.py b/hdwallet/cryptocurrencies/ritocoin.py index ef2b3160..87cfbed2 100644 --- a/hdwallet/cryptocurrencies/ritocoin.py +++ b/hdwallet/cryptocurrencies/ritocoin.py @@ -55,9 +55,9 @@ class Ritocoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/rsk.py b/hdwallet/cryptocurrencies/rsk.py index 3eabdf4b..e272d736 100644 --- a/hdwallet/cryptocurrencies/rsk.py +++ b/hdwallet/cryptocurrencies/rsk.py @@ -72,9 +72,9 @@ class RSK(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/rubycoin.py b/hdwallet/cryptocurrencies/rubycoin.py index fdb10817..f866f5e4 100644 --- a/hdwallet/cryptocurrencies/rubycoin.py +++ b/hdwallet/cryptocurrencies/rubycoin.py @@ -1,4 +1,4 @@ - #!/usr/bin/env python3 +#!/usr/bin/env python3 # Copyright © 2020-2025, Meheret Tesfaye Batu # Distributed under the MIT software license, see the accompanying @@ -54,9 +54,9 @@ class Rubycoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/safecoin.py b/hdwallet/cryptocurrencies/safecoin.py index d237b78b..fd01ad94 100644 --- a/hdwallet/cryptocurrencies/safecoin.py +++ b/hdwallet/cryptocurrencies/safecoin.py @@ -55,9 +55,9 @@ class Safecoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/saluscoin.py b/hdwallet/cryptocurrencies/saluscoin.py index c2329d29..955be970 100644 --- a/hdwallet/cryptocurrencies/saluscoin.py +++ b/hdwallet/cryptocurrencies/saluscoin.py @@ -55,9 +55,9 @@ class Saluscoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/scribe.py b/hdwallet/cryptocurrencies/scribe.py index 3af9131d..aa5796c1 100644 --- a/hdwallet/cryptocurrencies/scribe.py +++ b/hdwallet/cryptocurrencies/scribe.py @@ -29,7 +29,7 @@ class Mainnet(INetwork): }) MESSAGE_PREFIX = None WIF_PREFIX = 0x6e - + class Scribe(ICryptocurrency): @@ -54,9 +54,9 @@ class Scribe(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/secret.py b/hdwallet/cryptocurrencies/secret.py index 96576f14..ad9bfd66 100644 --- a/hdwallet/cryptocurrencies/secret.py +++ b/hdwallet/cryptocurrencies/secret.py @@ -51,9 +51,9 @@ class Secret(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/shadowcash.py b/hdwallet/cryptocurrencies/shadowcash.py index 3c7ebca6..6f3fb2bd 100644 --- a/hdwallet/cryptocurrencies/shadowcash.py +++ b/hdwallet/cryptocurrencies/shadowcash.py @@ -72,9 +72,9 @@ class ShadowCash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/shentu.py b/hdwallet/cryptocurrencies/shentu.py index 11058588..5b67e178 100644 --- a/hdwallet/cryptocurrencies/shentu.py +++ b/hdwallet/cryptocurrencies/shentu.py @@ -51,9 +51,9 @@ class Shentu(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/slimcoin.py b/hdwallet/cryptocurrencies/slimcoin.py index 3ce5ae8a..ef45d336 100644 --- a/hdwallet/cryptocurrencies/slimcoin.py +++ b/hdwallet/cryptocurrencies/slimcoin.py @@ -72,9 +72,9 @@ class Slimcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/smileycoin.py b/hdwallet/cryptocurrencies/smileycoin.py index 015ab02f..535f005a 100644 --- a/hdwallet/cryptocurrencies/smileycoin.py +++ b/hdwallet/cryptocurrencies/smileycoin.py @@ -55,9 +55,9 @@ class Smileycoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/solana.py b/hdwallet/cryptocurrencies/solana.py index a4953e4f..f07dda99 100644 --- a/hdwallet/cryptocurrencies/solana.py +++ b/hdwallet/cryptocurrencies/solana.py @@ -49,9 +49,9 @@ class Solana(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/solarcoin.py b/hdwallet/cryptocurrencies/solarcoin.py index 8b290c52..e97b79ac 100644 --- a/hdwallet/cryptocurrencies/solarcoin.py +++ b/hdwallet/cryptocurrencies/solarcoin.py @@ -56,9 +56,9 @@ class Solarcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/stafi.py b/hdwallet/cryptocurrencies/stafi.py index b9d057db..9d4d717d 100644 --- a/hdwallet/cryptocurrencies/stafi.py +++ b/hdwallet/cryptocurrencies/stafi.py @@ -51,9 +51,9 @@ class Stafi(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/stash.py b/hdwallet/cryptocurrencies/stash.py index b51b8200..1cb5f7ac 100644 --- a/hdwallet/cryptocurrencies/stash.py +++ b/hdwallet/cryptocurrencies/stash.py @@ -73,9 +73,9 @@ class Stash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/stellar.py b/hdwallet/cryptocurrencies/stellar.py index 107b5bce..0da69108 100644 --- a/hdwallet/cryptocurrencies/stellar.py +++ b/hdwallet/cryptocurrencies/stellar.py @@ -49,9 +49,9 @@ class Stellar(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/stratis.py b/hdwallet/cryptocurrencies/stratis.py index f361fdfb..d20d4e91 100644 --- a/hdwallet/cryptocurrencies/stratis.py +++ b/hdwallet/cryptocurrencies/stratis.py @@ -73,9 +73,9 @@ class Stratis(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/sugarchain.py b/hdwallet/cryptocurrencies/sugarchain.py index eec97d49..e409f973 100644 --- a/hdwallet/cryptocurrencies/sugarchain.py +++ b/hdwallet/cryptocurrencies/sugarchain.py @@ -90,9 +90,9 @@ class Sugarchain(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/sui.py b/hdwallet/cryptocurrencies/sui.py index caa0e687..53157286 100644 --- a/hdwallet/cryptocurrencies/sui.py +++ b/hdwallet/cryptocurrencies/sui.py @@ -49,9 +49,9 @@ class Sui(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/syscoin.py b/hdwallet/cryptocurrencies/syscoin.py index 58b2f2f6..1293d4b4 100644 --- a/hdwallet/cryptocurrencies/syscoin.py +++ b/hdwallet/cryptocurrencies/syscoin.py @@ -64,9 +64,9 @@ class Syscoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/terra.py b/hdwallet/cryptocurrencies/terra.py index db1ad078..20020907 100644 --- a/hdwallet/cryptocurrencies/terra.py +++ b/hdwallet/cryptocurrencies/terra.py @@ -51,9 +51,9 @@ class Terra(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/tezos.py b/hdwallet/cryptocurrencies/tezos.py index 3ec1b8ad..aa533c19 100644 --- a/hdwallet/cryptocurrencies/tezos.py +++ b/hdwallet/cryptocurrencies/tezos.py @@ -49,9 +49,9 @@ class Tezos(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/theta.py b/hdwallet/cryptocurrencies/theta.py index 0f4ac698..0db464df 100644 --- a/hdwallet/cryptocurrencies/theta.py +++ b/hdwallet/cryptocurrencies/theta.py @@ -50,9 +50,9 @@ class Theta(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/thoughtai.py b/hdwallet/cryptocurrencies/thoughtai.py index 3a0ecefe..2f886140 100644 --- a/hdwallet/cryptocurrencies/thoughtai.py +++ b/hdwallet/cryptocurrencies/thoughtai.py @@ -55,9 +55,9 @@ class ThoughtAI(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/toacoin.py b/hdwallet/cryptocurrencies/toacoin.py index 4c18fd73..40189e69 100644 --- a/hdwallet/cryptocurrencies/toacoin.py +++ b/hdwallet/cryptocurrencies/toacoin.py @@ -55,9 +55,9 @@ class TOACoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/tron.py b/hdwallet/cryptocurrencies/tron.py index b4f484f5..567ea186 100644 --- a/hdwallet/cryptocurrencies/tron.py +++ b/hdwallet/cryptocurrencies/tron.py @@ -56,9 +56,9 @@ class Tron(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/twins.py b/hdwallet/cryptocurrencies/twins.py index f8921958..1d6a2a23 100644 --- a/hdwallet/cryptocurrencies/twins.py +++ b/hdwallet/cryptocurrencies/twins.py @@ -72,9 +72,9 @@ class TWINS(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ultimatesecurecash.py b/hdwallet/cryptocurrencies/ultimatesecurecash.py index 743d38f6..675ab8a9 100644 --- a/hdwallet/cryptocurrencies/ultimatesecurecash.py +++ b/hdwallet/cryptocurrencies/ultimatesecurecash.py @@ -55,9 +55,9 @@ class UltimateSecureCash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/unobtanium.py b/hdwallet/cryptocurrencies/unobtanium.py index ccfef275..66ac7064 100644 --- a/hdwallet/cryptocurrencies/unobtanium.py +++ b/hdwallet/cryptocurrencies/unobtanium.py @@ -54,9 +54,9 @@ class Unobtanium(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/vcash.py b/hdwallet/cryptocurrencies/vcash.py index ecb43af4..a9c1305e 100644 --- a/hdwallet/cryptocurrencies/vcash.py +++ b/hdwallet/cryptocurrencies/vcash.py @@ -54,9 +54,9 @@ class Vcash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/vechain.py b/hdwallet/cryptocurrencies/vechain.py index 5bfd6cc3..811368b8 100644 --- a/hdwallet/cryptocurrencies/vechain.py +++ b/hdwallet/cryptocurrencies/vechain.py @@ -51,9 +51,9 @@ class VeChain(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/verge.py b/hdwallet/cryptocurrencies/verge.py index fc8df9a7..e43bc3dd 100644 --- a/hdwallet/cryptocurrencies/verge.py +++ b/hdwallet/cryptocurrencies/verge.py @@ -55,9 +55,9 @@ class Verge(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/vertcoin.py b/hdwallet/cryptocurrencies/vertcoin.py index b4558ca4..7daa8bb4 100644 --- a/hdwallet/cryptocurrencies/vertcoin.py +++ b/hdwallet/cryptocurrencies/vertcoin.py @@ -64,9 +64,9 @@ class Vertcoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/viacoin.py b/hdwallet/cryptocurrencies/viacoin.py index f0fefe4b..1323ceb3 100644 --- a/hdwallet/cryptocurrencies/viacoin.py +++ b/hdwallet/cryptocurrencies/viacoin.py @@ -90,9 +90,9 @@ class Viacoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/vivo.py b/hdwallet/cryptocurrencies/vivo.py index 74bc0652..7ac5849f 100644 --- a/hdwallet/cryptocurrencies/vivo.py +++ b/hdwallet/cryptocurrencies/vivo.py @@ -55,9 +55,9 @@ class Vivo(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) @@ -71,4 +71,3 @@ class Vivo(ICryptocurrency): "p2pkh", "p2sh" ] DEFAULT_SEMANTIC = "p2pkh" - \ No newline at end of file diff --git a/hdwallet/cryptocurrencies/voxels.py b/hdwallet/cryptocurrencies/voxels.py index 5a8a7ed8..898ae4f5 100644 --- a/hdwallet/cryptocurrencies/voxels.py +++ b/hdwallet/cryptocurrencies/voxels.py @@ -54,9 +54,9 @@ class Voxels(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/vpncoin.py b/hdwallet/cryptocurrencies/vpncoin.py index 1c466015..5bdbb4e3 100644 --- a/hdwallet/cryptocurrencies/vpncoin.py +++ b/hdwallet/cryptocurrencies/vpncoin.py @@ -54,9 +54,9 @@ class VPNCoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/wagerr.py b/hdwallet/cryptocurrencies/wagerr.py index e5e17dea..e7d15006 100644 --- a/hdwallet/cryptocurrencies/wagerr.py +++ b/hdwallet/cryptocurrencies/wagerr.py @@ -54,9 +54,9 @@ class Wagerr(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/whitecoin.py b/hdwallet/cryptocurrencies/whitecoin.py index e9d1ec36..f8dd2ee4 100644 --- a/hdwallet/cryptocurrencies/whitecoin.py +++ b/hdwallet/cryptocurrencies/whitecoin.py @@ -56,9 +56,9 @@ class Whitecoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/wincoin.py b/hdwallet/cryptocurrencies/wincoin.py index b460ddf2..d1c2c356 100644 --- a/hdwallet/cryptocurrencies/wincoin.py +++ b/hdwallet/cryptocurrencies/wincoin.py @@ -55,9 +55,9 @@ class Wincoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/xinfin.py b/hdwallet/cryptocurrencies/xinfin.py index 14970c31..a149ae0b 100644 --- a/hdwallet/cryptocurrencies/xinfin.py +++ b/hdwallet/cryptocurrencies/xinfin.py @@ -51,9 +51,9 @@ class XinFin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) @@ -70,4 +70,3 @@ class XinFin(ICryptocurrency): PARAMS = Params({ "ADDRESS_PREFIX": "xdc" }) - \ No newline at end of file diff --git a/hdwallet/cryptocurrencies/xuez.py b/hdwallet/cryptocurrencies/xuez.py index 3387efae..30da1acd 100644 --- a/hdwallet/cryptocurrencies/xuez.py +++ b/hdwallet/cryptocurrencies/xuez.py @@ -55,9 +55,9 @@ class XUEZ(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/ycash.py b/hdwallet/cryptocurrencies/ycash.py index 9015a80c..7f2ae764 100644 --- a/hdwallet/cryptocurrencies/ycash.py +++ b/hdwallet/cryptocurrencies/ycash.py @@ -55,9 +55,9 @@ class Ycash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/zcash.py b/hdwallet/cryptocurrencies/zcash.py index ef2fc732..f6b559b5 100644 --- a/hdwallet/cryptocurrencies/zcash.py +++ b/hdwallet/cryptocurrencies/zcash.py @@ -72,9 +72,9 @@ class Zcash(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/zclassic.py b/hdwallet/cryptocurrencies/zclassic.py index 0e687c9a..e8a912be 100644 --- a/hdwallet/cryptocurrencies/zclassic.py +++ b/hdwallet/cryptocurrencies/zclassic.py @@ -55,9 +55,9 @@ class ZClassic(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/zetacoin.py b/hdwallet/cryptocurrencies/zetacoin.py index 6ff35d0e..bd6e74f7 100644 --- a/hdwallet/cryptocurrencies/zetacoin.py +++ b/hdwallet/cryptocurrencies/zetacoin.py @@ -54,9 +54,9 @@ class Zetacoin(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/zilliqa.py b/hdwallet/cryptocurrencies/zilliqa.py index 7a4bc71a..32c3e6bd 100644 --- a/hdwallet/cryptocurrencies/zilliqa.py +++ b/hdwallet/cryptocurrencies/zilliqa.py @@ -51,9 +51,9 @@ class Zilliqa(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/cryptocurrencies/zoobc.py b/hdwallet/cryptocurrencies/zoobc.py index 23d4157c..82b02ff6 100644 --- a/hdwallet/cryptocurrencies/zoobc.py +++ b/hdwallet/cryptocurrencies/zoobc.py @@ -61,9 +61,9 @@ class ZooBC(ICryptocurrency): MNEMONICS = Mnemonics({ "BIP39" }) - SEEDS = Seeds({ - "BIP39" - }) + SEEDS = Seeds(( + "BIP39", "SLIP39" + )) HDS = HDs({ "BIP32", "BIP44" }) diff --git a/hdwallet/derivations/__init__.py b/hdwallet/derivations/__init__.py index 28f0ef81..f8b9d1c0 100644 --- a/hdwallet/derivations/__init__.py +++ b/hdwallet/derivations/__init__.py @@ -9,13 +9,13 @@ ) from ..exceptions import DerivationError -from .bip44 import ( +from .bip44 import ( # noqa: F401 BIP44Derivation, CHANGES ) from .bip49 import BIP49Derivation from .bip84 import BIP84Derivation from .bip86 import BIP86Derivation -from .cip1852 import ( +from .cip1852 import ( # noqa: F401 CIP1852Derivation, ROLES ) from .custom import CustomDerivation diff --git a/hdwallet/derivations/bip44.py b/hdwallet/derivations/bip44.py index 559b017a..4a0fee8e 100644 --- a/hdwallet/derivations/bip44.py +++ b/hdwallet/derivations/bip44.py @@ -5,7 +5,7 @@ # file COPYING or https://opensource.org/license/mit from typing import ( - Tuple, Union, Optional, Dict + List, Tuple, Optional, Union ) from ..utils import ( @@ -51,7 +51,9 @@ def __init__( coin_type: Union[str, int] = Bitcoin.COIN_TYPE, account: Union[str, int, Tuple[int, int]] = 0, change: Union[str, int] = CHANGES.EXTERNAL_CHAIN, - address: Union[str, int, Tuple[int, int]] = 0 + address: Union[str, int, Tuple[int, int]] = 0, + path: Optional[str] = None, + indexes: Optional[List[int]] = None, ) -> None: """ Initialize a BIP44 derivation path with specified parameters. @@ -64,10 +66,31 @@ def __init__( :type change: Union[str, int] :param address: The BIP44 address index or tuple. Defaults to 0. :type address: Union[str, int, Tuple[int, int]] + :param path: Optional derivation path string. + :type path: Optional[str] + :param indexes: Optional list of derivation indexes. + :type indexes: Optional[List[int]] :return: None """ - super(BIP44Derivation, self).__init__() + super(BIP44Derivation, self).__init__(path=path, indexes=indexes) + if len(self._indexes) >= 1: + if self._derivations[0] != self._purpose: + raise DerivationError( + "Incorrect derivation path Purpose", expected=self._purpose, got=self._derivations[0] + ) + if len(self._indexes) >= 2: + coin_type = self._derivations[1][0] + if len(self._indexes) >= 3: + account = self._derivations[2][0] + if len(self._indexes) >= 4: + change = self._derivations[3][0] + if len(self._indexes) >= 5: + address = self._derivations[4][0] + if len(self._indexes) > 5: + raise DerivationError( + "Incorrect number of derivation path segments", expected="<= 5", got=len(self._indexes) + ) self._coin_type = normalize_index(index=coin_type, hardened=True) self._account = normalize_index(index=account, hardened=True) @@ -97,7 +120,7 @@ def name(cls) -> str: def get_change_value(self, change: Union[str, int], name_only: bool = False): if isinstance(change, (list, tuple)): raise DerivationError( - "Bad change instance", expected ="int | str", got=type(change).__name__ + "Bad change instance", expected="int | str", got=type(change).__name__ ) external_change = [CHANGES.EXTERNAL_CHAIN, 0, '0'] internal_change = [CHANGES.INTERNAL_CHAIN, 1, '1'] diff --git a/hdwallet/derivations/cip1852.py b/hdwallet/derivations/cip1852.py index 0261fa9b..e4aefe2e 100644 --- a/hdwallet/derivations/cip1852.py +++ b/hdwallet/derivations/cip1852.py @@ -5,7 +5,7 @@ # file COPYING or https://opensource.org/license/mit from typing import ( - Tuple, Union, Optional, Dict + Tuple, Union ) from ..utils import ( @@ -84,7 +84,7 @@ def __init__( f"{index_tuple_to_string(index=self._role)}/" f"{index_tuple_to_string(index=self._address)}" )) - + @classmethod def name(cls) -> str: """ diff --git a/hdwallet/derivations/custom.py b/hdwallet/derivations/custom.py index 8258a6f2..5593f9d1 100644 --- a/hdwallet/derivations/custom.py +++ b/hdwallet/derivations/custom.py @@ -66,7 +66,7 @@ def from_path(self, path: str) -> "CustomDerivation": raise DerivationError("Bad path instance", expected=str, got=type(path)) elif path[0:2] != "m/": raise DerivationError( - f"Bad path format", expected="like this type of path \"m/0'/0\"", got=path + "Bad path format", expected="like this type of path \"m/0'/0\"", got=path ) self._path, self._indexes, self._derivations = normalize_derivation(path=path) diff --git a/hdwallet/derivations/hdw.py b/hdwallet/derivations/hdw.py index af4baea8..f17bf5b1 100644 --- a/hdwallet/derivations/hdw.py +++ b/hdwallet/derivations/hdw.py @@ -48,7 +48,7 @@ def __init__( :param account: The HDW account index or tuple. Defaults to 0. :type account: Union[str, int, Tuple[int, int]] - :param ecc: The HDW ecc index. + :param ecc: The HDW ecc index. :type ecc: Union[str, int, Type[IEllipticCurveCryptography]] :param address: The HDW address index or tuple. Defaults to 0. :type address: Union[str, int, Tuple[int, int]] @@ -105,7 +105,7 @@ def get_ecc_value( if curve not in expected_ecc: raise DerivationError( - f"Bad {self.name()} ECC index", + f"Bad {self.name()} ECC index", expected=expected_ecc, got=curve ) diff --git a/hdwallet/derivations/iderivation.py b/hdwallet/derivations/iderivation.py index 4c981537..1377ff0f 100644 --- a/hdwallet/derivations/iderivation.py +++ b/hdwallet/derivations/iderivation.py @@ -21,7 +21,8 @@ def __init__( self, path: Optional[str] = None, indexes: Optional[List[int]] = None, **kwargs ) -> None: """ - Initializes an object for iderivation. + Initializes an object for iderivation. Derived classes may support different + arguments, but should always support path and indexes. :param path: Optional derivation path string. :type path: Optional[str] diff --git a/hdwallet/eccs/__init__.py b/hdwallet/eccs/__init__.py index 33e660d0..addc0229 100644 --- a/hdwallet/eccs/__init__.py +++ b/hdwallet/eccs/__init__.py @@ -12,17 +12,17 @@ ECCError, PublicKeyError ) from ..utils import get_bytes -from .kholaw import ( +from .kholaw import ( # noqa: F401 KholawEd25519ECC, KholawEd25519Point, KholawEd25519PublicKey, KholawEd25519PrivateKey ) -from .slip10 import ( +from .slip10 import ( # noqa: F401 SLIP10Ed25519ECC, SLIP10Ed25519Point, SLIP10Ed25519PublicKey, SLIP10Ed25519PrivateKey, SLIP10Ed25519Blake2bECC, SLIP10Ed25519Blake2bPoint, SLIP10Ed25519Blake2bPublicKey, SLIP10Ed25519Blake2bPrivateKey, SLIP10Ed25519MoneroECC, SLIP10Ed25519MoneroPoint, SLIP10Ed25519MoneroPublicKey, SLIP10Ed25519MoneroPrivateKey, SLIP10Nist256p1ECC, SLIP10Nist256p1Point, SLIP10Nist256p1PublicKey, SLIP10Nist256p1PrivateKey, SLIP10Secp256k1ECC, SLIP10Secp256k1Point, SLIP10Secp256k1PublicKey, SLIP10Secp256k1PrivateKey ) -from .iecc import ( +from .iecc import ( # noqa: F401 IPoint, IPublicKey, IPrivateKey, IEllipticCurveCryptography ) @@ -132,7 +132,7 @@ def validate_and_get_public_key( :return: A valid IPublicKey instance. :rtype: IPublicKey """ - + try: if isinstance(public_key, bytes): public_key: IPublicKey = public_key_cls.from_bytes(public_key) @@ -147,7 +147,8 @@ def validate_and_get_public_key( ) return public_key except ValueError as error: - raise PublicKeyError("Invalid public key data") + raise PublicKeyError("Invalid public key data") from error + __all__: List[str] = [ "IPoint", "IPublicKey", "IPrivateKey", "IEllipticCurveCryptography", diff --git a/hdwallet/eccs/kholaw/ed25519/public_key.py b/hdwallet/eccs/kholaw/ed25519/public_key.py index 5f3d00f1..551b8738 100644 --- a/hdwallet/eccs/kholaw/ed25519/public_key.py +++ b/hdwallet/eccs/kholaw/ed25519/public_key.py @@ -31,4 +31,3 @@ def point(self) -> IPoint: """ return KholawEd25519Point(bytes(self.verify_key)) - diff --git a/hdwallet/eccs/slip10/__init__.py b/hdwallet/eccs/slip10/__init__.py index e398fb15..b7bd4929 100644 --- a/hdwallet/eccs/slip10/__init__.py +++ b/hdwallet/eccs/slip10/__init__.py @@ -30,4 +30,3 @@ "SLIP10Nist256p1ECC", "SLIP10Nist256p1Point", "SLIP10Nist256p1PublicKey", "SLIP10Nist256p1PrivateKey", "SLIP10Secp256k1ECC", "SLIP10Secp256k1Point", "SLIP10Secp256k1PublicKey", "SLIP10Secp256k1PrivateKey", ] - diff --git a/hdwallet/eccs/slip10/nist256p1/private_key.py b/hdwallet/eccs/slip10/nist256p1/private_key.py index 07f43b24..0afd207c 100644 --- a/hdwallet/eccs/slip10/nist256p1/private_key.py +++ b/hdwallet/eccs/slip10/nist256p1/private_key.py @@ -18,7 +18,7 @@ class SLIP10Nist256p1PrivateKey(IPrivateKey): - + signing_key: SigningKey def __init__(self, signing_key: SigningKey) -> None: diff --git a/hdwallet/eccs/slip10/nist256p1/public_key.py b/hdwallet/eccs/slip10/nist256p1/public_key.py index b44a7858..effc3189 100644 --- a/hdwallet/eccs/slip10/nist256p1/public_key.py +++ b/hdwallet/eccs/slip10/nist256p1/public_key.py @@ -19,7 +19,7 @@ class SLIP10Nist256p1PublicKey(IPublicKey): - + verify_key: VerifyingKey def __init__(self, verify_key: VerifyingKey) -> None: diff --git a/hdwallet/eccs/slip10/secp256k1/__init__.py b/hdwallet/eccs/slip10/secp256k1/__init__.py index 1c917df4..3cc93cc0 100644 --- a/hdwallet/eccs/slip10/secp256k1/__init__.py +++ b/hdwallet/eccs/slip10/secp256k1/__init__.py @@ -8,13 +8,13 @@ from ....consts import SLIP10_SECP256K1_CONST from ...iecc import IEllipticCurveCryptography -from .point import ( +from .point import ( # noqa: F401 SLIP10Secp256k1Point, SLIP10Secp256k1PointCoincurve, SLIP10Secp256k1PointECDSA ) -from .public_key import ( +from .public_key import ( # noqa: F401 SLIP10Secp256k1PublicKey, SLIP10Secp256k1PublicKeyCoincurve, SLIP10Secp256k1PublicKeyECDSA ) -from .private_key import ( +from .private_key import ( # noqa: F401 SLIP10Secp256k1PrivateKey, SLIP10Secp256k1PrivateKeyCoincurve, SLIP10Secp256k1PrivateKeyECDSA ) diff --git a/hdwallet/entropies/__init__.py b/hdwallet/entropies/__init__.py index be60ac3f..0dce79be 100644 --- a/hdwallet/entropies/__init__.py +++ b/hdwallet/entropies/__init__.py @@ -9,17 +9,20 @@ ) from ..exceptions import EntropyError -from .algorand import ( +from .algorand import ( # noqa: F401 AlgorandEntropy, ALGORAND_ENTROPY_STRENGTHS ) -from .bip39 import ( +from .bip39 import ( # noqa: F401 BIP39Entropy, BIP39_ENTROPY_STRENGTHS ) -from .electrum import ( +from .slip39 import ( # noqa: F401 + SLIP39Entropy, SLIP39_ENTROPY_STRENGTHS +) +from .electrum import ( # noqa: F401 ElectrumV1Entropy, ELECTRUM_V1_ENTROPY_STRENGTHS, ElectrumV2Entropy, ELECTRUM_V2_ENTROPY_STRENGTHS ) -from .monero import ( +from .monero import ( # noqa: F401 MoneroEntropy, MONERO_ENTROPY_STRENGTHS ) from .ientropy import IEntropy @@ -41,6 +44,8 @@ class ENTROPIES: +--------------+-------------------------------------------------------------+ | BIP39 | :class:`hdwallet.entropies.bip39.BIP39Entropy` | +--------------+-------------------------------------------------------------+ + | SLIP39 | :class:`hdwallet.entropies.bip39.SLIP39Entropy` | + +--------------+-------------------------------------------------------------+ | Electrum-V1 | :class:`hdwallet.entropies.electrum.v1.ElectrumV1Entropy` | +--------------+-------------------------------------------------------------+ | Electrum-V2 | :class:`hdwallet.entropies.electrum.v2.ElectrumV2Entropy` | @@ -52,6 +57,7 @@ class ENTROPIES: dictionary: Dict[str, Type[IEntropy]] = { AlgorandEntropy.name(): AlgorandEntropy, BIP39Entropy.name(): BIP39Entropy, + SLIP39Entropy.name(): SLIP39Entropy, ElectrumV1Entropy.name(): ElectrumV1Entropy, ElectrumV2Entropy.name(): ElectrumV2Entropy, MoneroEntropy.name(): MoneroEntropy @@ -116,6 +122,7 @@ def is_entropy(cls, name: str) -> bool: "IEntropy", "ALGORAND_ENTROPY_STRENGTHS", "BIP39_ENTROPY_STRENGTHS", + "SLIP39_ENTROPY_STRENGTHS", "ELECTRUM_V1_ENTROPY_STRENGTHS", "ELECTRUM_V2_ENTROPY_STRENGTHS", "MONERO_ENTROPY_STRENGTHS", diff --git a/hdwallet/entropies/ientropy.py b/hdwallet/entropies/ientropy.py index 00c11e0f..34b01ae0 100644 --- a/hdwallet/entropies/ientropy.py +++ b/hdwallet/entropies/ientropy.py @@ -9,7 +9,7 @@ ) import os -import re +import string from ..exceptions import EntropyError from ..utils import ( @@ -18,7 +18,7 @@ class IEntropy: - + _entropy: str _strength: int @@ -104,9 +104,11 @@ def is_valid(cls, entropy: str) -> bool: :rtype: bool """ - return isinstance(entropy, str) and bool(re.fullmatch( - r'^[0-9a-fA-F]+$', entropy - )) and cls.is_valid_strength(len(entropy) * 4) + return ( + isinstance(entropy, str) + and all(c in string.hexdigits for c in entropy) + and cls.is_valid_strength(len(entropy) * 4) + ) @classmethod def is_valid_strength(cls, strength: int) -> bool: @@ -148,4 +150,4 @@ def are_entropy_bits_enough(self, entropy: Union[bytes, int]) -> bool: """ if self.name() != "Electrum-V2": - raise NotImplemented + raise NotImplementedError diff --git a/hdwallet/entropies/slip39.py b/hdwallet/entropies/slip39.py new file mode 100644 index 00000000..0d4b8bf5 --- /dev/null +++ b/hdwallet/entropies/slip39.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +# Copyright © 2020-2024, Meheret Tesfaye Batu +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +from .ientropy import IEntropy + + +class SLIP39_ENTROPY_STRENGTHS: + """ + Constants representing the entropy strengths for SLIP39. + """ + ONE_HUNDRED_TWENTY_EIGHT: int = 128 + TWO_HUNDRED_FIFTY_SIX: int = 256 + FIVE_HUNDRED_TWELVE: int = 512 + + +class SLIP39Entropy(IEntropy): + """Stores entropy for SLIP-39. This data is used directly to create deterministic keys for + various cryptocurrencies. + + .. note:: + This class inherits from the ``IEntropy`` class, thereby ensuring that all functions are accessible. + + Here are available ``SLIP39_ENTROPY_STRENGTHS``: + + +--------------------------+-------+ + | Name | Value | + +==========================+=======+ + | ONE_HUNDRED_TWENTY_EIGHT | 128 | + +--------------------------+-------+ + | TWO_HUNDRED_FIFTY_SIX | 256 | + +--------------------------+-------+ + | FIVE_HUNDRED_TWELVE | 512 | + +--------------------------+-------+ + + """ + + strengths = [ + SLIP39_ENTROPY_STRENGTHS.ONE_HUNDRED_TWENTY_EIGHT, + SLIP39_ENTROPY_STRENGTHS.TWO_HUNDRED_FIFTY_SIX, + SLIP39_ENTROPY_STRENGTHS.FIVE_HUNDRED_TWELVE + ] + + @classmethod + def name(cls) -> str: + """ + Get the name of the entropy class. + + :return: The name of the entropy class. + :rtype: str + """ + + return "SLIP39" diff --git a/hdwallet/exceptions.py b/hdwallet/exceptions.py index 3b8f33cd..a71945ec 100644 --- a/hdwallet/exceptions.py +++ b/hdwallet/exceptions.py @@ -36,9 +36,11 @@ def __str__(self): class EntropyError(Error): pass + class ChecksumError(Error): pass + class MnemonicError(Error): pass @@ -100,4 +102,4 @@ class XPublicKeyError(Error): class XPrivateKeyError(Error): - pass \ No newline at end of file + pass diff --git a/hdwallet/hds/algorand.py b/hdwallet/hds/algorand.py index 17e3b1b5..91478b2c 100644 --- a/hdwallet/hds/algorand.py +++ b/hdwallet/hds/algorand.py @@ -62,10 +62,10 @@ def from_seed(self, seed: Union[bytes, str, ISeed], **kwargs) -> "AlgorandHD": seed.seed() if isinstance(seed, ISeed) else seed ) except ValueError as error: - raise SeedError("Invalid seed data") + raise SeedError("Invalid seed data") from error if len(self._seed) < 16: - raise Error(f"Invalid seed length", expected="< 16", got=len(self._seed)) + raise Error("Invalid seed length", expected="< 16", got=len(self._seed)) def clamp_kL(kL: bytearray): kL[0] &= 0b11111000 @@ -235,7 +235,7 @@ def xprivate_key( :rtype: Optional[str] """ return super(AlgorandHD, self).xprivate_key(version=version, encoded=encoded) - + def address(self, **kwargs) -> str: """ Generates a Algorand address using the AlgorandAddress encoding scheme. diff --git a/hdwallet/hds/bip141.py b/hdwallet/hds/bip141.py index bbb61194..99862cfb 100644 --- a/hdwallet/hds/bip141.py +++ b/hdwallet/hds/bip141.py @@ -202,7 +202,7 @@ def xpublic_key( """ return super(BIP141HD, self).xpublic_key( - version=(self._xpublic_key_version if version is None else version) , encoded=encoded + version=(self._xpublic_key_version if version is None else version), encoded=encoded ) def address( diff --git a/hdwallet/hds/bip32.py b/hdwallet/hds/bip32.py index f9b05d6b..426ec6ea 100644 --- a/hdwallet/hds/bip32.py +++ b/hdwallet/hds/bip32.py @@ -66,7 +66,7 @@ class BIP32HD(IHD): _root_index: int = 0 _depth: int = 0 _index: int = 0 - + def __init__( self, ecc: Type[IEllipticCurveCryptography], public_key_type: str = PUBLIC_KEY_TYPES.COMPRESSED, **kwargs ) -> None: @@ -141,10 +141,10 @@ def from_seed(self, seed: Union[bytes, str, ISeed], **kwargs) -> "BIP32HD": seed.seed() if isinstance(seed, ISeed) else seed ) except ValueError as error: - raise SeedError("Invalid seed data") + raise SeedError("Invalid seed data") from error if len(self._seed) < 16: - raise Error(f"Invalid seed length", expected="< 16", got=len(self._seed)) + raise Error("Invalid seed length", expected="< 16", got=len(self._seed)) hmac_half_length: int = hashlib.sha512().digest_size // 2 @@ -338,7 +338,7 @@ def from_private_key(self, private_key: str) -> "BIP32HD": self._strict = None return self except ValueError as error: - raise PrivateKeyError("Invalid private key data") + raise PrivateKeyError("Invalid private key data") from error def from_public_key(self, public_key: str) -> "BIP32HD": """ @@ -356,7 +356,7 @@ def from_public_key(self, public_key: str) -> "BIP32HD": self._strict = None return self except ValueError as error: - raise PublicKeyError("Invalid public key data") + raise PublicKeyError("Invalid public key data") from error def from_derivation(self, derivation: IDerivation) -> "BIP32HD": """ @@ -411,14 +411,13 @@ def clean_derivation(self) -> "BIP32HD": self._root_private_key, self._root_chain_code, (integer_to_bytes(0x00) * 4) ) self._public_key = self._private_key.public_key() - self._derivation.clean() - self._depth = 0 elif self._root_public_key: self._public_key, self._chain_code, self._parent_fingerprint = ( self._root_public_key, self._root_chain_code, (integer_to_bytes(0x00) * 4) ) + if self._derivation: self._derivation.clean() - self._depth = 0 + self._depth = 0 return self def drive(self, index: int) -> Optional["BIP32HD"]: diff --git a/hdwallet/hds/bip44.py b/hdwallet/hds/bip44.py index d46ba965..006b26c9 100644 --- a/hdwallet/hds/bip44.py +++ b/hdwallet/hds/bip44.py @@ -14,14 +14,14 @@ from ..addresses import P2PKHAddress from ..exceptions import DerivationError from ..derivations import ( - IDerivation, BIP44Derivation + IDerivation, BIP44Derivation, CustomDerivation ) from .bip32 import BIP32HD class BIP44HD(BIP32HD): - _derivation: BIP44Derivation + _derivation: Union[BIP44Derivation, CustomDerivation] def __init__( self, ecc: Type[IEllipticCurveCryptography], public_key_type: str = PUBLIC_KEY_TYPES.COMPRESSED, **kwargs @@ -134,16 +134,16 @@ def from_derivation(self, derivation: IDerivation) -> "BIP44HD": """ Initialize the BIP44HD object from a given derivation. - :param derivation: The BIP44 derivation object. + :param derivation: The BIP44-compatible derivation object. :type derivation: IDerivation :return: Updated BIP44HD object. :rtype: BIP44HD """ - if not isinstance(derivation, BIP44Derivation): + if not isinstance(derivation, (BIP44Derivation, CustomDerivation)): raise DerivationError( - "Invalid derivation instance", expected=BIP44Derivation, got=type(derivation) + "Invalid derivation instance", expected={BIP44Derivation, CustomDerivation}, got=type(derivation) ) self.clean_derivation() diff --git a/hdwallet/hds/bip49.py b/hdwallet/hds/bip49.py index f109d205..6aef9c64 100644 --- a/hdwallet/hds/bip49.py +++ b/hdwallet/hds/bip49.py @@ -14,14 +14,14 @@ from ..addresses import P2WPKHInP2SHAddress from ..exceptions import DerivationError from ..derivations import ( - IDerivation, BIP49Derivation + IDerivation, BIP49Derivation, CustomDerivation ) from .bip44 import BIP44HD class BIP49HD(BIP44HD): - _derivation: BIP49Derivation + _derivation: Union[BIP49Derivation, CustomDerivation] def __init__( self, ecc: Type[IEllipticCurveCryptography], public_key_type: str = PUBLIC_KEY_TYPES.COMPRESSED, **kwargs @@ -67,9 +67,9 @@ def from_derivation(self, derivation: IDerivation) -> "BIP49HD": :rtype: BIP49HD """ - if not isinstance(derivation, BIP49Derivation): + if not isinstance(derivation, (BIP49Derivation, CustomDerivation)): raise DerivationError( - "Invalid derivation instance", expected=BIP49Derivation, got=type(derivation) + "Invalid derivation instance", expected=(BIP49Derivation, CustomDerivation), got=type(derivation) ) self.clean_derivation() diff --git a/hdwallet/hds/bip84.py b/hdwallet/hds/bip84.py index b54ba7a7..10dde53e 100644 --- a/hdwallet/hds/bip84.py +++ b/hdwallet/hds/bip84.py @@ -14,14 +14,14 @@ from ..addresses import P2WPKHAddress from ..exceptions import DerivationError from ..derivations import ( - IDerivation, BIP84Derivation + IDerivation, BIP84Derivation, CustomDerivation ) from .bip44 import BIP44HD class BIP84HD(BIP44HD): - _derivation: BIP84Derivation + _derivation: Union[BIP84Derivation, CustomDerivation] def __init__( self, ecc: Type[IEllipticCurveCryptography], public_key_type: str = PUBLIC_KEY_TYPES.COMPRESSED, **kwargs @@ -68,9 +68,9 @@ def from_derivation(self, derivation: IDerivation) -> "BIP84HD": :rtype: BIP84HD """ - if not isinstance(derivation, BIP84Derivation): + if not isinstance(derivation, (BIP84Derivation, CustomDerivation)): raise DerivationError( - "Invalid derivation instance", expected=BIP84Derivation, got=type(derivation) + "Invalid derivation instance", expected=(BIP84Derivation, CustomDerivation), got=type(derivation) ) self.clean_derivation() diff --git a/hdwallet/hds/bip86.py b/hdwallet/hds/bip86.py index e4589583..778ff68c 100644 --- a/hdwallet/hds/bip86.py +++ b/hdwallet/hds/bip86.py @@ -14,17 +14,17 @@ from ..addresses import P2TRAddress from ..exceptions import DerivationError from ..derivations import ( - IDerivation, BIP86Derivation + IDerivation, BIP86Derivation, CustomDerivation ) from .bip44 import BIP44HD class BIP86HD(BIP44HD): - _derivation: BIP86Derivation + _derivation: Union[BIP86Derivation, CustomDerivation] def __init__( - self, ecc: Type[IEllipticCurveCryptography], public_key_type: str = PUBLIC_KEY_TYPES.COMPRESSED, **kwargs + self, ecc: Type[IEllipticCurveCryptography], public_key_type: str = PUBLIC_KEY_TYPES.COMPRESSED, **kwargs ) -> None: """ Initialize a BIP86HD instance. @@ -66,9 +66,9 @@ def from_derivation(self, derivation: IDerivation) -> "BIP86HD": :rtype: BIP86HD """ - if not isinstance(derivation, BIP86Derivation): + if not isinstance(derivation, (BIP86Derivation, CustomDerivation)): raise DerivationError( - "Invalid derivation instance", expected=BIP86Derivation, got=type(derivation) + "Invalid derivation instance", expected=(BIP86Derivation, CustomDerivation), got=type(derivation) ) self.clean_derivation() diff --git a/hdwallet/hds/cardano.py b/hdwallet/hds/cardano.py index 48678164..1a3c94c5 100644 --- a/hdwallet/hds/cardano.py +++ b/hdwallet/hds/cardano.py @@ -87,11 +87,11 @@ def from_seed(self, seed: Union[str, ISeed], passphrase: Optional[str] = None) - seed.seed() if isinstance(seed, ISeed) else seed ) except ValueError as error: - raise SeedError("Invalid seed data") + raise SeedError("Invalid seed data") from error if self._cardano_type == Cardano.TYPES.BYRON_LEGACY: if len(self._seed) != 32: - raise Error(f"Invalid seed length", expected=32, got=len(self._seed)) + raise Error("Invalid seed length", expected=32, got=len(self._seed)) def tweak_master_key_bits(data: bytes) -> bytes: data: bytearray = bytearray(data) @@ -127,7 +127,7 @@ def tweak_master_key_bits(data: bytes) -> bytes: Cardano.TYPES.BYRON_ICARUS, Cardano.TYPES.SHELLEY_ICARUS ]: if len(self._seed) < 16: - raise Error(f"Invalid seed length", expected="< 16", got=len(self._seed)) + raise Error("Invalid seed length", expected="< 16", got=len(self._seed)) pbkdf2_passphrase, pbkdf2_rounds, pbkdf2_output_length = ( (passphrase if passphrase else ""), 4096, 96 @@ -159,7 +159,7 @@ def tweak_master_key_bits(data: bytes) -> bytes: Cardano.TYPES.BYRON_LEDGER, Cardano.TYPES.SHELLEY_LEDGER ]: if len(self._seed) < 16: - raise Error(f"Invalid seed length", expected="< 16", got=len(self._seed)) + raise Error("Invalid seed length", expected="< 16", got=len(self._seed)) hmac_half_length: int = hashlib.sha512().digest_size // 2 @@ -232,7 +232,7 @@ def from_private_key(self, private_key: str) -> "CardanoHD": self._strict = None return self except ValueError as error: - raise PrivateKeyError("Invalid private key data") + raise PrivateKeyError("Invalid private key data") from error def from_public_key(self, public_key: str) -> "CardanoHD": """ @@ -254,7 +254,7 @@ def from_public_key(self, public_key: str) -> "CardanoHD": self._strict = None return self except ValueError as error: - raise PublicKeyError("Invalid public key data") + raise PublicKeyError("Invalid public key data") from error def drive(self, index: int) -> Optional["CardanoHD"]: """ @@ -334,7 +334,7 @@ def new_private_key_right_part(zr: bytes, kr: bytes) -> bytes: ), endianness="little" ) - z_hmacl, z_hmacr, _hmacl, _hmacr = ( + z_hmacl, z_hmacr, _hmacl, _hmacr = ( # noqa: F841 z_hmac[:hmac_half_length], z_hmac[hmac_half_length:], _hmac[:hmac_half_length], _hmac[hmac_half_length:] ) @@ -376,7 +376,7 @@ def new_public_key_point(public_key: IPublicKey, zl: bytes, ecc: IEllipticCurveC zl: int = bytes_to_integer(zl[:28], endianness="little") return public_key.point() + ((zl * 8) * ecc.GENERATOR) - z_hmacl, z_hmacr, _hmacl, _hmacr = ( + z_hmacl, z_hmacr, _hmacl, _hmacr = ( # noqa: F841 z_hmac[:hmac_half_length], z_hmac[hmac_half_length:], _hmac[:hmac_half_length], _hmac[hmac_half_length:] ) diff --git a/hdwallet/hds/electrum/v1.py b/hdwallet/hds/electrum/v1.py index d2cabe4e..75265d0a 100644 --- a/hdwallet/hds/electrum/v1.py +++ b/hdwallet/hds/electrum/v1.py @@ -114,7 +114,7 @@ def from_seed(self, seed: Union[bytes, str, ISeed], **kwargs) -> "ElectrumV1HD": self.from_private_key(private_key=self._seed) return self except ValueError as error: - raise SeedError("Invalid seed data") + raise SeedError("Invalid seed data") from error def from_private_key(self, private_key: Union[bytes, str, IPrivateKey]) -> "ElectrumV1HD": """ @@ -139,7 +139,7 @@ def from_private_key(self, private_key: Union[bytes, str, IPrivateKey]) -> "Elec self.__update__() return self except ValueError as error: - raise PrivateKeyError("Invalid private key data") + raise PrivateKeyError("Invalid private key data") from error def from_wif(self, wif: str) -> "ElectrumV1HD": """ @@ -180,7 +180,7 @@ def from_public_key(self, public_key: Union[bytes, str, IPublicKey]) -> "Electru self.__update__() return self except ValueError as error: - raise PublicKeyError("Invalid public key error") + raise PublicKeyError("Invalid public key error") from error def from_derivation(self, derivation: IDerivation) -> "ElectrumV1HD": """ diff --git a/hdwallet/hds/electrum/v2.py b/hdwallet/hds/electrum/v2.py index 672a6581..df985999 100644 --- a/hdwallet/hds/electrum/v2.py +++ b/hdwallet/hds/electrum/v2.py @@ -23,7 +23,7 @@ PUBLIC_KEY_TYPES, MODES, WIF_TYPES ) from ...exceptions import ( - Error, DerivationError, AddressError, WIFError + Error, DerivationError, AddressError ) from ..bip32 import BIP32HD from ..ihd import IHD @@ -117,7 +117,7 @@ def from_seed(self, seed: Union[bytes, str, ISeed], **kwargs) -> "ElectrumV2HD": self._bip32_hd.from_seed(seed=seed) self.__update__() return self - + def from_derivation(self, derivation: IDerivation) -> "ElectrumV2HD": """ Initialize the instance from a derivation. diff --git a/hdwallet/hds/ihd.py b/hdwallet/hds/ihd.py index e8238079..04da4cef 100644 --- a/hdwallet/hds/ihd.py +++ b/hdwallet/hds/ihd.py @@ -453,7 +453,6 @@ def public_key_type(self) -> str: :rtype: str """ - def mode(self) -> str: """ Get the mode of the ElectrumV2HD instance. @@ -537,7 +536,7 @@ def strict(self) -> Optional[bool]: def integrated_address(self, **kwargs) -> str: """ Generates the integrated Monero address associated with the spend and view public keys. - + :param kwargs: Additional keyword arguments. :return: Integrated Monero address. @@ -547,7 +546,7 @@ def integrated_address(self, **kwargs) -> str: def primary_address(self, **kwargs) -> str: """ Generates the primary Monero address associated with the spend and view public keys. - + :param kwargs: Additional keyword arguments. :return: Primary Monero address. @@ -559,7 +558,7 @@ def sub_address(self, **kwargs) -> str: Generates a sub-address associated with the given minor and major indexes or uses the current derivation indexes. :param kwargs: Additional keyword arguments. - + :return: Generated sub-address. :rtype: str """ @@ -572,4 +571,4 @@ def address(self, **kwargs) -> str: :return: The generated address. :rtype: str - """ \ No newline at end of file + """ diff --git a/hdwallet/hds/monero.py b/hdwallet/hds/monero.py index a2ae3481..f3814698 100644 --- a/hdwallet/hds/monero.py +++ b/hdwallet/hds/monero.py @@ -119,7 +119,7 @@ def from_seed(self, seed: Union[bytes, str, ISeed], **kwargs) -> "MoneroHD": spend_private_key=scalar_reduce(spend_private_key) ) except ValueError as error: - raise SeedError("Invalid seed data") + raise SeedError("Invalid seed data") from error def from_private_key(self, private_key: Union[bytes, str, IPrivateKey]) -> "MoneroHD": """ @@ -140,7 +140,7 @@ def from_private_key(self, private_key: Union[bytes, str, IPrivateKey]) -> "Mone spend_private_key=scalar_reduce(kekkak256(self._private_key)) ) except ValueError as error: - raise PrivateKeyError("Invalid private key data") + raise PrivateKeyError("Invalid private key data") from error def from_derivation(self, derivation: IDerivation) -> "MoneroHD": """ @@ -225,13 +225,13 @@ def from_watch_only( if isinstance(view_private_key, (bytes, str)): view_private_key: IPrivateKey = SLIP10Ed25519MoneroPrivateKey.from_bytes(get_bytes(view_private_key)) except ValueError as error: - raise PrivateKeyError("Invalid view private key data") + raise PrivateKeyError("Invalid view private key data") from error try: if isinstance(spend_public_key, (bytes, str)): spend_public_key: IPublicKey = SLIP10Ed25519MoneroPublicKey.from_bytes(get_bytes(spend_public_key)) except ValueError as error: - raise PublicKeyError("Invalid spend public key data") + raise PublicKeyError("Invalid spend public key data") from error self._spend_private_key = None self._view_private_key = view_private_key @@ -255,11 +255,11 @@ def drive(self, minor_index: int, major_index: int) -> Tuple[IPublicKey, IPublic maximum_index: int = 2 ** 32 - 1 if minor_index < 0 or minor_index > maximum_index: raise DerivationError( - f"Invalid minor index range", expected=f"0-{maximum_index}", got=minor_index + "Invalid minor index range", expected=f"0-{maximum_index}", got=minor_index ) if major_index < 0 or major_index > maximum_index: raise DerivationError( - f"Invalid major index range", expected=f"0-{maximum_index}", got=major_index + "Invalid major index range", expected=f"0-{maximum_index}", got=major_index ) if minor_index == 0 and major_index == 0: diff --git a/hdwallet/hdwallet.py b/hdwallet/hdwallet.py index 0ba19019..eedfe333 100644 --- a/hdwallet/hdwallet.py +++ b/hdwallet/hdwallet.py @@ -277,8 +277,13 @@ def __init__( self._hd = hd(network=self._network.NAME) def from_entropy(self, entropy: IEntropy) -> "HDWallet": - """ - Initialize the HDWallet from entropy. + """Initialize the HDWallet from entropy. + + Supplying SLIP-39 encoded (or some other) entropy can be valid, if the entropy can be + converted to the native (first) entropy type of this cryptocurrency; normally, the size + would just have to be compatible. + + The entropy is converted to the corresponding Mnemonic type. :param entropy: The entropy source to generate the mnemonic. :type entropy: IEntropy @@ -295,6 +300,8 @@ def from_entropy(self, entropy: IEntropy) -> "HDWallet": +----------------+-----------------------------------------------------------------------------------------------------------+ | BIP's | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/bips/from_entropy.py | +----------------+-----------------------------------------------------------------------------------------------------------+ + | SLIP-39 | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/bips/from_entropy_slip39.py | + +----------------+-----------------------------------------------------------------------------------------------------------+ | Cardano | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/cardano/from_entropy.py | +----------------+-----------------------------------------------------------------------------------------------------------+ | Electrum-V1 | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/electrum/v1/from_entropy.py | @@ -303,10 +310,18 @@ def from_entropy(self, entropy: IEntropy) -> "HDWallet": +----------------+-----------------------------------------------------------------------------------------------------------+ | Monero | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/monero/from_entropy.py | +----------------+-----------------------------------------------------------------------------------------------------------+ + """ if entropy.name() not in self._cryptocurrency.ENTROPIES.get_entropies(): - raise Error(f"Invalid entropy class for {self._cryptocurrency.NAME} cryptocurrency") + try: + entropy = ENTROPIES.entropy( + name=self._cryptocurrency.ENTROPIES.get_entropies()[0] + ).__call__( + entropy=entropy.entropy() + ) + except Exception as exc: + raise Error(f"Invalid entropy class {entropy.name()} for {self._cryptocurrency.NAME} cryptocurrency") from exc self._entropy = entropy if self._entropy.name() == "Electrum-V2": @@ -330,6 +345,7 @@ def from_entropy(self, entropy: IEntropy) -> "HDWallet": mnemonic=mnemonic, mnemonic_type=self._mnemonic_type ) ) + return self.from_mnemonic( mnemonic=MNEMONICS.mnemonic( name=self._entropy.name() @@ -339,8 +355,11 @@ def from_entropy(self, entropy: IEntropy) -> "HDWallet": ) def from_mnemonic(self, mnemonic: IMnemonic) -> "HDWallet": - """ - Initialize the HDWallet from a mnemonic. + """Initialize the HDWallet from a mnemonic. + + Providing a different Mnemonic such as SLIP-39 is fine, so long as the entropy it encodes is + compatible with the native Mnemonic type of the cryptocurrency. Uses the default (first) + language of the default Mnemonic type. :param mnemonic: The mnemonic instance to generate the seed. :type mnemonic: IMnemonic @@ -365,10 +384,20 @@ def from_mnemonic(self, mnemonic: IMnemonic) -> "HDWallet": +----------------+-----------------------------------------------------------------------------------------------------------+ | Monero | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/monero/from_mnemonic.py | +----------------+-----------------------------------------------------------------------------------------------------------+ + """ if mnemonic.name() not in self._cryptocurrency.MNEMONICS.get_mnemonics(): - raise Error(f"Invalid mnemonic class for {self._cryptocurrency.NAME} cryptocurrency") + raise Error(f"Invalid mnemonic class {mnemonic.name()} for {self._cryptocurrency.NAME} cryptocurrency") + # try: + # mnemonic_cls = MNEMONICS.mnemonic( + # self._cryptocurrency.MNEMONICS.get_mnemonics()[0] + # ) + # mnemonic = mnemonic_cls.from_entropy( + # entropy=mnemonic.decode(mnemonic.mnemonic()), language=mnemonic_cls.languages[0] + # ) + # except Exception as exc: + # raise Error(f"Invalid mnemonic class {mnemonic.name()} for {self._cryptocurrency.NAME} cryptocurrency") from exc self._mnemonic = mnemonic if self._mnemonic.name() == "Electrum-V2": @@ -412,22 +441,32 @@ def from_mnemonic(self, mnemonic: IMnemonic) -> "HDWallet": ).from_mnemonic( mnemonic=self._mnemonic.mnemonic() ) - return self.from_seed( - seed=SEEDS.seed( - name=( - "Cardano" if self._hd.name() == "Cardano" else self._mnemonic.name() + if self._hd.name() == "Cardano": + # We have to retain the specified Cardano seed type + return self.from_seed( + seed=SEEDS.seed( + name="Cardano" + ).__call__( + seed=seed, + cardano_type=self._cardano_type + ) + ) + else: + return self.from_seed( + seed=SEEDS.seed( + name=self._mnemonic.name() + ).__call__( + seed=seed ) - ).__call__( - seed=seed ) - ) - def from_seed(self, seed: ISeed) -> "HDWallet": + def from_seed(self, seed: Union[ISeed,bytes,str]) -> "HDWallet": """ Initialize the HDWallet from a seed. - :param seed: The seed instance to initialize the HD wallet. - :type seed: ISeed + + :param seed: The seed instance or data to initialize the HD wallet. + :type seed: Union[ISeed,bytes,str] :return: The initialized HDWallet instance. :rtype: HDWallet @@ -441,6 +480,8 @@ def from_seed(self, seed: ISeed) -> "HDWallet": +----------------+-----------------------------------------------------------------------------------------------------------+ | BIP's | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/bips/from_seed.py | +----------------+-----------------------------------------------------------------------------------------------------------+ + | SLIP-39 | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/bips/from_entropy_slip39.py | + +----------------+-----------------------------------------------------------------------------------------------------------+ | Cardano | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/cardano/from_seed.py | +----------------+-----------------------------------------------------------------------------------------------------------+ | Electrum-V1 | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/electrum/v1/from_seed.py | @@ -450,9 +491,29 @@ def from_seed(self, seed: ISeed) -> "HDWallet": | Monero | https://github.com/hdwallet-io/python-hdwallet/blob/master/examples/hdwallet/monero/from_seed.py | +----------------+-----------------------------------------------------------------------------------------------------------+ """ + if not isinstance(seed, ISeed): + # Convert raw hex seed data to the appropriate default ISeed for the HDWallet cryptocurrency. + # Certain Seed types require additional sub-type information + if type(seed) is bytes: + seed = seed.hex() + try: + seed_cls = SEEDS.seed( + name=self._cryptocurrency.SEEDS.get_seeds()[0] + ) + if seed_cls.name() == "Cardano": + seed = seed_cls( + seed=seed, + cardano_type=self._cardano_type + ) + else: + seed = seed_cls( + seed=seed + ) + except Exception as exc: + raise Error(f"Invalid seed for {self._cryptocurrency.NAME} cryptocurrency") from exc if seed.name() not in self._cryptocurrency.SEEDS.get_seeds(): - raise Error(f"Invalid seed class for {self._cryptocurrency.NAME} cryptocurrency") + raise Error(f"Invalid seed class {seed.name()} for {self._cryptocurrency.NAME} cryptocurrency") self._seed = seed self._hd.from_seed( @@ -557,6 +618,16 @@ def from_xpublic_key(self, xpublic_key: str, encoded: bool = True, strict: bool ) return self + def from_path(self, path: Optional[str]) -> "HDWallet": + """ + Use the existing derivation, but from the provided path. + """ + return self.from_derivation( + self._derivation.__class__( + path=path + ) + ) + def from_derivation(self, derivation: IDerivation) -> "HDWallet": """ Initialize the HDWallet from a derivation object. @@ -596,7 +667,8 @@ def clean_derivation(self) -> "HDWallet": """ self._hd.clean_derivation() - self._derivation.clean() + if self._derivation: + self._derivation.clean() return self def from_private_key(self, private_key: str) -> "HDWallet": @@ -715,7 +787,7 @@ def from_spend_private_key( self._hd.from_spend_private_key(spend_private_key=spend_private_key) return self except ValueError as error: - raise PrivateKeyError("Invalid spend private key data") + raise PrivateKeyError("Invalid spend private key data") from error def from_watch_only( self, diff --git a/hdwallet/info.py b/hdwallet/info.py index 75ce3d41..7abf5f72 100644 --- a/hdwallet/info.py +++ b/hdwallet/info.py @@ -7,7 +7,7 @@ from typing import List __name__: str = "hdwallet" -__version__: str = "v3.6.1" +__version__: str = "v3.7.0" __license__: str = "MIT" __author__: str = "Meheret Tesfaye Batu" __email__: str = "meherett.batu@gmail.com" @@ -20,7 +20,7 @@ __keywords__: List[str] = [ "ecc", "kholaw", "slip10", "ed25519", "nist256p1", "secp256k1", # ECC keywords "hd", "bip32", "bip44", "bip49", "bip84", "bip86", "bip141", "monero", "cardano", # HD keywords - "entropy", "mnemonic", "seed", "bip39", "algorand", "electrum", # Entropy, Mnemonic and Seed keywords + "entropy", "mnemonic", "seed", "bip39", "slip39", "algorand", "electrum", # Entropy, Mnemonic and Seed keywords "cryptocurrencies", "bitcoin", "ethereum", "cryptography", "cli", "cip1852" # Other keywords ] __websites__: List[str] = [ diff --git a/hdwallet/libs/base58.py b/hdwallet/libs/base58.py index 5a918155..6289655b 100644 --- a/hdwallet/libs/base58.py +++ b/hdwallet/libs/base58.py @@ -4,8 +4,6 @@ from Crypto.Hash import keccak from typing import List -import six - __base58_alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" @@ -27,7 +25,7 @@ def checksum_encode(address, crypto="eth"): def string_to_int(data): val = 0 - if type(data) == str: + if isinstance(data, str): data = bytearray(data) for (i, c) in enumerate(data[::-1]): @@ -36,9 +34,9 @@ def string_to_int(data): def ensure_string(data): - if isinstance(data, six.binary_type): + if isinstance(data, bytes): return data.decode("utf-8") - elif not isinstance(data, six.string_types): + elif not isinstance(data, str): raise ValueError("Invalid value for string") return data @@ -62,8 +60,7 @@ def check_encode(raw, alphabet=__base58_alphabet): def decode(data, alphabet=__base58_alphabet): - if bytes != str: - data = bytes(data, "ascii") + data = bytes(data, "ascii") val = 0 prefix = 0 diff --git a/hdwallet/libs/ecc.py b/hdwallet/libs/ecc.py index 7c9a2b2a..5e051f6f 100644 --- a/hdwallet/libs/ecc.py +++ b/hdwallet/libs/ecc.py @@ -232,7 +232,7 @@ class S256Point(Point): def __init__(self, x, y, a=None, b=None): a, b = S256Field(A), S256Field(B) - if type(x) == int: + if type(x) == int: # noqa: E721 super().__init__(x=S256Field(x), y=S256Field(y), a=a, b=b) else: super().__init__(x=x, y=y, a=a, b=b) @@ -423,4 +423,4 @@ def wif(self, compressed=True, testnet=False): else: suffix = b'' # encode_base58_checksum the whole thing - return encode_base58_checksum(prefix + secret_bytes + suffix) \ No newline at end of file + return encode_base58_checksum(prefix + secret_bytes + suffix) diff --git a/hdwallet/mnemonics/__init__.py b/hdwallet/mnemonics/__init__.py index 9a9d2499..729440e7 100644 --- a/hdwallet/mnemonics/__init__.py +++ b/hdwallet/mnemonics/__init__.py @@ -9,17 +9,20 @@ ) from ..exceptions import MnemonicError -from .algorand import ( +from .algorand import ( # noqa: F401 AlgorandMnemonic, ALGORAND_MNEMONIC_WORDS, ALGORAND_MNEMONIC_LANGUAGES ) -from .bip39 import ( +from .bip39 import ( # noqa: F401 BIP39Mnemonic, BIP39_MNEMONIC_WORDS, BIP39_MNEMONIC_LANGUAGES ) -from .electrum import ( +from .slip39 import ( # noqa: F401 + SLIP39Mnemonic, SLIP39_MNEMONIC_WORDS, SLIP39_MNEMONIC_LANGUAGES +) +from .electrum import ( # noqa: F401 ElectrumV1Mnemonic, ELECTRUM_V1_MNEMONIC_WORDS, ELECTRUM_V1_MNEMONIC_LANGUAGES, ElectrumV2Mnemonic, ELECTRUM_V2_MNEMONIC_WORDS, ELECTRUM_V2_MNEMONIC_LANGUAGES, ELECTRUM_V2_MNEMONIC_TYPES ) -from .monero import ( +from .monero import ( # noqa: F401 MoneroMnemonic, MONERO_MNEMONIC_WORDS, MONERO_MNEMONIC_LANGUAGES ) from .imnemonic import IMnemonic @@ -41,6 +44,8 @@ class MNEMONICS: +--------------+------------------------------------------------------------------------+ | BIP39 | :class:`hdwallet.mnemonics.bip39.mnemonic.BIP39Mnemonic` | +--------------+------------------------------------------------------------------------+ + | SLIP39 | :class:`hdwallet.mnemonics.slip39.mnemonic.SLIP39Mnemonic` | + +--------------+------------------------------------------------------------------------+ | Electrum-V1 | :class:`hdwallet.mnemonics.electrum.v1.mnemonic.ElectrumV1Mnemonic` | +--------------+------------------------------------------------------------------------+ | Electrum-V2 | :class:`hdwallet.mnemonics.electrum.v2.mnemonic.ElectrumV2Mnemonic` | @@ -52,9 +57,10 @@ class MNEMONICS: dictionary: Dict[str, Type[IMnemonic]] = { AlgorandMnemonic.name(): AlgorandMnemonic, BIP39Mnemonic.name(): BIP39Mnemonic, + SLIP39Mnemonic.name(): SLIP39Mnemonic, ElectrumV1Mnemonic.name(): ElectrumV1Mnemonic, ElectrumV2Mnemonic.name(): ElectrumV2Mnemonic, - MoneroMnemonic.name(): MoneroMnemonic + MoneroMnemonic.name(): MoneroMnemonic, } @classmethod @@ -117,6 +123,7 @@ def is_mnemonic(cls, name) -> bool: "IMnemonic", "ALGORAND_MNEMONIC_WORDS", "ALGORAND_MNEMONIC_LANGUAGES", "BIP39_MNEMONIC_WORDS", "BIP39_MNEMONIC_LANGUAGES", + "SLIP39_MNEMONIC_WORDS", "SLIP39_MNEMONIC_LANGUAGES", "ELECTRUM_V1_MNEMONIC_WORDS", "ELECTRUM_V1_MNEMONIC_LANGUAGES", "ELECTRUM_V2_MNEMONIC_WORDS", "ELECTRUM_V2_MNEMONIC_LANGUAGES", "ELECTRUM_V2_MNEMONIC_TYPES", "MONERO_MNEMONIC_WORDS", "MONERO_MNEMONIC_LANGUAGES", diff --git a/hdwallet/mnemonics/algorand/mnemonic.py b/hdwallet/mnemonics/algorand/mnemonic.py index 053dfa01..10004323 100644 --- a/hdwallet/mnemonics/algorand/mnemonic.py +++ b/hdwallet/mnemonics/algorand/mnemonic.py @@ -5,17 +5,15 @@ # file COPYING or https://opensource.org/license/mit from typing import ( - Union, Dict, List, Optional + Union, Dict, List, Mapping, Optional ) -import unicodedata - from ...entropies import ( IEntropy, AlgorandEntropy, ALGORAND_ENTROPY_STRENGTHS ) from ...crypto import sha512_256 from ...exceptions import ( - Error, EntropyError, MnemonicError, ChecksumError + EntropyError, MnemonicError, ChecksumError ) from ...utils import ( get_bytes, bytes_to_string, convert_bits @@ -62,6 +60,7 @@ class AlgorandMnemonic(IMnemonic): """ checksum_length: int = 2 + words_list_number: int = 2048 words_list: List[int] = [ ALGORAND_MNEMONIC_WORDS.TWENTY_FIVE ] @@ -158,12 +157,19 @@ def encode(cls, entropy: Union[str, bytes], language: str) -> str: word_indexes: Optional[List[int]] = convert_bits(entropy, 8, 11) assert word_indexes is not None - words_list: list = cls.normalize(cls.get_words_list_by_language(language=language)) + words_list: list = cls.get_words_list_by_language(language=language) indexes: list = word_indexes + [checksum_word_indexes[0]] - return " ".join(cls.normalize([words_list[index] for index in indexes])) + return " ".join( words_list[index] for index in indexes ) @classmethod - def decode(cls, mnemonic: str, **kwargs) -> str: + def decode( + cls, + mnemonic: str, + language: Optional[str] = None, + words_list: Optional[List[str]] = None, + words_list_with_index: Optional[Mapping[str, int]] = None, + **kwargs + ) -> str: """ Decodes a mnemonic phrase into entropy data. @@ -179,36 +185,41 @@ def decode(cls, mnemonic: str, **kwargs) -> str: if len(words) not in cls.words_list: raise MnemonicError("Invalid mnemonic words count", expected=cls.words_list, got=len(words)) - words_list, language = cls.find_language(mnemonic=words) - words_list_with_index: dict = { - words_list[i]: i for i in range(len(words_list)) - } - word_indexes = [words_list_with_index[word] for word in words] - entropy_list: Optional[List[int]] = convert_bits(word_indexes[:-1], 11, 8) - assert entropy_list is not None - entropy: bytes = bytes(entropy_list)[:-1] - - checksum: bytes = sha512_256(entropy)[:cls.checksum_length] - checksum_word_indexes: Optional[List[int]] = convert_bits(checksum, 8, 11) - assert checksum_word_indexes is not None - if checksum_word_indexes[0] != word_indexes[-1]: - raise ChecksumError( - "Invalid checksum", expected=words_list[checksum_word_indexes[0]], got=words_list[word_indexes[-1]] - ) - - return bytes_to_string(entropy) - - @classmethod - def normalize(cls, mnemonic: Union[str, List[str]]) -> List[str]: - """ - Normalizes the given mnemonic by splitting it into a list of words if it is a string. - - :param mnemonic: The mnemonic value, which can be a single string of words or a list of words. - :type mnemonic: Union[str, List[str]] - - :return: A list of words from the mnemonic. - :rtype: List[str] - """ + candidates: Mapping[str, Mapping[str, int]] = cls.word_indices_candidates( + words=words, language=language, words_list=words_list, + words_list_with_index=words_list_with_index + ) - mnemonic: list = mnemonic.split() if isinstance(mnemonic, str) else mnemonic - return list(map(lambda _: unicodedata.normalize("NFKD", _.lower()), mnemonic)) + exception = None + entropies: Mapping[Optional[str], str] = {} + for language, word_indices in candidates.items(): + try: + word_indexes = [word_indices[word] for word in words] + entropy_list: Optional[List[int]] = convert_bits(word_indexes[:-1], 11, 8) + assert entropy_list is not None + entropy: bytes = bytes(entropy_list)[:-1] + + checksum: bytes = sha512_256(entropy)[:cls.checksum_length] + checksum_word_indexes: Optional[List[int]] = convert_bits(checksum, 8, 11) + assert checksum_word_indexes is not None + if checksum_word_indexes[0] != word_indexes[-1]: + raise ChecksumError( + "Invalid checksum", expected=words_list_with_index.keys()[checksum_word_indexes[0]], got=words_list_with_index.keys()[word_indexes[-1]] + ) + + entropies[language] = bytes_to_string(entropy) + + except Exception as exc: + # Collect first Exception; highest quality languages are first. + if exception is None: + exception = exc + + if entropies: + (candidate, entropy), *extras = entropies.items() + if extras: + exception = MnemonicError( + f"Ambiguous languages {', '.join(c for c, _ in extras)} or {candidate} for mnemonic; specify a preferred language" + ) + else: + return entropy + raise exception diff --git a/hdwallet/mnemonics/bip39/mnemonic.py b/hdwallet/mnemonics/bip39/mnemonic.py index 1dc39f9f..0808d4d7 100644 --- a/hdwallet/mnemonics/bip39/mnemonic.py +++ b/hdwallet/mnemonics/bip39/mnemonic.py @@ -5,11 +5,9 @@ # file COPYING or https://opensource.org/license/mit from typing import ( - Union, Dict, List, Optional + Union, Dict, List, Mapping, Optional ) -import unicodedata - from ...entropies import ( IEntropy, BIP39Entropy, BIP39_ENTROPY_STRENGTHS ) @@ -79,30 +77,30 @@ class BIP39Mnemonic(IMnemonic): +-----------------------+----------------------+ | Name | Value | +=======================+======================+ - | CHINESE_SIMPLIFIED | chinese-simplified | - +-----------------------+----------------------+ - | CHINESE_TRADITIONAL | chinese-traditional | - +-----------------------+----------------------+ - | CZECH | czech | - +-----------------------+----------------------+ | ENGLISH | english | +-----------------------+----------------------+ | FRENCH | french | +-----------------------+----------------------+ - | ITALIAN | italian | + | SPANISH | spanish | +-----------------------+----------------------+ - | JAPANESE | japanese | + | ITALIAN | italian | +-----------------------+----------------------+ - | KOREAN | korean | + | RUSSIAN | russian | +-----------------------+----------------------+ | PORTUGUESE | portuguese | +-----------------------+----------------------+ - | RUSSIAN | russian | - +-----------------------+----------------------+ - | SPANISH | spanish | + | CZECH | czech | +-----------------------+----------------------+ | TURKISH | turkish | +-----------------------+----------------------+ + | KOREAN | korean | + +-----------------------+----------------------+ + | CHINESE_SIMPLIFIED | chinese-simplified | + +-----------------------+----------------------+ + | CHINESE_TRADITIONAL | chinese-traditional | + +-----------------------+----------------------+ + | JAPANESE | japanese | + +-----------------------+----------------------+ """ word_bit_length: int = 11 @@ -122,32 +120,32 @@ class BIP39Mnemonic(IMnemonic): BIP39_MNEMONIC_WORDS.TWENTY_FOUR: BIP39_ENTROPY_STRENGTHS.TWO_HUNDRED_FIFTY_SIX } languages: List[str] = [ - BIP39_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED, - BIP39_MNEMONIC_LANGUAGES.CHINESE_TRADITIONAL, - BIP39_MNEMONIC_LANGUAGES.CZECH, BIP39_MNEMONIC_LANGUAGES.ENGLISH, BIP39_MNEMONIC_LANGUAGES.FRENCH, + BIP39_MNEMONIC_LANGUAGES.SPANISH, BIP39_MNEMONIC_LANGUAGES.ITALIAN, - BIP39_MNEMONIC_LANGUAGES.JAPANESE, - BIP39_MNEMONIC_LANGUAGES.KOREAN, - BIP39_MNEMONIC_LANGUAGES.PORTUGUESE, BIP39_MNEMONIC_LANGUAGES.RUSSIAN, - BIP39_MNEMONIC_LANGUAGES.SPANISH, - BIP39_MNEMONIC_LANGUAGES.TURKISH + BIP39_MNEMONIC_LANGUAGES.PORTUGUESE, + BIP39_MNEMONIC_LANGUAGES.CZECH, + BIP39_MNEMONIC_LANGUAGES.TURKISH, + BIP39_MNEMONIC_LANGUAGES.KOREAN, + BIP39_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED, + BIP39_MNEMONIC_LANGUAGES.CHINESE_TRADITIONAL, + BIP39_MNEMONIC_LANGUAGES.JAPANESE, ] wordlist_path: Dict[str, str] = { - BIP39_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED: "bip39/wordlist/chinese_simplified.txt", - BIP39_MNEMONIC_LANGUAGES.CHINESE_TRADITIONAL: "bip39/wordlist/chinese_traditional.txt", - BIP39_MNEMONIC_LANGUAGES.CZECH: "bip39/wordlist/czech.txt", BIP39_MNEMONIC_LANGUAGES.ENGLISH: "bip39/wordlist/english.txt", BIP39_MNEMONIC_LANGUAGES.FRENCH: "bip39/wordlist/french.txt", + BIP39_MNEMONIC_LANGUAGES.SPANISH: "bip39/wordlist/spanish.txt", BIP39_MNEMONIC_LANGUAGES.ITALIAN: "bip39/wordlist/italian.txt", - BIP39_MNEMONIC_LANGUAGES.JAPANESE: "bip39/wordlist/japanese.txt", - BIP39_MNEMONIC_LANGUAGES.KOREAN: "bip39/wordlist/korean.txt", - BIP39_MNEMONIC_LANGUAGES.PORTUGUESE: "bip39/wordlist/portuguese.txt", BIP39_MNEMONIC_LANGUAGES.RUSSIAN: "bip39/wordlist/russian.txt", - BIP39_MNEMONIC_LANGUAGES.SPANISH: "bip39/wordlist/spanish.txt", - BIP39_MNEMONIC_LANGUAGES.TURKISH: "bip39/wordlist/turkish.txt" + BIP39_MNEMONIC_LANGUAGES.PORTUGUESE: "bip39/wordlist/portuguese.txt", + BIP39_MNEMONIC_LANGUAGES.CZECH: "bip39/wordlist/czech.txt", + BIP39_MNEMONIC_LANGUAGES.TURKISH: "bip39/wordlist/turkish.txt", + BIP39_MNEMONIC_LANGUAGES.KOREAN: "bip39/wordlist/korean.txt", + BIP39_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED: "bip39/wordlist/chinese_simplified.txt", + BIP39_MNEMONIC_LANGUAGES.CHINESE_TRADITIONAL: "bip39/wordlist/chinese_traditional.txt", + BIP39_MNEMONIC_LANGUAGES.JAPANESE: "bip39/wordlist/japanese.txt", } @classmethod @@ -210,6 +208,8 @@ def encode(cls, entropy: Union[str, bytes], language: str) -> str: This method converts a given entropy value into a mnemonic phrase according to the specified language. + It is NFC normalized for presentation, and must be NFKD normalized before conversion to a BIP-39 seed. + :param entropy: The entropy to encode into a mnemonic phrase. :type entropy: Union[str, bytes] :param language: The language for the mnemonic phrase. @@ -219,7 +219,7 @@ def encode(cls, entropy: Union[str, bytes], language: str) -> str: :rtype: str """ - entropy: bytes = get_bytes(entropy) + entropy: bytes = get_bytes(entropy, unhexlify=True) if not BIP39Entropy.is_valid_bytes_strength(len(entropy)): raise EntropyError( "Wrong entropy strength", expected=BIP39Entropy.strengths, got=(len(entropy) * 8) @@ -230,7 +230,7 @@ def encode(cls, entropy: Union[str, bytes], language: str) -> str: mnemonic_bin: str = entropy_binary_string + entropy_hash_binary_string[:len(entropy) // 4] mnemonic: List[str] = [] - words_list: List[str] = cls.normalize(cls.get_words_list_by_language(language=language)) + words_list: List[str] = cls.get_words_list_by_language(language=language) # Already NFC normalized if len(words_list) != cls.words_list_number: raise Error( "Invalid number of loaded words list", expected=cls.words_list_number, got=len(words_list) @@ -241,11 +241,17 @@ def encode(cls, entropy: Union[str, bytes], language: str) -> str: word_index: int = binary_string_to_integer(word_bin) mnemonic.append(words_list[word_index]) - return " ".join(cls.normalize(mnemonic)) + # Words from wordlist are normalized NFC for display + return " ".join(mnemonic) @classmethod def decode( - cls, mnemonic: str, checksum: bool = False, words_list: Optional[List[str]] = None, words_list_with_index: Optional[dict] = None + cls, + mnemonic: str, + language: Optional[str] = None, + checksum: bool = False, + words_list: Optional[List[str]] = None, + words_list_with_index: Optional[Mapping[str, int]] = None, ) -> str: """ Decodes a mnemonic phrase into its corresponding entropy. @@ -255,6 +261,8 @@ def decode( :param mnemonic: The mnemonic phrase to decode. :type mnemonic: str + :param language: The preferred language of the mnemonic phrase + :type language: Optional[str] :param checksum: Whether to include the checksum in the returned entropy. :type checksum: bool :param words_list: Optional list of words used to decode the mnemonic. If not provided, the method will use the default word list for the language detected. @@ -270,96 +278,63 @@ def decode( if len(words) not in cls.words_list: raise MnemonicError("Invalid mnemonic words count", expected=cls.words_list, got=len(words)) - if not words_list or not words_list_with_index: - words_list, language = cls.find_language(mnemonic=words) - if len(words_list) != cls.words_list_number: - raise Error( - "Invalid number of loaded words list", expected=cls.words_list_number, got=len(words_list) - ) - words_list_with_index: dict = { - words_list[i]: i for i in range(len(words_list)) - } - - if len(words_list) != cls.words_list_number: - raise Error( - "Invalid number of loaded words list", expected=cls.words_list_number, got=len(words_list) - ) + # May optionally provide a word<->index Mapping, or a language + words_list; if neither, the + # Mnemonic defaults are used. Providing words_list_with_index avoids needing find_language. - mnemonic_bin: str = "".join(map( - lambda word: integer_to_binary_string( - words_list_with_index[word], cls.word_bit_length - ), words - )) - - mnemonic_bit_length: int = len(mnemonic_bin) - checksum_length: int = mnemonic_bit_length // 33 - checksum_bin: str = mnemonic_bin[-checksum_length:] - entropy: bytes = binary_string_to_bytes( - mnemonic_bin[:-checksum_length], checksum_length * 8 + candidates: Mapping[str, Mapping[str, int]] = cls.word_indices_candidates( + words=words, language=language, words_list=words_list, + words_list_with_index=words_list_with_index ) - entropy_hash_bin: str = bytes_to_binary_string( - sha256(entropy), 32 * 8 - ) - checksum_bin_got: str = entropy_hash_bin[:checksum_length] - if checksum_bin != checksum_bin_got: - raise ChecksumError( - "Invalid checksum", expected=checksum_bin, got=checksum_bin_got - ) - - if checksum: - pad_bit_len: int = ( - mnemonic_bit_length - if mnemonic_bit_length % 8 == 0 else - mnemonic_bit_length + (8 - mnemonic_bit_length % 8) - ) - return bytes_to_string( - binary_string_to_bytes(mnemonic_bin, pad_bit_len // 4) - ) - return bytes_to_string(entropy) - @classmethod - def is_valid( - cls, - mnemonic: Union[str, List[str]], - words_list: Optional[List[str]] = None, - words_list_with_index: Optional[dict] = None - ) -> bool: - """ - Validates a mnemonic phrase. - - This method checks whether the provided mnemonic phrase is valid by attempting to decode it. - If the decoding is successful without raising any errors, the mnemonic is considered valid. - - :param mnemonic: The mnemonic phrase to validate. It can be a string or a list of words. - :type mnemonic: Union[str, List[str]] - :param words_list: Optional list of words to be used for validation. If not provided, the method will use the default word list. - :type words_list: Optional[List[str]] - :param words_list_with_index: Optional dictionary mapping words to their indices for validation. If not provided, the method will use the default mapping. - :type words_list_with_index: Optional[dict] - - :return: True if the mnemonic phrase is valid, False otherwise. - :rtype: bool - """ - - try: - cls.decode( - mnemonic=mnemonic, words_list=words_list, words_list_with_index=words_list_with_index - ) - return True - except (Error, KeyError): - return False - - @classmethod - def normalize(cls, mnemonic: Union[str, List[str]]) -> List[str]: - """ - Normalizes the given mnemonic by splitting it into a list of words if it is a string. - - :param mnemonic: The mnemonic value, which can be a single string of words or a list of words. - :type mnemonic: Union[str, List[str]] - - :return: A list of words from the mnemonic. - :rtype: List[str] - """ - - mnemonic: list = mnemonic.split() if isinstance(mnemonic, str) else mnemonic - return list(map(lambda _: unicodedata.normalize("NFKD", _.lower()), mnemonic)) + # Ensure exactly one candidate language produces validated entropy + exception = None + entropies: Mapping[Optional[str], str] = {} + for language, word_indices in candidates.items(): + try: + mnemonic_bin: str = "".join(map( + lambda word: integer_to_binary_string( + word_indices[word], cls.word_bit_length + ), words + )) + + mnemonic_bit_length: int = len(mnemonic_bin) + checksum_length: int = mnemonic_bit_length // 33 + checksum_bin: str = mnemonic_bin[-checksum_length:] + entropy: bytes = binary_string_to_bytes( + mnemonic_bin[:-checksum_length], checksum_length * 8 + ) + entropy_hash_bin: str = bytes_to_binary_string( + sha256(entropy), 32 * 8 + ) + checksum_bin_got: str = entropy_hash_bin[:checksum_length] + if checksum_bin != checksum_bin_got: + raise ChecksumError( + f"Invalid {language or '(custom word list)'} checksum", expected=checksum_bin, got=checksum_bin_got + ) + + if checksum: + pad_bit_len: int = ( + mnemonic_bit_length + if mnemonic_bit_length % 8 == 0 else + mnemonic_bit_length + (8 - mnemonic_bit_length % 8) + ) + entropies[language] = bytes_to_string( + binary_string_to_bytes(mnemonic_bin, pad_bit_len // 4) + ) + else: + entropies[language] = bytes_to_string(entropy) + + except Exception as exc: + # Collect first Exception; highest quality languages are first. + if exception is None: + exception = exc + + if entropies: + (candidate, entropy), *extras = entropies.items() + if extras: + exception = MnemonicError( + f"Ambiguous languages {', '.join(c for c, _ in extras)} or {candidate} for mnemonic; specify a preferred language" + ) + else: + return entropy + raise exception diff --git a/hdwallet/mnemonics/electrum/v1/mnemonic.py b/hdwallet/mnemonics/electrum/v1/mnemonic.py index 18424613..8d943b3a 100644 --- a/hdwallet/mnemonics/electrum/v1/mnemonic.py +++ b/hdwallet/mnemonics/electrum/v1/mnemonic.py @@ -5,16 +5,14 @@ # file COPYING or https://opensource.org/license/mit from typing import ( - Dict, List, Union, Optional + Dict, List, Mapping, Union, Optional ) -import unicodedata - from ....entropies import ( IEntropy, ElectrumV1Entropy, ELECTRUM_V1_ENTROPY_STRENGTHS ) from ....exceptions import ( - EntropyError, MnemonicError + Error, EntropyError, MnemonicError ) from ....utils import ( get_bytes, integer_to_bytes, bytes_to_integer, bytes_to_string @@ -179,7 +177,11 @@ def encode(cls, entropy: Union[str, bytes], language: str) -> str: @classmethod def decode( - cls, mnemonic: str, words_list: Optional[List[str]] = None, words_list_with_index: Optional[dict] = None + cls, + mnemonic: str, + language: Optional[str] = None, + words_list: Optional[List[str]] = None, + words_list_with_index: Optional[Mapping[str, int]] = None ) -> str: """ Decodes a mnemonic phrase back into entropy data. @@ -204,68 +206,43 @@ def decode( if len(words) not in cls.words_list: raise MnemonicError("Invalid mnemonic words count", expected=cls.words_list, got=len(words)) - if not words_list or not words_list_with_index: - words_list, language = cls.find_language(mnemonic=words) - words_list_with_index: dict = { - words_list[i]: i for i in range(len(words_list)) - } - - entropy: bytes = b"" - for index in range(len(words) // 3): - word_1, word_2, word_3 = words[index * 3:(index * 3) + 3] - - word_1_index: int = words_list_with_index[word_1] - word_2_index: int = words_list_with_index[word_2] % len(words_list) - word_3_index: int = words_list_with_index[word_3] % len(words_list) - - chunk: int = ( - word_1_index + - (len(words_list) * ((word_2_index - word_1_index) % len(words_list))) + - (len(words_list) * len(words_list) * ((word_3_index - word_2_index) % len(words_list))) - ) - entropy += integer_to_bytes(chunk, bytes_num=4, endianness="big") - - return bytes_to_string(entropy) - - @classmethod - def is_valid( - cls, mnemonic: Union[str, List[str]], words_list: Optional[List[str]] = None, words_list_with_index: Optional[dict] = None - ) -> bool: - """ - Checks if the given mnemonic phrase is valid. - - This method decodes the mnemonic phrase and verifies its validity using the specified word lists and index mappings. - - :param mnemonic: The mnemonic phrase to check, either as a space-separated string or a list of words. - :type mnemonic: Union[str, List[str]] - :param words_list: Optional list of valid words for the mnemonic phrase, normalized and in the correct order. - If not provided, uses `cls.get_words_list_by_language` to fetch the list based on the default language. - :type words_list: Optional[List[str]], optional - :param words_list_with_index: Optional dictionary mapping words to their indices for quick lookup. - If not provided, constructs this mapping based on `words_list`. - :type words_list_with_index: Optional[dict], optional - - :return: True if the mnemonic phrase is valid, False otherwise. - :rtype: bool - """ - - try: - cls.decode(mnemonic=mnemonic, words_list=words_list, words_list_with_index=words_list_with_index) - return True - except (ValueError, KeyError, MnemonicError): - return False - - @classmethod - def normalize(cls, mnemonic: Union[str, List[str]]) -> List[str]: - """ - Normalizes the given mnemonic by splitting it into a list of words if it is a string. - - :param mnemonic: The mnemonic value, which can be a single string of words or a list of words. - :type mnemonic: Union[str, List[str]] - - :return: A list of words from the mnemonic. - :rtype: List[str] - """ + candidates: Mapping[str, Mapping[str, int]] = cls.word_indices_candidates( + words=words, language=language, words_list=words_list, + words_list_with_index=words_list_with_index + ) - mnemonic: list = mnemonic.split() if isinstance(mnemonic, str) else mnemonic - return list(map(lambda _: unicodedata.normalize("NFKD", _.lower()), mnemonic)) + exception = None + entropies: Mapping[Optional[str], str] = {} + for language, word_indices in candidates.items(): + try: + entropy: bytes = b"" + for index in range(len(words) // 3): + word_1, word_2, word_3 = words[index * 3:(index * 3) + 3] + + word_1_index: int = word_indices[word_1] + word_2_index: int = word_indices[word_2] % cls.words_list_number + word_3_index: int = word_indices[word_3] % cls.words_list_number + + chunk: int = ( + word_1_index + + (cls.words_list_number * ((word_2_index - word_1_index) % cls.words_list_number)) + + (cls.words_list_number * cls.words_list_number * ((word_3_index - word_2_index) % cls.words_list_number)) + ) + entropy += integer_to_bytes(chunk, bytes_num=4, endianness="big") + + entropies[language] = bytes_to_string(entropy) + + except Exception as exc: + # Collect first Exception; highest quality languages are first. + if exception is None: + exception = exc + + if entropies: + (candidate, entropy), *extras = entropies.items() + if extras: + exception = MnemonicError( + f"Ambiguous languages {', '.join(c for c, _ in extras)} or {candidate} for mnemonic; specify a preferred language" + ) + else: + return entropy + raise exception diff --git a/hdwallet/mnemonics/electrum/v2/mnemonic.py b/hdwallet/mnemonics/electrum/v2/mnemonic.py index 8949dc07..e3f14872 100644 --- a/hdwallet/mnemonics/electrum/v2/mnemonic.py +++ b/hdwallet/mnemonics/electrum/v2/mnemonic.py @@ -5,7 +5,7 @@ # file COPYING or https://opensource.org/license/mit from typing import ( - Dict, List, Union, Optional + Dict, List, Mapping, Union, Optional ) import unicodedata @@ -77,13 +77,13 @@ class ElectrumV2Mnemonic(IMnemonic): +-----------------------+----------------------+ | Name | Value | +=======================+======================+ - | CHINESE_SIMPLIFIED | chinese-simplified | - +-----------------------+----------------------+ | ENGLISH | english | +-----------------------+----------------------+ + | SPANISH | spanish | + +-----------------------+----------------------+ | PORTUGUESE | portuguese | +-----------------------+----------------------+ - | SPANISH | spanish | + | CHINESE_SIMPLIFIED | chinese-simplified | +-----------------------+----------------------+ Here are available ``ELECTRUM_V2_MNEMONIC_TYPES``: @@ -102,6 +102,7 @@ class ElectrumV2Mnemonic(IMnemonic): """ word_bit_length: int = 11 + words_list_number: int = 2048 words_list: List[int] = [ ELECTRUM_V2_MNEMONIC_WORDS.TWELVE, ELECTRUM_V2_MNEMONIC_WORDS.TWENTY_FOUR @@ -111,16 +112,16 @@ class ElectrumV2Mnemonic(IMnemonic): ELECTRUM_V2_MNEMONIC_WORDS.TWENTY_FOUR: ELECTRUM_V2_ENTROPY_STRENGTHS.TWO_HUNDRED_SIXTY_FOUR } languages: List[str] = [ - ELECTRUM_V2_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED, ELECTRUM_V2_MNEMONIC_LANGUAGES.ENGLISH, + ELECTRUM_V2_MNEMONIC_LANGUAGES.SPANISH, ELECTRUM_V2_MNEMONIC_LANGUAGES.PORTUGUESE, - ELECTRUM_V2_MNEMONIC_LANGUAGES.SPANISH + ELECTRUM_V2_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED, ] wordlist_path: Dict[str, str] = { - ELECTRUM_V2_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED: "electrum/v2/wordlist/chinese_simplified.txt", ELECTRUM_V2_MNEMONIC_LANGUAGES.ENGLISH: "electrum/v2/wordlist/english.txt", + ELECTRUM_V2_MNEMONIC_LANGUAGES.SPANISH: "electrum/v2/wordlist/spanish.txt", ELECTRUM_V2_MNEMONIC_LANGUAGES.PORTUGUESE: "electrum/v2/wordlist/portuguese.txt", - ELECTRUM_V2_MNEMONIC_LANGUAGES.SPANISH: "electrum/v2/wordlist/spanish.txt" + ELECTRUM_V2_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED: "electrum/v2/wordlist/chinese_simplified.txt", } mnemonic_types: Dict[str, str] = { ELECTRUM_V2_MNEMONIC_TYPES.STANDARD: "01", @@ -220,25 +221,16 @@ def from_entropy( if ElectrumV2Entropy.are_entropy_bits_enough(entropy): - words_list: List[str] = cls.normalize(cls.get_words_list_by_language( + words_list: List[str] = cls.get_words_list_by_language( language=language, wordlist_path=cls.wordlist_path - )) - bip39_words_list: List[str] = cls.normalize(cls.get_words_list_by_language( - language=language, wordlist_path=BIP39Mnemonic.wordlist_path - )) - bip39_words_list_with_index: dict = { - bip39_words_list[i]: i for i in range(len(bip39_words_list)) - } + ) + bip39_words_indices: Optional[List[str]] = None + (_, _, bip39_words_indices), = BIP39Mnemonic.wordlist_indices(language=language) + electrum_v1_words_indices: Optional[List[str]] = None try: - electrum_v1_words_list: List[str] = cls.normalize(cls.get_words_list_by_language( - language=language, wordlist_path=ElectrumV1Mnemonic.wordlist_path - )) - electrum_v1_words_list_with_index: dict = { - electrum_v1_words_list[i]: i for i in range(len(electrum_v1_words_list)) - } - except KeyError: - electrum_v1_words_list: List[str] = [ ] - electrum_v1_words_list_with_index: dict = { } + (_, _, electrum_v1_words_indices), = ElectrumV1Mnemonic.wordlist_indices(language=language) + except ValueError: + pass entropy: int = bytes_to_integer(entropy) for index in range(max_attempts): @@ -249,10 +241,8 @@ def from_entropy( language=language, mnemonic_type=mnemonic_type, words_list=words_list, - bip39_words_list=bip39_words_list, - bip39_words_list_with_index=bip39_words_list_with_index, - electrum_v1_words_list=electrum_v1_words_list, - electrum_v1_words_list_with_index=electrum_v1_words_list_with_index + bip39_words_list_with_index=bip39_words_indices, + electrum_v1_words_list_with_index=electrum_v1_words_indices, ) except EntropyError: continue @@ -267,9 +257,9 @@ def encode( mnemonic_type: str = ELECTRUM_V2_MNEMONIC_TYPES.STANDARD, words_list: Optional[List[str]] = None, bip39_words_list: Optional[List[str]] = None, - bip39_words_list_with_index: Optional[dict] = None, + bip39_words_list_with_index: Optional[Mapping[str, int]] = None, electrum_v1_words_list: Optional[List[str]] = None, - electrum_v1_words_list_with_index: Optional[dict] = None + electrum_v1_words_list_with_index: Optional[Mapping[str, int]] = None ) -> str: """ Generates a mnemonic phrase from entropy data. @@ -304,14 +294,21 @@ def encode( mnemonic: List[str] = [] if not words_list: - words_list = cls.normalize(cls.get_words_list_by_language(language=language)) - while entropy > 0: - word_index: int = entropy % len(words_list) - entropy //= len(words_list) + words_list = cls.get_words_list_by_language(language=language) + if len(words_list) != cls.words_list_number: + raise Error( + "Invalid number of loaded words list", expected=cls.words_list_number, got=len(words_list) + ) + + # Produces mnemonics of valid length, even if entropy has trailing zero bits + while entropy > 0 or len(mnemonic) not in set(cls.words_list): + word_index: int = entropy % cls.words_list_number + entropy //= cls.words_list_number mnemonic.append(words_list[word_index]) if not cls.is_valid( mnemonic=mnemonic, + language=language, mnemonic_type=mnemonic_type, bip39_words_list=bip39_words_list, bip39_words_list_with_index=bip39_words_list_with_index, @@ -320,10 +317,17 @@ def encode( ): raise EntropyError("Entropy bytes are not suitable for generating a valid mnemonic") - return " ".join(cls.normalize(mnemonic)) + return " ".join(mnemonic) # mnemonic words already NFKC normalized @classmethod - def decode(cls, mnemonic: str, mnemonic_type: str = ELECTRUM_V2_MNEMONIC_TYPES.STANDARD) -> str: + def decode( + cls, + mnemonic: str, + language: Optional[str] = None, + mnemonic_type: str = ELECTRUM_V2_MNEMONIC_TYPES.STANDARD, + words_list: Optional[List[str]] = None, + words_list_with_index: Optional[Mapping[str, int]] = None + ) -> str: """ Decodes a mnemonic phrase into its original entropy value. @@ -343,29 +347,46 @@ def decode(cls, mnemonic: str, mnemonic_type: str = ELECTRUM_V2_MNEMONIC_TYPES.S if len(words) not in cls.words_list: raise MnemonicError("Invalid mnemonic words count", expected=cls.words_list, got=len(words)) - if not cls.is_valid(mnemonic, mnemonic_type=mnemonic_type): - raise MnemonicError(f"Invalid {mnemonic_type} mnemonic type words") - - words_list, language = cls.find_language(mnemonic=words) - words_list_with_index: dict = { - words_list[i]: i for i in range(len(words_list)) - } - - entropy: int = 0 - for word in reversed(words): - entropy: int = (entropy * len(words_list)) + words_list_with_index[word] + candidates: Mapping[str, Mapping[str, int]] = cls.word_indices_candidates( + words=words, language=language, words_list=words_list, + words_list_with_index=words_list_with_index + ) - return bytes_to_string(integer_to_bytes(entropy)) + exception = None + entropies: Mapping[Optional[str], str] = {} + for language, word_indices in candidates.items(): + try: + entropy: int = 0 + for word in reversed(words): + entropy: int = (entropy * len(word_indices)) + word_indices[word] + + entropies[language] = bytes_to_string(integer_to_bytes(entropy)) + + except Exception as exc: + # Collect first Exception; highest quality languages are first. + if exception is None: + exception = exc + + if entropies: + (candidate, entropy), *extras = entropies.items() + if extras: + exception = MnemonicError( + f"Ambiguous languages {', '.join(c for c, _ in extras)} or {candidate} for mnemonic; specify a preferred language" + ) + else: + return entropy + raise exception @classmethod def is_valid( cls, mnemonic: Union[str, List[str]], + language: Optional[str] = None, mnemonic_type: str = ELECTRUM_V2_MNEMONIC_TYPES.STANDARD, bip39_words_list: Optional[List[str]] = None, - bip39_words_list_with_index: Optional[dict] = None, + bip39_words_list_with_index: Optional[Mapping[str, int]] = None, electrum_v1_words_list: Optional[List[str]] = None, - electrum_v1_words_list_with_index: Optional[dict] = None + electrum_v1_words_list_with_index: Optional[Mapping[str, int]] = None ) -> bool: """ Checks if the given mnemonic is valid according to the specified mnemonic type. @@ -391,9 +412,9 @@ def is_valid( """ if BIP39Mnemonic.is_valid( - mnemonic, words_list=bip39_words_list, words_list_with_index=bip39_words_list_with_index + mnemonic, language=language, words_list=bip39_words_list, words_list_with_index=bip39_words_list_with_index ) or ElectrumV1Mnemonic.is_valid( - mnemonic, words_list=electrum_v1_words_list, words_list_with_index=electrum_v1_words_list_with_index + mnemonic, language=language, words_list=electrum_v1_words_list, words_list_with_index=electrum_v1_words_list_with_index ): return False return cls.is_type( @@ -404,8 +425,10 @@ def is_valid( def is_type( cls, mnemonic: Union[str, List[str]], mnemonic_type: str = ELECTRUM_V2_MNEMONIC_TYPES.STANDARD ) -> bool: - """ - Checks if the given mnemonic matches the specified mnemonic type. + """Checks if the given mnemonic matches the specified mnemonic type. + + All seed derivation related functions require NFKD Unicode normalization; + .normalize returns an NFC-normalized list of mnemonic words. :param mnemonic: The mnemonic phrase to check. :type mnemonic: str or List[str] @@ -414,9 +437,10 @@ def is_type( :return: True if the mnemonic matches the specified type, False otherwise. :rtype: bool + """ return bytes_to_string(hmac_sha512( - b"Seed version", " ".join(cls.normalize(mnemonic)) + b"Seed version", unicodedata.normalize("NFKD", " ".join(cls.normalize(mnemonic))) )).startswith( cls.mnemonic_types[mnemonic_type] ) @@ -430,18 +454,3 @@ def mnemonic_type(self) -> str: """ return self._mnemonic_type - - @classmethod - def normalize(cls, mnemonic: Union[str, List[str]]) -> List[str]: - """ - Normalizes the given mnemonic by splitting it into a list of words if it is a string. - - :param mnemonic: The mnemonic value, which can be a single string of words or a list of words. - :type mnemonic: Union[str, List[str]] - - :return: A list of words from the mnemonic. - :rtype: List[str] - """ - - mnemonic: list = mnemonic.split() if isinstance(mnemonic, str) else mnemonic - return list(map(lambda _: unicodedata.normalize("NFKD", _.lower()), mnemonic)) diff --git a/hdwallet/mnemonics/imnemonic.py b/hdwallet/mnemonics/imnemonic.py index 5522448d..62eeeed9 100644 --- a/hdwallet/mnemonics/imnemonic.py +++ b/hdwallet/mnemonics/imnemonic.py @@ -7,30 +7,388 @@ from abc import ( ABC, abstractmethod ) +from collections import ( + abc +) from typing import ( - Union, Dict, List, Tuple, Optional + Any, Callable, Collection, Dict, Generator, List, Mapping, MutableMapping, Optional, Sequence, Set, Tuple, Union ) import os +import string +import unicodedata +from functools import lru_cache +from fractions import Fraction + +from collections import defaultdict -from ..exceptions import MnemonicError +from ..exceptions import MnemonicError, ChecksumError from ..entropies import IEntropy +class TrieError(Exception): + pass + + +class Ambiguous(TrieError): + def __init__(self, message, word: str, options: Set[str]): + super().__init__( message ) + self.word = word + self.options = options + + +class TrieNode: + """ + Associates a value with a node in a trie. + + The EMPTY marker indicates that a word ending in this TrieNode was not inserted into the True; + replace with something that will never be provided as a word's 'value', preferably something + "Falsey". An insert defaults to PRESENT, preferably something "Truthy". + """ + EMPTY = None + PRESENT = True + + def __init__(self): + self.children: MutableMapping[str, TrieNode] = defaultdict(self.__class__) + self.value: Any = self.__class__.EMPTY + + +class Trie: + + def __init__(self, root=None): + self.root = root if root is not None else TrieNode() + + def insert(self, word: str, value: Optional[Any] = None) -> None: + """ + Inserts a 'word' into the Trie, associated with 'value'. + """ + current = self.root + for letter in word: + current = current.children[letter] + assert current.value is current.EMPTY, \ + f"Attempt to re-insert {word!r}; already present with value {current.value!r}" + current.value = current.PRESENT if value is None else value + + def find(self, word: str, current: Optional[TrieNode] = None) -> Generator[Tuple[bool, str, Optional[TrieNode]], None, None]: + """Finds all the TrieNode that match the word, optionally from the provided 'current' node. + + If the word isn't in the current Trie, terminates by producing None for the TrieNode. + + """ + if current is None: + current = self.root + yield current.value is not current.EMPTY, '', current + + for letter in word: + current = current.children.get(letter) + if current is None: + yield False, '', None + break + yield current.value is not current.EMPTY, letter, current + + def complete(self, current: TrieNode) -> Generator[Tuple[bool, str, TrieNode], None, None]: + """Generate (, key, node) tuples along an unambiguous path starting from after + the current TrieNode, until the next terminal TrieNode is encountered. + + Continues until a non-EMPTY value is found, or the path becomes ambiguous. Tests for a + terminal value *after* transitioning, so we can use .complete to move from unique terminal + node to unique terminal node, eg. 'ad' --> 'add' --> 'addict' + + Will only yield candidates that are on an unambiguous path; the final candidate's terminal + flag must be evaluated to determine if it indicates a completed word was found. + + """ + terminal = False + while current is not None and not terminal and len( current.children ) == 1: + # Follow unique path until we hit ambiguity or a terminal (non-empty) node + (key, current), = current.children.items() + terminal = current.value is not current.EMPTY + yield terminal, key, current + + def search(self, word: str, current: Optional[TrieNode] = None, complete: bool = False) -> Tuple[bool, str, Optional[TrieNode]]: + """Returns the matched stem, and associated TrieNode if the word is in the trie (otherwise None) + + If 'complete' and 'word' is an unambiguous abbreviation of some word with a non-EMPTY value, + return the node. + + The word could be complete and have a non-EMPTY TrieNode.value, but also could be a prefix + of other words, so the caller may need to consult the return TrieNode.children. + + """ + stem = '' + for terminal, c, current in self.find( word, current=current ): + stem += c + if complete and current is not None and current.value is current.EMPTY: + for terminal, c, current in self.complete( current=current ): + stem += c + return terminal, stem, current + + def __contains__(self, word: str) -> bool: + """True iff 'word' has been associated with (or is a unique prefix of) a value in the trie.""" + _, _, result = self.search(word, complete=True) + return result is not None + + def startswith(self, prefix: str) -> bool: + """ + Returns if there is any word(s) in the trie that start with the given prefix. + """ + _, _, result = self.search(prefix) + return result is not None + + def scan( + self, + prefix: str = '', + current: Optional[TrieNode] = None, + depth: int = 0, + predicate: Optional[Callable[[TrieNode], bool]] = None, # default: terminal + ) -> Generator[Tuple[str, TrieNode], None, None]: + """Yields all strings and their TrieNode that match 'prefix' and satisfy 'predicate' (or are + terminal), in depth-first order. + + Optionally start from the provided 'current' node. + + Any strings that are only prefixes for other string(s) will have node.value == node.EMPTY + (be non-terminal). + + """ + *_, (terminal, _, current) = self.find(prefix, current=current) + if current is None: + return + + satisfied = terminal if predicate is None else predicate( current ) + if satisfied: + yield prefix, current + + if not depth or depth > 1: + for char, child in current.children.items(): + for suffix, found in self.scan( current=child, depth=max(0, depth-1), predicate=predicate ): + yield prefix + char + suffix, found + + def options( + self, + prefix: str = '', + current: Optional[TrieNode] = None, + ) -> Generator[Tuple[bool, Set[str]], str, None]: + """With each symbol provided, yields the next available symbol options. + + Doesn't advance unless a truthy symbol is provided via send(symbol). + + Completes when the provided symbol doesn't match one of the available options. + """ + last: str = '' + *_, (terminal, _, current) = self.find(prefix, current=current) + while current is not None: + terminal = current.value is not current.EMPTY + symbol: str = yield (terminal, set(current.children)) + if symbol: + current = current.children.get(symbol) + + def dump_lines( + self, + current: Optional[TrieNode] = None, + indent: int = 6, + level: int = 0 + ) -> List[str]: + """Output the Trie and its mapped values in a human-comprehensible form.""" + if current is None: + current = self.root + + # There can be multiple routes to the same child (ie. glyphs with/without marks) + kids = defaultdict(set) + for char, child in current.children.items(): + kids[child].add(char) + + result = [] + if kids and current.value != current.EMPTY: + # The present node is both a terminal word (eg. "add"), AND has children (eg. "addict", ...) + result = [ "" ] + for i, (child, chars) in enumerate(kids.items()): + first, *rest = self.dump_lines( child, indent=indent, level=level+1 ) + result.append( f"{' ' * (bool(result) or bool(i)) * level * indent}{'/'.join( chars ):{indent}}{first}" ) + result.extend( rest ) + + if not result: + # No kids AND current value == current.EMPTY! This is a degenerate Trie, but support it. + result = [""] + if current.value != current.EMPTY: + result[0] += f"{' ' * max(0, 10 - level) * indent} == {current.value}" + return result + + def dump( + self, + current: Optional[TrieNode] = None, + indent: int = 6, + level: int = 0 + ) -> str: + return '\n'.join(self.dump_lines( current=current, indent=indent, level=level )) + + def __str__(self): + return self.dump() + + +def unmark( word_composed: str ) -> str: + """This word may contain composite characters with accents like "é" that decompose "e" + "'". + + Most mnemonic encodings require that mnemonic words without accents match the accented word. + Remove the Mark symbols. + + """ + return ''.join( + c + for c in unicodedata.normalize( "NFD", word_composed ) + if not unicodedata.category( c ).startswith('M') + ) + + +class WordIndices( abc.Mapping ): + """A Mapping which holds a Sequence of Mnemonic words. + + The underlying Trie is built during construction, but a WordIndices Mapping is not mutable. + + Acts like a basic { "word": index, ... } dict but with additional word flexibility. + + Also behaves like a ["word", "word", ...] list for iteration and indexing. + + Indexable either by int (returning the original canonical word), or by the original word (with + or without Unicode "Marks") or a unique abbreviations, returning the int index. + + The base mapping is str -> int, and keys()/iter() returns the canonical Mnemonic words. + + The index value for a certain mnemonic word (with our without "Marks") or an abbreviation + thereof can be obtained: + + [str(word)] + + The canonical mnemonic word in "NFC" form at a certain index can be obtained via: + + [int(index)] + .keys()[int(index)] + + """ + def __init__(self, sequence: Sequence[str]): + """Insert a sequence of Unicode words with a value equal to the enumeration, making the + "unmarked" version an alias of the regular Unicode version. + + """ + self._trie = Trie() + self._words: List[str] = [] + for i, word in enumerate( sequence ): + self._words.append( word ) + word_unmarked = unmark( word ) + + if word == word_unmarked or len( word ) != len( word_unmarked ): + # If the word has no marks, or if the unmarked word doesn't have the same number of + # glyphs, we can't "alias" it; insert the original word with NFC "combined" glyphs. + self._trie.insert( word, i ) + continue + + # Traverse the TrieNodes representing 'word_unmarked'. Each glyph in word and + # word_unmarked is joined by the TrieNode which contains it in .children, and we should + # never get a None (lose the plot) because we've just inserted 'word'! This will + # "alias" each glyph with a mark, to the .children entry for the non-marked glyph. + self._trie.insert( word_unmarked, i ) + for c, c_un, (_, _, n) in zip( word, word_unmarked, self._trie.find( word_unmarked )): + assert n is not None + if c != c_un: + if c in n.children and c_un in n.children: + assert n.children[c_un] is n.children[c], \ + f"Attempting to alias {c!r} to {c_un!r} but already exists as a non-alias" + n.children[c] = n.children[c_un] + + def __getitem__(self, key: Union[str, int]) -> Union[int, str]: + """A Mapping from "word" to index, or the reverse. + + Any unique abbreviation with/without UTF-8 "Marks" is accepted. We keep this return value + simple, to make WordIndices work similarly to a Dict[str, int] of mnemonic word/index pairs. + + """ + word, index, _ = self.get_details(key) + return index if isinstance( key, str ) else word + + def get_details(self, key: Union[int, str]) -> Tuple[str, int, Set[str]]: + """Provide a word (or unique prefix) or an index, and returns a value Tuple consisting of: + - The canonical word 'str', and + - The index value, and + - the set of options available from the end of word, if any + + If no such 'int' index exists, raises IndexError. If no word(s) are possible starting from + the given 'str', raises KeyError. + + """ + if isinstance( key, int ): + # The key'th word (or IndexError) + return self._words[key], key, set() + + terminal, prefix, node = self._trie.search( key, complete=True ) + if not terminal: + # We're nowhere in the Trie with this word + raise KeyError(f"{key!r} does not match any word") + assert node is not None + return self._words[node.value], node.value, set(node.children) + + def __len__(self): + return len( self._words ) + + def __iter__(self): + return iter( self._words ) + + def keys(self): + return self._words + + def values(self): + return map( self.__getitem__, self._words ) + + def items(self): + return zip( self._words, self.values() ) + + def unique(self): + """All full unique words in the Trie, with/without UTF-8 Marks.""" + for word, _node in self._trie.scan(): + yield word + + def abbreviations(self): + """All unique abbreviations of words in the Trie, with/without UTF-8 Marks. + + Scans the Trie, identifying each prefix that uniquely abbreviates a word. + + """ + def unique( current ): + terminal = False + for terminal, _, complete in self._trie.complete( current ): + pass + return terminal + + for abbrev, node in self._trie.scan( predicate=unique ): + if node.value is node.EMPTY: + # Only abbreviations (not terminal words) that led to a unique terminal word + yield abbrev + + def options(self, *args, **kwargs): + return self._trie.options(*args, **kwargs) + + def __str__(self): + return str(self._trie) + + class IMnemonic(ABC): + # The specified Mnemonic's details; including the deduced language and all of its word indices + # for decoding, including valid abbreviations and word with/without the accents. _mnemonic: List[str] _words: int _language: str _mnemonic_type: Optional[str] + _word_indices: Mapping[str, int] - words_list: List[int] + words_list: List[int] # The valid mnemonic length(s) available, in words languages: List[str] wordlist_path: Dict[str, str] def __init__(self, mnemonic: Union[str, List[str]], **kwargs) -> None: - """ - Initialize an instance of IMnemonic with a mnemonic. + """Initialize an instance of IMnemonic with a mnemonic. + + Converts the provided Mnemonics (abbreviated or missing UTF-8 Marks) to canonical Mnemonic + words in display-able UTF-8 "NFC" form. :param mnemonic: The mnemonic to initialize with, which can be a string or a list of strings. :type mnemonic: Union[str, List[str]] @@ -38,16 +396,34 @@ def __init__(self, mnemonic: Union[str, List[str]], **kwargs) -> None: :return: No return :rtype: NoneType + """ - self._mnemonic: List[str] = self.normalize(mnemonic) - if not self.is_valid(self._mnemonic, **kwargs): - raise MnemonicError("Invalid mnemonic words") + mnemonic_list: List[str] = self.normalize(mnemonic) - _, self._language = self.find_language(self._mnemonic) + # Attempt to unambiguously determine the Mnemonic's language using any preferred 'language'. + # Raises a MnemonicError if the words are not valid. Note that the supplied preferred + # language is only a hint, and the actual language matching the mnemonic will be selected. + self._word_indices, self._language = self.find_language(mnemonic_list, language=kwargs.get("language")) self._mnemonic_type = kwargs.get("mnemonic_type", None) + + # We now know with certainty that the list of Mnemonic words was valid in some language. + # However, they may have been abbreviations, or had optional UTF-8 Marks removed. So, use + # the _word_indices mapping twice, from str (matching word/abbrev) -> int (index) -> str + # (canonical word from keys). This will work with a find_languages that returns either a + # WordIndices Mapping or a simple dict word->index Mapping (but abbreviations or missing + # Marks will not be supported) + canonical_words = list(self._word_indices) + self._mnemonic: List[str] = [ + canonical_words[self._word_indices[word]] + for word in mnemonic_list + ] self._words = len(self._mnemonic) + # We have the canonical Mnemonic words. Decode them for validation, thus preserving the + # real MnemonicError details if the words do not form a valid Mnemonic. + self.decode(self._mnemonic, **kwargs) + @classmethod def name(cls) -> str: pass @@ -70,7 +446,7 @@ def mnemonic_type(self) -> str: :rtype: str """ - raise NotImplemented + raise NotImplementedError def language(self) -> str: """ @@ -111,80 +487,355 @@ def decode(cls, mnemonic: Union[str, List[str]], **kwargs) -> str: @classmethod def get_words_list_by_language( - cls, language: str, wordlist_path: Optional[Dict[str, str]] = None + cls, language: str, wordlist_path: Optional[Dict[str, Union[str, List[str]]]] = None ) -> List[str]: - """ - Retrieves the word list for the specified language. + """Retrieves the standardized (NFC normalized, lower-cased) word list for the specified language. + + Uses NFC normalization for internal processing consistency. BIP-39 wordlists are generally + stored in NFD format (with some exceptions like russian) but we normalize to NFC (for + internal word comparisons and lookups, and for display. + + We do not want to use 'normalize' to do this, because normalization of Mnemonics may have + additional functionality beyond just ensuring symbol and case standardization. + + Supports wordlist_path mapping language to either a path: + + {'language': '/some/path'}, + + or to the language's actual words_list data: + + {'language': ['words', 'list', ...]} + + Ignores blank lines and # ... comments :param language: The language for which to get the word list. :type language: str - :param wordlist_path: Optional dictionary mapping language names to file paths of their word lists. - :type wordlist_path: Optional[Dict[str, str]] + :param wordlist_path: Optional dictionary mapping language names to file paths of their word lists, or the words list. + :type wordlist_path: Optional[Dict[str, Tuple[str, List[str]]] - :return: A list of words for the specified language. + :return: A list of words for the specified language, normalized to NFC form. :rtype: List[str] + """ wordlist_path = cls.wordlist_path if wordlist_path is None else wordlist_path - with open(os.path.join(os.path.dirname(__file__), wordlist_path[language]), "r", encoding="utf-8") as fin: - words_list: List[str] = [ - word.strip() for word in fin.readlines() if word.strip() != "" and not word.startswith("#") - ] + + # May provide a filesystem path str, or a List-like sequence of words + if isinstance( wordlist_path[language], str ): + with open(os.path.join(os.path.dirname(__file__), wordlist_path[language]), "r", encoding="utf-8") as fin: + words_list_raw: List[str] = list( fin ) + else: + words_list_raw: List[str] = list( wordlist_path[language] ) + + # Ensure any words are provided in either NFKC or NFKD form. This eliminates words lists + # where the provided word is not in standard NFC or NFD form, down-cases them and removes + # any leading/trailing whitespace, then ignores empty lines or full-line comments (trailing + # comments are not supported). + words_list: List[str] = [] + for word in map( str.lower, map( str.strip, words_list_raw )): + if not word or word.startswith("#"): + continue + word_nfc = unicodedata.normalize("NFKC", word) + word_nfkd = unicodedata.normalize( "NFKD", word_nfc) + assert word == word_nfkd or word == word_nfc, \ + f"Original {language} word {word!r} failed to round-trip through NFC: {word_nfc!r} / NFKD: {word_nfkd!r}" + words_list.append(word_nfc) + return words_list @classmethod - def find_language( - cls, mnemonic: List[str], wordlist_path: Optional[Dict[str, str]] = None - ) -> Union[str, Tuple[List[str], str]]: + @lru_cache(maxsize=32) + def _get_cached_word_indices(cls, wordlist_tuple: tuple[str]) -> WordIndices: + """Create and cache WordIndices for a given language and wordlist. + + :param language: The language name for identification + :type language: str + :param wordlist_tuple: Tuple of words (hashable for caching) + :type wordlist_tuple: tuple + + :return: Cached WordIndices object + :rtype: WordIndices """ - Finds the language of the given mnemonic by checking against available word lists. + return WordIndices(wordlist_tuple) - :param mnemonic: The mnemonic to check, represented as a list of words. - :type mnemonic: List[str] - :param wordlist_path: Optional dictionary mapping language names to file paths of their word lists. - :type wordlist_path: Optional[Dict[str, str]] + @classmethod + def wordlist_indices( + cls, wordlist_path: Optional[Dict[str, Union[str, List[str]]]] = None, language: Optional[str] = None, + ) -> Tuple[str, List[str], WordIndices]: + """Yields each 'candidate' language, its NFKC-normalized words List, and its WordIndices + + Optionally restricts to the preferred language, if available. + + The WordIndices Mapping supporting indexing by 'int' word index, or 'str' with optional + accents and all unique abbreviations. - :return: A tuple containing the word list and the language name if found. - :rtype: Union[str, Tuple[List[str], str]] """ + for candidate in (wordlist_path.keys() if wordlist_path else cls.languages): + if language and candidate != language: + continue + # Normalized NFC, so characters and accents are combined + words_list: List[str] = cls.get_words_list_by_language( + language=candidate, wordlist_path=wordlist_path + ) + # Convert to tuple for hashing, cache the WordIndices creation + word_indices = cls._get_cached_word_indices(tuple(words_list)) + yield candidate, words_list, word_indices - for language in cls.languages: + @classmethod + def rank_languages( + cls, + mnemonic: List[str], + language: Optional[str] = None, + wordlist_path: Optional[Dict[str, Union[str, List[str]]]] = None, + ) -> Generator[Tuple[int, Mapping[str, int], str], None, None]: + """Finds all languages that can satisfy the given mnemonic. + + Returns a sequence of their relative quality, and the Mapping of words/abbreviations to + indices, and the language. """ + + language_indices: Dict[str, Mapping[str, int]] = {} + quality: Dict[str, Fraction] = defaultdict(Fraction) # What ratio of canonical language symbols were matched + for candidate, words_list, words_indices in cls.wordlist_indices( wordlist_path=wordlist_path ): + language_indices[candidate] = words_indices try: - words_list: list = cls.normalize( - cls.get_words_list_by_language( - language=language, wordlist_path=wordlist_path - ) - ) - words_list_with_index: dict = { - words_list[i]: i for i in range(len(words_list)) - } + # Check for exact matches and unique abbreviations, ensuring comparison occurs in + # composite "NFKC" normalized characters. for word in mnemonic: + word_composed = unicodedata.normalize( "NFKC", word ) try: - words_list_with_index[word] + index = words_indices[word_composed] except KeyError as ex: - raise MnemonicError(f"Unable to find word {word}") from ex - return words_list, language + if candidate in quality: + quality.pop(candidate) + raise MnemonicError(f"Unable to find word {word} in {candidate}") from ex + word_canonical = words_indices.keys()[index] + # The quality of a match is the ratio of symbols provided that exactly match, + # vs. total symbols in the canonical words. So, more abbreviations and missing + # symbols with Marks (accents) penalizes the candidate language. + len_exact = sum(c1 == c2 for c1, c2 in zip( word_composed, word_canonical )) + quality[candidate] += Fraction( len_exact, len( word_canonical )) + + if candidate == language: + # All words exactly matched word with or without accents, complete or uniquely + # abbreviated words in the preferred language! We're done - we don't need to + # test further candidate languages. + yield quality[candidate], words_indices, candidate + return + + # All words exactly matched words in this candidate language, or some words were + # found to be unique abbreviations of words in the candidate, but it isn't the + # preferred language (or no preferred language was specified). Keep track of its + # quality of match, but carry on testing other candidate languages. except (MnemonicError, ValueError): continue - raise MnemonicError(f"Invalid language for mnemonic '{mnemonic}'") + + # No unambiguous match to any preferred language found (or no language matched all words). + if not quality: + raise MnemonicError(f"Invalid {cls.name()} mnemonic words") + + # Select the best available, of the potentially matching Mnemonics. Sort by the number of + # canonical symbols exactly matched (more is better - less ambiguous). However, unless we now test + # for is_valid, this would still be a statistical method, and thus still dangerous -- we should + # fail instead of returning a bad guess! + for ratio, candidate in sorted(((v, k) for k, v in quality.items()), reverse=True): + yield ratio, language_indices[candidate], candidate + + @classmethod + def find_language( + cls, + mnemonic: List[str], + language: Optional[str] = None, + wordlist_path: Optional[Dict[str, Union[str, List[str]]]] = None, + ) -> Tuple[Mapping[str, int], str]: + """The traditional statistical method for deducing the language of a Mnemonic. + + Finds the language of the given mnemonic by checking against available word list(s), + preferring the specified 'language' if supplied and exactly matches an available language. + If a 'wordlist_path' dict of {language: path} is supplied, its languages are used. If a + 'language' (optional) is supplied, any ambiguity is resolved by selecting the preferred + language, if available and the mnemonic matches. If not, the least ambiguous language found + is selected. + + If an abbreviation match is found, then the language with the largest total number of + symbols matched (least ambiguity) is considered best. This handles the (rare) case where a + mnemonic is valid in multiple languages, either directly or as an abbreviation (or + completely valid in both languages): + + english: abandon about badge machine minute ozone salon science ... + french: abandon aboutir badge machine minute ozone salon science ... + + or the classics, where the first is valid in both languages (due to abbreviations), + but only one language yields a BIP-39 Mnemonic that passes is_valid: + + Entropy == 00000000000000000000000000000000: + english: abandon abandon ... abandon about (valid) + french: abandon abandon ... abandon aboutir (invalid) + + Entropy == 00200400801002004008010020040080: + english abandon abandon ... abandon absurd (invalid) + french: abandon abandon ... abandon absurde (valid) + + or, completely ambiguous mnemonics that are totally valid in both languages, composed of + canonical words and passing internal checksums, but yielding different seeds, of course: + + essence capable figure noble distance fruit intact amateur surprise distance vague unique + lecture orange stable romance aspect junior fatal prison voyage globe village figure mobile badge usage social correct jaguar bonus science aspect question service crucial + + Clearly, it is /possible/ to specify a Mnemonic which for which it is impossible to uniquely + determine the language! However, this Mnemonic would probably be encoding very poor + entropy, so is quite unlikely to occur in a Mnemonic storing true entropy. But, it is + certainly possible (see above); especially with abbreviations. + + For these Mnemonics, it is /impossible/ to know (or guess) which language the Mnemonic was + intended to be {en,de}coded with. Since an incorrect "guess" would lead to a different seed + and therefore different derived wallets -- a match to multiple languages with the same + quality ranking and with no preferred 'language' raises an Exception. + + Even the final word (which encodes some checksum bits) cannot determine the language with + finality, because it is only a statistical checksum! For 128-bit 12-word encodings, only 4 + bits of checksum are represented. Therefore, there is a 1/16 chance that any entropy that + encodes to words in both languages will *also* have the same 4 bits of checksum! 24-word + BIP-39 Mnemonics only encode 8 bits of checksum, so 1/256 of random entropy that encodes to + words common to both languages will pass the checksum test. + + Therefore, specifying a 'language' is necessary to eliminate the possibility of erroneously + recognizing the wrong language for some Mnemonic, and therefore producing the wrong derived + cryptographic keys. + + Furthermore, for implementing .decode, it is recommended that you use .rank_languages, and + actually attempt to decode each matching language, raising an Exception unless there is + exactly one mnemonic language found that passes validity checks. + + The returned Mapping[str, int] contains all accepted word -> index mappings, including all + acceptable abbreviations, with and without character accents. This is typically the + expected behavior for most Mnemonic encodings ('café' == 'cafe' for Mnemonic word matching). + + :param mnemonic: The mnemonic to check, represented as a list of words. + :type mnemonic: List[str] + :param wordlist_path: Optional dictionary mapping language names to file paths of their word lists, or the word list. + :type wordlist_path: Optional[Dict[str, Union[str, List[str]]]] + :param language: The preferred language, used if valid and mnemonic matches. + :type mnemonic: Optional[str] + + :return: A tuple containing the matching language's quality ratio, word indices and language name. + :rtype: Tuple[Fraction, Mapping[str, int], str] + + """ + + (ratio, word_indices, candidate), *worse = cls.rank_languages( mnemonic, language=language, wordlist_path=wordlist_path ) + + if worse and ratio == worse[0][0]: + # There are more than one matching candidate languages -- and they are both equivalent + # in quality. We cannot know (or guess) the language with any certainty. + raise MnemonicError(f"Ambiguous languages {', '.join(c for _r, _w, c in worse)} or {candidate} for mnemonic; specify a preferred language") + + return word_indices, candidate + + @classmethod + def word_indices_candidates( + cls, + words: List[str], # normalized mnemonic words + language: Optional[str], # required, if words_list provided + words_list: Optional[List[str]] = None, + words_list_with_index: Optional[Mapping[str, int]] = None, + ) -> Mapping[str, Mapping[str, int]]: + """Collect candidate language(s) and their word_indices. + + Uses .rank_languages to determine all the candidate languages that may match the mnemonic. + + Raises Exceptions on word_indices that don't match cls.words_list_number, so it must be + defined for each IMnemonic-derived class that uses this. + + """ + + candidates: Mapping[str, Mapping[str, int]] = {} + if words_list_with_index: + candidates[language] = words_list_with_index + else: + wordlist_path: Optional[Dict[str, Union[str, List[str]]]] = None + if words_list: + if not language: + raise Error( "Must provide language with words_list" ) + wordlist_path = { language: words_list } + for _rank, word_indices, language in cls.rank_languages( + mnemonic=words, language=language, wordlist_path=wordlist_path + ): + candidates[language] = word_indices + assert candidates # rank_languages will always return at least one + indices_lens = set( map( len, candidates.values() )) + if indices_lens != {cls.words_list_number}: + raise Error( + "Invalid number of loaded words list", expected=cls.words_list_number, got=indices_lens + ) + return candidates @classmethod - def is_valid(cls, mnemonic: Union[str, List[str]], **kwargs) -> bool: + def collect( + cls, + languages: Optional[Collection[str]] = None, + wordlist_path: Optional[Dict[str, Union[str, List[str]]]] = None, + ) -> Generator[Tuple[Set[str], bool, Set[str]], str, None]: + """A generator taking input symbols, and producing a sequence of sets of possible next + characters in all remaining languages. + + With each symbol provided, yields the remaining candidate languages, whether the symbol + indicated a terminal word in some language, and the available next symbols in all remaining + languages. + """ - Checks if the given mnemonic is valid. + candidates: Dict[str, WordIndices] = dict( + (candidate, words_indices) + for candidate, _, words_indices in cls.wordlist_indices( wordlist_path=wordlist_path ) + if languages is None or candidate in languages + ) + + word: str = '' + updaters = { + candidate: words_indices.options() + for candidate, words_indices in candidates.items() + } + + symbol = None + complete = set() + while complete < set(updaters): + terminal = False + possible = set() + for candidate, updater in updaters.items(): + try: + done, available = updater.send(symbol) + except StopIteration: + complete.add( candidate ) + terminal |= done + possible |= available + symbol = yield (set(updaters) - complete, terminal, possible) + + @classmethod + def is_valid( + cls, + mnemonic: Union[str, List[str]], + language: Optional[str] = None, + **kwargs + ) -> bool: + """Checks if the given mnemonic is valid. + + Catches mnemonic-validity related or word indexing Exceptions and returns False, but lets + others through; asserts, hdwallet.exceptions.Error, general programming errors, etc. :param mnemonic: The mnemonic to check. :type mnemonic: str + :param language: The preferred language of the mnemonic. + :type mnemonic: str :param kwargs: Additional keyword arguments. :return: True if the strength is valid, False otherwise. :rtype: bool + """ try: - cls.decode(mnemonic=mnemonic, **kwargs) + cls.decode(mnemonic=mnemonic, language=language, **kwargs) return True - except (ValueError, MnemonicError): + except (ValueError, KeyError, MnemonicError, ChecksumError): return False @classmethod @@ -204,12 +855,12 @@ def is_valid_language(cls, language: str) -> bool: @classmethod def is_valid_words(cls, words: int) -> bool: """ - Checks if the given words is valid. + Checks if the given number of words is valid. - :param words: The words to check. + :param words: The number of words to check. :type words: int - :return: True if the strength is valid, False otherwise. + :return: True if the number of mnemonic words is valid, False otherwise. :rtype: bool """ @@ -217,14 +868,34 @@ def is_valid_words(cls, words: int) -> bool: @classmethod def normalize(cls, mnemonic: Union[str, List[str]]) -> List[str]: - """ - Normalizes the given mnemonic by splitting it into a list of words if it is a string. + """Normalizes the given mnemonic by splitting it into a list of words if it is a string. + Resilient to extra whitespace, compatibility characters such as full-width symbols, + decomposed characters and accents, and down-cases uppercase symbols using NFKC + normalization. + + This does not canonicalize the Mnemonic, because we do not know the language, nor can we + reliably deduce it without a preferred language (since Mnemonics may be valid in multiple + languages). + + Recognizes hex strings (raw entropy), and attempts to normalize them as appropriate for the + IMnemonic-derived class using 'from_entropy'. Thus, all IMnemonics can accept either + mnemonic strings or raw hex-encoded entropy, if they use the IMnemonic.normalize base + method in their derived 'decode' and 'is_valid' implementations. + + This makes sense for most Mnemonics, which produce an repeatable encoding for the same entropy; + Mnemonics that produce different encodings will need alternative implementations. They should + handle raw entropy directly. :param mnemonic: The mnemonic value, which can be a single string of words or a list of words. :type mnemonic: Union[str, List[str]] - :return: A list of words from the mnemonic. + :return: A list of words from the mnemonic, normalized for internal processing. :rtype: List[str] - """ - return mnemonic.split() if isinstance(mnemonic, str) else mnemonic + """ + if isinstance(mnemonic, str): + if ( len(mnemonic.strip()) * 4 in cls.words_to_entropy_strength.values() + and all(c in string.hexdigits for c in mnemonic.strip())): + mnemonic: str = cls.from_entropy(mnemonic, language=cls.languages[0]) + mnemonic: List[str] = mnemonic.strip().split() + return list(unicodedata.normalize("NFKC", word.lower()) for word in mnemonic) diff --git a/hdwallet/mnemonics/monero/mnemonic.py b/hdwallet/mnemonics/monero/mnemonic.py index 3d968952..e3919811 100644 --- a/hdwallet/mnemonics/monero/mnemonic.py +++ b/hdwallet/mnemonics/monero/mnemonic.py @@ -3,13 +3,12 @@ # Copyright © 2020-2024, Meheret Tesfaye Batu # Distributed under the MIT software license, see the accompanying # file COPYING or https://opensource.org/license/mit +import unicodedata from typing import ( - Union, Dict, List + Union, Dict, List, Mapping, Optional ) -import unicodedata - from ...entropies import ( IEntropy, MoneroEntropy, MONERO_ENTROPY_STRENGTHS ) @@ -116,40 +115,40 @@ class MoneroMnemonic(IMnemonic): MONERO_MNEMONIC_WORDS.TWENTY_FIVE: MONERO_ENTROPY_STRENGTHS.TWO_HUNDRED_FIFTY_SIX } languages: List[str] = [ - MONERO_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED, - MONERO_MNEMONIC_LANGUAGES.DUTCH, MONERO_MNEMONIC_LANGUAGES.ENGLISH, MONERO_MNEMONIC_LANGUAGES.FRENCH, + MONERO_MNEMONIC_LANGUAGES.SPANISH, MONERO_MNEMONIC_LANGUAGES.GERMAN, + MONERO_MNEMONIC_LANGUAGES.DUTCH, MONERO_MNEMONIC_LANGUAGES.ITALIAN, - MONERO_MNEMONIC_LANGUAGES.JAPANESE, - MONERO_MNEMONIC_LANGUAGES.PORTUGUESE, MONERO_MNEMONIC_LANGUAGES.RUSSIAN, - MONERO_MNEMONIC_LANGUAGES.SPANISH + MONERO_MNEMONIC_LANGUAGES.PORTUGUESE, + MONERO_MNEMONIC_LANGUAGES.JAPANESE, + MONERO_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED, ] language_unique_prefix_lengths: Dict[str, int] = { - MONERO_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED: 1, - MONERO_MNEMONIC_LANGUAGES.DUTCH: 4, MONERO_MNEMONIC_LANGUAGES.ENGLISH: 3, MONERO_MNEMONIC_LANGUAGES.FRENCH: 4, + MONERO_MNEMONIC_LANGUAGES.SPANISH: 4, MONERO_MNEMONIC_LANGUAGES.GERMAN: 4, + MONERO_MNEMONIC_LANGUAGES.DUTCH: 4, MONERO_MNEMONIC_LANGUAGES.ITALIAN: 4, - MONERO_MNEMONIC_LANGUAGES.JAPANESE: 4, + MONERO_MNEMONIC_LANGUAGES.RUSSIAN: 4, MONERO_MNEMONIC_LANGUAGES.PORTUGUESE: 4, - MONERO_MNEMONIC_LANGUAGES.SPANISH: 4, - MONERO_MNEMONIC_LANGUAGES.RUSSIAN: 4 + MONERO_MNEMONIC_LANGUAGES.JAPANESE: 4, + MONERO_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED: 1, } wordlist_path: Dict[str, str] = { - MONERO_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED: "monero/wordlist/chinese_simplified.txt", - MONERO_MNEMONIC_LANGUAGES.DUTCH: "monero/wordlist/dutch.txt", MONERO_MNEMONIC_LANGUAGES.ENGLISH: "monero/wordlist/english.txt", MONERO_MNEMONIC_LANGUAGES.FRENCH: "monero/wordlist/french.txt", + MONERO_MNEMONIC_LANGUAGES.SPANISH: "monero/wordlist/spanish.txt", MONERO_MNEMONIC_LANGUAGES.GERMAN: "monero/wordlist/german.txt", + MONERO_MNEMONIC_LANGUAGES.DUTCH: "monero/wordlist/dutch.txt", MONERO_MNEMONIC_LANGUAGES.ITALIAN: "monero/wordlist/italian.txt", - MONERO_MNEMONIC_LANGUAGES.JAPANESE: "monero/wordlist/japanese.txt", - MONERO_MNEMONIC_LANGUAGES.PORTUGUESE: "monero/wordlist/portuguese.txt", MONERO_MNEMONIC_LANGUAGES.RUSSIAN: "monero/wordlist/russian.txt", - MONERO_MNEMONIC_LANGUAGES.SPANISH: "monero/wordlist/spanish.txt" + MONERO_MNEMONIC_LANGUAGES.PORTUGUESE: "monero/wordlist/portuguese.txt", + MONERO_MNEMONIC_LANGUAGES.JAPANESE: "monero/wordlist/japanese.txt", + MONERO_MNEMONIC_LANGUAGES.CHINESE_SIMPLIFIED: "monero/wordlist/chinese_simplified.txt", } @classmethod @@ -237,7 +236,7 @@ def encode(cls, entropy: Union[str, bytes], language: str, checksum: bool = Fals ) mnemonic: List[str] = [] - words_list: List[str] = cls.normalize(cls.get_words_list_by_language(language=language)) + words_list: List[str] = cls.get_words_list_by_language(language=language) # NFKC normalized if len(words_list) != cls.words_list_number: raise Error( "Invalid number of loaded words list", expected=cls.words_list_number, got=len(words_list) @@ -250,7 +249,7 @@ def encode(cls, entropy: Union[str, bytes], language: str, checksum: bool = Fals if checksum: unique_prefix_length = cls.language_unique_prefix_lengths[language] - prefixes = "".join(word[:unique_prefix_length] for word in mnemonic) + prefixes = "".join(unicodedata.normalize("NFD", word)[:unique_prefix_length] for word in mnemonic) checksum_word = mnemonic[ bytes_to_integer(crc32(prefixes)) % len(mnemonic) ] @@ -259,13 +258,22 @@ def encode(cls, entropy: Union[str, bytes], language: str, checksum: bool = Fals return " ".join(cls.normalize(mnemonic)) @classmethod - def decode(cls, mnemonic: str, **kwargs) -> str: + def decode( + cls, + mnemonic: str, + language: Optional[str] = None, + words_list: Optional[List[str]] = None, + words_list_with_index: Optional[Mapping[str, int]] = None, + **kwargs + ) -> str: """ Decodes a mnemonic phrase into entropy data. :param mnemonic: The mnemonic phrase to decode. :type mnemonic: str - :param kwargs: Additional keyword arguments (language, checksum). + :param language: The preferred mnemonic language. + :type language: str + :param kwargs: Additional keyword arguments (checksum). :return: The decoded entropy data. :rtype: str @@ -275,43 +283,45 @@ def decode(cls, mnemonic: str, **kwargs) -> str: if len(words) not in cls.words_list: raise MnemonicError("Invalid mnemonic words count", expected=cls.words_list, got=len(words)) - words_list, language = cls.find_language(mnemonic=words) - if len(words_list) != cls.words_list_number: - raise Error( - "Invalid number of loaded words list", expected=cls.words_list_number, got=len(words_list) - ) + candidates: Mapping[str, Mapping[str, int]] = cls.word_indices_candidates( + words=words, language=language, words_list=words_list, + words_list_with_index=words_list_with_index + ) - if len(words) in cls.words_checksum: - mnemonic: list = words[:-1] - unique_prefix_length = cls.language_unique_prefix_lengths[language] - prefixes = "".join(word[:unique_prefix_length] for word in mnemonic) - checksum_word = mnemonic[ - bytes_to_integer(crc32(prefixes)) % len(mnemonic) - ] - if words[-1] != checksum_word: - raise ChecksumError( - "Invalid checksum", expected=checksum_word, got=words[-1] + exception = None + entropies: Mapping[Optional[str], str] = {} + for language, word_indices in candidates.items(): + try: + if len(words) in cls.words_checksum: + mnemonic: list = words[:-1] + unique_prefix_length = cls.language_unique_prefix_lengths[language] + prefixes = "".join(unicodedata.normalize("NFD", word)[:unique_prefix_length] for word in mnemonic) + checksum_word = mnemonic[ + bytes_to_integer(crc32(prefixes)) % len(mnemonic) + ] + if words[-1] != checksum_word: + raise ChecksumError( + "Invalid checksum", expected=checksum_word, got=words[-1] + ) + + entropy: bytes = b"" + for index in range(len(words) // 3): + word_1, word_2, word_3 = words[index * 3:(index * 3) + 3] + entropy += words_to_bytes_chunk( + word_1, word_2, word_3, word_indices.keys(), "little" + ) + entropies[language] = bytes_to_string(entropy) + except Exception as exc: + # Collect first Exception; highest quality languages are first. + if exception is None: + exception = exc + + if entropies: + (candidate, entropy), *extras = entropies.items() + if extras: + exception = MnemonicError( + f"Ambiguous languages {', '.join(c for c, _ in extras)} or {candidate} for mnemonic; specify a preferred language" ) - - entropy: bytes = b"" - for index in range(len(words) // 3): - word_1, word_2, word_3 = words[index * 3:(index * 3) + 3] - entropy += words_to_bytes_chunk( - word_1, word_2, word_3, words_list, "little" - ) - return bytes_to_string(entropy) - - @classmethod - def normalize(cls, mnemonic: Union[str, List[str]]) -> List[str]: - """ - Normalizes the given mnemonic by splitting it into a list of words if it is a string. - - :param mnemonic: The mnemonic value, which can be a single string of words or a list of words. - :type mnemonic: Union[str, List[str]] - - :return: A list of words from the mnemonic. - :rtype: List[str] - """ - - mnemonic: list = mnemonic.split() if isinstance(mnemonic, str) else mnemonic - return list(map(lambda _: unicodedata.normalize("NFKD", _.lower()), mnemonic)) + else: + return entropy + raise exception diff --git a/hdwallet/mnemonics/slip39/__init__.py b/hdwallet/mnemonics/slip39/__init__.py new file mode 100644 index 00000000..73f1a9bb --- /dev/null +++ b/hdwallet/mnemonics/slip39/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Copyright © 2020-2024, Meheret Tesfaye Batu +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +from typing import List + +from .mnemonic import ( + SLIP39Mnemonic, SLIP39_MNEMONIC_WORDS, SLIP39_MNEMONIC_LANGUAGES +) + + +__all__: List[str] = [ + "SLIP39Mnemonic", + "SLIP39_MNEMONIC_WORDS", + "SLIP39_MNEMONIC_LANGUAGES" +] diff --git a/hdwallet/mnemonics/slip39/mnemonic.py b/hdwallet/mnemonics/slip39/mnemonic.py new file mode 100644 index 00000000..a35de75c --- /dev/null +++ b/hdwallet/mnemonics/slip39/mnemonic.py @@ -0,0 +1,710 @@ +#!/usr/bin/env python3 + +# Copyright © 2020-2024, Meheret Tesfaye Batu +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +import re +from typing import ( + Union, Dict, Iterable, List, Optional, Sequence, Collection, Tuple +) + +from ...entropies import ( + ENTROPIES, IEntropy, SLIP39Entropy, SLIP39_ENTROPY_STRENGTHS +) +from ...exceptions import ( + EntropyError, MnemonicError +) +from ...utils import ( + get_bytes, + bytes_to_string, +) +from ..imnemonic import IMnemonic + +from shamir_mnemonic import generate_mnemonics +from shamir_mnemonic.constants import MAX_SHARE_COUNT +from shamir_mnemonic.recovery import RecoveryState, Share + +from tabulate import tabulate + + +class SLIP39_MNEMONIC_WORDS: + + TWENTY: int = 20 + THIRTY_THREE: int = 33 + FIFTY_NINE: int = 59 + + +class SLIP39_MNEMONIC_LANGUAGES: + + ENGLISH: str = "english" + + +def group_parser( group_spec, size_default: Optional[int] = None) -> Tuple[str, Tuple[int, int]]: + """Parse a SLIP-39 group specification; a name up to the first digit, ( or /, then a + threshold/count spec: + + Frens6, Frens 6, Frens(6) - A 3/6 group (default is 1/2 of group size, rounded up) + Frens2/6, Frens(2/6) - A 2/6 group + + Prevents 1/N groups (use 1/1, and duplicate the mnemonic to the N participants). + + All aspects of a group specification are optional; an empty spec yields a default group. + + """ + g_match = group_parser.RE.match( group_spec ) + if not g_match: + raise ValueError( f"Invalid group specification: {group_spec!r}" ) + name = g_match.group( 'name' ) or "" + if name: + name = name.strip() + size = g_match.group( 'size' ) + require = g_match.group( 'require' ) + if not size: + # eg. default or inverse required/size ratio iff require provided. Otherwise can't guess. + if size_default: + size = size_default + elif require: + require = int( require ) + size = int( require / group_parser.REQUIRED_RATIO + 0.5 ) + if size == 1 or require == 1: + size = require + else: + size = 1 # No spec, no require; default to group size of 1 + size = int( size ) + if not require: + # eg. 2/4, 3/5 for size producing require > 1; else, require = size (avoids 1/N groups) + require = int( size * group_parser.REQUIRED_RATIO + 0.5 ) + if size == 1 or require == 1: + require = size + require = int(require) + if size < 1 or require > size or ( require == 1 and size > 1 ): + raise ValueError( f"Impossible group specification from {group_spec!r} w/ default size {size_default!r}: {name,(require,size)!r}" ) + + return (name, (require, size)) +group_parser.REQUIRED_RATIO = 1/2 # noqa: E305 +group_parser.RE = re.compile( + r""" + ^ + \s* + (?P [^\d(/]+ )? + \s* + [(]? + \s* + (?: + (?P \d* ) + \s* + [/] + )? + \s* + (?: + (?P \d* ) + \s* + )? + [)]? + \s* + $ + """, re.VERBOSE ) + + +def language_parser(language: str) -> Dict[Tuple[str, Tuple[int, int]], Dict[Union[str, int], Tuple[int, int]]]: + """ + Parse a SLIP-39 language dialect specification. + + optional sep default comma-separated mnemonic + name a secret spec group thresholds (optional if no /) + --------------------- --- -------------------------------------- - + + "Name threshold/groups" + ^^^^^^^^^^^^^^^^^^^^^ + - no separator or commas, must be a secret encoding specification + + "group1 thresh1/mnems1, g2(t2/n1) ..." + ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^ + - commas; can't be a secret encoding spec, so must be group specs + + Name threshold/groups [;: group1 thresh1/mnems1, g2(t2/n1) ... ] + ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^ + - separator; must be both a secret encoding spec, and 0 or more group specs + - spec(s) may be partial, eg. "3" (size) or "3/" (threshold + - sensible defaults are deduced for missing specs, if possible + + { + ("Name",(threshold/groups)): { + "group1": (thresh1/mnems1), + "g2": (t2/n2), + } + } + + """ + s_match = language_parser.RE.match(language) + if not s_match and language.strip(): + raise ValueError( f"Invalid SLIP-39 specification: {language!r}" ) + + groups = s_match and s_match.group("groups") or "" + groups_list = groups.strip().split(",") + secret = s_match and s_match.group("secret") or "" + s_size_default = len(groups_list) if groups_list else None + s_name, (s_thresh, s_size) = group_parser(secret, size_default=s_size_default) + groups_list += [''] * (s_size - len(groups_list)) # default any missing group specs + + g_names, g_sizes = [], [] + for group in groups_list: + # Default size inferred from Fibonacci sequence of mnemonics required by default + size_default = None if len(g_sizes) < 2 else min( + MAX_SHARE_COUNT, + 2 * ( g_sizes[-1][0] + g_sizes[-2][0] ) + ) + g_name, g_dims = group_parser(group, size_default=size_default) + if not g_name: + g_name = len(g_sizes) + g_names.append(g_name) + g_sizes.append(g_dims) + + return { (s_name.strip(), (s_thresh, s_size)): dict(zip(g_names, g_sizes)) } +language_parser.REQUIRED_RATIO = 1/2 # noqa: E305 +language_parser.RE = re.compile( + r""" + ^ + \s* + (?P # Any single name and/or spec w/ no separator or comma + ( + [\w\s]* \d* \s* /? \s* \d* + ) + )? + \s* [\[<{;:]? \s* # An optional separator or bracket may appear before group spec(s) + (?P # The group spec(s), comma separated + ( + [\w\s]* \d* \s* /? \s* \d* + ) + ( + \s* , + [\w\s]* \d* \s* /? \s* \d* + )* + )? + \s* [\]>}]? \s* # And optionally a trailing inverse bracket + $ + """, re.VERBOSE) + + +def ordinal( num ): + q, mod = divmod( num, 10 ) + suffix = q % 10 != 1 and ordinal.suffixes.get(mod) or "th" + return f"{num}{suffix}" +ordinal.suffixes = {1: "st", 2: "nd", 3: "rd"} # noqa: E305 + + +def tabulate_slip39( + groups: Dict[Union[str, int], Tuple[int, int]], + group_mnemonics: Sequence[Collection[str]], + columns: Optional[Union[bool, int]] = None, # default: columnize, but no wrapping +) -> str: + """Return SLIP-39 groups with group names/numbers, a separator, and tabulated mnemonics. + + Mnemonics exceeding 'columns' will be wrapped with no prefix except a continuation character. + + The default behavior (columns is falsey) is to NOT wrap the mnemonics (no columns limit). If + columns is True or 1 (truthy, but not a specific sensible column size), we'll use the + tabulate_slip39.default of 20. Otherwise, we'll use the specified specific columns. + + """ + if not columns: # False, None, 0 + limit = 0 + elif int(columns) > 1: # 2, ... + limit = int(columns) + else: # True, 1 + limit = tabulate_slip39.default + + def prefixed( groups, group_mnemonics ): + for g, ((name, (threshold, count)), mnemonics) in enumerate( zip( groups.items(), group_mnemonics )): + assert count == len( mnemonics ) + for o, mnem in enumerate( sorted( map( str.split, mnemonics ))): + siz = limit or len( mnem ) + end = len( mnem ) + rows = ( end + siz - 1 ) // siz + for r, col in enumerate( range( 0, end, siz )): + con = '' + if count == 1: # A 1/1 + if rows == 1: + sep = '━' # on 1 row + elif r == 0: + sep = '┭' # on multiple rows + con = '╎' + elif r+1 < rows: + sep = '├' + con = '╎' + else: + sep = '└' + elif rows == 1: # An N/M w/ full row mnemonics + if o == 0: # on 1 row, 1st mnemonic + sep = '┳' + con = '╏' + elif o+1 < count: + sep = '┣' + con = '╏' + else: + sep = '┗' + else: # An N/M, but multi-row mnemonics + if o == 0 and r == 0: # on 1st row, 1st mnemonic + sep = '┳' + con = '╎' + elif r == 0: # on 1st row, any mnemonic + sep = '┣' + con = '╎' + elif r+1 < rows: # on mid row, any mnemonic + sep = '├' + con = '╎' + elif o+1 < count: # on last row, but not last mneonic + sep = '└' + con = '╏' + else: + sep = '└' # on last row of last mnemonic + + # Output the prefix and separator + mnemonics + yield [ + f"{name} {threshold}/{count} " if o == 0 and col == 0 else "" + ] + [ + ordinal(o+1) if col == 0 else "" + ] + [ + sep + ] + mnem[col:col+siz] + + # And if not the last group and mnemonic, but a last row; Add a blank or continuation row + if r+1 == rows and not (g+1 == len(groups) and o+1 == count): + yield ["", "", con] if con else [None] + + return tabulate( prefixed( groups, group_mnemonics ), tablefmt='plain' ) + +tabulate_slip39.default = 20 # noqa: E305 + + +class SLIP39Mnemonic(IMnemonic): + """Implements the SLIP39 standard, allowing the creation of mnemonic phrases for + recovering deterministic keys. + + Here are available ``SLP39_MNEMONIC_WORDS``: + + +-----------------------+----------------------+ + | Name | Value | + +=======================+======================+ + | TWENTY | 20 | + +-----------------------+----------------------+ + | THIRTY_THREE | 33 | + +-----------------------+----------------------+ + | FIFTY_NINE | 59 | + +-----------------------+----------------------+ + + Here are available ``SLIP39_MNEMONIC_LANGUAGES``: + + +-----------------------+----------------------+ + | Name | Value | + +=======================+======================+ + | ENGLISH | english | + +-----------------------+----------------------+ + + + For SLIP-39, the language word dictionary is always the same (english) so is ignored (simply + used as a label for the generated SLIP-39), but the rest of the language string specifies + the "dialect" (threshold of groups required/generated, and the threshold of mnemonics + required/generated in each group). + + The default is 1/1: 1/1 (a single group of 1 required, with 1/1 mnemonic required) by supplying + a language without further specific secret recovery or group recovery details: + + "" + "english" + "Any Label" + + The default progression of group mnemonics required/provided is fibonacci over required: + + - A threshold is 1/2 the specified number of groups/mnemonics (rounded up), and + - groups of 1/1, 1/1, 2/4 and 3/6, ... mnemonics + + All of these language specifications produce the same 2/4 group SLIP-39 encoding: + + "Johnson 2/4" + "2: 1/1, 1/1, 2/4, 3/6" + "Johnson 2/4: Home 1/1, Office 1/1, Fam 2/4, Frens 3/6" + + """ + + word_bit_length: int = 10 + words_list_number: int = 1024 + words_list: List[int] = [ + SLIP39_MNEMONIC_WORDS.TWENTY, + SLIP39_MNEMONIC_WORDS.THIRTY_THREE, + SLIP39_MNEMONIC_WORDS.FIFTY_NINE, + ] + words_to_entropy_strength: Dict[int, int] = { + SLIP39_MNEMONIC_WORDS.TWENTY: SLIP39_ENTROPY_STRENGTHS.ONE_HUNDRED_TWENTY_EIGHT, + SLIP39_MNEMONIC_WORDS.THIRTY_THREE: SLIP39_ENTROPY_STRENGTHS.TWO_HUNDRED_FIFTY_SIX, + SLIP39_MNEMONIC_WORDS.FIFTY_NINE: SLIP39_ENTROPY_STRENGTHS.FIVE_HUNDRED_TWELVE, + } + languages: List[str] = [ + SLIP39_MNEMONIC_LANGUAGES.ENGLISH + ] + wordlist_path: Dict[str, str] = { + SLIP39_MNEMONIC_LANGUAGES.ENGLISH: "slip39/wordlist/english.txt", + } + + def __init__(self, mnemonic: Union[str, List[str]], **kwargs) -> None: + # Record the mnemonics, and the specified language. First handle any custom keywords + self._tabulate = kwargs.pop("tabulate", False) + + super().__init__(mnemonic, **kwargs) + + # We know that normalize has already validated _mnemonic's length. Compute the per-mnemonic + # words for SLIP-39; exactly one of the available SLIP-39 words_list lengths will divide the + # mnemonics evenly. + self._words, = filter(lambda w: len(self._mnemonic) % w == 0, self.words_list) + + # If a SLIP-39 language encoding and/or tabulation is desired, remember them. The default + # behavior deduces and stores the Mnemonic language; we want to remember any custom + # encoding supplied. + if kwargs.get("language"): + self._language = kwargs.get("language") + + @classmethod + def name(cls) -> str: + """ + Get the name of the mnemonic class. + + :return: The name of the entropy class. + :rtype: str + """ + return "SLIP39" + + def mnemonic(self) -> str: + """Get the mnemonic as a single string. + + SLIP-39 Mnemonics usually have multiple lines. Iterates the _mnemonic words list by the + computed self.words(), joining each length of words by spaces to for a line, and then joins + by newlines. + + If a non-default 'tabulate' (eg. True, None, ) was specified, attempt to use it and the + stored SLIP-39 encoding language to inform the tabulation. + + :return: The mnemonic as a single string joined by spaces and newlines. + :rtype: str + + """ + if self._tabulate is not False: + # Output the SLIP-39 mnemonics with their encoding language details and desired + # tabulation. We'll need to re-deduce the SLIP-39 secret and group specs from + # _language. Only if we successfully compute the same number of expected mnemonics and + # exactly match the expected prefixes, will we assume that everything is OK (someone + # hasn't created a SLIP39Mnemonic by hand with a custom _language and _mnemonics), and + # we'll output the tabulated mnemonics. + ((s_name, (s_thresh, s_size)), groups), = language_parser(self._language).items() + mnemonic = iter( self._mnemonic ) + try: + group_mnemonics: List[List[str]] = [ + [ + " ".join( next( mnemonic ) for _ in range( self._words )) + for _ in range( g_size ) + ] + for (_g_name, (_g_thresh, g_size)) in groups.items() + ] + except StopIteration: + # Too few mnemonics for SLIP-39 deduced from _language? Ignore and carry on with + # simple mnemonics output. + pass + else: + extras = list(mnemonic) + if not extras: + # Exactly consumed all _mnemonics according to SLIP-39 language spec! Success? + # Final check; each of the group_mnemonics lists should have a common prefix. + def common( strings: List[str] ) -> str: + prefix = None + for s in strings: + if prefix is None: + prefix = s + continue + for i, (cp, cs) in enumerate(zip(prefix, s)): + if cp != cs: + prefix = prefix[:i] + break + if not prefix: + break + return prefix + + if all( map( common, group_mnemonics )): + return tabulate_slip39( + groups=groups, + group_mnemonics=group_mnemonics, + columns=self._tabulate + ) + + # Either no common prefix in some group; Invalid deduction of group specs + # vs. mnemonics, or left-over/insufficient Mnemonics! Fall through and render it + # the old-fashioned way... + + mnemonic_chunks: Iterable[List[str]] = zip(*[iter(self._mnemonic)] * self._words) + mnemonic: Iterable[str] = map(" ".join, mnemonic_chunks) + return "\n".join(mnemonic) + + @classmethod + def from_words(cls, words: int, language: str) -> str: + """Generates a mnemonic phrase from a specified number of words. + + This method generates a mnemonic phrase based on the specified number of words and language. + + :param words: The number of words for the mnemonic phrase. + :type words: int + :param language: The language for the mnemonic phrase. + :type language: str + + :return: The generated mnemonic phrase. + :rtype: str + + """ + if words not in cls.words_list: + raise MnemonicError("Invalid mnemonic words number", expected=cls.words_list, got=words) + + return cls.from_entropy( + entropy=SLIP39Entropy.generate(cls.words_to_entropy_strength[words]), language=language + ) + + @classmethod + def from_entropy(cls, entropy: Union[str, bytes, IEntropy], language: str, **kwargs) -> str: + """ + Generates from entropy data. Any entropy of the correct size can be encoded as SLIP-39. + + :param entropy: The entropy data used to generate the mnemonic phrase. + :type entropy: Union[str, bytes, IEntropy] + :param language: The language for the mnemonic phrase. + :type language: str + + :return: The generated mnemonic phrase. + :rtype: str + """ + if isinstance(entropy, str) or isinstance(entropy, bytes): + return cls.encode(entropy=entropy, language=language, **kwargs) + elif isinstance(entropy, IEntropy) and entropy.strength() in SLIP39Entropy.strengths: + return cls.encode(entropy=entropy.entropy(), language=language, **kwargs) + raise EntropyError( + "Invalid entropy instance", expected=[str, bytes,]+list(ENTROPIES.dictionary.values()), got=type(entropy) + ) + + @classmethod + def is_valid_language(cls, language: str) -> bool: + try: + language_parser(language) + return True + except Exception: + return False + + @classmethod + def encode( + cls, + entropy: Union[str, bytes], + language: str, + passphrase: str = "", + extendable: bool = True, + iteration_exponent: int = 1, + tabulate: Optional[Union[bool, int]] = False, # False disables; other values control prefixing/columnization + ) -> str: + """Encodes entropy into a SLIP-39 mnemonic phrase according to the specified language. + + The language specifies the SLIP-39 encoding parameters, and not the mnemonic language (which + is always english). The SLIP-39 encoding has A Name optionally followed by the number of + required groups / total groups (will be deduced if missing), followed by 1 or more comma + separated group names, each optionally with a number of mnemonics required to recover the + group, and optionally with a / followed by the total number of mnemonics to produce. + + Family Name 3: Home 1, Office 1, Fam 2, Frens 3, Other + Family Name 3/5 < Home 1/1, Office 1/1, Fam 2, Frens 3, Other > + Family Name 3/5 < Home 1/1, Office 1/1, Fam 2, Frens 3, Other 5/10> + + + SLIP-39 mnemonics include a password. This is normally empty, and is not well supported + even on Trezor devices. It is better to use SLIP-39 to encode a BIP-39 Mnemonic's entropy + and then (after recovering it from SLIP-39), use a BIP-39 passphrase (which is well + supported across all devices), or use the "Passphrase Wallet" feature of your hardware + wallet device. + + When a password is supplied to encode, decode will always recover the original entropy with + the same password. The 'extendable' feature (now default) of SLIP-39 provides for + deterministically recovering a deterministic entropy for each *different* password supplied. + This supports the use case where the original entropy and password is used (but different + SLIP-39 encoding parameters are supplied) -- but the same decoded wallet entropies are + desired for multiple different passwords. + + Note that SLIP-39 includes additional entropy in the encoding process, so the same entropy + and password will always result in different output SLIP-39 mnemonics. + + :param entropy: The entropy to encode into a mnemonic phrase. + :type entropy: Union[str, bytes] + :param language: The language for the mnemonic phrase. + :type language: str + :param passphrase: The SLIP-39 passphrase (default: "") + :type passphrase: str + :param extendable: Derive deterministic entropy for alternate passwords + :type extendable: bool + + :return: The encoded mnemonic phrase. + :rtype: str + + """ + entropy: bytes = get_bytes(entropy, unhexlify=True) + if not SLIP39Entropy.is_valid_bytes_strength(len(entropy)): + raise EntropyError( + "Wrong entropy strength", expected=SLIP39Entropy.strengths, got=(len(entropy) * 8) + ) + + ((s_name, (s_thresh, s_size)), groups), = language_parser(language).items() + assert s_size == len(groups) + group_mnemonics: Sequence[Collection[str]] = generate_mnemonics( + group_threshold=s_thresh, + groups=groups.values(), + master_secret=entropy, + passphrase=passphrase.encode('UTF-8'), + extendable=extendable, + iteration_exponent=iteration_exponent, + ) + + if tabulate is not False: # None/0 imply no column limits + return tabulate_slip39(groups, group_mnemonics, columns=tabulate) + return "\n".join(sum(group_mnemonics, [])) + + @classmethod + def decode( + cls, + mnemonic: str, + passphrase: str = "", + language: Optional[str] = None, + ) -> str: + """Decodes SLIP-39 mnemonic phrases into its corresponding entropy. + + This method converts a given mnemonic phrase back into its original entropy value. It + verifies several internal hashes to ensure the mnemonic and decoding is valid. + + The passphrase has no verification; all derived entropies are considered equivalently valid + (you can use several passphrases to recover multiple, distinct sets of entropy.) So, it is + solely your responsibility to remember your correct passphrase(s): this is a design feature + of SLIP-39. + + :param mnemonic: The mnemonic phrase to decode. + :type mnemonic: str + :param language: The preferred language of the mnemonic phrase + :type language: Optional[str] + :param passphrase: The SLIP-39 passphrase (default: "") + :type passphrase: str + + :return: The decoded entropy as a string. + :rtype: str + + """ + mnemonic_list: List[str] = cls.normalize(mnemonic) + try: + mnemonic_words, = filter(lambda words: len(mnemonic_list) % words == 0, cls.words_list) + mnemonic_chunks: Iterable[List[str]] = zip(*[iter(mnemonic_list)] * mnemonic_words) + mnemonic_lines: Iterable[str] = map(" ".join, mnemonic_chunks) + recovery = RecoveryState() + for line in mnemonic_lines: + recovery.add_share(Share.from_mnemonic(line)) + if recovery.is_complete(): + break + else: + raise ValueError( + f"Incomplete: found {recovery.groups_complete()}" + + f"/{recovery.parameters.group_threshold} groups and " + + ", ".join( + "/".join(map(lambda x: str(x) if x >= 0 else "?", recovery.group_status(g))) + for g in range(recovery.parameters.group_count) + ) + + " mnemonics required" + ) + entropy: str = bytes_to_string(recovery.recover(passphrase.encode('UTF-8'))) + except Exception as exc: + raise MnemonicError("Failed to recover SLIP-39 Mnemonics", detail=exc) from exc + return entropy + + NORMALIZE = re.compile( + r""" + ^ + \s* + ( + [ \w\d\s()/]* # Group(1/1) 1st { <-- a single non-word/space/digit separator allowed + [^\w\d\s()/] # Any symbol not comprising a valid group_parser language symbol + )? + \s* + ( + [\w\s]*\w # word word ... word <-- must end with non-whitespace (strips whitespace) + )? + \s* + $ + """, re.VERBOSE ) + + @classmethod + def normalize(cls, mnemonic: Union[str, List[str]]) -> List[str]: + """Filter the supplied lines of mnemonics, rejecting groups of mnemonics not evenly divisible by + one of the recognized SLIP-39 mnemonic lengths. + + Also accepts a single hex raw entropy value (converting it into a simple single-mnemonic + (1/1 groups) SLIP-39 encoding). + + Filter out any prefixes consisting of word/space symbols followed by a single non-word/space + symbol, before any number of Mnemonic word/space symbols: + + Group 1 { word word ... + + Group 2 ╭ word word ... + ╰ word word ... + + Group 3 ┌ word word ... + ├ word word ... + └ word word ... + ^^^^^^^^ ^ ^^^^^^^^^^... + | | | + word/digit/space* | word/space* + | + single non-word/digit/space + + + Since multi-row mnemonics are possible, we cannot always confirm that the accumulated + mnemonic size is valid after every mnemonic row. We can certainly identify the end of a + mnemonic by a blank row (it doesn't make sense to allow a single Mnemonic to be split across + blank rows), or the end of input. + + """ + errors = [] + if isinstance( mnemonic, str ): + mnemonic_list: List[str] = [] + + for line_no, line in enumerate( mnemonic.split("\n")): + m = cls.NORMALIZE.match( line ) + if not m: + errors.append( f"@L{line_no+1}: unrecognized mnemonic line" ) + continue + + pref, mnem = m.groups() + if mnem: + mnemonic_list.extend( super().normalize( mnem )) + else: + # Blank lines or lines without Mnemonic skipped. But they do indicate the end + # of a mnemonic! At this moment, the total accumulated Mnemonic(s) must be + # valid -- or the last one must have been bad. + word_lengths = list(filter(lambda w: len(mnemonic_list) % w == 0, cls.words_list)) + if not word_lengths: + errors.append( f"@L{line_no}: odd length mnemonic encountered" ) + break + else: + mnemonic_list: List[str] = mnemonic + + # Regardless of the Mnemonic source; the total number of words must be a valid multiple of + # the SLIP-39 mnemonic word lengths. Fortunately, the LCM(20, 33) is 660, and LCM(20, 33 + # and 59) is 38940, so we are unlikely to encounter a sufficient body of mnemonics to ever + # run into an uncertain SLIP-39 Mnemonic length in words. + word_lengths = list(filter(lambda w: len(mnemonic_list) % w == 0, cls.words_list)) + if not word_lengths: + errors.append( "Mnemonics not a multiple of valid length, or a single hex entropy value" ) + if errors: + raise MnemonicError( + "Invalid SLIP39 Mnemonics", + expected=f"multiple of {', '.join(map(str, cls.words_list))}", + got=f"{len(mnemonic_list)} total words", + detail="; ".join(errors), + ) + + return mnemonic_list diff --git a/hdwallet/mnemonics/slip39/wordlist/english.txt b/hdwallet/mnemonics/slip39/wordlist/english.txt new file mode 100644 index 00000000..5673e7ca --- /dev/null +++ b/hdwallet/mnemonics/slip39/wordlist/english.txt @@ -0,0 +1,1024 @@ +academic +acid +acne +acquire +acrobat +activity +actress +adapt +adequate +adjust +admit +adorn +adult +advance +advocate +afraid +again +agency +agree +aide +aircraft +airline +airport +ajar +alarm +album +alcohol +alien +alive +alpha +already +alto +aluminum +always +amazing +ambition +amount +amuse +analysis +anatomy +ancestor +ancient +angel +angry +animal +answer +antenna +anxiety +apart +aquatic +arcade +arena +argue +armed +artist +artwork +aspect +auction +august +aunt +average +aviation +avoid +award +away +axis +axle +beam +beard +beaver +become +bedroom +behavior +being +believe +belong +benefit +best +beyond +bike +biology +birthday +bishop +black +blanket +blessing +blimp +blind +blue +body +bolt +boring +born +both +boundary +bracelet +branch +brave +breathe +briefing +broken +brother +browser +bucket +budget +building +bulb +bulge +bumpy +bundle +burden +burning +busy +buyer +cage +calcium +camera +campus +canyon +capacity +capital +capture +carbon +cards +careful +cargo +carpet +carve +category +cause +ceiling +center +ceramic +champion +change +charity +check +chemical +chest +chew +chubby +cinema +civil +class +clay +cleanup +client +climate +clinic +clock +clogs +closet +clothes +club +cluster +coal +coastal +coding +column +company +corner +costume +counter +course +cover +cowboy +cradle +craft +crazy +credit +cricket +criminal +crisis +critical +crowd +crucial +crunch +crush +crystal +cubic +cultural +curious +curly +custody +cylinder +daisy +damage +dance +darkness +database +daughter +deadline +deal +debris +debut +decent +decision +declare +decorate +decrease +deliver +demand +density +deny +depart +depend +depict +deploy +describe +desert +desire +desktop +destroy +detailed +detect +device +devote +diagnose +dictate +diet +dilemma +diminish +dining +diploma +disaster +discuss +disease +dish +dismiss +display +distance +dive +divorce +document +domain +domestic +dominant +dough +downtown +dragon +dramatic +dream +dress +drift +drink +drove +drug +dryer +duckling +duke +duration +dwarf +dynamic +early +earth +easel +easy +echo +eclipse +ecology +edge +editor +educate +either +elbow +elder +election +elegant +element +elephant +elevator +elite +else +email +emerald +emission +emperor +emphasis +employer +empty +ending +endless +endorse +enemy +energy +enforce +engage +enjoy +enlarge +entrance +envelope +envy +epidemic +episode +equation +equip +eraser +erode +escape +estate +estimate +evaluate +evening +evidence +evil +evoke +exact +example +exceed +exchange +exclude +excuse +execute +exercise +exhaust +exotic +expand +expect +explain +express +extend +extra +eyebrow +facility +fact +failure +faint +fake +false +family +famous +fancy +fangs +fantasy +fatal +fatigue +favorite +fawn +fiber +fiction +filter +finance +findings +finger +firefly +firm +fiscal +fishing +fitness +flame +flash +flavor +flea +flexible +flip +float +floral +fluff +focus +forbid +force +forecast +forget +formal +fortune +forward +founder +fraction +fragment +frequent +freshman +friar +fridge +friendly +frost +froth +frozen +fumes +funding +furl +fused +galaxy +game +garbage +garden +garlic +gasoline +gather +general +genius +genre +genuine +geology +gesture +glad +glance +glasses +glen +glimpse +goat +golden +graduate +grant +grasp +gravity +gray +greatest +grief +grill +grin +grocery +gross +group +grownup +grumpy +guard +guest +guilt +guitar +gums +hairy +hamster +hand +hanger +harvest +have +havoc +hawk +hazard +headset +health +hearing +heat +helpful +herald +herd +hesitate +hobo +holiday +holy +home +hormone +hospital +hour +huge +human +humidity +hunting +husband +hush +husky +hybrid +idea +identify +idle +image +impact +imply +improve +impulse +include +income +increase +index +indicate +industry +infant +inform +inherit +injury +inmate +insect +inside +install +intend +intimate +invasion +involve +iris +island +isolate +item +ivory +jacket +jerky +jewelry +join +judicial +juice +jump +junction +junior +junk +jury +justice +kernel +keyboard +kidney +kind +kitchen +knife +knit +laden +ladle +ladybug +lair +lamp +language +large +laser +laundry +lawsuit +leader +leaf +learn +leaves +lecture +legal +legend +legs +lend +length +level +liberty +library +license +lift +likely +lilac +lily +lips +liquid +listen +literary +living +lizard +loan +lobe +location +losing +loud +loyalty +luck +lunar +lunch +lungs +luxury +lying +lyrics +machine +magazine +maiden +mailman +main +makeup +making +mama +manager +mandate +mansion +manual +marathon +march +market +marvel +mason +material +math +maximum +mayor +meaning +medal +medical +member +memory +mental +merchant +merit +method +metric +midst +mild +military +mineral +minister +miracle +mixed +mixture +mobile +modern +modify +moisture +moment +morning +mortgage +mother +mountain +mouse +move +much +mule +multiple +muscle +museum +music +mustang +nail +national +necklace +negative +nervous +network +news +nuclear +numb +numerous +nylon +oasis +obesity +object +observe +obtain +ocean +often +olympic +omit +oral +orange +orbit +order +ordinary +organize +ounce +oven +overall +owner +paces +pacific +package +paid +painting +pajamas +pancake +pants +papa +paper +parcel +parking +party +patent +patrol +payment +payroll +peaceful +peanut +peasant +pecan +penalty +pencil +percent +perfect +permit +petition +phantom +pharmacy +photo +phrase +physics +pickup +picture +piece +pile +pink +pipeline +pistol +pitch +plains +plan +plastic +platform +playoff +pleasure +plot +plunge +practice +prayer +preach +predator +pregnant +premium +prepare +presence +prevent +priest +primary +priority +prisoner +privacy +prize +problem +process +profile +program +promise +prospect +provide +prune +public +pulse +pumps +punish +puny +pupal +purchase +purple +python +quantity +quarter +quick +quiet +race +racism +radar +railroad +rainbow +raisin +random +ranked +rapids +raspy +reaction +realize +rebound +rebuild +recall +receiver +recover +regret +regular +reject +relate +remember +remind +remove +render +repair +repeat +replace +require +rescue +research +resident +response +result +retailer +retreat +reunion +revenue +review +reward +rhyme +rhythm +rich +rival +river +robin +rocky +romantic +romp +roster +round +royal +ruin +ruler +rumor +sack +safari +salary +salon +salt +satisfy +satoshi +saver +says +scandal +scared +scatter +scene +scholar +science +scout +scramble +screw +script +scroll +seafood +season +secret +security +segment +senior +shadow +shaft +shame +shaped +sharp +shelter +sheriff +short +should +shrimp +sidewalk +silent +silver +similar +simple +single +sister +skin +skunk +slap +slavery +sled +slice +slim +slow +slush +smart +smear +smell +smirk +smith +smoking +smug +snake +snapshot +sniff +society +software +soldier +solution +soul +source +space +spark +speak +species +spelling +spend +spew +spider +spill +spine +spirit +spit +spray +sprinkle +square +squeeze +stadium +staff +standard +starting +station +stay +steady +step +stick +stilt +story +strategy +strike +style +subject +submit +sugar +suitable +sunlight +superior +surface +surprise +survive +sweater +swimming +swing +switch +symbolic +sympathy +syndrome +system +tackle +tactics +tadpole +talent +task +taste +taught +taxi +teacher +teammate +teaspoon +temple +tenant +tendency +tension +terminal +testify +texture +thank +that +theater +theory +therapy +thorn +threaten +thumb +thunder +ticket +tidy +timber +timely +ting +tofu +together +tolerate +total +toxic +tracks +traffic +training +transfer +trash +traveler +treat +trend +trial +tricycle +trip +triumph +trouble +true +trust +twice +twin +type +typical +ugly +ultimate +umbrella +uncover +undergo +unfair +unfold +unhappy +union +universe +unkind +unknown +unusual +unwrap +upgrade +upstairs +username +usher +usual +valid +valuable +vampire +vanish +various +vegan +velvet +venture +verdict +verify +very +veteran +vexed +victim +video +view +vintage +violence +viral +visitor +visual +vitamins +vocal +voice +volume +voter +voting +walnut +warmth +warn +watch +wavy +wealthy +weapon +webcam +welcome +welfare +western +width +wildlife +window +wine +wireless +wisdom +withdraw +wits +wolf +woman +work +worthy +wrap +wrist +writing +wrote +year +yelp +yield +yoga +zero diff --git a/hdwallet/seeds/__init__.py b/hdwallet/seeds/__init__.py index 113fc041..fbc0bd76 100644 --- a/hdwallet/seeds/__init__.py +++ b/hdwallet/seeds/__init__.py @@ -12,6 +12,7 @@ from ..exceptions import SeedError from .algorand import AlgorandSeed from .bip39 import BIP39Seed +from .slip39 import SLIP39Seed from .cardano import CardanoSeed from .electrum import ( ElectrumV1Seed, ElectrumV2Seed @@ -35,6 +36,8 @@ class SEEDS: +--------------+------------------------------------------------------+ | BIP39 | :class:`hdwallet.seeds.bip39.BIP39Seed` | +--------------+------------------------------------------------------+ + | SLIP39 | :class:`hdwallet.seeds.sli39.SLIP39Seed` | + +--------------+------------------------------------------------------+ | Cardano | :class:`hdwallet.seeds.cardano.CardanoSeed` | +--------------+------------------------------------------------------+ | Electrum-V1 | :class:`hdwallet.seeds.electrum.v1.ElectrumV1Seed` | @@ -48,6 +51,7 @@ class SEEDS: dictionary: Dict[str, Type[ISeed]] = { AlgorandSeed.name(): AlgorandSeed, BIP39Seed.name(): BIP39Seed, + SLIP39Seed.name(): SLIP39Seed, CardanoSeed.name(): CardanoSeed, ElectrumV1Seed.name(): ElectrumV1Seed, ElectrumV2Seed.name(): ElectrumV2Seed, diff --git a/hdwallet/seeds/algorand.py b/hdwallet/seeds/algorand.py index d6eed827..f0c46957 100644 --- a/hdwallet/seeds/algorand.py +++ b/hdwallet/seeds/algorand.py @@ -4,9 +4,10 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or https://opensource.org/license/mit -from typing import Union +from typing import ( + List, Optional, Union +) -from ..exceptions import MnemonicError from ..mnemonics import ( IMnemonic, AlgorandMnemonic ) @@ -24,7 +25,7 @@ class AlgorandSeed(ISeed): This class inherits from the ``ISeed`` class, thereby ensuring that all functions are accessible. """ - length = 64 + lengths: List[int] = [64] @classmethod def name(cls) -> str: @@ -38,7 +39,7 @@ def name(cls) -> str: return "Algorand" @classmethod - def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], **kwargs) -> str: + def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], language: Optional[str] = None, **kwargs) -> str: """ Converts a mnemonic phrase to its corresponding seed. @@ -48,11 +49,8 @@ def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], **kwargs) -> str: :return: The decoded entropy as a string. :rtype: str """ + if not isinstance(mnemonic, IMnemonic): + mnemonic = AlgorandMnemonic(mnemonic=mnemonic, language=language) + assert isinstance(mnemonic, AlgorandMnemonic) - mnemonic = ( - mnemonic.mnemonic() if isinstance(mnemonic, IMnemonic) else mnemonic - ) - if not AlgorandMnemonic.is_valid(mnemonic=mnemonic): - raise MnemonicError(f"Invalid {cls.name()} mnemonic words") - - return AlgorandMnemonic.decode(mnemonic=mnemonic) + return AlgorandMnemonic.decode(mnemonic=mnemonic.mnemonic(), language=mnemonic.language()) diff --git a/hdwallet/seeds/bip39.py b/hdwallet/seeds/bip39.py index f278d96d..fbe3f756 100644 --- a/hdwallet/seeds/bip39.py +++ b/hdwallet/seeds/bip39.py @@ -5,13 +5,12 @@ # file COPYING or https://opensource.org/license/mit from typing import ( - Optional, Union + List, Optional, Union ) import unicodedata from ..crypto import pbkdf2_hmac_sha512 -from ..exceptions import MnemonicError from ..utils import bytes_to_string from ..mnemonics import ( IMnemonic, BIP39Mnemonic @@ -26,14 +25,19 @@ class BIP39Seed(ISeed): phrases and converting them into a binary seed used for hierarchical deterministic wallets. + The supplied passphrase is always used in extending the original entropy + (encoded in the BIP-39 mnemonic phrase) into the 512-bit seed used to derive + HD wallets. + .. note:: This class inherits from the ``ISeed`` class, thereby ensuring that all functions are accessible. + """ seed_salt_modifier: str = "mnemonic" seed_pbkdf2_rounds: int = 2048 - length = 128 + lengths: List[int] = [128] @classmethod def name(cls) -> str: @@ -47,9 +51,17 @@ def name(cls) -> str: return "BIP39" @classmethod - def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], passphrase: Optional[str] = None) -> str: - """ - Converts a mnemonic phrase to its corresponding seed. + def from_mnemonic( + cls, + mnemonic: Union[str, IMnemonic], + passphrase: Optional[str] = None, + language: Optional[str] = None + ) -> str: + """Converts a canonical mnemonic phrase to its corresponding seed. Since a mnemonic string + may contain abbreviations, we canonicalize it by round-tripping it through the appropriate + IMnemonic type; this raises a MnemonicError exception for invalid mnemonics or languages. + + BIP39 stretches a prefix + (passphrase or "") + normalized mnemonic to produce the 512-bit seed. :param mnemonic: The mnemonic phrase to be decoded. Can be a string or an instance of `IMnemonic`. :type mnemonic: Union[str, IMnemonic] @@ -59,17 +71,19 @@ def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], passphrase: Optional[str :return: The decoded seed as a string. :rtype: str + """ + if not isinstance(mnemonic, IMnemonic): + mnemonic = BIP39Mnemonic(mnemonic=mnemonic, language=language) + assert isinstance(mnemonic, IMnemonic) - mnemonic = ( - mnemonic.mnemonic() if isinstance(mnemonic, IMnemonic) else mnemonic - ) - if not BIP39Mnemonic.is_valid(mnemonic=mnemonic): - raise MnemonicError(f"Invalid {cls.name()} mnemonic words") + # Normalize mnemonic to NFKD for seed generation as required by BIP-39 specification + normalized_mnemonic: str = unicodedata.normalize("NFKD", mnemonic.mnemonic()) + # Salt normalization should use NFKD as per BIP-39 specification salt: str = unicodedata.normalize("NFKD", ( (cls.seed_salt_modifier + passphrase) if passphrase else cls.seed_salt_modifier )) return bytes_to_string(pbkdf2_hmac_sha512( - password=mnemonic, salt=salt, iteration_num=cls.seed_pbkdf2_rounds + password=normalized_mnemonic, salt=salt, iteration_num=cls.seed_pbkdf2_rounds )) diff --git a/hdwallet/seeds/cardano.py b/hdwallet/seeds/cardano.py index 86be907c..0e1d74d2 100644 --- a/hdwallet/seeds/cardano.py +++ b/hdwallet/seeds/cardano.py @@ -5,11 +5,11 @@ # file COPYING or https://opensource.org/license/mit from typing import ( - Optional, Union, List + List, Optional, Set, Union ) import cbor2 -import re +import string from ..mnemonics import ( IMnemonic, BIP39Mnemonic @@ -17,7 +17,7 @@ from ..cryptocurrencies import Cardano from ..crypto import blake2b_256 from ..exceptions import ( - Error, MnemonicError, SeedError + Error, SeedError ) from ..utils import ( get_bytes, bytes_to_string @@ -38,15 +38,26 @@ class CardanoSeed(ISeed): This class inherits from the ``ISeed`` class, thereby ensuring that all functions are accessible. """ + # According to https://cardano-c.readthedocs.io/en/stable/api/bip39.html#, Cardano supports + # english BIP-39 Mnemonics in lengths: + # + # - 16 bytes / 32 hex (128 bits) → 12 words + # - 20 bytes / 40 hex (160 bits) → 15 words + # - 24 bytes / 48 hex (192 bits) → 18 words + # - 28 bytes / 56 hex (224 bits) → 21 words + # - 32 bytes / 64 hex (256 bits) → 24 words _cardano_type: str lengths: List[int] = [ - 32, # Byron-Icarus and Shelly-Icarus - 128, # Byron-Ledger and Shelly-Ledger - 64 # Byron-Legacy + 32, # Byron-Icarus and Shelly-Icarus; any valid BIP-39 Entropy + 40, + 48, + 56, + 64, # Byron-Legacy; special Blake2B 256-bit encoding only + 128, # Byron-Ledger and Shelly-Ledger 512-bit BIP-39 encoding ] def __init__( - self, seed: str, cardano_type: str = Cardano.TYPES.BYRON_ICARUS, passphrase: Optional[str] = None + self, seed: str, cardano_type: str = Cardano.TYPES.BYRON_ICARUS ) -> None: """ Initialize a CardanoSeed object. @@ -55,22 +66,21 @@ def __init__( :type seed: str :param cardano_type: The type of Cardano seed. Defaults to Cardano.TYPES.BYRON_ICARUS. :type cardano_type: str, optional - - :param passphrase: Optional passphrase for deriving the seed. Defaults to None. - :type passphrase: str, optional """ - - super(CardanoSeed, self).__init__( - seed=seed, cardano_type=cardano_type, passphrase=passphrase - ) - - if cardano_type not in Cardano.TYPES.get_cardano_types(): - raise SeedError( - "Invalid Cardano type", expected=Cardano.TYPES.get_cardano_types(), got=cardano_type + try: + super().__init__( + seed=seed, cardano_type=cardano_type ) - + except Exception as exc: + raise SeedError( + f"Invalid {cardano_type} seed size", + expected=( + ", ".join(f"{nibbles*4}-" for nibbles in sorted(self.cardano_type_lengths(cardano_type))) + + "bit" + ), + got=f"{len(seed)*4}-bit" + ) from exc self._cardano_type = cardano_type - self._seed = seed @classmethod def name(cls) -> str: @@ -93,6 +103,16 @@ def cardano_type(self) -> str: return self._cardano_type + @classmethod + def cardano_type_lengths(cls, cardano_type) -> Set[int]: + if cardano_type in [Cardano.TYPES.BYRON_ICARUS, Cardano.TYPES.SHELLEY_ICARUS]: + return set(cls.lengths[:-1]) # BIP-39 Entropy required + elif cardano_type == Cardano.TYPES.BYRON_LEGACY: + return set(cls.lengths[-2:-1]) # Blake2B 256-bit hash require + elif cardano_type in [Cardano.TYPES.BYRON_LEDGER, Cardano.TYPES.SHELLEY_LEDGER]: + return set(cls.lengths[-1:]) # Raw BIP-39 512-bit encoded seed required + return set() + @classmethod def is_valid(cls, seed: str, cardano_type: str = Cardano.TYPES.BYRON_ICARUS) -> bool: """ @@ -106,27 +126,19 @@ def is_valid(cls, seed: str, cardano_type: str = Cardano.TYPES.BYRON_ICARUS) -> :return: True if is valid, False otherwise. :rtype: bool """ + if super().is_valid(seed): + # But also, specific Cardano types must have a specific seed length + if len(seed) in cls.cardano_type_lengths(cardano_type): + return True + return False - if not isinstance(seed, str) or not bool(re.fullmatch( - r'^[0-9a-fA-F]+$', seed - )): return False - - if cardano_type in [Cardano.TYPES.BYRON_ICARUS, Cardano.TYPES.SHELLEY_ICARUS]: - return len(seed) == cls.lengths[0] - elif cardano_type in [Cardano.TYPES.BYRON_LEDGER, Cardano.TYPES.SHELLEY_LEDGER]: - return len(seed) == cls.lengths[1] - elif cardano_type == Cardano.TYPES.BYRON_LEGACY: - return len(seed) == cls.lengths[2] - else: - raise SeedError( - "Invalid Cardano type", expected=Cardano.TYPES.get_cardano_types(), got=cardano_type - ) @classmethod def from_mnemonic( cls, mnemonic: Union[str, IMnemonic], passphrase: Optional[str] = None, + language: Optional[str] = None, cardano_type: str = Cardano.TYPES.BYRON_ICARUS ) -> str: """ @@ -147,27 +159,26 @@ def from_mnemonic( :return: The generated Cardano wallet seed as a string. :rtype: str """ - if cardano_type == Cardano.TYPES.BYRON_ICARUS: - return cls.generate_byron_icarus(mnemonic=mnemonic) + return cls.generate_byron_icarus(mnemonic=mnemonic, language=language) if cardano_type == Cardano.TYPES.BYRON_LEDGER: return cls.generate_byron_ledger( - mnemonic=mnemonic, passphrase=passphrase + mnemonic=mnemonic, passphrase=passphrase, language=language, ) if cardano_type == Cardano.TYPES.BYRON_LEGACY: - return cls.generate_byron_legacy(mnemonic=mnemonic) + return cls.generate_byron_legacy(mnemonic=mnemonic, language=language) if cardano_type == Cardano.TYPES.SHELLEY_ICARUS: - return cls.generate_shelley_icarus(mnemonic=mnemonic) - elif cardano_type == Cardano.TYPES.SHELLEY_LEDGER: + return cls.generate_shelley_icarus(mnemonic=mnemonic, language=language) + if cardano_type == Cardano.TYPES.SHELLEY_LEDGER: return cls.generate_shelley_ledger( - mnemonic=mnemonic, passphrase=passphrase + mnemonic=mnemonic, passphrase=passphrase, language=language ) raise Error( "Invalid Cardano type", expected=Cardano.TYPES.get_cardano_types(), got=cardano_type ) @classmethod - def generate_byron_icarus(cls, mnemonic: Union[str, IMnemonic]) -> str: + def generate_byron_icarus(cls, mnemonic: Union[str, IMnemonic], language: Optional[str] = None) -> str: """ Generates a Byron Icarus seed from a given mnemonic phrase. @@ -177,19 +188,14 @@ def generate_byron_icarus(cls, mnemonic: Union[str, IMnemonic]) -> str: :return: The derived Byron Icarus seed as a string. :rtype: str """ + if not isinstance(mnemonic, IMnemonic): + mnemonic = BIP39Mnemonic(mnemonic=mnemonic, language=language) + assert isinstance(mnemonic, BIP39Mnemonic) - mnemonic = ( - mnemonic.mnemonic() - if isinstance(mnemonic, IMnemonic) else - mnemonic - ) - if not BIP39Mnemonic.is_valid(mnemonic=mnemonic): - raise MnemonicError(f"Invalid {BIP39Mnemonic.name()} mnemonic words") - - return BIP39Mnemonic.decode(mnemonic=mnemonic) + return BIP39Mnemonic.decode(mnemonic=mnemonic.mnemonic(), language=mnemonic.language()) @classmethod - def generate_byron_ledger(cls, mnemonic: Union[str, IMnemonic], passphrase: Optional[str] = None) -> str: + def generate_byron_ledger(cls, mnemonic: Union[str, IMnemonic], passphrase: Optional[str] = None, language: Optional[str] = None) -> str: """ Generates a Byron Ledger seed from a given mnemonic phrase and optional passphrase. @@ -202,16 +208,14 @@ def generate_byron_ledger(cls, mnemonic: Union[str, IMnemonic], passphrase: Opti :return: The derived Byron Ledger seed as a string. :rtype: str """ + if not isinstance(mnemonic, IMnemonic): + mnemonic = BIP39Mnemonic(mnemonic=mnemonic, language=language) + assert isinstance(mnemonic, BIP39Mnemonic) - mnemonic = ( - mnemonic.mnemonic() - if isinstance(mnemonic, IMnemonic) else - mnemonic - ) - return BIP39Seed.from_mnemonic(mnemonic=mnemonic, passphrase=passphrase) + return BIP39Seed.from_mnemonic(mnemonic=mnemonic.mnemonic(), language=mnemonic.language(), passphrase=passphrase) @classmethod - def generate_byron_legacy(cls, mnemonic: Union[str, IMnemonic]) -> str: + def generate_byron_legacy(cls, mnemonic: Union[str, IMnemonic], language: Optional[str] = None) -> str: """ Generates a Byron Legacy seed from a given mnemonic phrase. @@ -221,21 +225,16 @@ def generate_byron_legacy(cls, mnemonic: Union[str, IMnemonic]) -> str: :return: The derived Byron Legacy seed as a string. :rtype: str """ - - mnemonic = ( - mnemonic.mnemonic() - if isinstance(mnemonic, IMnemonic) else - mnemonic - ) - if not BIP39Mnemonic.is_valid(mnemonic=mnemonic): - raise MnemonicError(f"Invalid {BIP39Mnemonic.name()} mnemonic words") + if not isinstance(mnemonic, IMnemonic): + mnemonic = BIP39Mnemonic(mnemonic=mnemonic, language=language) + assert isinstance(mnemonic, BIP39Mnemonic) return bytes_to_string(blake2b_256( - cbor2.dumps(get_bytes(BIP39Mnemonic.decode(mnemonic=mnemonic))) + cbor2.dumps(get_bytes(BIP39Mnemonic.decode(mnemonic=mnemonic.mnemonic(), language=mnemonic.language()))) )) @classmethod - def generate_shelley_icarus(cls, mnemonic: Union[str, IMnemonic]) -> str: + def generate_shelley_icarus(cls, mnemonic: Union[str, IMnemonic], language: Optional[str] = None) -> str: """ Generates a Shelley Icarus seed from a given mnemonic phrase. @@ -247,11 +246,11 @@ def generate_shelley_icarus(cls, mnemonic: Union[str, IMnemonic]) -> str: """ return cls.generate_byron_icarus( - mnemonic=mnemonic + mnemonic=mnemonic, language=language ) @classmethod - def generate_shelley_ledger(cls, mnemonic: str, passphrase: Optional[str] = None) -> str: + def generate_shelley_ledger(cls, mnemonic: str, passphrase: Optional[str] = None, language: Optional[str] = None) -> str: """ Generates a Shelley ledger seed from a given mnemonic phrase and optional passphrase. @@ -265,5 +264,5 @@ def generate_shelley_ledger(cls, mnemonic: str, passphrase: Optional[str] = None """ return cls.generate_byron_ledger( - mnemonic=mnemonic, passphrase=passphrase + mnemonic=mnemonic, passphrase=passphrase, language=language ) diff --git a/hdwallet/seeds/electrum/v1.py b/hdwallet/seeds/electrum/v1.py index 5909c115..8a5575ad 100644 --- a/hdwallet/seeds/electrum/v1.py +++ b/hdwallet/seeds/electrum/v1.py @@ -4,10 +4,11 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or https://opensource.org/license/mit -from typing import Union +from typing import ( + List, Optional, Union +) from ...crypto import sha256 -from ...exceptions import MnemonicError from ...mnemonics import ( IMnemonic, ElectrumV1Mnemonic ) @@ -28,7 +29,7 @@ class ElectrumV1Seed(ISeed): hash_iteration_number: int = 10 ** 5 - length = 64 + lengths: List[int] = [64] @classmethod def name(cls) -> str: @@ -42,7 +43,12 @@ def name(cls) -> str: return "Electrum-V1" @classmethod - def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], **kwargs) -> str: + def from_mnemonic( + cls, + mnemonic: Union[str, IMnemonic], + language: Optional[str] = None, + **kwargs + ) -> str: """ Converts an Electrum V1 mnemonic phrase to its corresponding hashed entropy. @@ -52,14 +58,11 @@ def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], **kwargs) -> str: :return: The hashed entropy as a string. :rtype: str """ + if not isinstance(mnemonic, IMnemonic): + mnemonic = ElectrumV1Mnemonic(mnemonic=mnemonic, language=language) + assert isinstance(mnemonic, ElectrumV1Mnemonic) - mnemonic = ( - mnemonic.mnemonic() if isinstance(mnemonic, IMnemonic) else mnemonic - ) - if not ElectrumV1Mnemonic.is_valid(mnemonic=mnemonic): - raise MnemonicError(f"Invalid {cls.name()} mnemonic words") - - entropy: str = ElectrumV1Mnemonic.decode(mnemonic) + entropy: str = ElectrumV1Mnemonic.decode(mnemonic=mnemonic.mnemonic(), language=mnemonic.language(), **kwargs) entropy_hash: bytes = encode(entropy) for _ in range(cls.hash_iteration_number): entropy_hash = sha256(entropy_hash + encode(entropy)) diff --git a/hdwallet/seeds/electrum/v2.py b/hdwallet/seeds/electrum/v2.py index b30f802d..b8bfc5b7 100644 --- a/hdwallet/seeds/electrum/v2.py +++ b/hdwallet/seeds/electrum/v2.py @@ -5,13 +5,12 @@ # file COPYING or https://opensource.org/license/mit from typing import ( - Optional, Union + List, Optional, Union ) import unicodedata from ...crypto import pbkdf2_hmac_sha512 -from ...exceptions import MnemonicError from ...utils import bytes_to_string from ...mnemonics import ( IMnemonic, ElectrumV2Mnemonic, ELECTRUM_V2_MNEMONIC_TYPES @@ -33,7 +32,7 @@ class ElectrumV2Seed(ISeed): seed_salt_modifier: str = "electrum" seed_pbkdf2_rounds: int = 2048 - length = 128 + lengths: List[int] = [128] @classmethod def name(cls) -> str: @@ -51,6 +50,7 @@ def from_mnemonic( cls, mnemonic: Union[str, IMnemonic], passphrase: Optional[str] = None, + language: Optional[str] = None, mnemonic_type=ELECTRUM_V2_MNEMONIC_TYPES.STANDARD ) -> str: """ @@ -66,16 +66,13 @@ def from_mnemonic( :return: The derived seed as a string. :rtype: str """ - - mnemonic = ( - mnemonic.mnemonic() if isinstance(mnemonic, IMnemonic) else mnemonic - ) - if not ElectrumV2Mnemonic.is_valid(mnemonic=mnemonic, mnemonic_type=mnemonic_type): - raise MnemonicError(f"Invalid {cls.name()} mnemonic words") + if not isinstance(mnemonic, IMnemonic): + mnemonic = ElectrumV2Mnemonic(mnemonic=mnemonic, language=language) + assert isinstance(mnemonic, ElectrumV2Mnemonic) salt: str = unicodedata.normalize("NFKD", ( (cls.seed_salt_modifier + passphrase) if passphrase else cls.seed_salt_modifier )) return bytes_to_string(pbkdf2_hmac_sha512( - password=mnemonic, salt=salt, iteration_num=cls.seed_pbkdf2_rounds + password=unicodedata.normalize("NFKD", mnemonic.mnemonic()), salt=salt, iteration_num=cls.seed_pbkdf2_rounds )) diff --git a/hdwallet/seeds/iseed.py b/hdwallet/seeds/iseed.py index df6170ec..0f725652 100644 --- a/hdwallet/seeds/iseed.py +++ b/hdwallet/seeds/iseed.py @@ -7,23 +7,25 @@ from abc import ( ABC, abstractmethod ) -from typing import Union +from typing import ( + List, Optional, Union +) -import re +import string from ..mnemonics import IMnemonic - +from ..exceptions import SeedError class ISeed(ABC): _name: str _seed: str - length: int + lengths: List[int] # valid seed lengths, in hex symbols def __init__(self, seed: str, **kwargs) -> None: """ - Initialize an object with a seed value. + Initialize an object with a hex seed value. :param seed: The seed value used for initialization. :type seed: str @@ -31,7 +33,19 @@ def __init__(self, seed: str, **kwargs) -> None: :return: No return :rtype: NoneType """ - + if not self.is_valid(seed, **kwargs): + raise SeedError( + f"Invalid {self.name()} seed: {seed}", + expected=( + ", ".join(f"{nibbles*4}-" for nibbles in sorted(self.lengths)) + + "bit" + ), + got=( + f"{len(seed)*4}-bit " + + ("non-" if not all(c in string.hexdigits for c in seed) else "") + + "hex" + ) + ) self._seed = seed @classmethod @@ -50,9 +64,11 @@ def is_valid(cls, seed: str) -> bool: :rtype: bool """ - return isinstance(seed, str) and bool(re.fullmatch( - r'^[0-9a-fA-F]+$', seed - )) and len(seed) == cls.length + return ( + isinstance(seed, str) + and all(c in string.hexdigits for c in seed) + and len(seed) in set(cls.lengths) + ) def seed(self) -> str: """ @@ -66,5 +82,16 @@ def seed(self) -> str: @classmethod @abstractmethod - def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], **kwargs) -> str: + def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], language: Optional[str], **kwargs) -> str: + """ + Retrieves the seed associated with the Mnemonic. + + :param mnemonic: The mnemonic phrase to be decoded. Can be a string or an instance of `IMnemonic`. + :type mnemonic: Union[str, IMnemonic] + :param language: The preferred language, if known + :type language: Optional[str] + + :return: The seed as a string. + :rtype: str + """ pass diff --git a/hdwallet/seeds/monero.py b/hdwallet/seeds/monero.py index 3df5433c..84ff76e6 100644 --- a/hdwallet/seeds/monero.py +++ b/hdwallet/seeds/monero.py @@ -4,9 +4,10 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or https://opensource.org/license/mit -from typing import Union +from typing import ( + List, Optional, Union +) -from ..exceptions import MnemonicError from ..mnemonics import ( IMnemonic, MoneroMnemonic ) @@ -20,11 +21,13 @@ class MoneroSeed(ISeed): phrases and converting them into a binary seed used for hierarchical deterministic wallets. + Monero Mnemonic entropy is used directly as the private key, without modification. + .. note:: This class inherits from the ``ISeed`` class, thereby ensuring that all functions are accessible. """ - length = 32 + lengths: List[int] = [32, 64] @classmethod def name(cls) -> str: @@ -38,7 +41,7 @@ def name(cls) -> str: return "Monero" @classmethod - def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], **kwargs) -> str: + def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], language: Optional[str] = None, **kwargs) -> str: """ Converts a mnemonic phrase to its corresponding seed. @@ -48,10 +51,8 @@ def from_mnemonic(cls, mnemonic: Union[str, IMnemonic], **kwargs) -> str: :return: The decoded entropy as a string. :rtype: str """ - mnemonic = ( - mnemonic.mnemonic() if isinstance(mnemonic, IMnemonic) else mnemonic - ) - if not MoneroMnemonic.is_valid(mnemonic=mnemonic): - raise MnemonicError(f"Invalid {cls.name()} mnemonic words") + if not isinstance(mnemonic, IMnemonic): + mnemonic = MoneroMnemonic(mnemonic=mnemonic, language=language) + assert isinstance(mnemonic, MoneroMnemonic) - return MoneroMnemonic.decode(mnemonic=mnemonic) + return MoneroMnemonic.decode(mnemonic=mnemonic.mnemonic(), language=mnemonic.language()) diff --git a/hdwallet/seeds/slip39.py b/hdwallet/seeds/slip39.py new file mode 100644 index 00000000..28990d49 --- /dev/null +++ b/hdwallet/seeds/slip39.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +# Copyright © 2020-2024, Meheret Tesfaye Batu +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +from typing import ( + List, Optional, Union +) + +from ..exceptions import EntropyError +from ..mnemonics import IMnemonic +from ..mnemonics.bip39 import BIP39Mnemonic +from ..mnemonics.slip39 import SLIP39Mnemonic +from .iseed import ISeed + + +class SLIP39Seed(ISeed): + """This class transmits a seed collected from SLIP-39 recovery. This entropy is used /directly/ + to produce hierarchical deterministic wallets, unlike for BIP39 where the original entropy is + hashed and extended to 512 bits before being used. The 3 valid seed sizes are 128, 256 and 512 + bits. + + Once recovered from SLIP-39 encoding, the seed data is provided and presented as simple hex. + + .. note:: + This class inherits from the ``ISeed`` class, thereby ensuring that all functions are accessible. + + """ + lengths: List[int] = [32, 64, 128] + + @classmethod + def name(cls) -> str: + """ + Get the name of the seeds class. + + :return: The name of the seeds class. + :rtype: str + """ + + return "SLIP39" + + @classmethod + def from_mnemonic( + cls, + mnemonic: Union[str, IMnemonic], + passphrase: Optional[str] = None, + language: Optional[str] = None, + ) -> str: + """Converts a mnemonic phrase to its corresponding raw entropy. + + The Mnemonic representation for SLIP-39 seeds is simple hex, and must be of the supported + SLIP-39 entropy sizes: 128, 256 or 512 bits. + + To support the backup and recovery of BIP-39 mnemonic phrases to/from SLIP-39, we accept a + BIP39 IMnemonic or mnemonic phrase, and recover the underlying (original) entropy encoded by + the BIP-39 mnemonic phrase. + + In other words, you may supply a 12-word BIP39 Mnemonic like "zoo zoo ... zoo wrong", and + recover the original seed entropy 0xffff...ff. For SLIP-39 HD wallet derivations, this seed + entropy is used /directly/ to derive the wallets, unlike for BIP-39 which hashes the entropy + to extend it to 512 bits and uses the extended entropy to derive the wallets. + + :param mnemonic: The mnemonic phrase to be decoded. Can be a string or an instance of `IMnemonic`. + :type mnemonic: Union[str, IMnemonic] + + :param passphrase: An optional passphrase used for additional security when decoding the mnemonic phrase. + :type passphrase: Optional[str] + + :return: The decoded seed as a string. + :rtype: str + + """ + + if not isinstance(mnemonic, IMnemonic): + # Not an IMnemonic; must be a str. Try the supported mnemonic encodings we'll allow for + # SLIP39 seeds, converting the mnemonic phrase to an IMnemonic if recognized. + # + # TODO: Add other Seed entropy Mnemonics + allowed_entropy = [ + SLIP39Mnemonic, + BIP39Mnemonic, + ] + + for M in allowed_entropy: + if M.is_valid(mnemonic, language=language): + mnemonic = M(mnemonic=mnemonic, language=language) + break + else: + raise EntropyError( + "Invalid entropy instance", expected=[str, ] + allowed_entropy, got=type(mnemonic), + ) + + # Some kind of IMnemonic (eg. a BIP39Mnemonic); get and return its raw entropy as hex + entropy = mnemonic.decode(mnemonic=mnemonic.mnemonic(), language=mnemonic.language()) + if len(entropy) * 4 not in SLIP39Mnemonic.words_to_entropy_strength.values(): + raise EntropyError( + "Invalid entropy size in bits", expected=SLIP39Mnemonic.words_to_entropy_strength.values(), got=len(entropy) * 4, + ) + return entropy diff --git a/hdwallet/utils.py b/hdwallet/utils.py index 9754ea5a..30abe1ad 100644 --- a/hdwallet/utils.py +++ b/hdwallet/utils.py @@ -90,7 +90,7 @@ def path_to_indexes(path: str) -> List[int]: return [] elif path[0:2] != "m/": raise DerivationError( - f"Bad path format", expected="like this type of path \"m/0'/0\"", got=path + "Bad path format", expected="like this type of path \"m/0'/0\"", got=path ) indexes: List[int] = [] @@ -139,17 +139,17 @@ def normalize_index( if isinstance(index, tuple): if len(index) != 2: raise DerivationError( - f"Bad index length", expected=2, got=len(index) + "Bad index length", expected=2, got=len(index) ) elif not isinstance(index[0], int) or not isinstance(index[1], int): raise DerivationError( - f"Invalid index types", + "Invalid index types", expected="both indexes must be integer instance", got=f"{type(index[0])}-{type(index[0])}" ) elif index[0] < 0 or index[1] < 0: raise DerivationError( - f"Bad index format", expected="both must be non-negative-numbers", got=index + "Bad index format", expected="both must be non-negative-numbers", got=index ) elif index[0] > index[1]: raise DerivationError( @@ -175,18 +175,18 @@ def normalize_index( ) return from_index, to_index, hardened raise DerivationError( - f"Bad index format", expected="{non-negative-number} | {number}-{number}", got=index + "Bad index format", expected="{non-negative-number} | {number}-{number}", got=index ) elif isinstance(index, int): if index < 0: raise DerivationError( - f"Bad index format", expected="non-negative-number", got=index + "Bad index format", expected="non-negative-number", got=index ) return index, hardened raise DerivationError( - f"Invalid index instance", expected=(str, int, tuple), got=type(index) + "Invalid index instance", expected=(str, int, tuple), got=type(index) ) @@ -223,7 +223,7 @@ def normalize_derivation( return f"{_path}/", _indexes, _derivations elif path[0:2] != "m/": raise DerivationError( - f"Bad path format", expected="like this type of path \"m/0'/0\"", got=path + "Bad path format", expected="like this type of path \"m/0'/0\"", got=path ) elif not path: return f"{_path}/", _indexes, _derivations @@ -499,7 +499,7 @@ def get_bytes(data: AnyStr, unhexlify: bool = True) -> bytes: else: return bytes(data, 'utf-8') else: - raise TypeError("Agreement must be either 'bytes' or 'string'!") + raise TypeError("Agreement must be either 'bytes' or 'str'!") def bytes_reverse(data: bytes) -> bytes: @@ -518,27 +518,30 @@ def bytes_reverse(data: bytes) -> bytes: return bytes(tmp) -def bytes_to_string(data: Union[bytes, str]) -> str: - """ - Convert bytes or string data to a hexadecimal string representation. +def bytes_to_string(data: AnyStr, unhexlify: Optional[bool] = None) -> str: + """Convert bytes or string (hexadecimal, or UTF-8 decoded) data to a hexadecimal string representation. + + If the default unhexlify == None is provided, will attempt to auto-detect non-empty hex strings, + and thus reject hex strings of accidentally odd length instead of accepting them (surprisingly + and almost certainly incorrectly!) as UTF-8 encoded binary data (get_bytes is resilient to + surrounding whitespace, so we must be, too). :param data: The bytes or string data to convert to hexadecimal string. :type data: Union[bytes, str] + :param unhexlify: Flag indicating whether to interpret strings as hexadecimal (default None). + :type unhexlify: Optional[bool] - :return: The hexadecimal string representation of the input data. + :return: The hexadecimal string representation of the input data, empty if no data. :rtype: str + """ if not data: return '' - try: - bytes.fromhex(data) - return data - except (ValueError, TypeError): - pass - if not isinstance(data, bytes): - data = bytes(data, 'utf-8') - return data.hex() + if unhexlify is None: + unhexlify = isinstance(data, str) and all(c in string.hexdigits for c in data.strip()) + binary = get_bytes(data, unhexlify=unhexlify) + return binary.hex() def bytes_to_integer(data: bytes, endianness: Literal["little", "big"] = "big", signed: bool = False) -> int: @@ -775,7 +778,7 @@ def words_to_bytes_chunk( words_list[i]: i for i in range(len(words_list)) } - word_1_index, word_2_index, word_3_index = ( + word_1_index, word_2_index, word_3_index = ( words_list_with_index[word_1], words_list_with_index[word_2] % words_list_length, words_list_with_index[word_3] % words_list_length ) diff --git a/hdwallet/wif.py b/hdwallet/wif.py index 795ffb3f..e4ac3909 100644 --- a/hdwallet/wif.py +++ b/hdwallet/wif.py @@ -71,7 +71,7 @@ def decode_wif( raw: bytes = decode(wif) if not raw.startswith(integer_to_bytes(wif_prefix)): - raise WIFError(f"Invalid Wallet Import Format (WIF)") + raise WIFError("Invalid Wallet Import Format (WIF)") prefix_length: int = len(integer_to_bytes(wif_prefix)) prefix_got: bytes = raw[:prefix_length] @@ -84,7 +84,7 @@ def decode_wif( wif_type: str = "wif" if len(private_key) not in [33, 32]: - raise WIFError(f"Invalid Wallet Import Format (WIF)") + raise WIFError("Invalid Wallet Import Format (WIF)") elif len(private_key) == 33: private_key = private_key[:-len(integer_to_bytes(SLIP10_SECP256K1_CONST.PRIVATE_KEY_COMPRESSED_PREFIX))] wif_type = "wif-compressed" diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..698cb52b --- /dev/null +++ b/mypy.ini @@ -0,0 +1,18 @@ +[mypy] +pretty = True +show_error_codes = True + +python_version = 3.9 +# Some packages have the same name, eg. clients.bip44, hds.bip44 and derivations.bip44 +explicit_package_bases = True +warn_return_any = true +warn_unused_configs = true +check_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true + +exclude = ^(examples/|docs/|build/)$ diff --git a/nixpkgs.nix b/nixpkgs.nix new file mode 100644 index 00000000..526107e5 --- /dev/null +++ b/nixpkgs.nix @@ -0,0 +1,4 @@ +import (fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/25.05.tar.gz"; + sha256 = "1915r28xc4znrh2vf4rrjnxldw2imysz819gzhk9qlrkqanmfsxd"; +}) diff --git a/requirements.txt b/requirements.txt index 5b336f13..a7de3f81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,6 @@ ed25519-blake2b>=1.4.1,<2 coincurve>=20.0.0,<21 pynacl>=1.5.0,<2 base58>=2.1.1,<3 -cbor2>=5.6.1,<6 \ No newline at end of file +cbor2>=5.6.1,<6 +shamir-mnemonic-slip39>=0.4,<0.5 +tabulate-slip39>=0.10.6,<1 diff --git a/requirements/cli.txt b/requirements/cli.txt index a48dbd9b..117c611d 100644 --- a/requirements/cli.txt +++ b/requirements/cli.txt @@ -1,4 +1,3 @@ click>=8.1.7,<9 click-aliases>=1.0.5,<2 -tabulate>=0.9.0,<1 -bip38>=1.4.1,<2 \ No newline at end of file +bip38>=1.4.1,<2 diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 00000000..fe58f629 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,4 @@ +build +setuptools +wheel +flake8 diff --git a/requirements/docs.txt b/requirements/docs.txt index abd6441a..fcd4c94d 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,3 +1,3 @@ sphinx>=8.1.3,<9 sphinx-click>=6.0.0,<7 -furo==2024.8.6 \ No newline at end of file +furo==2024.8.6 diff --git a/requirements/tests.txt b/requirements/tests.txt index 8c5ae297..7736ca08 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,3 +1,4 @@ pytest>=8.3.2,<9 coverage>=7.6.4,<8 -tox>=4.23.2,<5 \ No newline at end of file +tox>=4.23.2,<5 +mypy diff --git a/setup.py b/setup.py index 386db809..b34cdaf7 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ # requirements/{name}.txt def get_requirements(name: str) -> List[str]: with open(f"{name}.txt", "r") as requirements: - return list(map(str.strip, requirements.read().split("\n"))) + return list(filter(None, map(str.strip, requirements.read().split("\n")))) # README.md diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..57fba898 --- /dev/null +++ b/shell.nix @@ -0,0 +1,19 @@ +{ pkgs ? import ./nixpkgs.nix {} }: + +let + targets = import ./default.nix { + inherit pkgs; + }; + targeted = builtins.getEnv "TARGET"; + selected = targeted + pkgs.lib.optionalString (targeted == "") "py313"; +in + +with pkgs; + +mkShell { + buildInputs = lib.getAttrFromPath [ selected "buildInputs" ] targets; + + shellHook = '' + echo "Welcome to the Python ${selected} environment!" + ''; +} diff --git a/tests/cli/test_cli_mnemonic.py b/tests/cli/test_cli_mnemonic.py index ab48f0b6..a3195093 100644 --- a/tests/cli/test_cli_mnemonic.py +++ b/tests/cli/test_cli_mnemonic.py @@ -6,9 +6,10 @@ # file COPYING or https://opensource.org/license/mit import json +import unicodedata from hdwallet.cli.__main__ import cli_main - +from hdwallet.mnemonics.imnemonic import unmark def check_mnemonics( cli_word, @@ -19,24 +20,43 @@ def check_mnemonics( entropy, mnemonic ): - - output_word = json.loads(cli_word.output) - output_entropy = json.loads(cli_entropy.output) - - assert cli_word.exit_code == 0 - assert cli_entropy.exit_code == 0 - - assert output_word["client"] == client - assert output_entropy["client"] == client - - assert output_word["words"] == words - assert output_entropy["words"] == words - - assert output_word["language"].lower() == language - assert output_entropy["language"].lower() == language - - assert output_entropy["mnemonic"] == mnemonic - + def nfc_unmarked_set( phrase ): + nfc = unicodedata.normalize( "NFC", phrase ) + return set( (nfc, unmark( nfc )) ) + + def json_parser( json_i ): + json_s = ''.join( json_i ) + try: + data = json.loads(json_s) + except Exception as exc: + print( f"Failed to parse JSON: {exc} from:\n{json_s}" ) + raise + return data + + try: + + output_word = json_parser( cli_word.output ) + output_entropy = json_parser( cli_entropy.output ) + + assert cli_word.exit_code == 0 + assert cli_entropy.exit_code == 0 + + assert output_word["client"] == client + assert output_entropy["client"] == client + + assert output_word["words"] == words + assert output_entropy["words"] == words + + assert output_word["language"].lower() == language + assert output_entropy["language"].lower() == language + + # Mnemonics recovered will be in NFC form, and my be with or without Unicode Marks (for + # example, mnemonics that are ambiguous between English and French may not have accents). + assert nfc_unmarked_set( mnemonic ) & nfc_unmarked_set( output_entropy["mnemonic"] ) + + except Exception as exc: + print( f"Failed {client} w/ {language} mnemonic: {mnemonic}: {exc}" ) + raise def test_cli_mnemonic(data, cli_tester): @@ -96,9 +116,11 @@ def test_cli_mnemonic(data, cli_tester): entropy_args.append("--checksum") entropy_args.append(str(mnemonic_data["checksum"])) + #print(" ".join(entropy_args)) cli_entropy = cli_tester.invoke( cli_main, entropy_args ) + #print(f" --> {cli_entropy.output}") check_mnemonics( cli_word=cli_word, diff --git a/tests/cli/test_cli_seed.py b/tests/cli/test_cli_seed.py index a6fae858..f265270a 100644 --- a/tests/cli/test_cli_seed.py +++ b/tests/cli/test_cli_seed.py @@ -21,7 +21,8 @@ def test_cli_seed(data, cli_tester): "generate", "seed", "--client", client, "--mnemonic-type", mnemonic_type, - "--mnemonic", data["seeds"][client][words][mnemonic_type][language]["mnemonic"] + "--mnemonic", data["seeds"][client][words][mnemonic_type][language]["mnemonic"], + "--language", language, ] if data["seeds"][client][words][mnemonic_type][language]["passphrases"] != None: @@ -31,9 +32,11 @@ def test_cli_seed(data, cli_tester): else: seed = data["seeds"][client][words][mnemonic_type][language]["non-passphrase-seed"] + #print(" ".join(cli_args)) cli = cli_tester.invoke( cli_main, cli_args ) + #print(f" --> {cli.output}") output = json.loads(cli.output) assert output["client"] == client @@ -45,7 +48,8 @@ def test_cli_seed(data, cli_tester): "generate", "seed", "--client", client, "--cardano-type", cardano_type, - "--mnemonic", data["seeds"][client][words][cardano_type][language]["mnemonic"] + "--mnemonic", data["seeds"][client][words][cardano_type][language]["mnemonic"], + "--language", language, ] if data["seeds"][client][words][cardano_type][language]["passphrases"] != None: @@ -55,9 +59,11 @@ def test_cli_seed(data, cli_tester): else: seed = data["seeds"][client][words][cardano_type][language]["non-passphrase-seed"] + #print(" ".join(cli_args)) cli = cli_tester.invoke( cli_main, cli_args ) + #print(f" --> {cli.output}") output = json.loads(cli.output) assert output["client"] == client @@ -67,7 +73,8 @@ def test_cli_seed(data, cli_tester): cli_args = [ "generate", "seed", "--client", client, - "--mnemonic", data["seeds"][client][words][language]["mnemonic"] + "--mnemonic", data["seeds"][client][words][language]["mnemonic"], + "--language", language, ] if data["seeds"][client][words][language]["passphrases"] != None: @@ -77,10 +84,12 @@ def test_cli_seed(data, cli_tester): else: seed = data["seeds"][client][words][language]["non-passphrase-seed"] + #print(" ".join(cli_args)) cli = cli_tester.invoke( cli_main, cli_args ) + #print(f" --> {cli.output}") output = json.loads(cli.output) assert output["client"] == client - assert output["seed"] == seed \ No newline at end of file + assert output["seed"] == seed diff --git a/tests/data/json/entropies.json b/tests/data/json/entropies.json index f7991992..32563150 100644 --- a/tests/data/json/entropies.json +++ b/tests/data/json/entropies.json @@ -33,6 +33,23 @@ "strength": 256 } }, + "SLIP39": { + "128": { + "name": "SLIP39", + "entropy": "ffffffffffffffffffffffffffffffff", + "strength": 128 + }, + "256": { + "name": "SLIP39", + "entropy": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "strength": 256 + }, + "512": { + "name": "SLIP39", + "entropy": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "strength": 512 + } + }, "Electrum-V1": { "128": { "name": "Electrum-V1", diff --git a/tests/data/json/hdwallet.json b/tests/data/json/hdwallet.json index 3870fc76..ed3e26e9 100644 --- a/tests/data/json/hdwallet.json +++ b/tests/data/json/hdwallet.json @@ -1210,6 +1210,84 @@ } ] }, + "byron-ledger": { + "cryptocurrency": "Cardano", + "symbol": "ADA", + "network": "mainnet", + "coin_type": 1815, + "entropy": "b36caf4ad6e4f90b5a46a497f8dbeb6aa10ddfb517eaec1ceb08eb5fdfcc0bc3", + "strength": 256, + "mnemonic": "recall grace sport punch exhibit mad harbor stand obey short width stem awkward used stairs wool ugly trap season stove worth toward congress jaguar", + "passphrase": null, + "language": "english", + "seed": "c167860ff4b291173e28ba7e886b1b58723c8ccf4455e003b3c56b99516378701454db9b96a7574445d6a519bcf4af1fa770a0ea325151d808949e1a7148a461", + "ecc": "Kholaw-Ed25519", + "hd": "Cardano", + "cardano_type": "byron-ledger", + "semantic": "p2pkh", + "root_xprivate_key": "xprv3QESAWYc9vDdZaGUCzZVAaBmCAxico5f7sW35WCQFemzjQV5k9NeqFMWYyMdLLyhEKDPWSSZyFLecgMhdiQyE2ReAgfySR2V9bYfjiw2h6GojoRnEnvyAmSF7x7BXNwNZdRriMReX3da9tsRMMS591a", + "root_xpublic_key": "xpub661MyMwAqRbcFaX4J1Dep5qKMZxtLWXBZZLKfdADBxwXiaiM5QSz3NMTmbHgHUaFjzs87US9FNqJh9B9v2L46SWrMhdvUDLgFyvKfMoeXYX", + "root_private_key": "a08cf85b564ecf3b947d8d4321fb96d70ee7bb760877e371899b14e2ccf88658104b884682b57efd97decbb318a45c05a527b9cc5c2f64f7352935a049ceea60", + "root_chain_code": "680d52308194ccef2a18e6812b452a5815fbd7f5babc083856919aaf668fe7e4", + "root_public_key": "00c368c07566d1218d6dd2c7d945fe8b627f8eb6900dba953e112184cbd213b993", + "strict": true, + "derivations": [ + { + "at": { + "path": "m/1852'/1815'/0'/0/0", + "indexes": [ + 2147485500, + 2147485463, + 2147483648, + 0, + 0 + ], + "depth": 5, + "purpose": 1852, + "coin_type": 1815, + "account": 0, + "role": "external-chain", + "address": 0 + }, + "xprivate_key": "xprv3TKYfq8Acwj13LurBapHCv9dEps7QYpPKxZLmNnJDCEBDLRNicYTj6bHDfZ5uWQ7mQQCF3vqgsgX3ZTrgkMQg9ypnYn8J4CsMXDowrBJ53gLiUYy8jRCn5H35N3zVrexTx9eoF8LaMpT6xyA4qow8sL", + "xpublic_key": "xpub6GQJoKhgd9wFz885guVH5Qn5ZenBvQAddy7DmyVHnEhYebc2AYxzaMho79G8oXUNNtgNw32FwYtXYh4zE7qAjyeCpF9mLsZjK6Nc5zaAchG", + "private_key": "90c9771c3b6d3daaba283b315036cee82a000ccb4a6e6227e1c7b2f2e4f88658d96d9ecb0e9e605ce723779ad0d3388d9abb504b0fd63a5129593709d1394449", + "chain_code": "563f688471af3a2de595a30813bbe676b0bd5aa06e3615895e915e3459bc70d8", + "public_key": "007fe6111e969e68d02f1d1e588c4c8f22584189a2714f9239a28888e645a2ee8a", + "hash": "3c4d110808d5bcd54972ea86add1c77cc77d47d7", + "fingerprint": "3c4d1108", + "parent_fingerprint": "86556a41", + "address": "Ae2tdPwUPEYxN6HXvUwEjLK2CMYXqH5oxtbi3DwxabvUuBkFp6CyHstUXpb" + }, + { + "at": { + "path": "m/1852'/1815'/1'/0/0", + "indexes": [ + 2147485500, + 2147485463, + 2147483649, + 0, + 0 + ], + "depth": 5, + "purpose": 1852, + "coin_type": 1815, + "account": 1, + "role": "external-chain", + "address": 0 + }, + "xprivate_key": "xprv3T2gYJLSzeavxXR7cw6aCkdAMrxXr66p6kk7TuJRBALoLxn2LUdEAisKKUqT9oT4U3C35pR64EiJiTURXXnCWnLYGuka3nGnJJ2RGTA6WTYVBDFxX1v4TosbkVRm2eP8TnnRwHwAQv8DTBXfZr2PuQi", + "xpublic_key": "xpub6FRY7y2QEcx78ubr3KZpK7Xaaqxi7g11Z3H68v8hLn1QTLB9Whm3B1NJtMnyotY6EZZwBa1uw6u1XHGFJGxDr2MTmH8RooYCr46SuhGkZ8c", + "private_key": "b81ee3b3e8e13968c85cfd6e33a444ec3aece4d7686fc31de9e309e0e2f88658e01aeab75ea1bf697ce2dfa9bf3e0aa578b34e09d11678e709a6f37faf597dde", + "chain_code": "7f3e38be94e9504c91e91bf3ce247dace7b4c384c253969c48be6eb304a8454e", + "public_key": "007efddb16c762e60c6f4d3084bda0f5b67dc794d6a58c5b84730df93d4426fdce", + "hash": "72ed3a72afa082404b3841f0ca4a90764f9963fd", + "fingerprint": "72ed3a72", + "parent_fingerprint": "012ac401", + "address": "Ae2tdPwUPEZ9de6gCHgEG71kpA9yHZHKaunzKBUCxAwF8dhZ6oMZDjNpnv4" + } + ] + }, "byron-legacy": { "cryptocurrency": "Cardano", "symbol": "ADA", @@ -1822,4 +1900,4 @@ ] } } -} \ No newline at end of file +} diff --git a/tests/data/json/mnemonics.json b/tests/data/json/mnemonics.json index 933c84f1..8ea1aa19 100644 --- a/tests/data/json/mnemonics.json +++ b/tests/data/json/mnemonics.json @@ -104,7 +104,39 @@ "spanish": "zona rama nuca carbón atar artista meta funda avena sonido baile educar ostra ruido juzgar chivo octavo lector brillo ingenio hora yeso sonido helio", "turkish": "zorlu siroz paraşüt defter bavul baca obez kapı beyoğlu tesir bodrum gusül polat tabaka masum doruk peçete mercek coğrafya liyakat kurmay zihinsel tesir köstebek" } - } + }, + { + "name": "BIP39", + "entropy": "98e612eaa0de01e80d1f99be9d52be46", + "words": 12, + "languages": { + "english": "ocean correct rival double theme village crucial veteran salon tunnel question minute" + } + }, + { + "name": "BIP39", + "entropy": "a9a70f43a52ea9f60ef7ccd57e37164e", + "words": 12, + "languages": { + "french": "ocean correct rival double theme village crucial veteran salon tunnel question minute" + } + }, + { + "name": "BIP39", + "entropy": "a2544a353acc0eac3c294c11775696a9cd1726b244f4211bad237b29e9e23d80", + "words": 24, + "languages": { + "english": "pelican pelican minute intact science figure vague civil badge rival pizza fatal sphere nation simple ozone canal talent emotion wagon ozone valve voyage angle" + } + }, + { + "name": "BIP39", + "entropy": "b536a67341dd7ecd3d599317da1eea31be1f45774d81287cdd477f9b03ecbfb8", + "words": 24, + "languages": { + "french": "pelican pelican minute intact science figure vague civil badge rival pizza fatal sphere nation simple ozone canal talent emotion wagon ozone valve voyage angle" + } + } ], "Electrum-V1": [ { @@ -285,16 +317,16 @@ "words": 12, "checksum": false, "languages": { - "chinese-simplified": "宽 事 理 密 道 催 霸 挂 制 妇 出 间", - "dutch": "motregen benadeeld atrium geslaagd bizar traktaat zulk sneu badderen robot afspeel best", "english": "mundane aunt answers gang beer thirsty zebra shuffled apricot rest affair awakened", + "dutch": "motregen benadeeld atrium geslaagd bizar traktaat zulk sneu badderen robot afspeel best", "french": "moudre avide arcade feuille baver site vrac repli article promener adresser azote", "german": "jäger ansporn amphibie erosion auftritt salat zollhund pedal anbieten muster abteil aorta", "italian": "medaglia arte andare egiziano aviatore specchio zattera rumore annuncio pupazzo affogato aspirina", "japanese": "ずっしり いもたれ いせき けおりもの うけつけ なげる ひかん ていおん いちど だんねつ あぶる いらい", "portuguese": "loquaz atazanar anfora faturista baqueta solver zabumba recusavel apito pecuniario adriatico aturdir", "russian": "октябрь билет ателье иметь бугор уран январь сфера аэропорт сердце аксиома бицепс", - "spanish": "hurto apio alteza derrota asa pasión rifa nube ámbar moda acuerdo aprobar" + "spanish": "hurto apio alteza derrota asa pasión rifa nube ámbar moda acuerdo aprobar", + "chinese-simplified": "宽 事 理 密 道 催 霸 挂 制 妇 出 间" } }, { @@ -303,16 +335,16 @@ "words": 13, "checksum": true, "languages": { - "chinese-simplified": "纸 呀 刑 斜 足 措 监 联 末 喊 穷 咱 监", - "dutch": "leguaan nullijn knaven pablo hekman nylon rein diode napijn tuma tout tulp nylon", "english": "leopard noted jingle pancakes hairy nowhere ravine dauntless nasty today testing tobacco tobacco", + "dutch": "leguaan nullijn knaven pablo hekman nylon rein diode napijn tuma tout tulp nylon", "french": "litre norme jardin parler fuser notre poupe cirage muse sombre singe solvant litre", "german": "habicht kaugummi gehweg lausbub fazit keilerei monat brandung jupiter schmied sachbuch schlager schlager", "italian": "insalata muraglia giusto panfilo farmacia musica primario carbone mettere spuntare spagnolo spumante spumante", "japanese": "じかん せつだん さぎょう そむりえ けんにん せつび たれる おどり すぼん なやむ ないせん なめる なやむ", "portuguese": "itrio miau icar noturno fumo migratorio ozonizar coordenativa luxuriante sujo sodomizar suite ozonizar", "russian": "мятый оценка мачта пузырь клятва очищать сбегать гильза орган уцелеть упасть уходить клятва", - "spanish": "gen jornada finca llama eficaz joya miembro burbuja imponer pelar parque peine imponer" + "spanish": "gen jornada finca llama eficaz joya miembro burbuja imponer pelar parque peine imponer", + "chinese-simplified": "纸 呀 刑 斜 足 措 监 联 末 喊 穷 咱 监" } }, { @@ -321,16 +353,16 @@ "words": 24, "checksum": false, "languages": { - "chinese-simplified": "李 舞 感 炼 熔 黑 能 铜 毒 炮 枝 柬 留 节 违 朗 株 纳 卡 坦 周 意 南 阀", - "dutch": "giebelen smoel fabel saksisch vieux hiaat alikruik pixel pieneman riskant tragedie soapbox ionisch eind tosti sediment sulfaat pakzadel peuk rugpijn eurocent boei dagprijs tabak", "english": "gemstone sewage equip runway unknown haystack ahead playful pigment reorder textbook sidekick identity drunk template satin sovereign pastry physics roomy emotion betting cohesive spying", + "dutch": "giebelen smoel fabel saksisch vieux hiaat alikruik pixel pieneman riskant tragedie soapbox ionisch eind tosti sediment sulfaat pakzadel peuk rugpijn eurocent boei dagprijs tabak", "french": "fils rendre doute raie tanin galop aigre pieu perte produire sinus respect haie crin sigle ratio rubis partir perdu quitter docteur bidule certes rustre", "german": "erwidern partner druck obdach taktung feldbett afrika lümmel lohn mumie saft person foliant cousin rüstung omelett quote leder locken neugier donner aussage biologe rapsöl", "italian": "emettere rotonda crimine rete tenebre femmina aiutare petrolio pentirsi pugilato sparire sacco forzare cigno sospiro rilevare scuola panorama pendenza recitare corrente babbuino cacciare seme", "japanese": "けさき つるみく きない ちめいど ぬまえび こいぬ あらゆる たいまつばな だいたい たんてい なおす ていこく こつぶ がっしょう どんぶり つうじょう てんき そよかぜ だいじょうぶ ちたい きすう うすい おおよそ てんぷら", "portuguese": "felicidade rasurar ejetar poquer toar gado agito oigale ocre pavoroso software regurgitar guaxinim diatribe slide pueril ruas nublar oceanografo pizzaria ecumenismo begonia chuvoso rural", "russian": "инфекция суровый емкость совет хозяин кодекс амбар ремонт рвать секунда упор счет ледяной держать умолять спать ткань пурга рапорт случай дыра бухта выгодный трибуна", - "spanish": "diablo norte cocina muro pleno élite agitar malo macho mitad parte nudo etnia carro parcela náusea olor llegar lustro mozo circo asno bondad óptica" + "spanish": "diablo norte cocina muro pleno élite agitar malo macho mitad parte nudo etnia carro parcela náusea olor llegar lustro mozo circo asno bondad óptica", + "chinese-simplified": "李 舞 感 炼 熔 黑 能 铜 毒 炮 枝 柬 留 节 违 朗 株 纳 卡 坦 周 意 南 阀" } }, { @@ -339,17 +371,17 @@ "words": 25, "checksum": true, "languages": { - "chinese-simplified": "往 祥 钢 众 砂 筒 完 阵 挂 勤 爱 氮 黄 斯 辉 起 永 接 腿 喝 教 碳 烯 做 起", - "dutch": "faliekant wals isaac erna whirlpool scout eenruiter ponywagen sneu twitteren guido vrekkig gekskap folder zijbeuk atsma omdoen clicheren spiraal valreep drol oudachtig soigneren cruijff sneu", "english": "error value idiom efficient vocal sanity donuts poker shuffled together greater upstairs fuming farming womanly antics obnoxious cafe slackens tsunami dewdrop oyster sifting civilian womanly", + "dutch": "faliekant wals isaac erna whirlpool scout eenruiter ponywagen sneu twitteren guido vrekkig gekskap folder zijbeuk atsma omdoen clicheren spiraal valreep drol oudachtig soigneren cruijff sneu", "french": "drame toque haine devoir tutu raser coupure pirate repli sondage foyer texte faune enfermer visite arceau obtenir cabinet rigide stagiaire cocon panda reste cavale cabinet", "german": "dünung traum folklore dezibel verb oktave chipsatz maibaum pedal schrank familie ticken erfüllen einfall wrack ampulle klee beladen polieren sitzbank bugwelle langmut pfa bezahlen familie", "italian": "cronaca tromba fosforo comune vaglio riferire chirurgo pigiama rumore staccare evacuare topolino ebbrezza deposito virgola androide nipote bombola sberla stufa cauzione padella sale buffo cauzione", "japanese": "きねん ねんぶつ こてい きかく はいご ちんもく かがし たえる ていおん ならぶ けろけろ ねほりはほり ぐんて きわめる はんかく いぜん せんげん えすて てすり にっけい おんどけい そなえる ていし おうじ おんどけい", "portuguese": "elmo umero gude druso valvula pterossauros deltoide olvidavel recusavel suntuoso fominha tufo facultativo enzimatico vozes aniversario morubixaba bucolico ridiculo tatuar cupula noel rejuvenescimento ceifeiro druso", "russian": "жажда членство лежать древний шикарный соус двор рифма сфера учитель кибитка цирк излагать замечать эскиз атлас пачка вишневый таможня финал гость пруд сшивать всадник атлас", - "spanish": "cofre producto evadir chiste quince nasal capitán manso nube peligro dulce posible dedo cordón rencor altivo lacio barba ocaso pesa calamar linterna nueve bobina nueve" + "spanish": "cofre producto evadir chiste quince nasal capitán manso nube peligro dulce posible dedo cordón rencor altivo lacio barba ocaso pesa calamar linterna nueve bobina nueve", + "chinese-simplified": "往 祥 钢 众 砂 筒 完 阵 挂 勤 爱 氮 黄 斯 辉 起 永 接 腿 喝 教 碳 烯 做 起" } } ] -} \ No newline at end of file +} diff --git a/tests/data/json/seeds.json b/tests/data/json/seeds.json index b874c122..679b3d0e 100644 --- a/tests/data/json/seeds.json +++ b/tests/data/json/seeds.json @@ -500,6 +500,35 @@ } } }, + "SLIP39": { + "12": { + "english": { + "mnemonic": "ffffffffffffffffffffffffffffffff", + "non-passphrase-seed": "ffffffffffffffffffffffffffffffff", + "passphrases": null + }, + "english": { + "mnemonic": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "non-passphrase-seed": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "passphrases": null + }, + "english": { + "mnemonic": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "non-passphrase-seed": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "passphrases": null + }, + "english": { + "mnemonic": "edge typical academic academic academic boring radar cluster domestic ticket fumes remove velvet fluff video crazy chest average script universe exhaust remind helpful lamp declare garlic repeat unknown bucket adorn sled adult triumph source divorce premium genre glimpse level listen ancestor wildlife writing document wrist judicial medical detect frost leaves language jerky increase glasses extra alto deploy demand greatest", + "non-passphrase-seed": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "passphrases": null + }, + "english": { + "mnemonic": "bracelet cleanup acrobat easy acquire critical exceed agency verify envy story best facility process syndrome discuss health twin ugly spew unknown spider level academic lying large slap venture hairy election legal away negative easel learn item trial miracle hour provide survive pleasure clock acne faint priest loyalty sunlight award forget ambition failure threaten kind dictate lips branch slice space\nbracelet cleanup beard easy acne visitor scroll finger skin trash browser union energy endorse scramble staff sprinkle salt alpha dive sweater pickup cage obtain leader clothes acid dive frozen category desert thorn music western home owner manager apart much hobo march adequate eraser crazy short smith force flame primary phrase sprinkle frost trial crunch fancy piece crunch scroll triumph", + "non-passphrase-seed": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "passphrases": null + } + } + }, "Cardano": { "12": { "byron-icarus": { @@ -2434,14 +2463,6 @@ "Electrum-V2": { "12": { "standard": { - "chinese-simplified": { - "mnemonic": "轧 驻 省 刮 碰 示 拖 黎 典 班 其 抹", - "non-passphrase-seed": "9c5af65ec3d73641f7a691ab41185ca3217d5592a3356547e4d9ff3d4f13b812f0edff3734d271f463935fe25bdd5699882a4594941f210355368bdddb0de607", - "passphrases": { - "hdwallet": "371cc005017bc30ba69d36fb16843c488562e20ed49c203204b94c9120c5a1e38f988eca33474541b4b4aed81f3dc0215b018d29c895f107861e819bfd7620c2", - "ゆしゅつ": "e435d04ca4213f9327e858c97a53c297860fa416ec9b2b4e3df2aebda08fe69e389c102b3c7ece7735e116ffa6b33bf988c4f0b6342637722872fab8f3a6414a" - } - }, "english": { "mnemonic": "gold engine arch point merge review extend diesel allow negative act night", "non-passphrase-seed": "45bc3118e9de3719d677440ca5a6df51e8528b8660f902a87493e08b019f1cb9755aee31503737e604d13b7cc61346583f36c795ded8a359729d80ee96845873", @@ -2465,6 +2486,14 @@ "hdwallet": "ec01762b64aa958965872608d5911126648776e9da5e303c68744d16540e5dd0e1ae93ecc739d9c03faea4829e83e1957b2b5692fb865a98fc8f9ffcd29b8a8f", "ゆしゅつ": "7ed4b7c6a21eb6359922eaf7d5a6a1ef284316a0d7873e839570595452060ee5426e6b961d20ea21546525777890b857ddd118efd335d346dc456d677a2cf7bf" } + }, + "chinese-simplified": { + "mnemonic": "轧 驻 省 刮 碰 示 拖 黎 典 班 其 抹", + "non-passphrase-seed": "9c5af65ec3d73641f7a691ab41185ca3217d5592a3356547e4d9ff3d4f13b812f0edff3734d271f463935fe25bdd5699882a4594941f210355368bdddb0de607", + "passphrases": { + "hdwallet": "371cc005017bc30ba69d36fb16843c488562e20ed49c203204b94c9120c5a1e38f988eca33474541b4b4aed81f3dc0215b018d29c895f107861e819bfd7620c2", + "ゆしゅつ": "e435d04ca4213f9327e858c97a53c297860fa416ec9b2b4e3df2aebda08fe69e389c102b3c7ece7735e116ffa6b33bf988c4f0b6342637722872fab8f3a6414a" + } } }, "segwit": { @@ -2919,4 +2948,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/data/raw/languages.txt b/tests/data/raw/languages.txt index 4ac8bad4..0af39e74 100644 --- a/tests/data/raw/languages.txt +++ b/tests/data/raw/languages.txt @@ -5,18 +5,23 @@ English BIP39 Languages ------------------- -Chinese-Simplified -Chinese-Traditional -Czech English French +Spanish Italian -Japanese -Korean -Portuguese Russian -Spanish +Portuguese +Czech Turkish +Korean +Chinese-Simplified +Chinese-Traditional +Japanese + + +SLIP39 Languages +------------------ +English Electrum-V1 Languages @@ -26,23 +31,21 @@ English Electrum-V2 Languages ----------------------- -Chinese-Simplified English -Portuguese Spanish +Portuguese +Chinese-Simplified Monero Languages ------------------ -Chinese-Simplified -Dutch English French +Spanish German +Dutch Italian -Japanese -Portuguese Russian -Spanish - - +Portuguese +Japanese +Chinese-Simplified diff --git a/tests/data/raw/strengths.txt b/tests/data/raw/strengths.txt index 1aaf58c4..19735481 100644 --- a/tests/data/raw/strengths.txt +++ b/tests/data/raw/strengths.txt @@ -12,6 +12,13 @@ BIP39 Strengths 256 +SLIP39 Strengths +------------------ +128 +256 +512 + + Electrum-V1 Strengths ----------------------- 128 diff --git a/tests/hdwallet/entropies/test_entropies_slip39.py b/tests/hdwallet/entropies/test_entropies_slip39.py new file mode 100644 index 00000000..44a3e9a4 --- /dev/null +++ b/tests/hdwallet/entropies/test_entropies_slip39.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +# Copyright © 2020-2024, Meheret Tesfaye Batu +# 2024, Eyoel Tadesse +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +import json +import os +import pytest + +from hdwallet.entropies.slip39 import ( + SLIP39Entropy, SLIP39_ENTROPY_STRENGTHS +) +from hdwallet.utils import get_bytes +from hdwallet.exceptions import EntropyError + + +def test_slip39_entropy(data): + + assert SLIP39_ENTROPY_STRENGTHS.ONE_HUNDRED_TWENTY_EIGHT == 128 + assert SLIP39_ENTROPY_STRENGTHS.TWO_HUNDRED_FIFTY_SIX == 256 + assert SLIP39_ENTROPY_STRENGTHS.FIVE_HUNDRED_TWELVE == 512 + + assert SLIP39Entropy.is_valid_strength(strength=SLIP39_ENTROPY_STRENGTHS.ONE_HUNDRED_TWENTY_EIGHT) + assert SLIP39Entropy.is_valid_strength(strength=SLIP39_ENTROPY_STRENGTHS.TWO_HUNDRED_FIFTY_SIX) + assert SLIP39Entropy.is_valid_strength(strength=SLIP39_ENTROPY_STRENGTHS.FIVE_HUNDRED_TWELVE) + + assert SLIP39Entropy.is_valid_bytes_strength(bytes_strength=len(get_bytes(data["entropies"]["SLIP39"]["128"]["entropy"]))) + assert SLIP39Entropy.is_valid_bytes_strength(bytes_strength=len(get_bytes(data["entropies"]["SLIP39"]["256"]["entropy"]))) + assert SLIP39Entropy.is_valid_bytes_strength(bytes_strength=len(get_bytes(data["entropies"]["SLIP39"]["512"]["entropy"]))) + + assert SLIP39Entropy(entropy=SLIP39Entropy.generate(strength=SLIP39_ENTROPY_STRENGTHS.ONE_HUNDRED_TWENTY_EIGHT)).strength() == 128 + assert SLIP39Entropy(entropy=SLIP39Entropy.generate(strength=SLIP39_ENTROPY_STRENGTHS.TWO_HUNDRED_FIFTY_SIX)).strength() == 256 + assert SLIP39Entropy(entropy=SLIP39Entropy.generate(strength=SLIP39_ENTROPY_STRENGTHS.FIVE_HUNDRED_TWELVE)).strength() == 512 + + slip39_128 = SLIP39Entropy(entropy=data["entropies"]["SLIP39"]["128"]["entropy"]) + slip39_256 = SLIP39Entropy(entropy=data["entropies"]["SLIP39"]["256"]["entropy"]) + slip39_512 = SLIP39Entropy(entropy=data["entropies"]["SLIP39"]["512"]["entropy"]) + + assert slip39_128.name() == data["entropies"]["SLIP39"]["128"]["name"] + assert slip39_256.name() == data["entropies"]["SLIP39"]["256"]["name"] + assert slip39_512.name() == data["entropies"]["SLIP39"]["512"]["name"] + + assert slip39_128.strength() == data["entropies"]["SLIP39"]["128"]["strength"] + assert slip39_256.strength() == data["entropies"]["SLIP39"]["256"]["strength"] + assert slip39_512.strength() == data["entropies"]["SLIP39"]["512"]["strength"] + + assert slip39_128.entropy() == data["entropies"]["SLIP39"]["128"]["entropy"] + assert slip39_256.entropy() == data["entropies"]["SLIP39"]["256"]["entropy"] + assert slip39_512.entropy() == data["entropies"]["SLIP39"]["512"]["entropy"] + + with pytest.raises(EntropyError, match="Invalid entropy data"): + SLIP39Entropy(entropy="INVALID_ENTROPY") + + with pytest.raises(EntropyError, match="Invalid entropy data"): + SLIP39Entropy(entropy="f"*(512//4-1)) + with pytest.raises(EntropyError, match="Unsupported entropy strength"): + SLIP39Entropy(entropy="f"*(512//4-2)) diff --git a/tests/hdwallet/hdwallet/cardano/byron-icarus/test_cardano_byron_icarus_from_seed.py b/tests/hdwallet/hdwallet/cardano/byron-icarus/test_cardano_byron_icarus_from_seed.py index 1a48bdb0..2393383d 100644 --- a/tests/hdwallet/hdwallet/cardano/byron-icarus/test_cardano_byron_icarus_from_seed.py +++ b/tests/hdwallet/hdwallet/cardano/byron-icarus/test_cardano_byron_icarus_from_seed.py @@ -27,7 +27,8 @@ def test_byron_icarus_from_seed(data): address_type=cryptocurrency.ADDRESS_TYPES.PUBLIC_KEY ).from_seed( seed=CardanoSeed( - seed=data["hdwallet"]["Cardano"]["byron-icarus"]["seed"] + seed=data["hdwallet"]["Cardano"]["byron-icarus"]["seed"], + cardano_type=data["hdwallet"]["Cardano"]["byron-icarus"]["cardano_type"] ) ).from_derivation( derivation=DERIVATIONS.derivation(data["hdwallet"]["Cardano"]["derivation"]["name"])( diff --git a/tests/hdwallet/hdwallet/cardano/byron-ledger/test_cardano_byron_ledger_from_seed.py b/tests/hdwallet/hdwallet/cardano/byron-ledger/test_cardano_byron_ledger_from_seed.py index 291a435d..9763ae8a 100644 --- a/tests/hdwallet/hdwallet/cardano/byron-ledger/test_cardano_byron_ledger_from_seed.py +++ b/tests/hdwallet/hdwallet/cardano/byron-ledger/test_cardano_byron_ledger_from_seed.py @@ -27,7 +27,8 @@ def test_byron_ledger_from_seed(data): address_type=cryptocurrency.ADDRESS_TYPES.PUBLIC_KEY ).from_seed( seed=CardanoSeed( - seed=data["hdwallet"]["Cardano"]["byron-ledger"]["seed"] + seed=data["hdwallet"]["Cardano"]["byron-ledger"]["seed"], + cardano_type=data["hdwallet"]["Cardano"]["byron-ledger"]["cardano_type"] ) ).from_derivation( derivation=DERIVATIONS.derivation(data["hdwallet"]["Cardano"]["derivation"]["name"])( diff --git a/tests/hdwallet/hdwallet/cardano/byron-legacy/test_cardano_byron_legacy_from_seed.py b/tests/hdwallet/hdwallet/cardano/byron-legacy/test_cardano_byron_legacy_from_seed.py index 009da13a..5be70af5 100644 --- a/tests/hdwallet/hdwallet/cardano/byron-legacy/test_cardano_byron_legacy_from_seed.py +++ b/tests/hdwallet/hdwallet/cardano/byron-legacy/test_cardano_byron_legacy_from_seed.py @@ -27,7 +27,8 @@ def test_byron_legacy_from_seed(data): address_type=cryptocurrency.ADDRESS_TYPES.PUBLIC_KEY ).from_seed( seed=CardanoSeed( - seed=data["hdwallet"]["Cardano"]["byron-legacy"]["seed"] + seed=data["hdwallet"]["Cardano"]["byron-legacy"]["seed"], + cardano_type=data["hdwallet"]["Cardano"]["byron-legacy"]["cardano_type"], ) ).from_derivation( derivation=DERIVATIONS.derivation(data["hdwallet"]["Cardano"]["derivation"]["name"])( diff --git a/tests/hdwallet/hdwallet/cardano/shelley-icarus/test_cardano_shelley_icarus_from_seed.py b/tests/hdwallet/hdwallet/cardano/shelley-icarus/test_cardano_shelley_icarus_from_seed.py index 75a581ea..35bdd023 100644 --- a/tests/hdwallet/hdwallet/cardano/shelley-icarus/test_cardano_shelley_icarus_from_seed.py +++ b/tests/hdwallet/hdwallet/cardano/shelley-icarus/test_cardano_shelley_icarus_from_seed.py @@ -27,7 +27,8 @@ def test_shelley_icarus_from_seed(data): address_type=cryptocurrency.ADDRESS_TYPES.STAKING ).from_seed( seed=CardanoSeed( - seed=data["hdwallet"]["Cardano"]["shelley-icarus"]["seed"] + seed=data["hdwallet"]["Cardano"]["shelley-icarus"]["seed"], + cardano_type=data["hdwallet"]["Cardano"]["shelley-icarus"]["cardano_type"] ) ).from_derivation( derivation=DERIVATIONS.derivation(data["hdwallet"]["Cardano"]["derivation"]["name"])( diff --git a/tests/hdwallet/hdwallet/cardano/shelley-ledger/test_cardano_shelley_ledger_from_seed.py b/tests/hdwallet/hdwallet/cardano/shelley-ledger/test_cardano_shelley_ledger_from_seed.py index 62a5e415..1a647f92 100644 --- a/tests/hdwallet/hdwallet/cardano/shelley-ledger/test_cardano_shelley_ledger_from_seed.py +++ b/tests/hdwallet/hdwallet/cardano/shelley-ledger/test_cardano_shelley_ledger_from_seed.py @@ -27,7 +27,8 @@ def test_shelley_ledger_from_seed(data): address_type=cryptocurrency.ADDRESS_TYPES.STAKING ).from_seed( seed=CardanoSeed( - seed=data["hdwallet"]["Cardano"]["shelley-ledger"]["seed"] + seed=data["hdwallet"]["Cardano"]["shelley-ledger"]["seed"], + cardano_type=data["hdwallet"]["Cardano"]["shelley-ledger"]["cardano_type"], ) ).from_derivation( derivation=DERIVATIONS.derivation(data["hdwallet"]["Cardano"]["derivation"]["name"])( diff --git a/tests/hdwallet/mnemonics/test_mnemonics_bip39.py b/tests/hdwallet/mnemonics/test_mnemonics_bip39.py index 3d3b4b7c..6f1e80f3 100644 --- a/tests/hdwallet/mnemonics/test_mnemonics_bip39.py +++ b/tests/hdwallet/mnemonics/test_mnemonics_bip39.py @@ -8,6 +8,7 @@ import json import os import pytest +import unicodedata from hdwallet.mnemonics.bip39.mnemonic import ( BIP39Mnemonic, BIP39_MNEMONIC_LANGUAGES, BIP39_MNEMONIC_WORDS @@ -43,18 +44,30 @@ def test_bip39_mnemonics(data): assert BIP39Mnemonic.is_valid_words(words=__["words"]) for language in __["languages"].keys(): - assert BIP39Mnemonic.is_valid_language(language=language) - assert BIP39Mnemonic.is_valid(mnemonic=__["languages"][language]) + # A BIP-39 Mnemonic must have a preferred language, to be deterministically decoded as + # is_valid in all cases. + assert BIP39Mnemonic.is_valid(mnemonic=__["languages"][language], language=language) + + # Create a random Mnemonic of the given strength in words, and the specified language, + # and ensure we can recover it mnemonic = BIP39Mnemonic.from_words(words=__["words"], language=language) assert len(mnemonic.split()) == __["words"] - assert BIP39Mnemonic(mnemonic=mnemonic).language().lower() == language + assert BIP39Mnemonic(mnemonic=mnemonic, language=language).language().lower() == language + + # Load the provided mnemonic. We assume NF[K]C encoding for Mnemonics we + # generate/normalize, so ensure that the reference mnemonic is in the same form. + # Recovering a Mnemonic from entropy or from a mnemonic phrase should yield the same + # canonicalized mnemonic; full BIP-39 words with UTF-8 Marks such as accents, regardless + # of whether the original Mnemonic had them or not. - assert BIP39Mnemonic.from_entropy(entropy=__["entropy"], language=language) == __["languages"][language] - assert BIP39Mnemonic.decode(mnemonic=__["languages"][language]) == __["entropy"] + # If a Mnemonic is valid in multiple languages, a preferred language must be provided. + mnemonic = BIP39Mnemonic(mnemonic=__["languages"][language], language=language) + assert BIP39Mnemonic.from_entropy(entropy=__["entropy"], language=language) == mnemonic.mnemonic() - mnemonic = BIP39Mnemonic(mnemonic=__["languages"][language]) + # We can of course recover the entropy from the Mnemonic + assert BIP39Mnemonic.decode(mnemonic=__["languages"][language], language=language) == __["entropy"] assert mnemonic.name() == __["name"] assert mnemonic.language().lower() == language diff --git a/tests/hdwallet/mnemonics/test_mnemonics_electrum_v1.py b/tests/hdwallet/mnemonics/test_mnemonics_electrum_v1.py index ce90f8df..84f03ca6 100644 --- a/tests/hdwallet/mnemonics/test_mnemonics_electrum_v1.py +++ b/tests/hdwallet/mnemonics/test_mnemonics_electrum_v1.py @@ -38,14 +38,14 @@ def test_electrum_v1_mnemonics(data): assert ElectrumV1Mnemonic.from_entropy(entropy=__["entropy"], language=language) == __["languages"][language] assert ElectrumV1Mnemonic.decode(mnemonic=__["languages"][language]) == __["entropy"] - mnemonic = ElectrumV1Mnemonic(mnemonic=__["languages"][language]) + mnemonic = ElectrumV1Mnemonic(mnemonic=__["languages"][language], language=language) assert mnemonic.name() == __["name"] assert mnemonic.language().lower() == language - with pytest.raises(Exception, match="Invalid mnemonic words"): + with pytest.raises(Exception, match="Invalid Electrum-V1 mnemonic words"): ElectrumV1Mnemonic( - mnemonic="flower letter world foil coin poverty romance tongue taste hip cradle follow proud pluck ten improve" + mnemonic="flower letter world foil coin poverty romance tongue taste hip cradle follow proud pluck ten improve", ) with pytest.raises(MnemonicError, match="Invalid mnemonic words number"): diff --git a/tests/hdwallet/mnemonics/test_mnemonics_electrum_v2.py b/tests/hdwallet/mnemonics/test_mnemonics_electrum_v2.py index 67faf9fb..3dd10a49 100644 --- a/tests/hdwallet/mnemonics/test_mnemonics_electrum_v2.py +++ b/tests/hdwallet/mnemonics/test_mnemonics_electrum_v2.py @@ -8,6 +8,7 @@ import json import os import pytest +import unicodedata from hdwallet.mnemonics.electrum.v2.mnemonic import ( ElectrumV2Mnemonic, ELECTRUM_V2_MNEMONIC_LANGUAGES, ELECTRUM_V2_MNEMONIC_WORDS @@ -33,30 +34,32 @@ def test_electrum_v2_mnemonics(data): for mnemonic_type in __["mnemonic-types"].keys(): for language in __["mnemonic-types"][mnemonic_type].keys(): - assert ElectrumV2Mnemonic.is_valid_language(language=language) + mnemonic_words=__["mnemonic-types"][mnemonic_type][language]["mnemonic"] + assert ElectrumV2Mnemonic.is_valid( - mnemonic=__["mnemonic-types"][mnemonic_type][language]["mnemonic"], mnemonic_type=mnemonic_type + mnemonic=mnemonic_words, language=language, mnemonic_type=mnemonic_type ) mnemonic = ElectrumV2Mnemonic.from_words(words=__["words"], language=language, mnemonic_type=mnemonic_type) assert len(mnemonic.split()) == __["words"] - assert ElectrumV2Mnemonic(mnemonic=mnemonic, mnemonic_type=mnemonic_type).language().lower() == language + assert ElectrumV2Mnemonic(mnemonic=mnemonic, language=language, mnemonic_type=mnemonic_type).language().lower() == language assert ElectrumV2Mnemonic.from_entropy( entropy=__["entropy-not-suitable"], mnemonic_type=mnemonic_type, language=language - ) == __["mnemonic-types"][mnemonic_type][language]["mnemonic"] + ) == unicodedata.normalize("NFC", __["mnemonic-types"][mnemonic_type][language]["mnemonic"]) assert ElectrumV2Mnemonic.decode( - mnemonic=__["mnemonic-types"][mnemonic_type][language]["mnemonic"], mnemonic_type=mnemonic_type + mnemonic=__["mnemonic-types"][mnemonic_type][language]["mnemonic"], language=language, mnemonic_type=mnemonic_type ) == __["mnemonic-types"][mnemonic_type][language]["entropy-suitable"] mnemonic = ElectrumV2Mnemonic( - mnemonic=__["mnemonic-types"][mnemonic_type][language]["mnemonic"], mnemonic_type=mnemonic_type + mnemonic=__["mnemonic-types"][mnemonic_type][language]["mnemonic"], language=language, mnemonic_type=mnemonic_type ) assert mnemonic.name() == __["name"] assert mnemonic.language().lower() == language assert mnemonic.mnemonic_type() == mnemonic_type + with pytest.raises(Exception, match="Invalid mnemonic words"): ElectrumV2Mnemonic( diff --git a/tests/hdwallet/mnemonics/test_mnemonics_monero.py b/tests/hdwallet/mnemonics/test_mnemonics_monero.py index de75e3db..28f4df65 100644 --- a/tests/hdwallet/mnemonics/test_mnemonics_monero.py +++ b/tests/hdwallet/mnemonics/test_mnemonics_monero.py @@ -8,6 +8,7 @@ import json import os import pytest +import unicodedata from hdwallet.mnemonics.monero.mnemonic import ( MoneroMnemonic, MONERO_MNEMONIC_LANGUAGES, MONERO_MNEMONIC_WORDS @@ -39,23 +40,21 @@ def test_monero_mnemonics(data): assert MoneroMnemonic.is_valid_words(words=__["words"]) for language in __["languages"].keys(): - - assert MoneroMnemonic.is_valid_language(language=language) - assert MoneroMnemonic.is_valid(mnemonic=__["languages"][language]) + assert MoneroMnemonic.is_valid(mnemonic=__["languages"][language], language=language) mnemonic = MoneroMnemonic.from_words(words=__["words"], language=language) assert len(mnemonic.split()) == __["words"] - assert MoneroMnemonic(mnemonic=mnemonic).language().lower() == language + assert MoneroMnemonic(mnemonic=mnemonic, language=language).language().lower() == language - assert MoneroMnemonic.from_entropy(entropy=__["entropy"], checksum=__["checksum"], language=language) == __["languages"][language] - assert MoneroMnemonic.decode(mnemonic=__["languages"][language]) == __["entropy"] + assert MoneroMnemonic.from_entropy(entropy=__["entropy"], checksum=__["checksum"], language=language) == unicodedata.normalize("NFC", __["languages"][language]) + assert MoneroMnemonic.decode(mnemonic=__["languages"][language], language=language) == __["entropy"] - mnemonic = MoneroMnemonic(mnemonic=__["languages"][language]) + mnemonic = MoneroMnemonic(mnemonic=__["languages"][language], language=language) assert mnemonic.name() == __["name"] assert mnemonic.language().lower() == language - with pytest.raises(MnemonicError, match="Invalid mnemonic words"): + with pytest.raises(MnemonicError, match="Invalid Monero mnemonic words"): MoneroMnemonic( mnemonic="flower letter world foil coin poverty romance tongue taste hip cradle follow proud pluck ten improve" ) diff --git a/tests/hdwallet/mnemonics/test_mnemonics_slip39.py b/tests/hdwallet/mnemonics/test_mnemonics_slip39.py new file mode 100644 index 00000000..240c85ed --- /dev/null +++ b/tests/hdwallet/mnemonics/test_mnemonics_slip39.py @@ -0,0 +1,467 @@ +import contextlib +import pytest + +from hdwallet.exceptions import MnemonicError +from hdwallet.mnemonics.imnemonic import ( + Trie, WordIndices, +) +from hdwallet.mnemonics.slip39.mnemonic import ( + SLIP39Mnemonic, language_parser, group_parser, +) + +import shamir_mnemonic + + +def test_slip39_language(): + + # Any name; no spec --> simplest 1/1 group encoding yielding single mnemonic + spec = language_parser("english") + assert spec == { + ("english",(1,1)): { + 0: (1,1), + }, + } + + # No name or secret spec, and a single group w/ default size based on group threshold + group = group_parser("Name 3 / ", size_default=None) + assert group == ("Name",(3,6)) + spec = language_parser(": Name 3 / ") + assert spec == { + ("",(1,1)): { + "Name": (3,6), + }, + } + + # A secret w/ threshold 3 required, of the default 4 groups of fibonacci required mnemonics + for language in [ + " 3 / 4 ", + " 3 / 4 [ ] ", + " 3 / : ,,, ", + " 3 / : 1/, 1, 4, 3/ ", + ]: + spec = language_parser(language) + assert spec == { + ("",(3,4)): { + 0: (1,1), + 1: (1,1), + 2: (2,4), + 3: (3,6), + }, + }, f"Language {language} yielded incorrect encoding: {spec!r}" + + # If some group specs are provided, the rest are deduced in a fibonacci-ish sequence + spec = language_parser("Satoshi Nakamoto 7 [ 2/3 ] ") + assert spec == { + ("Satoshi Nakamoto",(4,7)): { + 0: (2,3), + 1: (1,1), + 2: (3,6), + 3: (4,8), + 4: (7,14), + 5: (8,16), + 6: (8,16), + }, + } + + assert language_parser("Fibonacci Defaults 3 / 5") == { + ("Fibonacci Defaults",(3,5)): { + 0: (1,1), + 1: (1,1), + 2: (2,4), + 3: (3,6), + 4: (5,10), + }, + } + +def test_slip39_mnemonics(): + + # Ensure our prefix and whitespace handling works correctly + assert SLIP39Mnemonic.NORMALIZE.match( + " Group 1 { word word" + ).groups() == ("Group 1 {","word word") + + assert SLIP39Mnemonic.NORMALIZE.match( + " Group 1 { word word " + ).groups() == ("Group 1 {","word word") + + assert SLIP39Mnemonic.NORMALIZE.match( + " word word " + ).groups() == (None,"word word") + + assert SLIP39Mnemonic.NORMALIZE.match( + " Group 1 { " + ).groups() == ("Group 1 {",None) + + entropy = "ff"*(256//8) + mnemonic = SLIP39Mnemonic.encode(entropy=entropy, language="") + mnemonic_list = SLIP39Mnemonic.normalize(mnemonic) + recovered = SLIP39Mnemonic.decode(mnemonic_list) + assert recovered == entropy + + expected_entropy = "ff" * (512 // 8) # 64 bytes of 0xFF + + slip39 = SLIP39Mnemonic(mnemonic) + assert slip39._mnemonic == mnemonic_list + assert slip39.mnemonic() == mnemonic + + for mnemonic in [ + "curly agency academic academic academic boring radar cluster domestic ticket fumes remove velvet fluff video crazy chest average script universe exhaust remind helpful lamp declare garlic repeat unknown bucket adorn sled adult triumph source divorce premium genre glimpse level listen ancestor wildlife writing document wrist judicial medical detect frost leaves language jerky increase glasses extra alto burden iris swing", + "trend cleanup acrobat easy acid military timber boundary museum dictate argue always grasp bundle welcome silent campus exhaust snake magazine kitchen surface unfold theory adequate gasoline exotic counter fantasy magazine slow mailman metric thumb listen ruler elite mansion diet hybrid withdraw swing makeup repeat glasses density express ting estimate climate scholar loyalty unfold bumpy ecology briefing much fiscal mental\ntrend cleanup beard easy acne extra profile window craft custody owner plot inherit injury starting iris talent curious squeeze retreat density decision hush rainbow extra grumpy humidity income should spray elevator drove large source game pajamas sprinkle dining security class adapt credit therapy verify realize retailer scatter suitable stick hearing lecture mountain dragon talent medal decision equip cleanup aircraft", + "salon email acrobat romp acid lunar rival view daughter exchange privacy pickup moisture forbid welcome amount estimate therapy sled theory says member scroll sister smell erode scene tension glance laden ting cricket apart senior legend transfer describe crowd exceed saver lilac episode cluster pipeline sniff window loyalty manual behavior raspy problem fraction story playoff scroll aunt benefit element execute\nsalon email beard romp acquire vocal plan aviation nervous package unhappy often goat forward closet material fortune fitness wireless terminal slap resident aunt artist source cover perfect grant military ruin taught depend criminal theater decision standard salary priority equation license prisoner rhyme indicate academic shaft express kernel airport tolerate market owner erode dance orange beaver distance smug plunge level\nsalon email ceramic roster academic spark starting says phantom tension saver erode ugly smoking crazy screw pumps display funding fortune mixture ancestor industry glad paces junk laden timber hunting secret program ruin gather clogs legal sugar adjust check crazy genuine predator national swimming twice admit desert system sidewalk check class spelling early morning liberty grief election antenna merchant adjust\nsalon email ceramic scared acid cultural object wildlife percent include wealthy geology capture lift evidence envy identify game guilt curly garbage reaction early scatter practice metric mild earth subject axis verdict juice sled dominant ranked blimp sympathy credit example typical float prisoner ting paces husband adequate amuse display worthy amuse depict civil learn modify lecture mother paid evil stadium\nsalon email ceramic shadow acquire critical ugly desire piece romp piece olympic benefit cargo forbid superior credit username library usher beyond include verify pipeline volume pistol ajar mild carbon acrobat receiver decrease champion calcium flea email picture funding tracks junior fishing thorn regret lily tofu decent romp hazard loud cards peaceful alien retreat single pregnant unfold trial wrist jury\nsalon email ceramic sister acne spirit parking aquatic phrase fact order racism tendency example disaster finance trip multiple ranked lobe tackle smirk regular auction satoshi elephant traveler estimate practice sprinkle true making manual adjust herald mama jacket fishing lecture volume phantom symbolic liberty usher moment alcohol born nervous flip desert element budget pink switch envy discuss laden check promise\nsalon email decision round acquire voting damage briefing emphasis parking airport nylon umbrella coding fake cylinder chubby bolt superior client shame museum reward domain briefing forget guilt group leaf teacher that remind blind judicial soul library dismiss guard provide smoking robin blue focus relate tricycle flexible meaning painting venture trip manager stay flexible rebuild group elephant papa dismiss activity\nsalon email decision scatter acid idle veteran knife thorn theory remember volume cluster writing drove process staff usual sprinkle observe sympathy says birthday lunar leaves salary belong license submit anxiety award spray body victim domestic solution decent geology huge preach human scared desktop email frost verify says predator debris peasant burden swing owner safari reaction broken glimpse jacket deal\nsalon email decision shaft academic breathe mental capital midst guest tracks bolt twin change usual rescue profile taxi paces penalty vitamins emphasis story acquire exhaust salt quantity junction shame midst saver peanut acquire trash duke spend remember predator miracle vintage rich multiple story inmate depend example together blimp coding depart acid diminish petition sister mountain explain thumb density kidney\nsalon email decision skin acne owner finance kernel deal crazy fortune kernel cause warn ordinary document forward alto mixed burning theater axis hybrid review squeeze force shelter owner minister jump darkness smith advance greatest stadium listen prune prisoner exceed medal hospital else race lying liquid tolerate preach capture therapy junction method demand glasses relate emerald blind club income exceed\nsalon email decision snake acne repair sidewalk window video knit resident alien window weapon chubby pacific segment artwork nuclear erode thorn replace wits snapshot founder shaped quiet spray sled depend decent cage income pecan estimate purchase frequent trash chew luxury glimpse category move pipeline scout snake source entrance laundry skunk gravity briefing ancestor hormone security husky snake nylon prospect\nsalon email decision spider academic dramatic axis overall finger early alive health decent ceiling explain capture deploy trip mother viral valid unwrap filter holiday saver fake sharp decorate mustang stay survive hybrid hybrid cowboy peanut that findings umbrella worthy venture quick various watch filter impact jury paid elevator retreat literary viral capacity skin bumpy blue criminal behavior surface legal", + ]: + assert SLIP39Mnemonic.is_valid(mnemonic) + slip39 = SLIP39Mnemonic(mnemonic) + assert slip39.words() == 59 + assert SLIP39Mnemonic.decode(slip39.mnemonic()) == expected_entropy + + # Let's make sure we can detect missing mnemonics reliably. With random subsets, we should + # either decode the Mnemonic, or get a MnemonicError detailing what was missing + mnemonic_list = mnemonic.split('\n') + import random + + # Start with full list size and decrease + for subset_size in range(len(mnemonic_list), 0, -1): + # Create random subset + subset = random.sample(mnemonic_list, subset_size) + + try: + result = SLIP39Mnemonic.decode( '\n'.join(subset) ) + + # If decode succeeds, verify it returns expected entropy + assert result == expected_entropy, ( + f"Subset size {subset_size}: Expected entropy {expected_entropy}, " + f"got {result}" + ) + except MnemonicError as e: + # Verify it's the expected "Incomplete" error + assert "Incomplete: found" in str(e), ( + f"Subset size {subset_size}: Expected 'Incomplete; found' error, " + f"got: {str(e)}" + ) + except Exception as e: + # Unexpected error type + pytest.fail( + f"Subset size {subset_size}: Unexpected error type {type(e)}: {e}" + ) + + +class substitute( contextlib.ContextDecorator ): + """The SLIP-39 standard includes random data in portions of the as share. Replace the random + function during testing to get determinism in resultant nmenomics. + + """ + def __init__( self, thing, attribute, value ): + self.thing = thing + self.attribute = attribute + self.value = value + self.saved = None + + def __enter__( self ): + self.saved = getattr( self.thing, self.attribute ) + setattr( self.thing, self.attribute, self.value ) + + def __exit__( self, *exc ): + setattr( self.thing, self.attribute, self.saved ) + + +@substitute( shamir_mnemonic.shamir, 'RANDOM_BYTES', lambda n: b'\0' * n ) +def test_slip39_tabulate(): + """Details of the SLIP-39 specifications' 'language' and output 'tabulate' value must be kept, + so .mnemonic() reflects them. + + """ + + entropy_128 = "ff"*(128//8) + entropy_256 = "ff"*(256//8) + entropy_512 = "ff"*(512//8) + + + + family = "Perry Kundert [ One 1/1, Two 1/1, Fam 2/4, Frens 3/6 ]" + family_tabulate_None = """\ +One 1/1 1st ━ academic agency acrobat romp course prune deadline umbrella darkness salt bishop impact vanish squeeze moment segment privacy bolt making enjoy + +Two 1/1 1st ━ academic agency beard romp downtown inmate hamster counter rainbow grocery veteran decorate describe bedroom disease suitable peasant editor welfare spider + +Fam 2/4 1st ┳ academic agency ceramic roster crystal critical forbid sled building glad legs angry enlarge ting ranked round solution legend ending lips + ╏ + 2nd ┣ academic agency ceramic scared drink verdict funding dragon activity verify fawn yoga devote perfect jacket database picture genius process pipeline + ╏ + 3rd ┣ academic agency ceramic shadow avoid leaf fantasy midst crush fraction cricket taxi velvet gasoline daughter august rhythm excuse wrist increase + ╏ + 4th ┗ academic agency ceramic sister capital flexible favorite grownup diminish sidewalk yelp blanket market class testify temple silent prevent born galaxy + +Frens 3/6 1st ┳ academic agency decision round academic academic academic academic academic academic academic academic academic academic academic academic academic phrase trust golden + ╏ + 2nd ┣ academic agency decision scatter desert wisdom birthday fatigue lecture detailed destroy realize recover lilac genre venture jacket mountain blessing pulse + ╏ + 3rd ┣ academic agency decision shaft birthday debut benefit shame market devote angel finger traveler analysis pipeline extra funding lawsuit editor guilt + ╏ + 4th ┣ academic agency decision skin category skin alpha observe artwork advance earth thank fact material sheriff peaceful club evoke robin revenue + ╏ + 5th ┣ academic agency decision snake anxiety acrobat inform home patrol alpha erode steady cultural juice emerald reject flash license royal plunge + ╏ + 6th ┗ academic agency decision spider earth woman gasoline dryer civil deliver laser hospital mountain wrist clinic evidence database public dwarf lawsuit""" + + family_tabulate_False = """\ +academic agency acrobat romp course prune deadline umbrella darkness salt bishop impact vanish squeeze moment segment privacy bolt making enjoy +academic agency beard romp downtown inmate hamster counter rainbow grocery veteran decorate describe bedroom disease suitable peasant editor welfare spider +academic agency ceramic roster crystal critical forbid sled building glad legs angry enlarge ting ranked round solution legend ending lips +academic agency ceramic scared drink verdict funding dragon activity verify fawn yoga devote perfect jacket database picture genius process pipeline +academic agency ceramic shadow avoid leaf fantasy midst crush fraction cricket taxi velvet gasoline daughter august rhythm excuse wrist increase +academic agency ceramic sister capital flexible favorite grownup diminish sidewalk yelp blanket market class testify temple silent prevent born galaxy +academic agency decision round academic academic academic academic academic academic academic academic academic academic academic academic academic phrase trust golden +academic agency decision scatter desert wisdom birthday fatigue lecture detailed destroy realize recover lilac genre venture jacket mountain blessing pulse +academic agency decision shaft birthday debut benefit shame market devote angel finger traveler analysis pipeline extra funding lawsuit editor guilt +academic agency decision skin category skin alpha observe artwork advance earth thank fact material sheriff peaceful club evoke robin revenue +academic agency decision snake anxiety acrobat inform home patrol alpha erode steady cultural juice emerald reject flash license royal plunge +academic agency decision spider earth woman gasoline dryer civil deliver laser hospital mountain wrist clinic evidence database public dwarf lawsuit""" + + assert SLIP39Mnemonic.encode(entropy=entropy_128, language=family, tabulate=None) == family_tabulate_None + assert SLIP39Mnemonic.encode(entropy=entropy_128, language=family) == family_tabulate_False + + # Now, ensure that a SLIP39Mnemonic instance remembers its SLIP-39 encoding parameters and desired tabulation. + slip39 = SLIP39Mnemonic(mnemonic=family_tabulate_False, language=family, tabulate=None) + assert slip39.mnemonic() == family_tabulate_None + + + + assert SLIP39Mnemonic.encode(entropy=entropy_512, language=family, tabulate=None) == """\ +One 1/1 1st ━ academic agency acrobat romp acid airport meaning source sympathy junction symbolic lyrics install enjoy remind trend blind vampire type idle kind facility venture image inherit talent burning woman devote guest prevent news rich type unkind clay venture raisin oasis crisis firefly change index hanger belong true floral fawn busy fridge invasion member hesitate railroad campus edge ocean woman spill + +Two 1/1 1st ━ academic agency beard romp acid ruler execute bishop tolerate paid likely decent lips carbon exchange saver diminish year credit pacific deliver treat pacific aviation email river paper being deadline hawk gasoline nylon favorite duration spine lungs mixed stadium briefing prisoner fragment submit material fatal ultimate mixture sprinkle genuine educate sympathy anatomy visual carbon station exceed enemy mayor custody lyrics + +Fam 2/4 1st ┳ academic agency ceramic roster academic lyrics envelope tendency flexible careful shelter often plunge headset educate freshman isolate flea receiver hunting training tricycle legal snapshot rainbow pencil enforce priority spine hesitate civil scandal makeup privacy vitamins platform inherit sheriff relate evil breathe lilac vitamins theater render patrol airport vitamins clogs hour standard sugar exceed shadow laundry involve ticket public cargo + ╏ + 2nd ┣ academic agency ceramic scared academic western unknown daughter valid satisfy remember toxic chubby various become pile craft taste group listen amazing phantom rescue sugar patrol require discuss amazing software guitar race observe window medical sister fatal else species mule hesitate formal flash steady isolate express repair fangs expand likely fumes evoke champion screw space imply dive yoga ordinary rebound + ╏ + 3rd ┣ academic agency ceramic shadow academic harvest rebuild knit beard pickup corner clogs payroll detailed tendency ultimate sugar earth pharmacy wits deploy capacity fiction aide observe very breathe genre swing ancient arcade juice guest leaves mixture superior born wavy endorse lying omit coding angry bishop evening yelp pitch satoshi impact avoid username practice easy wavy scout credit emperor physics crazy + ╏ + 4th ┗ academic agency ceramic sister academic browser axle quantity recover junk float forbid criminal premium puny boundary mama regret intimate body spark false hour aunt march typical grumpy scene strategy award observe clinic bucket parcel pink charity clothes that hand platform syndrome video clay medical rhythm tracks writing junior spew dynamic health eyebrow silent theater shadow grasp garbage mandate length + +Frens 3/6 1st ┳ academic agency decision round academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic fragment receiver provide + ╏ + 2nd ┣ academic agency decision scatter academic wealthy health losing moisture display damage scout junk roster percent society income lying bolt again privacy visual firm infant coal lawsuit scout eraser campus alpha force fragment obtain very acquire firefly eyebrow judicial primary pecan entrance counter snake parking anxiety general strategy manual wireless provide timber level warn join frost episode primary percent maximum + ╏ + 3rd ┣ academic agency decision shaft acquire likely unfair grill course news fake bulge trip drift treat news manual corner game depart item devote writing taste cleanup leaves taste jewelry speak fumes darkness spider execute canyon legs unfair sniff tackle actress laden kernel rhythm smear ranked regular describe cause bike snapshot scandal sniff dress aspect task kidney wrote junction pistol suitable + ╏ + 4th ┣ academic agency decision skin acquire junction lobe teammate require pajamas laser talent mild wits exclude entrance yield pants epidemic dilemma sprinkle roster pink prayer admit yelp building depend slim floral inherit luxury spirit unhappy lecture resident legend picture pregnant strategy depict museum carpet biology quarter filter webcam paid crisis industry desktop rhyme vitamins pharmacy charity receiver mama research ticket + ╏ + 5th ┣ academic agency decision snake acne intimate empty treat agency ceiling destroy industry river machine editor standard prospect alarm spider security aquatic satisfy rapids inform very threaten withdraw market desktop furl devote squeeze anxiety lamp patrol oasis grill regret artwork downtown invasion shadow grant pecan tidy gray credit amazing expand secret trip mixed perfect remind best lobe adult airport penalty + ╏ + 6th ┗ academic agency decision spider acne memory daisy humidity nail bucket burden puny scandal epidemic tidy alarm satoshi medal safari saver party detailed taxi acid spine obtain dive seafood cradle focus heat makeup method mason patent sister dictate rumor pajamas package early teammate race ajar unhappy agency very lips railroad invasion avoid away frost romp exotic smear vegan bolt nylon""" + + assert SLIP39Mnemonic.encode(entropy=entropy_512, language=family, tabulate=True) == """\ +One 1/1 1st ┭ academic agency acrobat romp acid airport meaning source sympathy junction symbolic lyrics install enjoy remind trend blind vampire type idle + ├ kind facility venture image inherit talent burning woman devote guest prevent news rich type unkind clay venture raisin oasis crisis + └ firefly change index hanger belong true floral fawn busy fridge invasion member hesitate railroad campus edge ocean woman spill + +Two 1/1 1st ┭ academic agency beard romp acid ruler execute bishop tolerate paid likely decent lips carbon exchange saver diminish year credit pacific + ├ deliver treat pacific aviation email river paper being deadline hawk gasoline nylon favorite duration spine lungs mixed stadium briefing prisoner + └ fragment submit material fatal ultimate mixture sprinkle genuine educate sympathy anatomy visual carbon station exceed enemy mayor custody lyrics + +Fam 2/4 1st ┳ academic agency ceramic roster academic lyrics envelope tendency flexible careful shelter often plunge headset educate freshman isolate flea receiver hunting + ├ training tricycle legal snapshot rainbow pencil enforce priority spine hesitate civil scandal makeup privacy vitamins platform inherit sheriff relate evil + └ breathe lilac vitamins theater render patrol airport vitamins clogs hour standard sugar exceed shadow laundry involve ticket public cargo + ╏ + 2nd ┣ academic agency ceramic scared academic western unknown daughter valid satisfy remember toxic chubby various become pile craft taste group listen + ├ amazing phantom rescue sugar patrol require discuss amazing software guitar race observe window medical sister fatal else species mule hesitate + └ formal flash steady isolate express repair fangs expand likely fumes evoke champion screw space imply dive yoga ordinary rebound + ╏ + 3rd ┣ academic agency ceramic shadow academic harvest rebuild knit beard pickup corner clogs payroll detailed tendency ultimate sugar earth pharmacy wits + ├ deploy capacity fiction aide observe very breathe genre swing ancient arcade juice guest leaves mixture superior born wavy endorse lying + └ omit coding angry bishop evening yelp pitch satoshi impact avoid username practice easy wavy scout credit emperor physics crazy + ╏ + 4th ┣ academic agency ceramic sister academic browser axle quantity recover junk float forbid criminal premium puny boundary mama regret intimate body + ├ spark false hour aunt march typical grumpy scene strategy award observe clinic bucket parcel pink charity clothes that hand platform + └ syndrome video clay medical rhythm tracks writing junior spew dynamic health eyebrow silent theater shadow grasp garbage mandate length + +Frens 3/6 1st ┳ academic agency decision round academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic + ├ academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic + └ academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic fragment receiver provide + ╏ + 2nd ┣ academic agency decision scatter academic wealthy health losing moisture display damage scout junk roster percent society income lying bolt again + ├ privacy visual firm infant coal lawsuit scout eraser campus alpha force fragment obtain very acquire firefly eyebrow judicial primary pecan + └ entrance counter snake parking anxiety general strategy manual wireless provide timber level warn join frost episode primary percent maximum + ╏ + 3rd ┣ academic agency decision shaft acquire likely unfair grill course news fake bulge trip drift treat news manual corner game depart + ├ item devote writing taste cleanup leaves taste jewelry speak fumes darkness spider execute canyon legs unfair sniff tackle actress laden + └ kernel rhythm smear ranked regular describe cause bike snapshot scandal sniff dress aspect task kidney wrote junction pistol suitable + ╏ + 4th ┣ academic agency decision skin acquire junction lobe teammate require pajamas laser talent mild wits exclude entrance yield pants epidemic dilemma + ├ sprinkle roster pink prayer admit yelp building depend slim floral inherit luxury spirit unhappy lecture resident legend picture pregnant strategy + └ depict museum carpet biology quarter filter webcam paid crisis industry desktop rhyme vitamins pharmacy charity receiver mama research ticket + ╏ + 5th ┣ academic agency decision snake acne intimate empty treat agency ceiling destroy industry river machine editor standard prospect alarm spider security + ├ aquatic satisfy rapids inform very threaten withdraw market desktop furl devote squeeze anxiety lamp patrol oasis grill regret artwork downtown + └ invasion shadow grant pecan tidy gray credit amazing expand secret trip mixed perfect remind best lobe adult airport penalty + ╏ + 6th ┣ academic agency decision spider acne memory daisy humidity nail bucket burden puny scandal epidemic tidy alarm satoshi medal safari saver + ├ party detailed taxi acid spine obtain dive seafood cradle focus heat makeup method mason patent sister dictate rumor pajamas package + └ early teammate race ajar unhappy agency very lips railroad invasion avoid away frost romp exotic smear vegan bolt nylon""" + + mnemonics = SLIP39Mnemonic.encode(entropy=entropy_512, language=family, tabulate=10) + assert mnemonics == """\ +One 1/1 1st ┭ academic agency acrobat romp acid airport meaning source sympathy junction + ├ symbolic lyrics install enjoy remind trend blind vampire type idle + ├ kind facility venture image inherit talent burning woman devote guest + ├ prevent news rich type unkind clay venture raisin oasis crisis + ├ firefly change index hanger belong true floral fawn busy fridge + └ invasion member hesitate railroad campus edge ocean woman spill + +Two 1/1 1st ┭ academic agency beard romp acid ruler execute bishop tolerate paid + ├ likely decent lips carbon exchange saver diminish year credit pacific + ├ deliver treat pacific aviation email river paper being deadline hawk + ├ gasoline nylon favorite duration spine lungs mixed stadium briefing prisoner + ├ fragment submit material fatal ultimate mixture sprinkle genuine educate sympathy + └ anatomy visual carbon station exceed enemy mayor custody lyrics + +Fam 2/4 1st ┳ academic agency ceramic roster academic lyrics envelope tendency flexible careful + ├ shelter often plunge headset educate freshman isolate flea receiver hunting + ├ training tricycle legal snapshot rainbow pencil enforce priority spine hesitate + ├ civil scandal makeup privacy vitamins platform inherit sheriff relate evil + ├ breathe lilac vitamins theater render patrol airport vitamins clogs hour + └ standard sugar exceed shadow laundry involve ticket public cargo + ╏ + 2nd ┣ academic agency ceramic scared academic western unknown daughter valid satisfy + ├ remember toxic chubby various become pile craft taste group listen + ├ amazing phantom rescue sugar patrol require discuss amazing software guitar + ├ race observe window medical sister fatal else species mule hesitate + ├ formal flash steady isolate express repair fangs expand likely fumes + └ evoke champion screw space imply dive yoga ordinary rebound + ╏ + 3rd ┣ academic agency ceramic shadow academic harvest rebuild knit beard pickup + ├ corner clogs payroll detailed tendency ultimate sugar earth pharmacy wits + ├ deploy capacity fiction aide observe very breathe genre swing ancient + ├ arcade juice guest leaves mixture superior born wavy endorse lying + ├ omit coding angry bishop evening yelp pitch satoshi impact avoid + └ username practice easy wavy scout credit emperor physics crazy + ╏ + 4th ┣ academic agency ceramic sister academic browser axle quantity recover junk + ├ float forbid criminal premium puny boundary mama regret intimate body + ├ spark false hour aunt march typical grumpy scene strategy award + ├ observe clinic bucket parcel pink charity clothes that hand platform + ├ syndrome video clay medical rhythm tracks writing junior spew dynamic + └ health eyebrow silent theater shadow grasp garbage mandate length + +Frens 3/6 1st ┳ academic agency decision round academic academic academic academic academic academic + ├ academic academic academic academic academic academic academic academic academic academic + ├ academic academic academic academic academic academic academic academic academic academic + ├ academic academic academic academic academic academic academic academic academic academic + ├ academic academic academic academic academic academic academic academic academic academic + └ academic academic academic academic academic academic fragment receiver provide + ╏ + 2nd ┣ academic agency decision scatter academic wealthy health losing moisture display + ├ damage scout junk roster percent society income lying bolt again + ├ privacy visual firm infant coal lawsuit scout eraser campus alpha + ├ force fragment obtain very acquire firefly eyebrow judicial primary pecan + ├ entrance counter snake parking anxiety general strategy manual wireless provide + └ timber level warn join frost episode primary percent maximum + ╏ + 3rd ┣ academic agency decision shaft acquire likely unfair grill course news + ├ fake bulge trip drift treat news manual corner game depart + ├ item devote writing taste cleanup leaves taste jewelry speak fumes + ├ darkness spider execute canyon legs unfair sniff tackle actress laden + ├ kernel rhythm smear ranked regular describe cause bike snapshot scandal + └ sniff dress aspect task kidney wrote junction pistol suitable + ╏ + 4th ┣ academic agency decision skin acquire junction lobe teammate require pajamas + ├ laser talent mild wits exclude entrance yield pants epidemic dilemma + ├ sprinkle roster pink prayer admit yelp building depend slim floral + ├ inherit luxury spirit unhappy lecture resident legend picture pregnant strategy + ├ depict museum carpet biology quarter filter webcam paid crisis industry + └ desktop rhyme vitamins pharmacy charity receiver mama research ticket + ╏ + 5th ┣ academic agency decision snake acne intimate empty treat agency ceiling + ├ destroy industry river machine editor standard prospect alarm spider security + ├ aquatic satisfy rapids inform very threaten withdraw market desktop furl + ├ devote squeeze anxiety lamp patrol oasis grill regret artwork downtown + ├ invasion shadow grant pecan tidy gray credit amazing expand secret + └ trip mixed perfect remind best lobe adult airport penalty + ╏ + 6th ┣ academic agency decision spider acne memory daisy humidity nail bucket + ├ burden puny scandal epidemic tidy alarm satoshi medal safari saver + ├ party detailed taxi acid spine obtain dive seafood cradle focus + ├ heat makeup method mason patent sister dictate rumor pajamas package + ├ early teammate race ajar unhappy agency very lips railroad invasion + └ avoid away frost romp exotic smear vegan bolt nylon""" + + + # Now test recovery from the prefixed mnemonics. First, normalize should work, giving us a + # straight list of all Mnemonics, of a length divisible by a valid SLIP-39 Mnemonic word length; + # in this case 59 (for 512-bit secrets). + import json + normalized = SLIP39Mnemonic.normalize( mnemonics ) + normalized_json = json.dumps( + [ + " ".join(normalized[col:col+59]) + for col in range(0,len(normalized),59) + ], indent=4 + ) + #print( normalized_json ) + assert normalized_json == """[ + "academic agency acrobat romp acid airport meaning source sympathy junction symbolic lyrics install enjoy remind trend blind vampire type idle kind facility venture image inherit talent burning woman devote guest prevent news rich type unkind clay venture raisin oasis crisis firefly change index hanger belong true floral fawn busy fridge invasion member hesitate railroad campus edge ocean woman spill", + "academic agency beard romp acid ruler execute bishop tolerate paid likely decent lips carbon exchange saver diminish year credit pacific deliver treat pacific aviation email river paper being deadline hawk gasoline nylon favorite duration spine lungs mixed stadium briefing prisoner fragment submit material fatal ultimate mixture sprinkle genuine educate sympathy anatomy visual carbon station exceed enemy mayor custody lyrics", + "academic agency ceramic roster academic lyrics envelope tendency flexible careful shelter often plunge headset educate freshman isolate flea receiver hunting training tricycle legal snapshot rainbow pencil enforce priority spine hesitate civil scandal makeup privacy vitamins platform inherit sheriff relate evil breathe lilac vitamins theater render patrol airport vitamins clogs hour standard sugar exceed shadow laundry involve ticket public cargo", + "academic agency ceramic scared academic western unknown daughter valid satisfy remember toxic chubby various become pile craft taste group listen amazing phantom rescue sugar patrol require discuss amazing software guitar race observe window medical sister fatal else species mule hesitate formal flash steady isolate express repair fangs expand likely fumes evoke champion screw space imply dive yoga ordinary rebound", + "academic agency ceramic shadow academic harvest rebuild knit beard pickup corner clogs payroll detailed tendency ultimate sugar earth pharmacy wits deploy capacity fiction aide observe very breathe genre swing ancient arcade juice guest leaves mixture superior born wavy endorse lying omit coding angry bishop evening yelp pitch satoshi impact avoid username practice easy wavy scout credit emperor physics crazy", + "academic agency ceramic sister academic browser axle quantity recover junk float forbid criminal premium puny boundary mama regret intimate body spark false hour aunt march typical grumpy scene strategy award observe clinic bucket parcel pink charity clothes that hand platform syndrome video clay medical rhythm tracks writing junior spew dynamic health eyebrow silent theater shadow grasp garbage mandate length", + "academic agency decision round academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic fragment receiver provide", + "academic agency decision scatter academic wealthy health losing moisture display damage scout junk roster percent society income lying bolt again privacy visual firm infant coal lawsuit scout eraser campus alpha force fragment obtain very acquire firefly eyebrow judicial primary pecan entrance counter snake parking anxiety general strategy manual wireless provide timber level warn join frost episode primary percent maximum", + "academic agency decision shaft acquire likely unfair grill course news fake bulge trip drift treat news manual corner game depart item devote writing taste cleanup leaves taste jewelry speak fumes darkness spider execute canyon legs unfair sniff tackle actress laden kernel rhythm smear ranked regular describe cause bike snapshot scandal sniff dress aspect task kidney wrote junction pistol suitable", + "academic agency decision skin acquire junction lobe teammate require pajamas laser talent mild wits exclude entrance yield pants epidemic dilemma sprinkle roster pink prayer admit yelp building depend slim floral inherit luxury spirit unhappy lecture resident legend picture pregnant strategy depict museum carpet biology quarter filter webcam paid crisis industry desktop rhyme vitamins pharmacy charity receiver mama research ticket", + "academic agency decision snake acne intimate empty treat agency ceiling destroy industry river machine editor standard prospect alarm spider security aquatic satisfy rapids inform very threaten withdraw market desktop furl devote squeeze anxiety lamp patrol oasis grill regret artwork downtown invasion shadow grant pecan tidy gray credit amazing expand secret trip mixed perfect remind best lobe adult airport penalty", + "academic agency decision spider acne memory daisy humidity nail bucket burden puny scandal epidemic tidy alarm satoshi medal safari saver party detailed taxi acid spine obtain dive seafood cradle focus heat makeup method mason patent sister dictate rumor pajamas package early teammate race ajar unhappy agency very lips railroad invasion avoid away frost romp exotic smear vegan bolt nylon" +]""" + # So decode should simply work, ignoring all the Group specification language prefixes and + # separator/continuation symbols. + assert SLIP39Mnemonic.decode( mnemonics ) == entropy_512 + + # And invalid ones should note why they failed. First, a valid one: + assert SLIP39Mnemonic.decode( """\ +One 1/1 1st ┭ academic agency acrobat romp acid airport meaning source sympathy junction symbolic lyrics install enjoy remind trend blind vampire type idle + ├ kind facility venture image inherit talent burning woman devote guest prevent news rich type unkind clay venture raisin oasis crisis + └ firefly change index hanger belong true floral fawn busy fridge invasion member hesitate railroad campus edge ocean woman spill + +Two 1/1 1st ┭ academic agency beard romp acid ruler execute bishop tolerate paid likely decent lips carbon exchange saver diminish year credit pacific + ├ deliver treat pacific aviation email river paper being deadline hawk gasoline nylon favorite duration spine lungs mixed stadium briefing prisoner + └ fragment submit material fatal ultimate mixture sprinkle genuine educate sympathy anatomy visual carbon station exceed enemy mayor custody lyrics + """) == entropy_512 + + # Missing last word of 1st Mnemonic (on line 3): + with pytest.raises(MnemonicError, match="@L3: odd length mnemonic encountered"): + SLIP39Mnemonic.decode( """\ +One 1/1 1st ┭ academic agency acrobat romp acid airport meaning source sympathy junction symbolic lyrics install enjoy remind trend blind vampire type idle + ├ kind facility venture image inherit talent burning woman devote guest prevent news rich type unkind clay venture raisin oasis crisis + └ firefly change index hanger belong true floral fawn busy fridge invasion member hesitate railroad campus edge ocean woman + +Two 1/1 1st ┭ academic agency beard romp acid ruler execute bishop tolerate paid likely decent lips carbon exchange saver diminish year credit pacific + ├ deliver treat pacific aviation email river paper being deadline hawk gasoline nylon favorite duration spine lungs mixed stadium briefing prisoner + └ fragment submit material fatal ultimate mixture sprinkle genuine educate sympathy anatomy visual carbon station exceed enemy mayor custody lyrics + """) + + # Funky lines + with pytest.raises(MnemonicError, match="@L4: unrecognized mnemonic line"): + SLIP39Mnemonic.decode( """\ +One 1/1 1st ┭ academic agency acrobat romp acid airport meaning source sympathy junction symbolic lyrics install enjoy remind trend blind vampire type idle + ├ kind facility venture image inherit talent burning woman devote guest prevent news rich type unkind clay venture raisin oasis crisis + └ firefly change index hanger belong true floral fawn busy fridge invasion member hesitate railroad campus edge ocean woman spill +# we don't support comments so this Mnemonic will fail due to invalid symbols +Two 1/1 1st ┭ academic agency beard romp acid ruler execute bishop tolerate paid likely decent lips carbon exchange saver diminish year credit pacific + ├ deliver treat pacific aviation email river paper being deadline hawk gasoline nylon favorite duration spine lungs mixed stadium briefing prisoner + └ fragment submit material fatal ultimate mixture sprinkle genuine educate sympathy anatomy visual carbon station exceed enemy mayor custody lyrics + """) + + # Bad Mnemonic words + with pytest.raises(MnemonicError, match="Failed to recover SLIP-39 Mnemonics Invalid mnemonic word 'we'."): + SLIP39Mnemonic.decode( """\ +One 1/1 1st ┭ academic agency acrobat romp acid airport meaning source sympathy junction symbolic lyrics install enjoy remind trend blind vampire type idle + ├ kind facility venture image inherit talent burning woman devote guest prevent news rich type unkind clay venture raisin oasis crisis + └ firefly change index hanger belong true floral fawn busy fridge invasion member hesitate railroad campus edge ocean woman spill +# we do not support comments so this Mnemonic will fail due to bad mnemonic words even though it happens to be the right length +# we do not support comments so this Mnemonic will fail due to bad mnemonic words even though it happens to be the right length +# because we purposely expertly accidentally made this line eleven words long +Two 1/1 1st ┭ academic agency beard romp acid ruler execute bishop tolerate paid likely decent lips carbon exchange saver diminish year credit pacific + ├ deliver treat pacific aviation email river paper being deadline hawk gasoline nylon favorite duration spine lungs mixed stadium briefing prisoner + └ fragment submit material fatal ultimate mixture sprinkle genuine educate sympathy anatomy visual carbon station exceed enemy mayor custody lyrics + """) diff --git a/tests/hdwallet/seeds/test_seeds_bip39.py b/tests/hdwallet/seeds/test_seeds_bip39.py index 400f2f6b..718ddb35 100644 --- a/tests/hdwallet/seeds/test_seeds_bip39.py +++ b/tests/hdwallet/seeds/test_seeds_bip39.py @@ -24,8 +24,3 @@ def test_bip39_seeds(data): assert BIP39Seed.from_mnemonic( mnemonic= data["seeds"]["BIP39"][words][lang]["mnemonic"], passphrase=passphrase ) == data["seeds"]["BIP39"][words][lang]["passphrases"][passphrase] - - assert BIP39Seed.from_mnemonic( - mnemonic= data["seeds"]["BIP39"][words][lang]["mnemonic"], passphrase=passphrase - ) == data["seeds"]["BIP39"][words][lang]["passphrases"][passphrase] - diff --git a/tests/hdwallet/seeds/test_seeds_cardano.py b/tests/hdwallet/seeds/test_seeds_cardano.py index 4acee83a..ba850c87 100644 --- a/tests/hdwallet/seeds/test_seeds_cardano.py +++ b/tests/hdwallet/seeds/test_seeds_cardano.py @@ -31,10 +31,3 @@ def test_cardano_seeds(data): passphrase=passphrase, cardano_type=cardano_type ) == data["seeds"]["Cardano"][words][cardano_type][lang]["passphrases"][passphrase] - - assert CardanoSeed.from_mnemonic( - mnemonic= data["seeds"]["Cardano"][words][cardano_type][lang]["mnemonic"], - passphrase=passphrase, - cardano_type=cardano_type - ) == data["seeds"]["Cardano"][words][cardano_type][lang]["passphrases"][passphrase] - diff --git a/tests/hdwallet/seeds/test_seeds_electrum_v2.py b/tests/hdwallet/seeds/test_seeds_electrum_v2.py index 2b0ffeb4..92beb13e 100644 --- a/tests/hdwallet/seeds/test_seeds_electrum_v2.py +++ b/tests/hdwallet/seeds/test_seeds_electrum_v2.py @@ -17,21 +17,19 @@ def test_electrum_v2_seeds(data): for words in data["seeds"]["Electrum-V2"].keys(): for mnemonic_type in data["seeds"]["Electrum-V2"][words].keys(): - for lang in data["seeds"]["Electrum-V2"][words][mnemonic_type].keys(): - assert ElectrumV2Seed.from_mnemonic( - mnemonic= data["seeds"]["Electrum-V2"][words][mnemonic_type][lang]["mnemonic"], mnemonic_type=mnemonic_type - ) == data["seeds"]["Electrum-V2"][words][mnemonic_type][lang]["non-passphrase-seed"] - - for passphrase in data["seeds"]["Electrum-V2"][words][mnemonic_type][lang]["passphrases"].keys(): - assert ElectrumV2Seed.from_mnemonic( - mnemonic=data["seeds"]["Electrum-V2"][words][mnemonic_type][lang]["mnemonic"], - passphrase=passphrase, - mnemonic_type=mnemonic_type - ) == data["seeds"]["Electrum-V2"][words][mnemonic_type][lang]["passphrases"][passphrase] - + for language in data["seeds"]["Electrum-V2"][words][mnemonic_type].keys(): + mnemonic = data["seeds"]["Electrum-V2"][words][mnemonic_type][language]["mnemonic"] + non_passphrase_seed = ElectrumV2Seed.from_mnemonic( + mnemonic=mnemonic, + language=language, + mnemonic_type=mnemonic_type + ) + assert non_passphrase_seed == data["seeds"]["Electrum-V2"][words][mnemonic_type][language]["non-passphrase-seed"] + + for passphrase in data["seeds"]["Electrum-V2"][words][mnemonic_type][language]["passphrases"].keys(): assert ElectrumV2Seed.from_mnemonic( - mnemonic=data["seeds"]["Electrum-V2"][words][mnemonic_type][lang]["mnemonic"], + mnemonic=mnemonic, passphrase=passphrase, + language=language, mnemonic_type=mnemonic_type - ) == data["seeds"]["Electrum-V2"][words][mnemonic_type][lang]["passphrases"][passphrase] - + ) == data["seeds"]["Electrum-V2"][words][mnemonic_type][language]["passphrases"][passphrase] diff --git a/tests/hdwallet/seeds/test_seeds_slip39.py b/tests/hdwallet/seeds/test_seeds_slip39.py new file mode 100644 index 00000000..9e8a338e --- /dev/null +++ b/tests/hdwallet/seeds/test_seeds_slip39.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +# Copyright © 2020-2024, Meheret Tesfaye Batu +# 2024, Eyoel Tadesse +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +import json +import logging +import os +import pytest + +from hdwallet.seeds.slip39 import SLIP39Seed +from hdwallet.mnemonics.bip39 import BIP39Mnemonic + + +def test_slip39_seeds(data): + + for words in data["seeds"]["SLIP39"].keys(): + for lang in data["seeds"]["SLIP39"][words].keys(): + mnemonic = data["seeds"]["SLIP39"][words][lang]["mnemonic"] + # A SLIP-39 "backup" for another Mnemonic (eg. BIP-39) backs up the original entropy + # (not the derived seed, which will often be stretched to a different length). SLIP-39 + # /can/ store 512-bit data (the output of a BIP-39 seed, after hashing it with its + # passphrase, but this is not generally supported by hardware wallets supporting + # SLIP-39, such as the Trezor. This is unfortunate, as it prevents backing up BIP-39 + # derived seeds including the passphrase. + assert SLIP39Seed.from_mnemonic( + mnemonic = mnemonic + ) == data["seeds"]["SLIP39"][words][lang]["non-passphrase-seed"] + diff --git a/tests/test_bip39_cross_language.py b/tests/test_bip39_cross_language.py new file mode 100644 index 00000000..6c0d6169 --- /dev/null +++ b/tests/test_bip39_cross_language.py @@ -0,0 +1,659 @@ +#!/usr/bin/env python3 + +import os +import random +import unicodedata +from typing import List, Set + +import pytest + +from hdwallet.mnemonics.bip39 import BIP39Mnemonic +from hdwallet.mnemonics.imnemonic import Trie, TrieNode, WordIndices +from hdwallet.exceptions import ChecksumError, MnemonicError + +class TestBIP39CrossLanguage: + """Test BIP39 mnemonics that work in both English and French languages. + + This test explores the theoretical possibility of mnemonics that can be validly + decoded in multiple languages. About 2% (100/2048) of words are common between + the English and French BIP-39 wordlists. + + For randomly generated entropy uses only common words: + - Probability for a 12-word mnemonic: (100/2048)^12*1/16 ≈ 1.15*10^-17 + - Probability for a 24-word mnemonic: (100/2048)^24*1/256 ≈ 1.32x10^-34 + + Most wallets allow abbreviations; only the first few characters of the word need to be entered: + the words are guaranteed to be unique after entering at least 4 letters (including the word end + symbol; eg. 'run' 'runway, and 'sea' 'search' 'season' 'seat'). + + If we include full words in one language that are abbreviations in the other, the probabilities + increase: + + + + These probabilities are astronomically small, so naturally occurring mnemonics + will essentially never be composed entirely of common words. + + This test deliberately constructs mnemonics using only common words, + then tests what fraction pass checksum validation in both languages: + - For 12-word mnemonics: ~1/16 (6.25%) due to 4-bit checksum + - For 24-word mnemonics: ~1/256 (0.39%) due to 8-bit checksum + + This demonstrates the theoretical cross-language compatibility while showing + why it's not a practical security concern for real-world usage. + + For details about BIP-39 word list selection for various languages, see: + + https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md + + Particularly interesting for abbreviations is the fact that any letter with an accent is + considered equal to the same letter without the accent for entry and word selection. + + """ + + @classmethod + def setup_class(cls, languages: list[str] = None): + """Load wordlists and find common words between specified languages. + + Args: + languages: List of language names to load (defaults to ['english', 'french']) + """ + if languages is None: + languages = ['english', 'french'] + + if len(languages) < 2: + raise ValueError("At least 2 languages are required for cross-language testing") + + # Load all specified languages + language_data = {} + for language, words, indices in BIP39Mnemonic.wordlist_indices(): + language_data[language] = dict( + indices = indices, + words = set( indices.keys() ), # canonical words + unique = set( indices.unique() ), # unique words with/without UTF-8 Marks + abbrevs = set( indices.abbreviations() ), # unique abbreviations + ) + if language not in languages: + continue + + # Set class attributes for backward compatibility + setattr(cls, f"{language}_words", language_data[language]['words']) + setattr(cls, f"{language}_unique", language_data[language]['unique']) + setattr(cls, f"{language}_indices", language_data[language]['indices']) + setattr(cls, f"{language}_abbrevs", language_data[language]['abbrevs']) + + # Find common words across all languages - only process requested languages + requested_data = {lang: language_data[lang] for lang in languages if lang in language_data} + all_word_sets = [data['words'] for data in requested_data.values()] + all_unique_sets = [data['unique'] for data in requested_data.values()] + all_abbrev_lists = [data['abbrevs'] for data in requested_data.values()] + + cls.common_words = set.intersection(*all_word_sets) if all_word_sets else set() + cls.common_unique = set.intersection(*all_unique_sets) if all_unique_sets else set() + cls.common_abbrevs = set.intersection(*all_abbrev_lists) if all_abbrev_lists else set() + + # Print statistics. Given that UTF-8 marks may or may not be supplied, there may be more + # unique words than the 2048 base UTF-8 BIP-39 words in the language. + for lang, data in requested_data.items(): + print(f"{lang.capitalize()} UTF-8 words base: {len(data['words'])} unique: {len(data['unique'])}, abbreviations: {len(data['abbrevs'])}") + + print(f"Common canonical words found: {len(cls.common_words)}") + print(f"First 20 common canonical words: {sorted(cls.common_words)[:20]}") + print(f"Common unique words found: {len(cls.common_unique)}") + print(f"First 20 common unique words: {sorted(cls.common_unique)[:20]}") + print(f"Common abbrevs found: {len(cls.common_abbrevs)}") + print(f"First 20 common abbrevs: {sorted(cls.common_abbrevs)[:20]}") + + def create_random_mnemonic_from_common_words(self, word_count: int) -> str: + """Create a random mnemonic using only common words.""" + if len(self.common_words) < word_count: + raise ValueError(f"Not enough common words ({len(self.common_words)}) to create {word_count}-word mnemonic") + + selected_words = random.choices(list(self.common_words), k=word_count) + return selected_words + + def test_common_words_exist(self): + """Test that there are common words between English and French wordlists.""" + assert len(self.common_words) > 0, "No common words found between English and French wordlists" + + def dual_language_N_word_mnemonics(self, words=12, expected_rate=1/16, total_attempts=1000): + """Test N-word mnemonics that work in both English and French. + + Actual rate for 2 languages will be expected_rate ^ 2.""" + successful_both_languages: List[List[str]] = [] + successful_english: int = 0 + for _ in range(total_attempts): + try: + # Generate a random N-word mnemonic from common canonical words + mnemonic = self.create_random_mnemonic_from_common_words(words) + + # Try to decode as both English and French - both must succeed (pass checksum) + # Note: We expect different entropy values since words have different indices + entropy_english = BIP39Mnemonic.decode(' '.join(mnemonic), language='english') + entropy_french = BIP39Mnemonic.decode(' '.join(mnemonic), language='french') + + # If both decode successfully, the mnemonic is valid in both languages + successful_both_languages.append(mnemonic) + print(f"{words}-word common mnemonics {' '.join(mnemonic)!r}") + + except ChecksumError as exc: + # Skip invalid mnemonics (e.g., checksum failures) + continue + + success_rate = len(successful_both_languages) / total_attempts + + print(f"{words}-word mnemonics: {len(successful_both_languages)}/{total_attempts} successful ({success_rate:.6f})") + print(f"Expected rate: ~{expected_rate:.6f} ^ 2 == {expected_rate ** 2:.6f}") + + # These rates are too small to test, in the small samples we're likely to use + + # # Assert we found at least some successful mnemonics + # assert success_rate > 0, f"No {words}-word mnemonics worked in both languages" + + # # The success rate should be roughly around the expected rate, but due to + # # randomness and limited common words, we'll accept a broader range + # tolerance = 0.5 # 50% tolerance due to statistical variance + # assert expected_rate * (1 - tolerance) < success_rate < expected_rate * (1 + tolerance), \ + # f"Success rate {success_rate:.6f} not in expected range around {expected_rate:.4f}" + return successful_both_languages + + def test_cross_language_12_word_mnemonics(self): + """Test 12-word mnemonics that work in both English and French. + + For example, these match in terms of unique words (missing UTF-8 marks): + 'ocean correct rival double theme village crucial veteran salon tunnel question minute' + 'puzzle mobile video pelican bicycle ocean effort train junior brave effort theme' + 'elegant cruel science guide fortune nation humble lecture ozone dragon question village' + 'innocent prison romance jaguar voyage depart fruit crucial video salon reunion fatigue' + 'position dragon correct question figure notable service vague civil public distance emotion' + + But completely ambiguous mnemonics in multiple languages (no UTF-8 Marks to make one + slightly less of a match) are somewhat rare, but *certainly* not unlikely: + 'essence capable figure noble distance fruit intact amateur surprise distance vague unique' + 'nature crucial aspect mobile nation muscle surface usage valve concert impact label' + 'animal double noble volume innocent fatigue abandon minute panda vague label stable' + 'surface client simple junior volume palace amateur brave surprise bonus talent million' + + """ + # Make sure trying to decode a completely ambiguous BIP-39 Mnemonic reports as Ambiguous, + # but providing a preferred language works + ambiguous = 'essence capable figure noble distance fruit intact amateur surprise distance vague unique' + with pytest.raises(MnemonicError, match="Ambiguous languages"): + BIP39Mnemonic.decode( mnemonic=ambiguous ) + assert BIP39Mnemonic.decode( mnemonic=ambiguous, language="french" ) == "5aa5219a52a47adc20e858e528f7d5f9" + assert BIP39Mnemonic.decode( mnemonic=ambiguous, language="english" ) == "4d443d584ad3fabb9d603dda67f7c2f6" + + candidates = self.dual_language_N_word_mnemonics(words=12, expected_rate=1/16) + + def test_cross_language_24_word_mnemonics(self): + """Test 24-word mnemonics that work in both English and French. + + For example (ambiguous in unique words): + 'pelican pelican minute intact science figure vague civil badge rival pizza fatal sphere nation simple ozone canal talent emotion wagon ozone valve voyage angle' + 'pizza intact noble fragile piece suspect legal badge vital guide coyote volume nature wagon badge festival danger train desert intact opinion veteran romance metal' + + Totally ambiguous in canonical words: + 'lecture orange stable romance aspect junior fatal prison voyage globe village figure mobile badge usage social correct jaguar bonus science aspect question service crucial' + 'aspect loyal stable bonus label question effort virus digital fruit junior nature abandon concert crucial brave double aspect capable figure orange unique unique machine' + """ + # Make sure trying to decode a completely ambiguous BIP-39 Mnemonic reports as Ambiguous, + # but providing a preferred language works + ambiguous = ( + 'lecture orange stable romance aspect junior fatal prison voyage globe village figure' + ' mobile badge usage social correct jaguar bonus science aspect question service crucial' + ) + with pytest.raises(MnemonicError, match="Ambiguous languages"): + BIP39Mnemonic.decode( mnemonic=ambiguous ) + assert BIP39Mnemonic.decode( mnemonic=ambiguous, language="french" ) == "8c75af88e8a1291158de0cfeee5fec3349d62fbd16f13870b8836bf1298b36e1" + assert BIP39Mnemonic.decode( mnemonic=ambiguous, language="english" ) == "7f137b4f5dc0d6f254e558f60c6bd02b08e622fbe66f308ee465e070d75f3109" + + candidates = self.dual_language_N_word_mnemonics(words=24, expected_rate=1/256) + + def test_wordlist_properties(self): + """Test basic properties of the wordlists.""" + # Verify wordlist sizes + assert len(self.english_words) == 2048, f"English base wordlist should have 2048 words, got {len(self.english_words)}" + assert len(self.english_unique) == 2048, f"English full unique wordlist should have 2048 words, got {len(self.english_unique)}" + assert len(self.french_words) == 2048, f"French base wordlist should have 2048 words, got {len(self.french_words)}" + assert len(self.french_unique) == 2774, f"French full unique wordlist should have 2774 words, got {len(self.french_unique)}" + + # Verify common words list properties + assert len(self.common_words) > 0, "No common words found" + assert len(set(self.common_words)) == len(self.common_words), "Common words list contains duplicates" + + def test_trie_functionality(self): + """Test the new Trie and TrieNode classes for abbreviation handling.""" + + # Test 1: Default TrieNode markers + trie = Trie() + test_words = [ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "add", + "addict", + "address", + "adjust", + "access", + "accident", + "account", + "accuse", + "achieve", + ] + + # Insert all test words with their indices + for index, word in enumerate( test_words ): + trie.insert(word, index) + + # Test exact word lookups + terminal, stem, current = trie.search("abandon") + assert terminal and stem == "abandon" and current.value == 0, \ + "Should find exact word 'abandon'" + terminal, stem, current = trie.search("ability") + assert terminal and stem == "ability" and current.value == 1, \ + "Should find exact word 'ability'" + terminal, stem, current = trie.search("nonexistent") + assert not terminal and stem == "" and current is None, \ + "Should not find non-existent word" + + # Test __contains__ method + assert "abandon" in trie, "Trie should contain 'abandon'" + assert "ability" in trie, "Trie should contain 'ability'" + assert "nonexistent" not in trie, "Trie should not contain 'nonexistent'" + + # Test prefix detection with startswith + assert trie.startswith("aba"), "Should find prefix 'aba'" + assert trie.startswith("abil"), "Should find prefix 'abil'" + assert trie.startswith("xyz") == False, "Should not find non-existent prefix 'xyz'" + + # Test unambiguous abbreviation completion + # 'aba' should complete to 'abandon' since it's the only word starting with 'aba' + terminal, stem, current = trie.search("aba", complete=True) + assert terminal and stem == "abandon" and current.value == 0, "Should complete 'aba' to 'abandon' (index 0)" + + # 'abi' should complete to 'ability' since it's the only word starting with 'abi' + terminal, stem, current = trie.search("abi", complete=True) + assert terminal and stem == "ability" and current.value == 1, "Should complete 'abi' to 'ability' (index 1)" + + # 'ab' is ambiguous (abandon, ability, able, about, above, absent, absorb, abstract, absurd, abuse) + terminal, stem, current = trie.search("ab", complete=True) + assert not terminal and stem == "ab" and current.value is current.EMPTY, "Should not complete ambiguous prefix 'ab'" + + # 'acc' is also ambiguous (access, accident, account, accuse) + terminal, stem, current = trie.search("acc", complete=True) + assert not terminal and stem == "acc" and current.value is current.EMPTY, "Should not complete ambiguous prefix 'acc'" + + # 'accid' should complete to 'accident' since it's unambiguous + terminal, stem, current = trie.search("accid", complete=True) + assert terminal and stem == "accident" and current.value == 15, "Should complete 'accid' to 'accident' (index 15)" + + # Test edge cases + terminal, stem, current = trie.search("") + assert not terminal and stem == "" and current.value is TrieNode.EMPTY, "Empty string should return EMPTY; it's a prefix, but no value" + terminal, stem, current = trie.search("", complete=True) + assert not terminal and stem == "a" and current.value is TrieNode.EMPTY, "Empty string with complete should complete to 'a' (all words start with a) and return EMPTY" + + # Test very short abbreviations that should be unambiguous + # 'abl' should complete to 'able' since it's the only match + terminal, stem, current = trie.search("abl", complete=True) + assert terminal and stem == "able" and current.value == 2, "Should complete 'abl' to 'able' (index 2)" + + # Test abbreviations that are longer than needed but still valid; particularly that + # complete=True doesn't jump over a fully complete word. + terminal, stem, current = trie.search("abandon", complete=True) + assert terminal and stem == "abandon" and current.value == 0, "Full word should still work with complete=True" + + def scan_value( w_n ): + return w_n[0], w_n[1].value + + # Test scans of various depths + assert sorted( map( scan_value, trie.scan("abs"))) == [ + ( 'absent', 5, ), + ( 'absorb', 6, ), + ( 'abstract', 7 ), + ( 'absurd', 8 ), + ] + + # Now we see words that are prefixes of other words + + assert sorted( map( scan_value, trie.scan("ad", depth=1 ))) == [ + ] + assert sorted( map( scan_value, trie.scan("ad", depth=1, predicate=lambda _: True ))) == [ + ( 'ad', None ), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=2 ))) == [ + ( 'add', 10), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=2, predicate=lambda _: True ))) == [ + ( 'ad', None ), + ( 'add', 10), + ( 'adj', None ), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=3 ))) == [ + ( 'add', 10), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=3, predicate=lambda _: True ))) == [ + ( 'ad', None ), + ( 'add', 10), + ( 'addi', None ), + ( 'addr', None ), + ( 'adj', None ), + ( 'adju', None ), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=4 ))) == [ + ( 'add', 10), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=4, predicate=lambda _: True ))) == [ + ( 'ad', None ), + ( 'add', 10), + ( 'addi', None ), + ( 'addic', None ), + ( 'addr', None ), + ( 'addre', None ), + ( 'adj', None ), + ( 'adju', None ), + ( 'adjus', None ), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=5 ))) == [ + ( 'add', 10), + ( 'addict', 11), + ( 'adjust', 13), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=5, predicate=lambda _: True ))) == [ + ( 'ad', None ), + ( 'add', 10), + ( 'addi', None ), + ( 'addic', None ), + ( 'addict', 11), + ( 'addr', None ), + ( 'addre', None ), + ( 'addres', None ), + ( 'adj', None ), + ( 'adju', None ), + ( 'adjus', None ), + ( 'adjust', 13 ), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=6 ))) == [ + ( 'add', 10), + ( 'addict', 11), + ( 'address', 12), + ( 'adjust', 13), + ] + assert sorted( map( scan_value, trie.scan("ad", depth=6, predicate=lambda _: True ))) == [ + ( 'ad', None ), + ( 'add', 10), + ( 'addi', None ), + ( 'addic', None ), + ( 'addict', 11 ), + ( 'addr', None ), + ( 'addre', None ), + ( 'addres', None ), + ( 'address', 12 ), + ( 'adj', None ), + ( 'adju', None ), + ( 'adjus', None ), + ( 'adjust', 13 ), + ] + + # Test 2: Custom TrieNode with different markers + class CustomTrieNode(TrieNode): + EMPTY = "CUSTOM_EMPTY" + + custom_root = CustomTrieNode() + custom_trie = Trie(custom_root) + + # Test that custom markers are used + assert custom_trie.root.EMPTY == "CUSTOM_EMPTY" + + # Insert some words + custom_trie.insert("test", 42) + custom_trie.insert("testing", 99) + + # Verify custom markers are returned for non-existent words + terminal, stem, current = custom_trie.search("nonexistent") + assert not terminal and stem == "" and current is None + terminal, stem, current = custom_trie.search("") + assert not terminal and stem == "" and current.value == "CUSTOM_EMPTY" # Root has EMPTY value + + # Verify normal functionality still works + terminal, stem, current = custom_trie.search("test") + assert terminal and stem == "test" and current.value == 42 + terminal, stem, current = custom_trie.search("testing") + assert terminal and stem == "testing" and current.value == 99 + assert "test" in custom_trie + assert "nonexistent" not in custom_trie + + # Test abbreviation completion with custom markers + terminal, stem, current = custom_trie.search("tes", complete=False) + assert stem == "tes" and current.value == "CUSTOM_EMPTY" # Ambiguous: "test" vs "testing" + assert not terminal + terminal, stem, current = custom_trie.search("tes", complete=True) + assert terminal and stem == "test" and current.value == 42 # Single path to "test" vs "testing" + *_, (_, _, current) = custom_trie.complete(current=current) + assert current.value == 99 # Should carry on completing the single path from "test" to "testing" + terminal, stem, current = custom_trie.search("testin", complete=True) + assert terminal and stem == "testing" and current.value == 99 # Unambiguous: completes to "testing" + + test_indices = WordIndices(test_words) + assert str(test_indices) == """\ +a b a n d o n == 0 + i l i t y == 1 + l e == 2 + o u t == 3 + v e == 4 + s e n t == 5 + o r b == 6 + t r a c t == 7 + u r d == 8 + u s e == 9 + d d == 10 + i c t == 11 + r e s s == 12 + j u s t == 13 + c c e s s == 14 + i d e n t == 15 + o u n t == 16 + u s e == 17 + h i e v e == 18""" + + + + options = trie.options() + + assert next( options ) == (False, set( 'a' )) + assert options.send( 'a' ) == (False, set( 'bcd' )) + assert options.send( 'd' ) == (False, set( 'dj' )) + assert options.send( 'd' ) == (True, set( 'ir' )) + assert options.send( 'i' ) == (False, set( 'c' )) + assert options.send( 'c' ) == (False, set( 't' )) + assert next(options) == (False, set( 't' )) + assert options.send('') == (False, set( 't' )) + assert options.send( 't' ) == (True, set()) + + + def test_ambiguous_languages(self): + """Test that find_language correctly detects and raises errors for ambiguous mnemonics. + + This test verifies that when a mnemonic contains words common to multiple languages + with equal quality scores, find_language raises a MnemonicError indicating the ambiguity. + + + """ + # Try some problematic ones; not completely ambiguous, but guessing based on symbol matching + # fails, while validating all candidate languages succeeds: + for problem, language, entropy in [ + ( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "english", # guessed by .find_language; correct! + "00000000000000000000000000000000", + ), + ( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon absurd", + "english", # guessed by .find_language; wrong! + "00200400801002004008010020040080", # but .decode finds correct french entropy by checksum + ), + ]: + # for rank, indices, candidate in BIP39Mnemonic.rank_languages( BIP39Mnemonic.normalize( problem )): + # print( f"{str(rank):12} == {float(rank):8.4f} {candidate}" ) + _indices, guessed = BIP39Mnemonic.find_language( BIP39Mnemonic.normalize( problem )) + assert guessed == language, \ + f"Wrong language {guessed} for: {problem}" + recovered = BIP39Mnemonic.decode( problem ) + assert recovered == entropy, \ + f"Wrong entropy {recovered} for: {problem}" + + from hdwallet.exceptions import MnemonicError + + # Create a test mnemonic using only common words between languages + # Use enough words to create realistic test cases + if len(self.common_words) < 12: + pytest.skip(f"Not enough common words ({len(self.common_words)}) for ambiguity testing") + + # Test with 12-word mnemonics using only common words + test_mnemonic = list(self.common_words)[:12] # Use first 12 common words + + # Test 1: find_language should detect ambiguity when no preferred language is specified + try: + word_indices, detected_language = BIP39Mnemonic.find_language(test_mnemonic) + # If this succeeds, it means one language had a higher quality score than others + # This is valid behavior - not all common word combinations are equally ambiguous + #print(f"Mnemonic resolved to {detected_language} (quality was decisive)") + except MnemonicError as e: + # This is the expected behavior for truly ambiguous mnemonics + #assert "Ambiguous languages" in str(e), f"Expected ambiguity error, got: {e}" + #assert "specify a preferred language" in str(e), f"Expected preference suggestion, got: {e}" + #print(f"✓ Correctly detected ambiguous mnemonic: {e}") + pass + + # Test 2: Verify that specifying a preferred language resolves the ambiguity + # Try with each available language that contains these common words + resolved_languages = [] + for language in ['english', 'french']: # Test both languages we know have common words + try: + word_indices, detected_language = BIP39Mnemonic.find_language( + test_mnemonic, language=language + ) + resolved_languages.append(detected_language) + #print(f"✓ Successfully resolved with preferred language '{language}' -> {detected_language}") + except MnemonicError as e: + print(f"Failed to resolve with language '{language}': {e}") + raise + + # At least one language should successfully resolve the mnemonic + assert len(resolved_languages) > 0, "No language could resolve the test mnemonic" + + # Test 3: Test with a different set of common words to ensure robustness + if len(self.common_words) >= 24: + # Try with different common words (offset by 6 to get different words) + alt_test_mnemonic = list(self.common_words)[6:18] # Words 6-17 (12 words) + + try: + word_indices, detected_language = BIP39Mnemonic.find_language(alt_test_mnemonic) + #print(f"Alternative mnemonic resolved to {detected_language}") + except MnemonicError as e: + if "Ambiguous languages" in str(e): + #print(f"✓ Alternative mnemonic also correctly detected as ambiguous: {e}") + # Test that preferred language resolves it + word_indices, detected_language = BIP39Mnemonic.find_language( + alt_test_mnemonic, language='english' + ) + #print(f"✓ Alternative mnemonic resolved with preferred language: {detected_language}") + else: + raise # Re-raise unexpected errors + + # Test 4: Verify behavior with abbreviations if common abbreviations exist + if len(self.common_abbrevs) >= 12: + abbrev_mnemonic = list(self.common_abbrevs)[:12] + + try: + word_indices, detected_language = BIP39Mnemonic.find_language(abbrev_mnemonic) + #print(f"Abbreviation mnemonic resolved to {detected_language}") + except MnemonicError as e: + if "Ambiguous languages" in str(e): + print(f"✓ Abbreviation mnemonic correctly detected as ambiguous") + # Verify preferred language resolves it + word_indices, detected_language = BIP39Mnemonic.find_language( + abbrev_mnemonic, language='english' + ) + #print(f"✓ Abbreviation mnemonic resolved with preferred language: {detected_language}") + else: + raise # Re-raise unexpected errors + + +def test_bip39_collection(): + + languages = {'english', 'french', 'spanish', 'russian'} + + collect = BIP39Mnemonic.collect(languages=languages) + assert collect.send(None) == (languages, False, set('abcedefghijklmnopqrstuvwxyzáéíóúабвгдежзиклмнопрстуфхцчшщэюя')) + assert collect.send('a') == ({'english', 'french', 'spanish'}, False, set('bcedefghijlmnpqrstuvwxyzéñ')) + assert collect.send('d') == ({'english', 'french', 'spanish'}, False, set('adehijmoruvé')) + assert collect.send('d') == ({'english'}, True , set('ir')) + + + + + +def test_bip39_korean(): + # Confirm that UTF-8 Mark handling works in other languages (particularly Korean) + (_, korean_nfc, korean_indices), = BIP39Mnemonic.wordlist_indices( + dict( + korean = BIP39Mnemonic.wordlist_path["korean"] + ) + ) + korean_nfc_20 = "\n".join(korean_nfc[:20]) + #print( korean_nfc_20 ) + assert korean_nfc_20 == """\ +가격 +가끔 +가난 +가능 +가득 +가르침 +가뭄 +가방 +가상 +가슴 +가운데 +가을 +가이드 +가입 +가장 +가정 +가족 +가죽 +각오 +각자""" + korean_trie_20 = "\n".join(korean_indices._trie.dump_lines()[:20]) + #print(korean_trie_20) + assert korean_trie_20 == """\ +가 격 == 0 + 끔 == 1 + 난 == 2 + 능 == 3 + 득 == 4 + 르 침 == 5 + 뭄 == 6 + 방 == 7 + 상 == 8 + 슴 == 9 + 운 데 == 10 + 을 == 11 + 이 드 == 12 + 입 == 13 + 장 == 14 + 정 == 15 + 족 == 16 + 죽 == 17 +각 오 == 18 + 자 == 19""" + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/tests/test_clients.py b/tests/test_clients.py new file mode 100644 index 00000000..d04be880 --- /dev/null +++ b/tests/test_clients.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import pytest +from pathlib import Path + + +# Discover all clientscripts +CLIENTS_DIR = Path(__file__).parent.parent / "clients" +CLIENTS_SCRIPTS = sorted(CLIENTS_DIR.rglob("*.py")) + +# Project root directory (for PYTHONPATH) +PROJECT_ROOT = Path(__file__).parent.parent + + +@pytest.mark.clients +@pytest.mark.parametrize("script_path", CLIENTS_SCRIPTS, ids=lambda p: str(p.relative_to(CLIENTS_DIR))) +def test_client_script_runs(script_path): + """Test that clients scripts execute without raising exceptions.""" + # Set PYTHONPATH to use local source instead of installed package + env = os.environ.copy() + env["PYTHONPATH"] = str(PROJECT_ROOT) + + result = subprocess.run( + ["python3", str(script_path)], + capture_output=True, + text=True, + timeout=30, + env=env + ) + print(result.stdout) + assert result.returncode == 0, ( + f"Script {script_path.name} failed with exit code {result.returncode}\n" + f"STDOUT:\n{result.stdout}\n" + f"STDERR:\n{result.stderr}" + ) diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 00000000..3ed047bf --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import pytest +from pathlib import Path + + +# Discover all example scripts +EXAMPLES_DIR = Path(__file__).parent.parent / "examples" +EXAMPLE_SCRIPTS = sorted(EXAMPLES_DIR.rglob("*.py")) + +# Project root directory (for PYTHONPATH) +PROJECT_ROOT = Path(__file__).parent.parent + + +@pytest.mark.examples +@pytest.mark.parametrize("script_path", EXAMPLE_SCRIPTS, ids=lambda p: str(p.relative_to(EXAMPLES_DIR))) +def test_example_script_runs(script_path): + """Test that example scripts execute without raising exceptions.""" + # Set PYTHONPATH to use local source instead of installed package + env = os.environ.copy() + env["PYTHONPATH"] = str(PROJECT_ROOT) + + result = subprocess.run( + ["python3", str(script_path)], + capture_output=True, + text=True, + timeout=30, + env=env + ) + + assert result.returncode == 0, ( + f"Script {script_path.name} failed with exit code {result.returncode}\n" + f"STDOUT:\n{result.stdout}\n" + f"STDERR:\n{result.stderr}" + )