test: add offline integrity test suite with frozen vectors

This commit is contained in:
LC mac
2026-01-07 23:54:25 +08:00
parent 28f01a613a
commit 5a52eb0954
5 changed files with 723 additions and 204 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.venv/
.venv312/
__pycache__/
*.pyc
.vscode/

View File

@@ -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

104
tests/bootstrap_vectors.py Normal file
View File

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

136
tests/test_vectors.py Normal file
View File

@@ -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

482
tests/vectors.json Normal file
View File

@@ -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"
}
]
}
}
}
]
}