From 5a52eb0954ac36ab07df9849e06f69974b934fce Mon Sep 17 00:00:00 2001 From: LC mac Date: Wed, 7 Jan 2026 23:54:25 +0800 Subject: [PATCH] test: add offline integrity test suite with frozen vectors --- .gitignore | 1 + recover-stdin-interactive.patch | 204 -------------- tests/bootstrap_vectors.py | 104 +++++++ tests/test_vectors.py | 136 +++++++++ tests/vectors.json | 482 ++++++++++++++++++++++++++++++++ 5 files changed, 723 insertions(+), 204 deletions(-) delete mode 100644 recover-stdin-interactive.patch create mode 100644 tests/bootstrap_vectors.py create mode 100644 tests/test_vectors.py create mode 100644 tests/vectors.json diff --git a/.gitignore b/.gitignore index 6cc509b..96f1cf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .venv/ +.venv312/ __pycache__/ *.pyc .vscode/ diff --git a/recover-stdin-interactive.patch b/recover-stdin-interactive.patch deleted file mode 100644 index c7bea64..0000000 --- a/recover-stdin-interactive.patch +++ /dev/null @@ -1,204 +0,0 @@ -diff --git a/src/pyhdwallet.py b/src/pyhdwallet.py -index 1111111..2222222 100755 ---- a/src/pyhdwallet.py -+++ b/src/pyhdwallet.py -@@ -1,6 +1,6 @@ - #!/usr/bin/env python3 - """ --pyhdwallet v1.0.4 (Python 3.11+) -+pyhdwallet v1.0.4 (Python 3.11+) - - Commands: - - fetchkey (online): download ASCII-armored PGP public key and print SHA256 + fingerprint -@@ -33,6 +33,7 @@ import tempfile - import urllib.request - import warnings - from pathlib import Path -+from typing import Iterable - from typing import Any, Dict, List, Optional, Tuple - - # ----------------------------------------------------------------------------- -@@ -112,6 +113,123 @@ def gen_base58_password(length: int) -> str: - return "".join(secrets.choice(alphabet) for _ in range(length)) - - -+# ----------------------------------------------------------------------------- -+# Recover input helpers (interactive + stdin) -+# ----------------------------------------------------------------------------- -+ -+ -+def _prompt_hidden_or_visible(prompt: str) -> str: -+ """ -+ Prompt for sensitive input. -+ -+ Prefer getpass (no echo). If getpass can't reliably hide input in the current -+ environment, warn and fall back to visible input (user-selected behavior B). -+ """ -+ try: -+ with warnings.catch_warnings(record=True) as w: -+ warnings.simplefilter("always") -+ s = getpass.getpass(prompt) -+ if w: -+ print("⚠️ WARNING: getpass could not reliably hide input in this environment.", file=sys.stderr) -+ print("⚠️ Falling back to visible input. Use a real terminal for hidden input.", file=sys.stderr) -+ return input(prompt.replace("(hidden)", "(VISIBLE)")) -+ return s -+ except (Exception, KeyboardInterrupt): -+ print("⚠️ WARNING: Hidden prompt failed; falling back to visible input.", file=sys.stderr) -+ return input(prompt.replace("(hidden)", "(VISIBLE)")) -+ -+ -+def _read_stdin_all() -> str: -+ # Read all of stdin (for piping from secure sources). -+ return sys.stdin.read().strip() -+ -+ -+def _bip39_english_wordset() -> set[str]: -+ """ -+ Load BIP39 English wordlist using bip_utils built-in word list getter. -+ """ -+ from bip_utils import Bip39Languages, Bip39WordsListGetter -+ -+ wl = Bip39WordsListGetter.GetByLanguage(Bip39Languages.ENGLISH) -+ # wl provides the official 2048-word list via bip_utils. [web:342] -+ return set(wl.GetWordList()) -+ -+ -+def _prompt_word_count() -> int: -+ valid_counts = {12, 15, 18, 21, 24} -+ while True: -+ raw = input("Word count (12/15/18/21/24): ").strip() -+ if raw.isdigit() and int(raw) in valid_counts: -+ return int(raw) -+ print("❌ Invalid word count. Choose 12/15/18/21/24.") -+ -+ -+def interactive_mnemonic_word_by_word() -> str: -+ """ -+ Guided mnemonic entry (English-only): -+ - choose word count -+ - enter each word with membership validation (✅/❌) -+ - final checksum validation via Bip39MnemonicValidator -+ """ -+ from bip_utils import Bip39MnemonicValidator -+ -+ wordset = _bip39_english_wordset() -+ n = _prompt_word_count() -+ -+ words: List[str] = [] -+ i = 1 -+ while i <= n: -+ w = _prompt_hidden_or_visible(f"{i}) Enter word (hidden): ").strip().lower() -+ if w in wordset: -+ print(f"{i}) ✅") -+ words.append(w) -+ i += 1 -+ else: -+ print(f"{i}) ❌ Invalid BIP39 English word. Try again.") -+ -+ mnemonic = " ".join(words) -+ if not Bip39MnemonicValidator().IsValid(mnemonic): -+ raise ValueError("Mnemonic checksum/format invalid (words valid but checksum failed).") -+ return mnemonic -+ -+ - # ----------------------------------------------------------------------------- - # Fingerprint - # ----------------------------------------------------------------------------- -@@ -474,39 +592,52 @@ def cmd_recover(args) -> None: - if args.off_screen: - print("⚠️ Off-screen mode enabled: Sensitive data will not be printed to stdout.") - - if args.export_private and not args.pgp_pubkey_file: - raise ValueError("--export-private requires --pgp-pubkey-file") - - from bip_utils import Bip39MnemonicValidator, Bip39SeedGenerator - -- # Validate input methods -- input_methods = [bool(args.mnemonic), bool(args.seed), args.interactive] -- if sum(input_methods) > 1: -- raise ValueError("Provide only one of --mnemonic, --seed, or --interactive") -- if sum(input_methods) == 0: -- raise ValueError("Missing input (use --mnemonic/--seed/--interactive)") -- - mnemonic: Optional[str] = None - seed_hex: Optional[str] = None - -- if args.interactive: -- mode = input("Enter 'm' for mnemonic or 's' for seed: ").strip().lower() -- if mode == "m": -- mnemonic = getpass.getpass("BIP39 mnemonic (hidden): ").strip() -- elif mode == "s": -- seed_hex = getpass.getpass("Seed hex (hidden, 128 hex chars): ").strip() -- else: -- raise ValueError("Invalid choice") -- elif args.mnemonic: -- mnemonic = args.mnemonic.strip() -- else: -- seed_hex = args.seed.strip() -+ # New input model: exactly one of: -+ # --interactive (guided mnemonic only, English-only) -+ # --mnemonic-stdin (read full phrase from stdin) -+ # --seed-stdin (read seed hex from stdin) -+ if args.interactive: -+ mnemonic = interactive_mnemonic_word_by_word() -+ elif args.mnemonic_stdin: -+ mnemonic = _read_stdin_all() -+ if not mnemonic: -+ raise ValueError("Empty stdin for --mnemonic-stdin") -+ elif args.seed_stdin: -+ seed_hex = _read_stdin_all() -+ if not seed_hex: -+ raise ValueError("Empty stdin for --seed-stdin") -+ else: -+ raise ValueError("Missing input mode (use --interactive / --mnemonic-stdin / --seed-stdin)") - - passphrase = getpass.getpass("Enter BIP39 passphrase (hidden): ") if args.passphrase else "" - - if mnemonic: - if not Bip39MnemonicValidator().IsValid(mnemonic): - raise ValueError("Invalid BIP39 mnemonic") - seed_bytes = Bip39SeedGenerator(mnemonic).Generate(passphrase) - else: - b = bytes.fromhex(seed_hex or "") - if len(b) != 64: - raise ValueError("Invalid seed hex (expected 64 bytes / 128 hex chars)") - seed_bytes = b - - fp = get_master_fingerprint(seed_bytes) -@@ -676,18 +807,30 @@ def build_parser() -> argparse.ArgumentParser: - - # recover - p_rec = sub.add_parser("recover", help="Recover addresses from mnemonic/seed (offline)") - add_common(p_rec) -- p_rec.add_argument("--mnemonic", default="") -- p_rec.add_argument("--seed", default="") -- p_rec.add_argument("--interactive", action="store_true") -+ -+ # New safer input model for recover: -+ # - No plaintext secrets in CLI args. -+ # - Use guided interactive entry or stdin. -+ g = p_rec.add_mutually_exclusive_group(required=True) -+ g.add_argument( -+ "--interactive", -+ action="store_true", -+ help="Guided mnemonic entry (English-only, per-word validation)", -+ ) -+ g.add_argument( -+ "--mnemonic-stdin", -+ dest="mnemonic_stdin", -+ action="store_true", -+ help="Read BIP39 mnemonic from stdin (non-interactive)", -+ ) -+ g.add_argument( -+ "--seed-stdin", -+ dest="seed_stdin", -+ action="store_true", -+ help="Read seed hex (128 hex chars) from stdin (non-interactive)", -+ ) - - # test - p_test = sub.add_parser("test", help="Run minimal offline tests") - p_test.add_argument("--off-screen", action="store_true", help="Enable off-screen mode: no printing of sensitive data to stdout.") - - return parser diff --git a/tests/bootstrap_vectors.py b/tests/bootstrap_vectors.py new file mode 100644 index 0000000..fc89f06 --- /dev/null +++ b/tests/bootstrap_vectors.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Bootstrap script to generate tests/vectors.json. +Run this ONCE (online allowed if needed, though this logic is offline) +to freeze the expected outputs of the current pyhdwallet implementation. +""" +import sys +import json +import os +from pathlib import Path + +# Add src to path so we can import pyhdwallet +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +import pyhdwallet +from bip_utils import Bip39SeedGenerator + +def main(): + print("Bootstrapping test vectors...") + vectors = {} + + # 1. PGP Fingerprint Vector + recipient_path = Path(__file__).parent / "data" / "recipient.asc" + if not recipient_path.exists(): + print(f"Error: {recipient_path} not found. Please create it first.") + sys.exit(1) + + with open(recipient_path, "r", encoding="utf-8") as f: + armored = f.read() + + # Calculate expected fingerprint using current code logic + expected_fpr = pyhdwallet.pgp_fingerprint(armored) + + vectors["pgp"] = { + "recipient_file": "recipient.asc", + "expected_fingerprint": expected_fpr + } + print(f"PGP: Pinned fingerprint {expected_fpr}") + + # 2. BIP39 & Derivation Vectors + mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + vectors["bip39"] = [] + + # Case A: No Passphrase + seed_bytes = Bip39SeedGenerator(mnemonic).Generate("") + seed_hex = seed_bytes.hex() + + # Generate addresses for all supported chains/profiles + chains = ["ethereum", "bitcoin", "solana"] + # Fixed profile names to use underscores instead of hyphens + sol_profiles = ["phantom_bip44change", "phantom_bip44", "solana_bip39_first32"] + + derived_data = {} + + # Run derivation for each sol profile logic + for profile in sol_profiles: + res = pyhdwallet.derive_all( + seed_bytes, + chains, + count=5, + sol_profile=profile, + export_private=False + ) + derived_data[profile] = res["addresses"] + + vectors["bip39"].append({ + "mnemonic": mnemonic, + "passphrase": "", + "expected_seed_hex": seed_hex, + "derived_addresses": derived_data + }) + + # Case B: With Passphrase (Regression test) + passphrase = "TREZOR" + seed_bytes_p = Bip39SeedGenerator(mnemonic).Generate(passphrase) + seed_hex_p = seed_bytes_p.hex() + + res_p = pyhdwallet.derive_all( + seed_bytes_p, + chains, + count=1, + sol_profile="phantom_bip44change", + export_private=False + ) + + vectors["bip39"].append({ + "mnemonic": mnemonic, + "passphrase": passphrase, + "expected_seed_hex": seed_hex_p, + "derived_addresses": {"phantom_bip44change": res_p["addresses"]} + }) + + print("Derivation: Generated vectors for empty and non-empty passphrases.") + + # 3. Save + out_path = Path(__file__).parent / "vectors.json" + with open(out_path, "w", encoding="utf-8") as f: + json.dump(vectors, f, indent=2) + + print(f"Success! Vectors written to {out_path}") + +if __name__ == "__main__": + main() diff --git a/tests/test_vectors.py b/tests/test_vectors.py new file mode 100644 index 0000000..9c162d3 --- /dev/null +++ b/tests/test_vectors.py @@ -0,0 +1,136 @@ +import sys +import os +import json +import pytest +import subprocess +from pathlib import Path + +# Add src to path at the very top +TEST_DIR = Path(__file__).parent +SRC_DIR = TEST_DIR.parent / "src" +sys.path.insert(0, str(SRC_DIR)) + +import pyhdwallet +from bip_utils import Bip39SeedGenerator + +DATA_DIR = TEST_DIR / "data" +VECTORS_FILE = TEST_DIR / "vectors.json" + +@pytest.fixture +def vectors(): + if not VECTORS_FILE.exists(): + pytest.fail("tests/vectors.json missing. Run tests/bootstrap_vectors.py first.") + with open(VECTORS_FILE, "r") as f: + return json.load(f) + +@pytest.fixture +def recipient_key_content(): + path = DATA_DIR / "recipient.asc" + if not path.exists(): + pytest.fail("tests/data/recipient.asc missing.") + with open(path, "r", encoding="utf-8") as f: + return f.read() + +def test_pgp_fingerprint_calculation(vectors, recipient_key_content): + """ + Verifies that pgp_fingerprint computes the expected fingerprint + for the stored recipient key. + """ + expected = vectors["pgp"]["expected_fingerprint"] + actual = pyhdwallet.pgp_fingerprint(recipient_key_content) + assert actual == expected, "Fingerprint calculation logic has changed!" + +def test_pgp_require_fingerprint_match(vectors): + """ + Verifies the safety check require_fingerprint_match enforces exact matches. + """ + expected = vectors["pgp"]["expected_fingerprint"] + + # Should not raise + pyhdwallet.require_fingerprint_match(expected, expected, "test") + + # Should raise on mismatch + wrong = expected.replace("A", "B") if "A" in expected else expected.replace("0", "1") + with pytest.raises(ValueError, match="test: PGP fingerprint mismatch"): + pyhdwallet.require_fingerprint_match(wrong, expected, "test") + +def test_bip39_seed_derivation(vectors): + """ + Verifies that mnemonics convert to seeds exactly as they did at bootstrap time. + """ + for case in vectors["bip39"]: + mnemonic = case["mnemonic"] + passphrase = case["passphrase"] + expected_hex = case["expected_seed_hex"] + + # Verify internal Bip39SeedGenerator usage matches expected hex + actual_bytes = Bip39SeedGenerator(mnemonic).Generate(passphrase) + assert actual_bytes.hex() == expected_hex + +def test_address_derivation_integrity(vectors): + """ + Verifies derive_all produces the exact same addresses for supported chains. + """ + for case in vectors["bip39"]: + seed_hex = case["expected_seed_hex"] + seed_bytes = bytes.fromhex(seed_hex) + + for profile, expected_addresses in case["derived_addresses"].items(): + # Infer count from ethereum addresses in the expected data + # (Ethereum always generates `count` addresses, unlike Solana which depends on profile) + count = len(expected_addresses.get("ethereum", [])) + + result = pyhdwallet.derive_all( + seed_bytes, + chains=["ethereum", "bitcoin", "solana"], + count=count, + sol_profile=profile, + export_private=False + ) + assert result["addresses"] == expected_addresses, f"Mismatch for profile {profile}" + +def test_cli_recover_smoke(tmp_path, vectors): + """ + Runs the CLI in a subprocess to verify end-to-end wiring + without network (recover mode). + """ + vector = vectors["bip39"][0] + mnemonic = vector["mnemonic"] + expected_fp = vectors["pgp"]["expected_fingerprint"] + recipient_file = DATA_DIR / "recipient.asc" + + # 1. Successful Recovery + cmd = [ + sys.executable, "src/pyhdwallet.py", "recover", + "--mnemonic-stdin", + "--pgp-pubkey-file", str(recipient_file), + "--expected-fingerprint", expected_fp, + "--file", + "--wallet-location", str(tmp_path), + "--off-screen", + "--zip-password-mode", "auto" + ] + + result = subprocess.run( + cmd, + input=mnemonic.encode("utf-8"), + capture_output=True + ) + + assert result.returncode == 0, f"CLI failed: {result.stderr.decode()}" + zips = list(tmp_path.glob("*.zip")) + assert len(zips) == 1, "Expected exactly one zip file output" + + # 2. Failed Recovery (Wrong Fingerprint) + wrong_fp = expected_fp.replace("A", "B") + cmd_fail = list(cmd) + cmd_fail[cmd_fail.index(expected_fp)] = wrong_fp + + result_fail = subprocess.run( + cmd_fail, + input=mnemonic.encode("utf-8"), + capture_output=True + ) + + assert result_fail.returncode != 0 + assert b"fingerprint mismatch" in result_fail.stderr or b"fingerprint mismatch" in result_fail.stdout diff --git a/tests/vectors.json b/tests/vectors.json new file mode 100644 index 0000000..cb949ce --- /dev/null +++ b/tests/vectors.json @@ -0,0 +1,482 @@ +{ + "pgp": { + "recipient_file": "recipient.asc", + "expected_fingerprint": "A27B96F2B169B5491013D2DA892B822C14A9AA18" + }, + "bip39": [ + { + "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "passphrase": "", + "expected_seed_hex": "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4", + "derived_addresses": { + "phantom_bip44change": { + "ethereum": [ + { + "index": 0, + "path": "m/44'/60'/0'/0/0", + "address": "0x9858EfFD232B4033E47d90003D41EC34EcaEda94" + }, + { + "index": 1, + "path": "m/44'/60'/0'/0/1", + "address": "0x6Fac4D18c912343BF86fa7049364Dd4E424Ab9C0" + }, + { + "index": 2, + "path": "m/44'/60'/0'/0/2", + "address": "0xb6716976A3ebe8D39aCEB04372f22Ff8e6802D7A" + }, + { + "index": 3, + "path": "m/44'/60'/0'/0/3", + "address": "0xF3f50213C1d2e255e4B2bAD430F8A38EEF8D718E" + }, + { + "index": 4, + "path": "m/44'/60'/0'/0/4", + "address": "0x51cA8ff9f1C0a99f88E86B8112eA3237F55374cA" + } + ], + "solana": [ + { + "index": 0, + "path": "m/44'/501'/0'/0'", + "address": "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk" + }, + { + "index": 1, + "path": "m/44'/501'/1'/0'", + "address": "Hh8QwFUA6MtVu1qAoq12ucvFHNwCcVTV7hpWjeY1Hztb" + }, + { + "index": 2, + "path": "m/44'/501'/2'/0'", + "address": "7WktogJEd2wQ9eH2oWusmcoFTgeYi6rS632UviTBJ2jm" + }, + { + "index": 3, + "path": "m/44'/501'/3'/0'", + "address": "3YqEpfo3c818GhvbQ1UmVY1nJxw16vtu4JB9peJXT94k" + }, + { + "index": 4, + "path": "m/44'/501'/4'/0'", + "address": "6nod592sTfEWD3VSVPdQndLMVBCNmMc6ngt7MyGBK21j" + } + ], + "bitcoin": [ + { + "index": 0, + "path": "m/84'/0'/0'/0/0", + "address_type": "native_segwit", + "address": "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu" + }, + { + "index": 0, + "path": "m/49'/0'/0'/0/0", + "address_type": "segwit", + "address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf" + }, + { + "index": 0, + "path": "m/44'/0'/0'/0/0", + "address_type": "legacy", + "address": "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA" + }, + { + "index": 1, + "path": "m/84'/0'/0'/0/1", + "address_type": "native_segwit", + "address": "bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g" + }, + { + "index": 1, + "path": "m/49'/0'/0'/0/1", + "address_type": "segwit", + "address": "3LtMnn87fqUeHBUG414p9CWwnoV6E2pNKS" + }, + { + "index": 1, + "path": "m/44'/0'/0'/0/1", + "address_type": "legacy", + "address": "1Ak8PffB2meyfYnbXZR9EGfLfFZVpzJvQP" + }, + { + "index": 2, + "path": "m/84'/0'/0'/0/2", + "address_type": "native_segwit", + "address": "bc1qp59yckz4ae5c4efgw2s5wfyvrz0ala7rgvuz8z" + }, + { + "index": 2, + "path": "m/49'/0'/0'/0/2", + "address_type": "segwit", + "address": "3B4cvWGR8X6Xs8nvTxVUoMJV77E4f7oaia" + }, + { + "index": 2, + "path": "m/44'/0'/0'/0/2", + "address_type": "legacy", + "address": "1MNF5RSaabFwcbtJirJwKnDytsXXEsVsNb" + }, + { + "index": 3, + "path": "m/84'/0'/0'/0/3", + "address_type": "native_segwit", + "address": "bc1qgl5vlg0zdl7yvprgxj9fevsc6q6x5dmcyk3cn3" + }, + { + "index": 3, + "path": "m/49'/0'/0'/0/3", + "address_type": "segwit", + "address": "38CahkVftQneLonbWtfWxiiaT2fdnzsEAN" + }, + { + "index": 3, + "path": "m/44'/0'/0'/0/3", + "address_type": "legacy", + "address": "1MVGa13XFvvpKGZdX389iU8b3qwtmAyrsJ" + }, + { + "index": 4, + "path": "m/84'/0'/0'/0/4", + "address_type": "native_segwit", + "address": "bc1qm97vqzgj934vnaq9s53ynkyf9dgr05rargr04n" + }, + { + "index": 4, + "path": "m/49'/0'/0'/0/4", + "address_type": "segwit", + "address": "37mbeJptxfQC6SNNLJ9a8efCY4BwBh5Kak" + }, + { + "index": 4, + "path": "m/44'/0'/0'/0/4", + "address_type": "legacy", + "address": "1Gka4JdwhLxRwXaC6oLNH4YuEogeeSwqW7" + } + ] + }, + "phantom_bip44": { + "ethereum": [ + { + "index": 0, + "path": "m/44'/60'/0'/0/0", + "address": "0x9858EfFD232B4033E47d90003D41EC34EcaEda94" + }, + { + "index": 1, + "path": "m/44'/60'/0'/0/1", + "address": "0x6Fac4D18c912343BF86fa7049364Dd4E424Ab9C0" + }, + { + "index": 2, + "path": "m/44'/60'/0'/0/2", + "address": "0xb6716976A3ebe8D39aCEB04372f22Ff8e6802D7A" + }, + { + "index": 3, + "path": "m/44'/60'/0'/0/3", + "address": "0xF3f50213C1d2e255e4B2bAD430F8A38EEF8D718E" + }, + { + "index": 4, + "path": "m/44'/60'/0'/0/4", + "address": "0x51cA8ff9f1C0a99f88E86B8112eA3237F55374cA" + } + ], + "solana": [ + { + "index": 0, + "path": "m/44'/501'/0'", + "address": "GjJyeC1r2RgkuoCWMyPYkCWSGSGLcz266EaAkLA27AhL" + }, + { + "index": 1, + "path": "m/44'/501'/1'", + "address": "ANf3TEKFL6jPWjzkndo4CbnNdUNkBk4KHPggJs2nu8Xi" + }, + { + "index": 2, + "path": "m/44'/501'/2'", + "address": "Ag74i82rUZBTgMGLacCA1ZLnotvAca8CLscXcrG6Nwem" + }, + { + "index": 3, + "path": "m/44'/501'/3'", + "address": "weCFpgyyyrzum6nA8XdmJXjDGDTXmG5P2DdgHv59hgQ" + }, + { + "index": 4, + "path": "m/44'/501'/4'", + "address": "4w6V162fV7HJQNma7vZvxjunqmkie8hM2x1DqaFQqxqL" + } + ], + "bitcoin": [ + { + "index": 0, + "path": "m/84'/0'/0'/0/0", + "address_type": "native_segwit", + "address": "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu" + }, + { + "index": 0, + "path": "m/49'/0'/0'/0/0", + "address_type": "segwit", + "address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf" + }, + { + "index": 0, + "path": "m/44'/0'/0'/0/0", + "address_type": "legacy", + "address": "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA" + }, + { + "index": 1, + "path": "m/84'/0'/0'/0/1", + "address_type": "native_segwit", + "address": "bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g" + }, + { + "index": 1, + "path": "m/49'/0'/0'/0/1", + "address_type": "segwit", + "address": "3LtMnn87fqUeHBUG414p9CWwnoV6E2pNKS" + }, + { + "index": 1, + "path": "m/44'/0'/0'/0/1", + "address_type": "legacy", + "address": "1Ak8PffB2meyfYnbXZR9EGfLfFZVpzJvQP" + }, + { + "index": 2, + "path": "m/84'/0'/0'/0/2", + "address_type": "native_segwit", + "address": "bc1qp59yckz4ae5c4efgw2s5wfyvrz0ala7rgvuz8z" + }, + { + "index": 2, + "path": "m/49'/0'/0'/0/2", + "address_type": "segwit", + "address": "3B4cvWGR8X6Xs8nvTxVUoMJV77E4f7oaia" + }, + { + "index": 2, + "path": "m/44'/0'/0'/0/2", + "address_type": "legacy", + "address": "1MNF5RSaabFwcbtJirJwKnDytsXXEsVsNb" + }, + { + "index": 3, + "path": "m/84'/0'/0'/0/3", + "address_type": "native_segwit", + "address": "bc1qgl5vlg0zdl7yvprgxj9fevsc6q6x5dmcyk3cn3" + }, + { + "index": 3, + "path": "m/49'/0'/0'/0/3", + "address_type": "segwit", + "address": "38CahkVftQneLonbWtfWxiiaT2fdnzsEAN" + }, + { + "index": 3, + "path": "m/44'/0'/0'/0/3", + "address_type": "legacy", + "address": "1MVGa13XFvvpKGZdX389iU8b3qwtmAyrsJ" + }, + { + "index": 4, + "path": "m/84'/0'/0'/0/4", + "address_type": "native_segwit", + "address": "bc1qm97vqzgj934vnaq9s53ynkyf9dgr05rargr04n" + }, + { + "index": 4, + "path": "m/49'/0'/0'/0/4", + "address_type": "segwit", + "address": "37mbeJptxfQC6SNNLJ9a8efCY4BwBh5Kak" + }, + { + "index": 4, + "path": "m/44'/0'/0'/0/4", + "address_type": "legacy", + "address": "1Gka4JdwhLxRwXaC6oLNH4YuEogeeSwqW7" + } + ] + }, + "solana_bip39_first32": { + "ethereum": [ + { + "index": 0, + "path": "m/44'/60'/0'/0/0", + "address": "0x9858EfFD232B4033E47d90003D41EC34EcaEda94" + }, + { + "index": 1, + "path": "m/44'/60'/0'/0/1", + "address": "0x6Fac4D18c912343BF86fa7049364Dd4E424Ab9C0" + }, + { + "index": 2, + "path": "m/44'/60'/0'/0/2", + "address": "0xb6716976A3ebe8D39aCEB04372f22Ff8e6802D7A" + }, + { + "index": 3, + "path": "m/44'/60'/0'/0/3", + "address": "0xF3f50213C1d2e255e4B2bAD430F8A38EEF8D718E" + }, + { + "index": 4, + "path": "m/44'/60'/0'/0/4", + "address": "0x51cA8ff9f1C0a99f88E86B8112eA3237F55374cA" + } + ], + "solana": [ + { + "index": 0, + "path": "BIP39 seed[0:32]", + "address": "EHqmfkN89RJ7Y33CXM6uCzhVeuywHoJXZZLszBHHZy7o" + } + ], + "bitcoin": [ + { + "index": 0, + "path": "m/84'/0'/0'/0/0", + "address_type": "native_segwit", + "address": "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu" + }, + { + "index": 0, + "path": "m/49'/0'/0'/0/0", + "address_type": "segwit", + "address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf" + }, + { + "index": 0, + "path": "m/44'/0'/0'/0/0", + "address_type": "legacy", + "address": "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA" + }, + { + "index": 1, + "path": "m/84'/0'/0'/0/1", + "address_type": "native_segwit", + "address": "bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g" + }, + { + "index": 1, + "path": "m/49'/0'/0'/0/1", + "address_type": "segwit", + "address": "3LtMnn87fqUeHBUG414p9CWwnoV6E2pNKS" + }, + { + "index": 1, + "path": "m/44'/0'/0'/0/1", + "address_type": "legacy", + "address": "1Ak8PffB2meyfYnbXZR9EGfLfFZVpzJvQP" + }, + { + "index": 2, + "path": "m/84'/0'/0'/0/2", + "address_type": "native_segwit", + "address": "bc1qp59yckz4ae5c4efgw2s5wfyvrz0ala7rgvuz8z" + }, + { + "index": 2, + "path": "m/49'/0'/0'/0/2", + "address_type": "segwit", + "address": "3B4cvWGR8X6Xs8nvTxVUoMJV77E4f7oaia" + }, + { + "index": 2, + "path": "m/44'/0'/0'/0/2", + "address_type": "legacy", + "address": "1MNF5RSaabFwcbtJirJwKnDytsXXEsVsNb" + }, + { + "index": 3, + "path": "m/84'/0'/0'/0/3", + "address_type": "native_segwit", + "address": "bc1qgl5vlg0zdl7yvprgxj9fevsc6q6x5dmcyk3cn3" + }, + { + "index": 3, + "path": "m/49'/0'/0'/0/3", + "address_type": "segwit", + "address": "38CahkVftQneLonbWtfWxiiaT2fdnzsEAN" + }, + { + "index": 3, + "path": "m/44'/0'/0'/0/3", + "address_type": "legacy", + "address": "1MVGa13XFvvpKGZdX389iU8b3qwtmAyrsJ" + }, + { + "index": 4, + "path": "m/84'/0'/0'/0/4", + "address_type": "native_segwit", + "address": "bc1qm97vqzgj934vnaq9s53ynkyf9dgr05rargr04n" + }, + { + "index": 4, + "path": "m/49'/0'/0'/0/4", + "address_type": "segwit", + "address": "37mbeJptxfQC6SNNLJ9a8efCY4BwBh5Kak" + }, + { + "index": 4, + "path": "m/44'/0'/0'/0/4", + "address_type": "legacy", + "address": "1Gka4JdwhLxRwXaC6oLNH4YuEogeeSwqW7" + } + ] + } + } + }, + { + "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "passphrase": "TREZOR", + "expected_seed_hex": "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", + "derived_addresses": { + "phantom_bip44change": { + "ethereum": [ + { + "index": 0, + "path": "m/44'/60'/0'/0/0", + "address": "0x9c32F71D4DB8Fb9e1A58B0a80dF79935e7256FA6" + } + ], + "solana": [ + { + "index": 0, + "path": "m/44'/501'/0'/0'", + "address": "7zSmbu6gKkb6HB7UDPtHYjwCWuBHU1D4TpNZFm4sndQe" + } + ], + "bitcoin": [ + { + "index": 0, + "path": "m/84'/0'/0'/0/0", + "address_type": "native_segwit", + "address": "bc1qv5rmq0kt9yz3pm36wvzct7p3x6mtgehjul0feu" + }, + { + "index": 0, + "path": "m/49'/0'/0'/0/0", + "address_type": "segwit", + "address": "3Aho3kS7vgVWKTpRHjcqBoPXiCujiSuTaZ" + }, + { + "index": 0, + "path": "m/44'/0'/0'/0/0", + "address_type": "legacy", + "address": "1PEha8dk5Me5J1rZWpgqSt5F4BroTBLS5y" + } + ] + } + } + } + ] +} \ No newline at end of file