Compare commits
24 Commits
b9ab22c131
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6c84f81ee | ||
| 109829f1f5 | |||
|
|
2f7433b704 | ||
|
|
6457ec2cee | ||
|
|
0949fe9792 | ||
| 21b9389591 | |||
|
|
369c8595a1 | ||
|
|
2807982209 | ||
|
|
84953dbe5a | ||
|
|
6fd7cd4e79 | ||
|
|
5a52eb0954 | ||
|
|
28f01a613a | ||
| a9af9d33af | |||
| 129b09fcd9 | |||
|
|
d02e1d872e | ||
|
|
875fa17d6c | ||
|
|
ccd070dc56 | ||
|
|
ce26b3560a | ||
| 4cf32f9ba0 | |||
| 94fcb993db | |||
| 5c343c7944 | |||
| 6679e4ef9e | |||
| f02e04389c | |||
| a4f188af36 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.venv/
|
||||
.venv312/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.vscode/
|
||||
@@ -9,6 +10,7 @@ logs/
|
||||
*.log
|
||||
coverage/
|
||||
_toDelete/
|
||||
_toDelete/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
@@ -22,3 +24,6 @@ build/
|
||||
*.sqlite3
|
||||
*.db
|
||||
*.asc
|
||||
.venv/
|
||||
.wallet/
|
||||
.potentialfix.md
|
||||
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
# Dockerfile
|
||||
# Build environment for hdwalletpy: Python 3.12 + build tools + venv support
|
||||
# Used for: (1) Building Linux x86_64 wheels, (2) Development container
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Install build tools, headers, and venv module
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
python3-dev \
|
||||
libffi-dev \
|
||||
python3-venv \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory for bind mounts
|
||||
WORKDIR /app
|
||||
|
||||
# Default command (can be overridden)
|
||||
CMD ["/bin/bash"]
|
||||
169
Makefile
Normal file
169
Makefile
Normal file
@@ -0,0 +1,169 @@
|
||||
# Makefile for hdwalletpy workflow
|
||||
# - Build reusable Docker image (Python 3.12)
|
||||
# - Vendor multi-platform wheels for offline air-gapped use
|
||||
# - Compile wheels in container (wheelhouse)
|
||||
# - Create host venv and install from wheelhouse or vendor/
|
||||
# - Optionally run inside a warm container
|
||||
|
||||
# ---------- Config ----------
|
||||
IMAGE := hdwallet-build:3.12
|
||||
CONTAINER := hdwallet-dev
|
||||
WORKDIR := /app
|
||||
VENV_HOST := .venv
|
||||
VENV_CONTAINER := /opt/venv
|
||||
WHEELHOUSE := wheelhouse
|
||||
|
||||
# Vendor directories for air-gapped deployment
|
||||
VENDOR_MACOS := vendor/macos-arm64
|
||||
VENDOR_LINUX := vendor/linux-x86_64
|
||||
|
||||
# ---------- Help ----------
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Vendoring (for offline/air-gapped use):"
|
||||
@echo " make vendor-macos - Build macOS ARM64 wheels (native)"
|
||||
@echo " make vendor-linux - Build Linux x86_64 wheels (Docker)"
|
||||
@echo " make vendor-all - Build wheels for both platforms"
|
||||
@echo " make verify-vendor - Test offline installation from vendor/"
|
||||
@echo ""
|
||||
@echo "Development workflow:"
|
||||
@echo " make build-image - Build Docker image (Python 3.12)"
|
||||
@echo " make wheels - Build wheels into ./$(WHEELHOUSE)"
|
||||
@echo " make install - Create venv and install dependencies"
|
||||
@echo " make test - Run test suite"
|
||||
@echo " make up - Start warm dev container"
|
||||
@echo " make shell - Open shell in warm container"
|
||||
@echo " make down - Stop and remove dev container"
|
||||
@echo ""
|
||||
@echo "Cleanup:"
|
||||
@echo " make clean - Remove venv, wheelhouse, vendor/"
|
||||
@echo " make clean-vendor - Remove vendor/ only"
|
||||
|
||||
# ---------- Build reusable image ----------
|
||||
.PHONY: build-image
|
||||
build-image:
|
||||
docker build -t $(IMAGE) .
|
||||
|
||||
# ---------- Vendoring for Air-Gapped Use ----------
|
||||
.PHONY: vendor-macos
|
||||
vendor-macos: requirements.txt
|
||||
@echo "Building macOS ARM64 wheels (native)..."
|
||||
@if [ ! -f ".venv312/bin/pip" ]; then \
|
||||
echo "ERROR: .venv312 not found. Create it first:"; \
|
||||
echo " python3.12 -m venv .venv312 && source .venv312/bin/activate"; \
|
||||
exit 1; \
|
||||
fi
|
||||
mkdir -p $(VENDOR_MACOS)
|
||||
.venv312/bin/pip download --dest $(VENDOR_MACOS) -r requirements.txt
|
||||
cd $(VENDOR_MACOS) && shasum -a 256 *.whl > SHA256SUMS
|
||||
@echo "✓ macOS ARM64 wheels: $(VENDOR_MACOS)/"
|
||||
|
||||
.PHONY: vendor-linux
|
||||
vendor-linux: requirements.txt build-image
|
||||
@echo "Building Linux x86_64 wheels (Docker)..."
|
||||
mkdir -p $(VENDOR_LINUX)
|
||||
docker run --rm \
|
||||
-v "$$PWD":$(WORKDIR) \
|
||||
-w $(WORKDIR) \
|
||||
$(IMAGE) \
|
||||
bash -c " \
|
||||
pip install --upgrade pip && \
|
||||
pip download --dest $(VENDOR_LINUX) -r requirements.txt && \
|
||||
pip wheel --wheel-dir $(VENDOR_LINUX) --no-deps $(VENDOR_LINUX)/*.tar.gz 2>/dev/null || true && \
|
||||
rm -f $(VENDOR_LINUX)/*.tar.gz && \
|
||||
cd $(VENDOR_LINUX) && sha256sum *.whl > SHA256SUMS \
|
||||
"
|
||||
@echo "✓ Linux x86_64 wheels: $(VENDOR_LINUX)/"
|
||||
|
||||
.PHONY: vendor-all
|
||||
vendor-all: vendor-macos vendor-linux
|
||||
@echo ""
|
||||
@echo "✓ All platforms vendored:"
|
||||
@echo " macOS ARM64: $(VENDOR_MACOS)/ ($$(ls $(VENDOR_MACOS)/*.whl 2>/dev/null | wc -l | xargs) wheels)"
|
||||
@echo " Linux x86_64: $(VENDOR_LINUX)/ ($$(ls $(VENDOR_LINUX)/*.whl 2>/dev/null | wc -l | xargs) wheels)"
|
||||
@echo ""
|
||||
@echo "Commit with: git add vendor/ && git commit -m 'vendor: update wheels'"
|
||||
|
||||
.PHONY: verify-vendor
|
||||
verify-vendor:
|
||||
@echo "Testing offline installation from vendor/..."
|
||||
@bash -c ' \
|
||||
if [[ "$$OSTYPE" == "darwin"* ]]; then \
|
||||
PLATFORM="macos-arm64"; \
|
||||
else \
|
||||
PLATFORM="linux-x86_64"; \
|
||||
fi; \
|
||||
echo "Platform: $$PLATFORM"; \
|
||||
python3.12 -m venv .venv-verify && \
|
||||
source .venv-verify/bin/activate && \
|
||||
pip install --no-index --find-links=vendor/$$PLATFORM -r requirements.txt && \
|
||||
pytest -v tests/test_vectors.py && \
|
||||
python src/pyhdwallet.py test && \
|
||||
echo "" && \
|
||||
echo "✅ Vendor installation verified!" && \
|
||||
deactivate && \
|
||||
rm -rf .venv-verify \
|
||||
'
|
||||
|
||||
# ---------- Development Workflow ----------
|
||||
.PHONY: wheels
|
||||
wheels: requirements.txt build-image
|
||||
docker run --rm \
|
||||
-v "$$PWD":$(WORKDIR) \
|
||||
-w $(WORKDIR) \
|
||||
$(IMAGE) \
|
||||
bash -c " \
|
||||
pip install --upgrade pip setuptools wheel && \
|
||||
mkdir -p $(WHEELHOUSE) && \
|
||||
pip wheel -r requirements.txt -w $(WHEELHOUSE) \
|
||||
"
|
||||
|
||||
.PHONY: install
|
||||
install: requirements.txt
|
||||
@if [ ! -d "$(VENV_HOST)" ]; then \
|
||||
echo "Creating venv: $(VENV_HOST)"; \
|
||||
python3.12 -m venv $(VENV_HOST); \
|
||||
fi
|
||||
. $(VENV_HOST)/bin/activate && \
|
||||
pip install --upgrade pip && \
|
||||
pip install -r requirements.txt && \
|
||||
echo "✓ Virtual environment ready: $(VENV_HOST)"
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@if [ ! -d "$(VENV_HOST)" ]; then \
|
||||
echo "ERROR: Run 'make install' first"; \
|
||||
exit 1; \
|
||||
fi
|
||||
. $(VENV_HOST)/bin/activate && \
|
||||
pytest -v tests/test_vectors.py && \
|
||||
python src/pyhdwallet.py test
|
||||
|
||||
# ---------- Warm container lifecycle ----------
|
||||
.PHONY: up
|
||||
up: build-image
|
||||
docker run -dit \
|
||||
-v "$$PWD":$(WORKDIR) \
|
||||
-w $(WORKDIR) \
|
||||
--name $(CONTAINER) \
|
||||
$(IMAGE) \
|
||||
bash
|
||||
|
||||
.PHONY: shell
|
||||
shell:
|
||||
docker exec -it $(CONTAINER) bash
|
||||
|
||||
.PHONY: down
|
||||
down:
|
||||
- docker rm -f $(CONTAINER)
|
||||
|
||||
# ---------- Cleanup ----------
|
||||
.PHONY: clean-vendor
|
||||
clean-vendor:
|
||||
rm -rf vendor/
|
||||
|
||||
.PHONY: clean
|
||||
clean: down
|
||||
rm -rf $(VENV_HOST) $(WHEELHOUSE) vendor/ .venv-verify .venv-offline-test
|
||||
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
|
||||
212
README.md
212
README.md
@@ -1,31 +1,203 @@
|
||||
# pyhdwallet v1.0.2
|
||||
# pyhdwallet – Secure HD Wallet Tool
|
||||
|
||||
A secure, offline command-line tool for generating and recovering BIP39 HD wallets with support for Ethereum, Solana, and Bitcoin.
|
||||
A Python command-line tool for generating and recovering BIP39 HD wallets with support for Ethereum, Solana, and Bitcoin. Includes **BIP85 deterministic child mnemonic derivation** for creating multiple isolated wallets from a single master seed. Designed for offline operation with optional PGP encryption and AES-encrypted ZIP artifacts.
|
||||
|
||||
## Purpose
|
||||
---
|
||||
|
||||
pyhdwallet helps you create new wallets or recover from existing mnemonics/seeds, with built-in PGP encryption for secure storage. It's designed for privacy-conscious users who want offline, auditable wallet management.
|
||||
## 📦 Installation
|
||||
|
||||
## Installation
|
||||
### **Quick Start (macOS/Linux with Internet)**
|
||||
|
||||
1. Ensure Python 3.11+ is installed.
|
||||
2. Clone/download the repo.
|
||||
3. Create a virtual environment: `python -m venv .venv && source .venv/bin/activate`
|
||||
4. Install dependencies: `pip install -r requirements.txt`
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/<your-username>/hdwalletpy.git
|
||||
cd hdwalletpy
|
||||
|
||||
## Basic Usage
|
||||
# Install using automated script
|
||||
./install_offline.sh
|
||||
```
|
||||
|
||||
Run the tool with a subcommand. For help, use `-h`.
|
||||
The script automatically:
|
||||
|
||||
- Generate a wallet: `python ./src/pyhdwallet.py gen --chains ethereum --addresses 3`
|
||||
- Recover from mnemonic: `python ./src/pyhdwallet.py recover --mnemonic "your words" --chains bitcoin`
|
||||
- Fetch PGP key: `python ./src/pyhdwallet.py fetchkey "https://example.com/key.asc"`
|
||||
- Run tests: `python ./src/pyhdwallet.py test`
|
||||
- Creates Python 3.12 virtual environment
|
||||
- Installs from vendored wheels (offline-capable)
|
||||
- Verifies installation with test suite
|
||||
- Leaves you in activated venv
|
||||
|
||||
For detailed examples and security tips, see `playbook.md`.
|
||||
---
|
||||
|
||||
## Security
|
||||
### **Air-Gapped Installation (No Internet)**
|
||||
|
||||
- Operates offline by default.
|
||||
- Use `--secure-mode` for high-security operations.
|
||||
- Always verify PGP keys and run on trusted systems.
|
||||
This repository includes pre-built Python wheels for offline use.
|
||||
|
||||
**Supported platforms:**
|
||||
|
||||
- macOS ARM64 (M1/M2/M3/M4) - Python 3.12
|
||||
- Linux x86_64 (Ubuntu/Tails) - Python 3.12
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. On an online machine, clone and verify:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<your-username>/hdwalletpy.git
|
||||
cd hdwalletpy
|
||||
|
||||
# Verify checksums
|
||||
cd vendor/linux-x86_64 # or macos-arm64
|
||||
sha256sum -c SHA256SUMS # Linux
|
||||
shasum -a 256 -c SHA256SUMS # macOS
|
||||
```
|
||||
|
||||
2. Transfer entire repo to USB drive
|
||||
|
||||
3. On air-gapped machine:
|
||||
|
||||
```bash
|
||||
cd hdwalletpy
|
||||
./install_offline.sh
|
||||
```
|
||||
|
||||
4. Generate wallet:
|
||||
|
||||
```bash
|
||||
python src/pyhdwallet.py gen --off-screen --file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Developer Installation (with Docker)**
|
||||
|
||||
For development or building wheels for other platforms:
|
||||
|
||||
```bash
|
||||
# Build Docker image
|
||||
make build-image
|
||||
|
||||
# Build wheels for all platforms
|
||||
make vendor-all
|
||||
|
||||
# Install development environment
|
||||
make install
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Basic Usage
|
||||
|
||||
```bash
|
||||
# Generate wallet (prints mnemonic - debug mode)
|
||||
python src/pyhdwallet.py gen
|
||||
|
||||
# Generate with off-screen mode + encrypted ZIP
|
||||
python src/pyhdwallet.py gen --off-screen --file
|
||||
|
||||
# Generate with PGP encryption + ZIP
|
||||
python src/pyhdwallet.py gen \
|
||||
--pgp-pubkey-file pubkeys/mykey.asc \
|
||||
--expected-fingerprint A27B96F2B169B5491013D2DA892B822C14A9AA18 \
|
||||
--off-screen \
|
||||
--file
|
||||
|
||||
# Derive BIP85 child mnemonic (12/15/18/21/24 words)
|
||||
python src/pyhdwallet.py gen-child \
|
||||
--interactive \
|
||||
--words 12 \
|
||||
--index 0
|
||||
|
||||
# Derive BIP85 child with passphrase + encrypted output
|
||||
python src/pyhdwallet.py gen-child \
|
||||
--mnemonic-stdin \
|
||||
--passphrase \
|
||||
--words 24 \
|
||||
--index 0 \
|
||||
--pgp-pubkey-file pubkeys/mykey.asc \
|
||||
--off-screen \
|
||||
--file
|
||||
|
||||
# Recover wallet from mnemonic
|
||||
python src/pyhdwallet.py recover --interactive --file
|
||||
|
||||
# Fetch PGP public key (requires internet)
|
||||
python src/pyhdwallet.py fetchkey "https://example.com/key.asc" --out mykey.asc
|
||||
|
||||
# Run tests
|
||||
python src/pyhdwallet.py test
|
||||
pytest -v tests/test_vectors.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Features
|
||||
|
||||
- **Offline-first**: Network access blocked during key generation/recovery
|
||||
- **BIP85 deterministic entropy**: Derive unlimited child mnemonics from master seed
|
||||
- **Test suite**: Regression tests with frozen vectors ensure derivation logic integrity
|
||||
- **PGP fingerprint pinning**: Prevents key substitution attacks
|
||||
- **TTY safety guard**: Refuses to print secrets when stdout is piped/redirected
|
||||
- **AES-encrypted outputs**: Wallet artifacts encrypted with `pyzipper`
|
||||
- **No shell history leaks**: Use `--interactive` or `--mnemonic-stdin` for recovery
|
||||
- **Interoperable**: BIP85 children compatible with Coldcard, Ian Coleman tool, etc.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Makefile Targets
|
||||
|
||||
**Vendoring (for air-gapped deployment):**
|
||||
|
||||
```bash
|
||||
make vendor-macos # Build macOS ARM64 wheels
|
||||
make vendor-linux # Build Linux x86_64 wheels (Docker)
|
||||
make vendor-all # Build for both platforms
|
||||
make verify-vendor # Test offline installation
|
||||
```
|
||||
|
||||
**Development:**
|
||||
|
||||
```bash
|
||||
make install # Create venv and install dependencies
|
||||
make test # Run test suite
|
||||
make build-image # Build Docker image
|
||||
make shell # Open shell in Docker container
|
||||
make clean # Remove venvs and build artifacts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Full Documentation
|
||||
|
||||
- **[playbook.md](playbook.md)** - Complete command reference and operational guide
|
||||
- **[tests/](tests/)** - Regression test suite documentation
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Recommended Air-Gapped Setup
|
||||
|
||||
For maximum security when generating production wallets:
|
||||
|
||||
1. Use fresh Ubuntu Live USB or Tails OS
|
||||
2. Never connect to network after booting
|
||||
3. Transfer this repository via separate USB
|
||||
4. Run `./install_offline.sh`
|
||||
5. Generate wallet: `python src/pyhdwallet.py gen --off-screen --file`
|
||||
6. Write mnemonic to paper/metal backup
|
||||
7. Transfer encrypted ZIP to secure storage
|
||||
8. Wipe USB drives securely
|
||||
|
||||
See [playbook.md](playbook.md) for detailed air-gapped procedures.
|
||||
|
||||
---
|
||||
|
||||
## 🆕 What's New in v1.1.0
|
||||
|
||||
- **BIP85 child mnemonic derivation** via new `gen-child` command
|
||||
- Derive 12/15/18/21/24-word child mnemonics from master seed
|
||||
- Full interoperability with BIP85-compatible wallets (Coldcard, etc.)
|
||||
- Optional master BIP39 passphrase support
|
||||
- Verified against official BIP85 test vectors
|
||||
- No new dependencies required (uses existing bip-utils + stdlib)
|
||||
|
||||
---
|
||||
|
||||
@@ -1,655 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
hdwallet_recovery.py (Python 3.12)
|
||||
|
||||
Subcommands:
|
||||
- fetchkey (online): download ASCII-armored PGP pubkey, print SHA256 + fingerprint
|
||||
- gen: generate BIP39 mnemonic (12/15/18/24 words) with optional passphrase and dice entropy
|
||||
- recover (default): derive ADDRESSES ONLY for ETH/SOL/BTC from mnemonic/seed
|
||||
- optionally encrypt a secret payload using a PGP public key
|
||||
|
||||
Security invariant:
|
||||
- recover never performs network I/O
|
||||
- fetchkey refuses mnemonic/seed/passphrase inputs
|
||||
- gen generates secure mnemonics compliant with BIP39
|
||||
|
||||
Solana derivation (Mode "A"):
|
||||
- Derive accounts as: m/44'/501'/{i}'/0'
|
||||
- Use SLIP-0010 ed25519 hardened derivation from the BIP39 seed. [page:0][page:1]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import json
|
||||
import getpass
|
||||
import hashlib
|
||||
import hmac
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Dependency checks
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def _require(mod: str, pkg: str):
|
||||
try:
|
||||
__import__(mod)
|
||||
except ImportError:
|
||||
print(f"❌ Missing dependency: {pkg}", file=sys.stderr)
|
||||
print(f"Install with: pip install {pkg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def require_for_fetchkey():
|
||||
_require("pgpy", "PGPy")
|
||||
|
||||
|
||||
def require_for_derive(export_private: bool, chains: List[str]):
|
||||
_require("bip_utils", "bip-utils")
|
||||
_require("pgpy", "PGPy")
|
||||
|
||||
# Needed to compute Solana pubkey/address + optional Phantom secret exports
|
||||
if "solana" in chains:
|
||||
_require("nacl", "PyNaCl")
|
||||
_require("base58", "base58")
|
||||
|
||||
|
||||
def get_master_fingerprint(seed_bytes: bytes) -> str:
|
||||
from bip_utils import Bip32Slip10Secp256k1, Hash160
|
||||
master = Bip32Slip10Secp256k1.FromSeed(seed_bytes)
|
||||
pubkey_bytes = master.PublicKey().RawCompressed().ToBytes()
|
||||
hash160 = Hash160.QuickDigest(pubkey_bytes)
|
||||
return hash160[:4].hex().upper()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PGP helpers (PGPy)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def pgp_load_pubkey(armored: str):
|
||||
import pgpy
|
||||
key, _ = pgpy.PGPKey.from_blob(armored)
|
||||
if not key.is_public:
|
||||
raise ValueError("Provided key is not a public key")
|
||||
return key
|
||||
|
||||
|
||||
def pgp_fingerprint(armored: str) -> str:
|
||||
return pgp_load_pubkey(armored).fingerprint
|
||||
|
||||
|
||||
def pgp_encrypt_ascii_armored(pubkey_armored: str, payload: Dict[str, Any]) -> str:
|
||||
import pgpy
|
||||
pub_key = pgp_load_pubkey(pubkey_armored)
|
||||
msg = pgpy.PGPMessage.new(json.dumps(payload, indent=2))
|
||||
enc = pub_key.encrypt(msg)
|
||||
return str(enc)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# fetchkey (network allowed ONLY here)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def fetch_ascii_armored_text(url: str, timeout: int = 15) -> str:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "hdwallet-recovery/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
data = resp.read()
|
||||
text = data.decode("utf-8", errors="strict")
|
||||
if "-----BEGIN PGP PUBLIC KEY BLOCK-----" not in text:
|
||||
raise ValueError("Downloaded content does not look like an ASCII-armored PGP public key")
|
||||
return text
|
||||
|
||||
|
||||
def sha256_hex_text(s: str) -> str:
|
||||
return hashlib.sha256(s.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def cmd_fetchkey(url: str, out_path: Optional[str], timeout: int):
|
||||
require_for_fetchkey()
|
||||
|
||||
armored = fetch_ascii_armored_text(url, timeout=timeout)
|
||||
s256 = sha256_hex_text(armored)
|
||||
fpr = pgp_fingerprint(armored)
|
||||
|
||||
if out_path:
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(armored)
|
||||
|
||||
print("✅ Downloaded PGP public key")
|
||||
print(f"URL: {url}")
|
||||
if out_path:
|
||||
print(f"Saved: {out_path}")
|
||||
print(f"SHA256: {s256}")
|
||||
print(f"Fingerprint: {fpr}")
|
||||
|
||||
if not out_path:
|
||||
print("\n-----BEGIN DOWNLOADED KEY-----")
|
||||
print(armored.strip())
|
||||
print("-----END DOWNLOADED KEY-----")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Derivation
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class AddrOut:
|
||||
index: int
|
||||
path: str
|
||||
address: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class BtcAddrOut:
|
||||
index: int
|
||||
path: str
|
||||
address_type: str
|
||||
address: str
|
||||
|
||||
|
||||
def _solana_phantom_export_from_seed32(seed32: bytes) -> Dict[str, Any]:
|
||||
# Phantom-compatible 64-byte secret key = seed32 || pubkey32 (ed25519).
|
||||
from nacl.signing import SigningKey
|
||||
import base58
|
||||
|
||||
sk = SigningKey(seed32)
|
||||
pub32 = sk.verify_key.encode()
|
||||
secret64 = seed32 + pub32
|
||||
|
||||
return {"phantom_base58": base58.b58encode(secret64).decode("ascii")}
|
||||
|
||||
|
||||
# ---- SLIP-0010 ed25519 (matches micro-ed25519-hdkey behavior) ----
|
||||
# Master key generation for ed25519 is:
|
||||
# I = HMAC-SHA512(key="ed25519 seed", data=seed)
|
||||
# k = I_L (32 bytes), c = I_R (32 bytes)
|
||||
# Child derivation (hardened only):
|
||||
# I = HMAC-SHA512(key=c_par, data=0x00 || k_par || ser32(i))
|
||||
# k_i = I_L, c_i = I_R
|
||||
# This is per SLIP-0010. [page:0]
|
||||
_ED25519_SEED_KEY = b"ed25519 seed"
|
||||
|
||||
|
||||
def _hmac_sha512(key: bytes, data: bytes) -> bytes:
|
||||
return hmac.new(key, data, hashlib.sha512).digest()
|
||||
|
||||
|
||||
def _ser32(i: int) -> bytes:
|
||||
return i.to_bytes(4, "big", signed=False)
|
||||
|
||||
|
||||
def _parse_path_ed25519_hardened(path: str) -> List[int]:
|
||||
"""
|
||||
Parse BIP32-like path; for ed25519 we only support hardened indices.
|
||||
Accept both "m/44'/501'/0'/0'" and "m/44/501/0/0" by promoting to hardened,
|
||||
matching the "force hardened" convenience described by micro-ed25519-hdkey. [page:1]
|
||||
"""
|
||||
if not path.startswith("m/"):
|
||||
raise ValueError("Path must start with m/")
|
||||
|
||||
out: List[int] = []
|
||||
for comp in path[2:].split("/"):
|
||||
if comp == "":
|
||||
continue
|
||||
hardened = comp.endswith("'")
|
||||
n_str = comp[:-1] if hardened else comp
|
||||
if not n_str.isdigit():
|
||||
raise ValueError(f"Invalid path component: {comp}")
|
||||
n = int(n_str)
|
||||
# Promote to hardened (ed25519 hardened-only). [page:0][page:1]
|
||||
n = (n | 0x80000000)
|
||||
out.append(n)
|
||||
return out
|
||||
|
||||
|
||||
def _slip10_ed25519_master(seed_bytes: bytes) -> Tuple[bytes, bytes]:
|
||||
I = _hmac_sha512(_ED25519_SEED_KEY, seed_bytes)
|
||||
return I[:32], I[32:]
|
||||
|
||||
|
||||
def _slip10_ed25519_ckd_priv(k_par: bytes, c_par: bytes, index_hardened: int) -> Tuple[bytes, bytes]:
|
||||
if (index_hardened & 0x80000000) == 0:
|
||||
raise ValueError("ed25519 SLIP-0010 supports hardened derivation only") # [page:0]
|
||||
data = b"\x00" + k_par + _ser32(index_hardened)
|
||||
I = _hmac_sha512(c_par, data)
|
||||
return I[:32], I[32:]
|
||||
|
||||
|
||||
def solana_seed32_from_bip39_seed_slip10(seed_bytes: bytes, path: str) -> bytes:
|
||||
"""
|
||||
Derive 32-byte ed25519 private key bytes (seed) from BIP39 seed bytes using SLIP-0010. [page:0]
|
||||
"""
|
||||
idxs = _parse_path_ed25519_hardened(path)
|
||||
k, c = _slip10_ed25519_master(seed_bytes)
|
||||
for i in idxs:
|
||||
k, c = _slip10_ed25519_ckd_priv(k, c, i)
|
||||
return k
|
||||
|
||||
|
||||
def derive_addresses_and_maybe_secrets(seed_bytes: bytes, chains: List[str], count: int, export_private: bool) -> Dict[str, Any]:
|
||||
from bip_utils import (
|
||||
Bip44, Bip44Coins, Bip44Changes,
|
||||
Bip49, Bip49Coins,
|
||||
Bip84, Bip84Coins,
|
||||
)
|
||||
|
||||
out: Dict[str, Any] = {"addresses": {}}
|
||||
if export_private:
|
||||
out["secrets"] = {}
|
||||
|
||||
# ETH (BIP44 m/44'/60'/0'/0/i)
|
||||
if "ethereum" in chains:
|
||||
root = Bip44.FromSeed(seed_bytes, Bip44Coins.ETHEREUM)
|
||||
ctx = root.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
|
||||
|
||||
addrs: List[Dict[str, Any]] = []
|
||||
secrets: List[Dict[str, Any]] = []
|
||||
for i in range(count):
|
||||
node = ctx.AddressIndex(i)
|
||||
addrs.append({"index": i, "path": f"m/44'/60'/0'/0/{i}", "address": node.PublicKey().ToAddress()})
|
||||
if export_private:
|
||||
secrets.append({
|
||||
"index": i,
|
||||
"path": f"m/44'/60'/0'/0/{i}",
|
||||
"privkey_hex": node.PrivateKey().Raw().ToHex(),
|
||||
})
|
||||
|
||||
out["addresses"]["ethereum"] = addrs
|
||||
if export_private and secrets:
|
||||
out["secrets"]["ethereum"] = secrets
|
||||
|
||||
# SOL (Mode "A": m/44'/501'/{i}'/0') derived via SLIP-0010 like micro-ed25519-hdkey. [page:0][page:1]
|
||||
if "solana" in chains:
|
||||
from nacl.signing import SigningKey
|
||||
import base58
|
||||
|
||||
addrs: List[Dict[str, Any]] = []
|
||||
secrets: List[Dict[str, Any]] = []
|
||||
|
||||
for i in range(count):
|
||||
path = f"m/44'/501'/{i}'/0'"
|
||||
seed32 = solana_seed32_from_bip39_seed_slip10(seed_bytes, path)
|
||||
|
||||
sk = SigningKey(seed32)
|
||||
pub32 = sk.verify_key.encode()
|
||||
address = base58.b58encode(pub32).decode("ascii")
|
||||
|
||||
addrs.append({"index": i, "path": path, "address": address})
|
||||
|
||||
if export_private:
|
||||
secrets.append({
|
||||
"index": i,
|
||||
"path": path,
|
||||
"phantom": _solana_phantom_export_from_seed32(seed32),
|
||||
})
|
||||
|
||||
out["addresses"]["solana"] = addrs
|
||||
if export_private and secrets:
|
||||
out["secrets"]["solana"] = secrets
|
||||
|
||||
# BTC (addresses only even with --export-private)
|
||||
if "bitcoin" in chains:
|
||||
addrs: List[Dict[str, Any]] = []
|
||||
|
||||
r44 = Bip44.FromSeed(seed_bytes, Bip44Coins.BITCOIN)
|
||||
c44 = r44.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
|
||||
|
||||
r49 = Bip49.FromSeed(seed_bytes, Bip49Coins.BITCOIN)
|
||||
c49 = r49.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
|
||||
|
||||
r84 = Bip84.FromSeed(seed_bytes, Bip84Coins.BITCOIN)
|
||||
c84 = r84.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
|
||||
|
||||
for i in range(count):
|
||||
n84 = c84.AddressIndex(i)
|
||||
n49 = c49.AddressIndex(i)
|
||||
n44 = c44.AddressIndex(i)
|
||||
|
||||
addrs.append({"index": i, "path": f"m/84'/0'/0'/0/{i}", "address_type": "native_segwit", "address": n84.PublicKey().ToAddress()})
|
||||
addrs.append({"index": i, "path": f"m/49'/0'/0'/0/{i}", "address_type": "segwit", "address": n49.PublicKey().ToAddress()})
|
||||
addrs.append({"index": i, "path": f"m/44'/0'/0'/0/{i}", "address_type": "legacy", "address": n44.PublicKey().ToAddress()})
|
||||
|
||||
out["addresses"]["bitcoin"] = addrs
|
||||
|
||||
# Don't create empty secrets dicts
|
||||
if export_private and "secrets" in out:
|
||||
out["secrets"] = {k: v for k, v in out["secrets"].items() if v}
|
||||
if not out["secrets"]:
|
||||
del out["secrets"]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def format_addresses_human(result: Dict[str, Any]) -> str:
|
||||
lines: List[str] = []
|
||||
lines.append("\n" + "=" * 80)
|
||||
lines.append("MULTI-CHAIN ADDRESS DERIVATION (ADDRESSES ONLY)")
|
||||
lines.append("=" * 80 + "\n")
|
||||
|
||||
for chain, addrs in result["addresses"].items():
|
||||
lines.append("─" * 80)
|
||||
lines.append(f"{chain.upper()} ADDRESSES")
|
||||
lines.append("─" * 80)
|
||||
|
||||
if chain == "bitcoin":
|
||||
by_type: Dict[str, List[Dict[str, Any]]] = {}
|
||||
for a in addrs:
|
||||
by_type.setdefault(a["address_type"], []).append(a)
|
||||
for t in ["native_segwit", "segwit", "legacy"]:
|
||||
if t in by_type:
|
||||
lines.append(f"\n {t.upper()}:")
|
||||
for a in by_type[t]:
|
||||
lines.append(f" [{a['index']}] {a['path']}")
|
||||
lines.append(f" → {a['address']}")
|
||||
else:
|
||||
for a in addrs:
|
||||
lines.append(f" [{a['index']}] {a['path']}")
|
||||
lines.append(f" → {a['address']}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("=" * 80 + "\n")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def cmd_gen(args):
|
||||
require_for_derive(False, args.chains)
|
||||
|
||||
import secrets
|
||||
from bip_utils import Bip39MnemonicGenerator, Bip39Languages, Bip39SeedGenerator
|
||||
|
||||
words_to_entropy = {12: 128, 15: 160, 18: 192, 24: 256}
|
||||
entropy_bits = words_to_entropy[args.words]
|
||||
entropy_bytes_len = entropy_bits // 8
|
||||
|
||||
if args.dice_rolls:
|
||||
rolls = args.dice_rolls.strip().split()
|
||||
if not rolls or not all(r.isdigit() and 1 <= int(r) <= 6 for r in rolls):
|
||||
raise ValueError("--dice-rolls must be space-separated integers 1-6")
|
||||
dice_bytes = " ".join(rolls).encode("utf-8")
|
||||
crypto_bytes = secrets.token_bytes(entropy_bytes_len)
|
||||
combined = dice_bytes + crypto_bytes
|
||||
entropy = hashlib.sha256(combined).digest()[:entropy_bytes_len]
|
||||
else:
|
||||
entropy = secrets.token_bytes(entropy_bytes_len)
|
||||
|
||||
generator = Bip39MnemonicGenerator(Bip39Languages.ENGLISH)
|
||||
mnemonic = generator.FromEntropy(entropy)
|
||||
|
||||
print(f"📍 Generated {args.words}-word BIP39 mnemonic...")
|
||||
seed_bytes = Bip39SeedGenerator(mnemonic).Generate(args.passphrase or "")
|
||||
fingerprint_with = get_master_fingerprint(seed_bytes)
|
||||
fingerprint_without = None
|
||||
if args.passphrase:
|
||||
seed_without = Bip39SeedGenerator(mnemonic).Generate("")
|
||||
fingerprint_without = get_master_fingerprint(seed_without)
|
||||
|
||||
result = derive_addresses_and_maybe_secrets(seed_bytes, args.chains, args.addresses, False)
|
||||
|
||||
if args.output == "json":
|
||||
data = {
|
||||
"mnemonic": mnemonic,
|
||||
"passphrase_set": bool(args.passphrase),
|
||||
"master_fingerprint": fingerprint_with,
|
||||
"addresses": result["addresses"],
|
||||
}
|
||||
if fingerprint_without:
|
||||
data["master_fingerprint_no_passphrase"] = fingerprint_without
|
||||
if args.dice_rolls:
|
||||
data["dice_rolls_used"] = True
|
||||
out_text = json.dumps(data, indent=2)
|
||||
else:
|
||||
fp_text = f"Master Fingerprint: {fingerprint_with}"
|
||||
if fingerprint_without:
|
||||
fp_text += f"\nMaster Fingerprint (no passphrase): {fingerprint_without}"
|
||||
dice_note = "\nDice rolls used for extra entropy: Yes" if args.dice_rolls else ""
|
||||
out_text = (
|
||||
f"Generated Mnemonic ({args.words} words):\n{mnemonic}\n\n"
|
||||
f"Passphrase set: {bool(args.passphrase)}\n{fp_text}{dice_note}\n\n"
|
||||
+ format_addresses_human(result)
|
||||
)
|
||||
|
||||
print(out_text)
|
||||
|
||||
if args.file:
|
||||
with open(args.file, "w", encoding="utf-8") as f:
|
||||
f.write(out_text)
|
||||
print(f"✅ Saved to {args.file}")
|
||||
|
||||
if args.pgp_pubkey_file:
|
||||
with open(args.pgp_pubkey_file, "r", encoding="utf-8") as f:
|
||||
pgp_pub = f.read()
|
||||
|
||||
payload = {
|
||||
"version": "v4",
|
||||
"purpose": "hdwallet generated mnemonic",
|
||||
"mnemonic": mnemonic,
|
||||
"passphrase": args.passphrase or "",
|
||||
"master_fingerprint": fingerprint_with,
|
||||
"dice_rolls_used": bool(args.dice_rolls),
|
||||
"addresses": result["addresses"],
|
||||
}
|
||||
if fingerprint_without:
|
||||
payload["master_fingerprint_no_passphrase"] = fingerprint_without
|
||||
|
||||
armored = pgp_encrypt_ascii_armored(pgp_pub, payload)
|
||||
print("\n===== BEGIN PGP ENCRYPTED PAYLOAD =====")
|
||||
print(armored)
|
||||
print("===== END PGP ENCRYPTED PAYLOAD =====\n")
|
||||
|
||||
|
||||
def cmd_recover(args):
|
||||
require_for_derive(args.export_private, args.chains)
|
||||
|
||||
from bip_utils import Bip39MnemonicValidator, Bip39SeedGenerator
|
||||
|
||||
if args.export_private and not args.pgp_pubkey_file:
|
||||
raise ValueError("--export-private requires --pgp-pubkey-file (secrets must not go to stdout)")
|
||||
|
||||
mnemonic = None
|
||||
seed_hex = 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): ").strip()
|
||||
else:
|
||||
raise ValueError("Invalid choice")
|
||||
elif args.mnemonic:
|
||||
mnemonic = args.mnemonic.strip()
|
||||
elif args.seed:
|
||||
seed_hex = args.seed.strip()
|
||||
else:
|
||||
raise ValueError("Missing input")
|
||||
|
||||
if mnemonic:
|
||||
if not Bip39MnemonicValidator().IsValid(mnemonic):
|
||||
raise ValueError("Invalid BIP39 mnemonic")
|
||||
seed_bytes = Bip39SeedGenerator(mnemonic).Generate(args.passphrase or "")
|
||||
else:
|
||||
b = bytes.fromhex(seed_hex)
|
||||
if len(b) != 64:
|
||||
raise ValueError(f"Seed must be 64 bytes (128 hex chars), got {len(b)}")
|
||||
seed_bytes = b
|
||||
|
||||
fingerprint_with = get_master_fingerprint(seed_bytes)
|
||||
fingerprint_without = None
|
||||
if args.passphrase and mnemonic:
|
||||
seed_without = Bip39SeedGenerator(mnemonic).Generate("")
|
||||
fingerprint_without = get_master_fingerprint(seed_without)
|
||||
|
||||
print(f"📍 Recovering {len(args.chains)} chain(s), {args.addresses} address(es) each...")
|
||||
result = derive_addresses_and_maybe_secrets(seed_bytes, args.chains, args.addresses, args.export_private)
|
||||
|
||||
# Always print addresses-only output (safe)
|
||||
if args.output == "json":
|
||||
data = {"addresses": result["addresses"], "master_fingerprint": fingerprint_with}
|
||||
if fingerprint_without:
|
||||
data["master_fingerprint_no_passphrase"] = fingerprint_without
|
||||
out_text = json.dumps(data, indent=2)
|
||||
else:
|
||||
fp_text = f"Master Fingerprint: {fingerprint_with}"
|
||||
if fingerprint_without:
|
||||
fp_text += f"\nMaster Fingerprint (no passphrase): {fingerprint_without}"
|
||||
out_text = fp_text + "\n\n" + format_addresses_human({"addresses": result["addresses"]})
|
||||
print(out_text)
|
||||
|
||||
if args.file:
|
||||
with open(args.file, "w", encoding="utf-8") as f:
|
||||
f.write(out_text)
|
||||
print(f"✅ Addresses saved to {args.file}")
|
||||
|
||||
# Optional encrypted payload
|
||||
if args.pgp_pubkey_file:
|
||||
with open(args.pgp_pubkey_file, "r", encoding="utf-8") as f:
|
||||
pgp_pub = f.read()
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"version": "v4",
|
||||
"purpose": "hdwallet recovery secret payload",
|
||||
"master_fingerprint": fingerprint_with,
|
||||
}
|
||||
if fingerprint_without:
|
||||
payload["master_fingerprint_no_passphrase"] = fingerprint_without
|
||||
|
||||
# Always include mnemonic/seed_hex if present (your requirement)
|
||||
if mnemonic:
|
||||
payload["mnemonic"] = mnemonic
|
||||
else:
|
||||
payload["seed_hex"] = seed_hex
|
||||
|
||||
if args.export_private:
|
||||
payload["passphrase_set"] = bool(args.passphrase)
|
||||
payload["passphrase_hint"] = args.passphrase_hint or ""
|
||||
payload["note"] = "Private keys were derived from mnemonic/seed + (optional) passphrase. Passphrase value is intentionally omitted."
|
||||
payload["derived_private_keys"] = result.get("secrets", {})
|
||||
payload["addresses"] = result["addresses"]
|
||||
else:
|
||||
payload["passphrase"] = args.passphrase or ""
|
||||
|
||||
armored = pgp_encrypt_ascii_armored(pgp_pub, payload)
|
||||
print("\n===== BEGIN PGP ENCRYPTED PAYLOAD =====")
|
||||
print(armored)
|
||||
print("===== END PGP ENCRYPTED PAYLOAD =====\n")
|
||||
|
||||
|
||||
def cmd_test(args):
|
||||
with NetworkGuard("test"):
|
||||
require_for_offline(["ethereum", "solana"])
|
||||
|
||||
from bip_utils import Bip39SeedGenerator
|
||||
|
||||
print("🧪 Running tests...")
|
||||
|
||||
# --- Existing vector (yours) ---
|
||||
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
passphrase = "" # empty
|
||||
seed_bytes = Bip39SeedGenerator(mnemonic).Generate(passphrase)
|
||||
|
||||
# --- NEW: Solana path->address test ---
|
||||
expected_addr = "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk"
|
||||
path = "m/44'/501'/0'/0'"
|
||||
|
||||
seed32 = slip10_ed25519_seed32_from_path(seed_bytes, path) # or slip10_ed25519_seed32_by_path(...)
|
||||
got_addr = sol_pubkey_b58_from_seed32(seed32)
|
||||
|
||||
if got_addr != expected_addr:
|
||||
raise RuntimeError(f"Solana address mismatch for {path}: got {got_addr}, expected {expected_addr}")
|
||||
|
||||
print(f"✅ Solana OK: {path} => {got_addr}")
|
||||
print("✅ All tests passed")
|
||||
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="HD wallet recovery + fetchkey + mnemonic generation")
|
||||
subparsers = parser.add_subparsers(dest="cmd")
|
||||
|
||||
p_fetch = subparsers.add_parser("fetchkey", help="Download ASCII-armored PGP pubkey from URL")
|
||||
p_fetch.add_argument("url", help="URL to fetch (e.g., https://github.com/<user>.gpg)")
|
||||
p_fetch.add_argument("--out", help="Write key to file (recommended)", default=None)
|
||||
p_fetch.add_argument("--timeout", type=int, default=15, help="HTTP timeout seconds")
|
||||
|
||||
p_test = subparsers.add_parser("test", help="Run tests against Trezor BIP39/BIP32 vectors")
|
||||
|
||||
p_gen = subparsers.add_parser("gen", help="Generate BIP39 mnemonic with optional dice entropy")
|
||||
p_recover = subparsers.add_parser("recover", help="Derive addresses from mnemonic/seed")
|
||||
|
||||
# Common args for gen and recover
|
||||
for p in [p_gen, p_recover, parser]:
|
||||
p.add_argument("--passphrase", default="", help="BIP39 passphrase (optional)")
|
||||
p.add_argument("--chains", nargs="+", choices=["ethereum", "solana", "bitcoin"],
|
||||
default=["ethereum", "solana", "bitcoin"])
|
||||
p.add_argument("--addresses", type=int, default=5)
|
||||
p.add_argument("--output", choices=["text", "json"], default="text")
|
||||
p.add_argument("--file", help="Save output to file", default=None)
|
||||
p.add_argument("--pgp-pubkey-file", help="ASCII-armored PGP public key file to encrypt payload", default=None)
|
||||
|
||||
# Gen specific
|
||||
p_gen.add_argument("--words", type=int, choices=[12, 15, 18, 24], default=12, help="Number of words for mnemonic")
|
||||
p_gen.add_argument("--dice-rolls", help="Space-separated die rolls (1-6) for extra entropy")
|
||||
|
||||
# Recover specific (default parser args, so recover works without subcommand too)
|
||||
parser.add_argument("--mnemonic", help="BIP39 mnemonic (12/24 words)")
|
||||
parser.add_argument("--seed", help="64-byte seed hex (128 hex chars)")
|
||||
parser.add_argument("--interactive", action="store_true", help="Prompt for input via stdin")
|
||||
parser.add_argument("--passphrase-hint", default="", help="Hint/reminder for passphrase (stored only in encrypted payload when --export-private)")
|
||||
parser.add_argument("--export-private", action="store_true",
|
||||
help="Encrypt derived private keys into the PGP payload (never printed). Requires --pgp-pubkey-file.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.cmd == "fetchkey":
|
||||
forbidden = any([
|
||||
getattr(args, "mnemonic", None),
|
||||
getattr(args, "seed", None),
|
||||
getattr(args, "interactive", False),
|
||||
getattr(args, "passphrase", ""),
|
||||
getattr(args, "passphrase_hint", ""),
|
||||
getattr(args, "pgp_pubkey_file", None),
|
||||
getattr(args, "export_private", False),
|
||||
getattr(args, "words", 12) != 12,
|
||||
getattr(args, "dice_rolls", None),
|
||||
])
|
||||
if forbidden:
|
||||
raise ValueError("fetchkey mode must not be used with mnemonic/seed/passphrase/export-private/words/dice-rolls options")
|
||||
cmd_fetchkey(args.url, args.out, args.timeout)
|
||||
return
|
||||
|
||||
elif args.cmd == "gen":
|
||||
if any([
|
||||
getattr(args, "mnemonic", None),
|
||||
getattr(args, "seed", None),
|
||||
getattr(args, "interactive", False),
|
||||
getattr(args, "export_private", False),
|
||||
getattr(args, "passphrase_hint", ""),
|
||||
]):
|
||||
raise ValueError("gen mode must not be used with mnemonic/seed/interactive/export-private/passphrase-hint options")
|
||||
cmd_gen(args)
|
||||
return
|
||||
|
||||
elif args.cmd == "test":
|
||||
cmd_test(args)
|
||||
return
|
||||
|
||||
# recover (default or explicit)
|
||||
if not (args.mnemonic or args.seed or args.interactive):
|
||||
raise ValueError("Provide --mnemonic or --seed or --interactive (or use subcommand gen/fetchkey)")
|
||||
|
||||
cmd_recover(args)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,520 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
mypywallet.py (Python 3.12)
|
||||
|
||||
Offline HD wallet gen/recover for ETH/SOL/BTC + optional online fetchkey.
|
||||
|
||||
Solana profiles:
|
||||
- js_ed25519_hd_key (DEFAULT): fixed path m/44'/501'/0'/0' (matches common JS examples using ed25519-hd-key derivePath)
|
||||
- phantom_bip44change: m/44'/501'/{index}'/0' (Phantom bip44Change grouping) [web:7]
|
||||
- phantom_bip44: m/44'/501'/{index}' (Phantom bip44 grouping) [web:7]
|
||||
- phantom_deprecated: m/501'/{index}'/0/0 (Phantom deprecated) [web:7]
|
||||
- solana_bip39_first32: seed[0:32] (Solana Cookbook "BIP39 format mnemonics") [web:47]
|
||||
|
||||
Notes:
|
||||
- Some Ed25519 derivation libs do not support non-hardened indices; for phantom_deprecated we try
|
||||
m/501'/{i}'/0/0 first, then fallback to m/501'/{i}'/0'/0' if required.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import json
|
||||
import getpass
|
||||
import hashlib
|
||||
import urllib.request
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Dependency checks
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def _require(mod: str, pkg: str):
|
||||
try:
|
||||
__import__(mod)
|
||||
except ImportError:
|
||||
print(f"❌ Missing dependency: {pkg}", file=sys.stderr)
|
||||
print(f"Install with: pip install {pkg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def require_for_fetchkey():
|
||||
_require("pgpy", "PGPy")
|
||||
|
||||
|
||||
def require_for_derive(export_private: bool, chains: List[str]):
|
||||
_require("bip_utils", "bip-utils")
|
||||
_require("base58", "base58")
|
||||
_require("nacl", "PyNaCl")
|
||||
_require("pgpy", "PGPy")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Offline network guard
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
class NetworkGuard:
|
||||
def __init__(self, mode_name: str):
|
||||
self.mode_name = mode_name
|
||||
self._orig = None
|
||||
|
||||
def __enter__(self):
|
||||
self._orig = urllib.request.urlopen
|
||||
|
||||
def blocked_urlopen(*args, **kwargs):
|
||||
raise RuntimeError(f"Network I/O is disabled in {self.mode_name} mode")
|
||||
|
||||
urllib.request.urlopen = blocked_urlopen
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
urllib.request.urlopen = self._orig
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# BIP32 master fingerprint
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def get_master_fingerprint(seed_bytes: bytes) -> str:
|
||||
from bip_utils import Bip32Slip10Secp256k1, Hash160
|
||||
master = Bip32Slip10Secp256k1.FromSeed(seed_bytes)
|
||||
pubkey_bytes = master.PublicKey().RawCompressed().ToBytes()
|
||||
h160 = Hash160.QuickDigest(pubkey_bytes)
|
||||
return h160[:4].hex().upper()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PGP helpers (PGPy)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def pgp_load_pubkey(armored: str):
|
||||
import pgpy
|
||||
key, _ = pgpy.PGPKey.from_blob(armored)
|
||||
if not key.is_public:
|
||||
raise ValueError("Provided key is not a public key")
|
||||
return key
|
||||
|
||||
|
||||
def pgp_fingerprint(armored: str) -> str:
|
||||
return pgp_load_pubkey(armored).fingerprint
|
||||
|
||||
|
||||
def pgp_encrypt_ascii_armored(pubkey_armored: str, payload: Dict[str, Any], ignore_usage_flags: bool = False) -> str:
|
||||
import pgpy
|
||||
pub_key = pgpy.PGPKey.from_blob(pubkey_armored)[0]
|
||||
if ignore_usage_flags:
|
||||
pub_key._require_usage_flags = False
|
||||
msg = pgpy.PGPMessage.new(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||
enc = pub_key.encrypt(msg)
|
||||
return str(enc)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# fetchkey (network allowed ONLY here)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def fetch_ascii_armored_text(url: str, timeout: int = 15) -> Tuple[str, bytes]:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "mypywallet/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
data = resp.read()
|
||||
text = data.decode("utf-8", errors="strict")
|
||||
if "-----BEGIN PGP PUBLIC KEY BLOCK-----" not in text:
|
||||
raise ValueError("Downloaded content does not look like an ASCII-armored PGP public key")
|
||||
return text, data
|
||||
|
||||
|
||||
def sha256_hex_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def cmd_fetchkey(url: str, out_path: Optional[str], timeout: int):
|
||||
require_for_fetchkey()
|
||||
armored_text, data = fetch_ascii_armored_text(url, timeout=timeout)
|
||||
s256 = sha256_hex_bytes(data)
|
||||
fpr = pgp_fingerprint(armored_text)
|
||||
|
||||
if out_path:
|
||||
with open(out_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(armored_text)
|
||||
|
||||
print("✅ Downloaded PGP public key")
|
||||
print(f"URL: {url}")
|
||||
if out_path:
|
||||
print(f"Saved: {out_path}")
|
||||
print(f"SHA256: {s256}")
|
||||
print(f"Fingerprint: {fpr}")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Solana helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def sol_pubkey_b58_from_seed32(seed32: bytes) -> str:
|
||||
from nacl.signing import SigningKey
|
||||
import base58
|
||||
sk = SigningKey(seed32)
|
||||
pub32 = sk.verify_key.encode()
|
||||
return base58.b58encode(pub32).decode("ascii")
|
||||
|
||||
|
||||
def sol_secret64_b58_from_seed32(seed32: bytes) -> str:
|
||||
# Phantom-compatible 64-byte secret = seed32 || pubkey32. [web:7]
|
||||
from nacl.signing import SigningKey
|
||||
import base58
|
||||
sk = SigningKey(seed32)
|
||||
pub32 = sk.verify_key.encode()
|
||||
secret64 = seed32 + pub32
|
||||
return base58.b58encode(secret64).decode("ascii")
|
||||
|
||||
|
||||
def derive_ed25519_seed32_by_path(seed_bytes: bytes, path: str) -> bytes:
|
||||
from bip_utils import Bip32Slip10Ed25519
|
||||
bip32 = Bip32Slip10Ed25519.FromSeedAndPath(seed_bytes, path)
|
||||
seed32 = bip32.PrivateKey().Raw().ToBytes()
|
||||
if len(seed32) != 32:
|
||||
raise ValueError(f"Unexpected derived private key length: {len(seed32)}")
|
||||
return seed32
|
||||
|
||||
|
||||
def sol_seed32_phantom_deprecated(seed_bytes: bytes, index: int) -> Tuple[str, bytes]:
|
||||
# Phantom documents deprecated structure m/501'/{index}'/0/0. [web:7]
|
||||
path1 = f"m/501'/{index}'/0/0"
|
||||
try:
|
||||
return path1, derive_ed25519_seed32_by_path(seed_bytes, path1)
|
||||
except Exception:
|
||||
# Fallback for libs that reject non-hardened indices under Ed25519 derivation.
|
||||
path2 = f"m/501'/{index}'/0'/0'"
|
||||
return path2, derive_ed25519_seed32_by_path(seed_bytes, path2)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Derivation core
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def derive_addresses(
|
||||
seed_bytes: bytes,
|
||||
chains: List[str],
|
||||
count: int,
|
||||
sol_profile: str,
|
||||
sol_match: str = "",
|
||||
sol_scan: int = 50,
|
||||
export_private: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
from bip_utils import (
|
||||
Bip44, Bip44Coins, Bip44Changes,
|
||||
Bip49, Bip49Coins,
|
||||
Bip84, Bip84Coins,
|
||||
)
|
||||
|
||||
out: Dict[str, Any] = {"addresses": {}}
|
||||
if export_private:
|
||||
out["secrets"] = {}
|
||||
|
||||
# ETH
|
||||
if "ethereum" in chains:
|
||||
root = Bip44.FromSeed(seed_bytes, Bip44Coins.ETHEREUM)
|
||||
ctx = root.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
|
||||
out["addresses"]["ethereum"] = [
|
||||
{"index": i, "path": f"m/44'/60'/0'/0/{i}", "address": ctx.AddressIndex(i).PublicKey().ToAddress()}
|
||||
for i in range(count)
|
||||
]
|
||||
|
||||
# BTC
|
||||
if "bitcoin" in chains:
|
||||
addrs = []
|
||||
|
||||
r44 = Bip44.FromSeed(seed_bytes, Bip44Coins.BITCOIN)
|
||||
c44 = r44.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
|
||||
|
||||
r49 = Bip49.FromSeed(seed_bytes, Bip49Coins.BITCOIN)
|
||||
c49 = r49.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
|
||||
|
||||
r84 = Bip84.FromSeed(seed_bytes, Bip84Coins.BITCOIN)
|
||||
c84 = r84.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
|
||||
|
||||
for i in range(count):
|
||||
addrs.append({"index": i, "path": f"m/84'/0'/0'/0/{i}", "address_type": "native_segwit", "address": c84.AddressIndex(i).PublicKey().ToAddress()})
|
||||
addrs.append({"index": i, "path": f"m/49'/0'/0'/0/{i}", "address_type": "segwit", "address": c49.AddressIndex(i).PublicKey().ToAddress()})
|
||||
addrs.append({"index": i, "path": f"m/44'/0'/0'/0/{i}", "address_type": "legacy", "address": c44.AddressIndex(i).PublicKey().ToAddress()})
|
||||
|
||||
out["addresses"]["bitcoin"] = addrs
|
||||
|
||||
# SOL
|
||||
if "solana" in chains:
|
||||
from bip_utils import Bip44, Bip44Coins, Bip44Changes
|
||||
root = Bip44.FromSeed(seed_bytes, Bip44Coins.SOLANA)
|
||||
base = root.Purpose().Coin()
|
||||
|
||||
def derive_one(profile: str, i: int) -> Tuple[str, str, bytes]:
|
||||
# returns (path, address, seed32)
|
||||
if profile == "js_ed25519_hd_key":
|
||||
# Fixed path: m/44'/501'/0'/0' (JS derivePath examples)
|
||||
path = "m/44'/501'/0'/0'"
|
||||
seed32 = derive_ed25519_seed32_by_path(seed_bytes, path)
|
||||
addr = sol_pubkey_b58_from_seed32(seed32)
|
||||
return path, addr, seed32
|
||||
|
||||
if profile == "phantom_bip44change":
|
||||
path = f"m/44'/501'/{i}'/0'"
|
||||
seed32 = derive_ed25519_seed32_by_path(seed_bytes, path)
|
||||
addr = sol_pubkey_b58_from_seed32(seed32)
|
||||
return path, addr, seed32
|
||||
|
||||
if profile == "phantom_bip44":
|
||||
path = f"m/44'/501'/{i}'"
|
||||
seed32 = derive_ed25519_seed32_by_path(seed_bytes, path)
|
||||
addr = sol_pubkey_b58_from_seed32(seed32)
|
||||
return path, addr, seed32
|
||||
|
||||
|
||||
if profile == "phantom_deprecated":
|
||||
# m/501'/{index}'/0/0 [web:7]
|
||||
path, seed32 = sol_seed32_phantom_deprecated(seed_bytes, i)
|
||||
addr = sol_pubkey_b58_from_seed32(seed32)
|
||||
return path, addr, seed32
|
||||
|
||||
if profile == "solana_bip39_first32":
|
||||
# Solana Cookbook: seed[0:32] [web:47]
|
||||
seed32 = seed_bytes[:32]
|
||||
addr = sol_pubkey_b58_from_seed32(seed32)
|
||||
return "BIP39 seed[0:32]", addr, seed32
|
||||
|
||||
raise ValueError(f"Unknown Solana profile: {profile}")
|
||||
|
||||
# Profiles that only yield a single address (fixed seed/path)
|
||||
if sol_profile in ("solana_bip39_first32", "js_ed25519_hd_key"):
|
||||
count = 1
|
||||
|
||||
if sol_match:
|
||||
profiles = ["js_ed25519_hd_key", "phantom_bip44change", "phantom_bip44", "phantom_deprecated", "solana_bip39_first32"]
|
||||
for prof in profiles:
|
||||
max_i = 1 if prof in ("solana_bip39_first32", "js_ed25519_hd_key") else sol_scan
|
||||
for i in range(max_i):
|
||||
path, addr, _seed32 = derive_one(prof, i)
|
||||
if addr == sol_match:
|
||||
out["addresses"]["solana_match"] = [{"profile": prof, "index": i, "path": path, "address": addr}]
|
||||
return out
|
||||
out["addresses"]["solana_match"] = []
|
||||
return out
|
||||
|
||||
addrs = []
|
||||
secrets = []
|
||||
for i in range(count):
|
||||
path, addr, seed32 = derive_one(sol_profile, i)
|
||||
addrs.append({"index": i, "path": path, "address": addr})
|
||||
if export_private:
|
||||
secrets.append({"index": i, "path": path, "phantom_secret_base58": sol_secret64_b58_from_seed32(seed32)})
|
||||
|
||||
out["addresses"]["solana"] = addrs
|
||||
if export_private:
|
||||
out["secrets"]["solana"] = secrets
|
||||
|
||||
if export_private:
|
||||
out["secrets"] = {k: v for k, v in out["secrets"].items() if v}
|
||||
if not out["secrets"]:
|
||||
del out["secrets"]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def format_addresses_human(addresses: Dict[str, Any]) -> str:
|
||||
lines: List[str] = []
|
||||
lines.append("\n" + "=" * 80)
|
||||
lines.append("MULTI-CHAIN ADDRESS DERIVATION (ADDRESSES ONLY)")
|
||||
lines.append("=" * 80 + "\n")
|
||||
|
||||
for chain, addrs in addresses.items():
|
||||
lines.append("─" * 80)
|
||||
lines.append(f"{chain.upper()} ADDRESSES")
|
||||
lines.append("─" * 80)
|
||||
|
||||
if chain == "bitcoin":
|
||||
by_type: Dict[str, List[Dict[str, Any]]] = {}
|
||||
for a in addrs:
|
||||
by_type.setdefault(a["address_type"], []).append(a)
|
||||
for t in ["native_segwit", "segwit", "legacy"]:
|
||||
if t in by_type:
|
||||
lines.append(f"\n {t.upper()}:")
|
||||
for a in by_type[t]:
|
||||
lines.append(f" [{a['index']}] {a['path']}")
|
||||
lines.append(f" → {a['address']}")
|
||||
else:
|
||||
for a in addrs:
|
||||
prof = a.get("profile")
|
||||
if prof:
|
||||
lines.append(f" [{a.get('index', 0)}] {a['path']} (profile={prof})")
|
||||
else:
|
||||
lines.append(f" [{a.get('index', 0)}] {a['path']}")
|
||||
lines.append(f" → {a['address']}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("=" * 80 + "\n")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Commands
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def cmd_gen(args):
|
||||
with NetworkGuard("gen"):
|
||||
require_for_derive(False, args.chains)
|
||||
|
||||
import secrets
|
||||
from bip_utils import Bip39MnemonicGenerator, Bip39Languages, Bip39SeedGenerator
|
||||
|
||||
words_to_entropy = {12: 128, 15: 160, 18: 192, 21: 224, 24: 256}
|
||||
entropy_len = (words_to_entropy[args.words] // 8)
|
||||
|
||||
if args.dice_rolls:
|
||||
rolls = args.dice_rolls.strip().split()
|
||||
if not rolls or not all(r.isdigit() and 1 <= int(r) <= 6 for r in rolls):
|
||||
raise ValueError("--dice-rolls must be space-separated integers 1-6")
|
||||
dice_bytes = (" ".join(rolls)).encode("utf-8")
|
||||
crypto_bytes = secrets.token_bytes(entropy_len)
|
||||
entropy = hashlib.sha256(dice_bytes + crypto_bytes).digest()[:entropy_len]
|
||||
else:
|
||||
entropy = secrets.token_bytes(entropy_len)
|
||||
|
||||
mnemonic = Bip39MnemonicGenerator(Bip39Languages.ENGLISH).FromEntropy(entropy)
|
||||
seed_bytes = Bip39SeedGenerator(mnemonic).Generate(args.passphrase or "")
|
||||
fp_with = get_master_fingerprint(seed_bytes)
|
||||
|
||||
print(f"📍 Generated {args.words}-word BIP39 mnemonic.")
|
||||
if args.unsafe_print:
|
||||
print(f"\nMnemonic:\n{mnemonic}\n")
|
||||
|
||||
result = derive_addresses(seed_bytes, args.chains, args.addresses, args.sol_profile)
|
||||
|
||||
if args.output == "json":
|
||||
out_text = json.dumps({
|
||||
"master_fingerprint": fp_with,
|
||||
"solana_profile": args.sol_profile,
|
||||
"addresses": result["addresses"],
|
||||
}, indent=2, ensure_ascii=False)
|
||||
else:
|
||||
out_text = f"Master Fingerprint: {fp_with}\n\n" + format_addresses_human(result["addresses"])
|
||||
print(out_text)
|
||||
|
||||
|
||||
def cmd_recover(args):
|
||||
with NetworkGuard("recover"):
|
||||
require_for_derive(args.export_private, args.chains)
|
||||
|
||||
from bip_utils import Bip39MnemonicValidator, Bip39SeedGenerator
|
||||
|
||||
mnemonic = None
|
||||
seed_hex = 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()
|
||||
elif args.seed:
|
||||
seed_hex = args.seed.strip()
|
||||
else:
|
||||
raise ValueError("Missing input")
|
||||
|
||||
if mnemonic:
|
||||
if not Bip39MnemonicValidator().IsValid(mnemonic):
|
||||
raise ValueError("Invalid BIP39 mnemonic")
|
||||
seed_bytes = Bip39SeedGenerator(mnemonic).Generate(args.passphrase or "")
|
||||
else:
|
||||
b = bytes.fromhex(seed_hex)
|
||||
if len(b) != 64:
|
||||
raise ValueError(f"Seed must be 64 bytes (128 hex chars), got {len(b)} bytes")
|
||||
seed_bytes = b
|
||||
|
||||
fp_with = get_master_fingerprint(seed_bytes)
|
||||
print(f"📍 Recovering {len(args.chains)} chain(s); deriving {args.addresses} per chain/profile...")
|
||||
print(f"Master Fingerprint: {fp_with}\n")
|
||||
|
||||
result = derive_addresses(
|
||||
seed_bytes=seed_bytes,
|
||||
chains=args.chains,
|
||||
count=args.addresses,
|
||||
sol_profile=args.sol_profile,
|
||||
sol_match=(args.sol_match or "").strip(),
|
||||
sol_scan=args.sol_scan,
|
||||
export_private=args.export_private,
|
||||
)
|
||||
|
||||
if args.output == "json":
|
||||
out_text = json.dumps({
|
||||
"master_fingerprint": fp_with,
|
||||
"solana_profile": args.sol_profile,
|
||||
"solana_match": (args.sol_match or "").strip(),
|
||||
"addresses": result["addresses"],
|
||||
}, indent=2, ensure_ascii=False)
|
||||
else:
|
||||
out_text = format_addresses_human(result["addresses"])
|
||||
print(out_text)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Offline HD wallet gen/recover + online fetchkey")
|
||||
subparsers = parser.add_subparsers(dest="cmd")
|
||||
|
||||
p_fetch = subparsers.add_parser("fetchkey", help="Download ASCII-armored PGP pubkey from URL (online)")
|
||||
p_fetch.add_argument("url", help="URL to fetch (e.g., https://github.com/<user>.gpg)")
|
||||
p_fetch.add_argument("--out", help="Write key to file (recommended)", default=None)
|
||||
p_fetch.add_argument("--timeout", type=int, default=15, help="HTTP timeout seconds")
|
||||
|
||||
p_gen = subparsers.add_parser("gen", help="Generate BIP39 mnemonic (offline)")
|
||||
p_recover = subparsers.add_parser("recover", help="Recover addresses from mnemonic/seed (offline)")
|
||||
|
||||
for p in [p_gen, p_recover]:
|
||||
p.add_argument("--passphrase", default="", help="BIP39 passphrase (optional)")
|
||||
p.add_argument("--chains", nargs="+", choices=["ethereum", "solana", "bitcoin"],
|
||||
default=["ethereum", "solana", "bitcoin"])
|
||||
p.add_argument("--addresses", type=int, default=5)
|
||||
p.add_argument("--output", choices=["text", "json"], default="text")
|
||||
p.add_argument("--sol-profile",
|
||||
choices=["js_ed25519_hd_key", "phantom_bip44change", "phantom_bip44", "phantom_deprecated", "solana_bip39_first32"],
|
||||
default="js_ed25519_hd_key",
|
||||
help="Solana derivation behavior; Phantom supports multiple groupings; Solana cookbook also shows a BIP39-first32 shortcut. [web:7][web:47]")
|
||||
p.add_argument("--export-private", action="store_true",
|
||||
help="Print Solana secret keys (base58) to stdout (danger).")
|
||||
|
||||
p_gen.add_argument("--words", type=int, choices=[12, 15, 18, 21, 24], default=12)
|
||||
p_gen.add_argument("--dice-rolls", default="")
|
||||
p_gen.add_argument("--unsafe-print", action="store_true")
|
||||
|
||||
p_recover.add_argument("--mnemonic", default="")
|
||||
p_recover.add_argument("--seed", default="")
|
||||
p_recover.add_argument("--interactive", action="store_true")
|
||||
p_recover.add_argument("--sol-match", default="",
|
||||
help="Search for this Solana address across supported Solana profiles. [web:7]")
|
||||
p_recover.add_argument("--sol-scan", type=int, default=50,
|
||||
help="How many indices to scan when using --sol-match.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.cmd == "fetchkey":
|
||||
cmd_fetchkey(args.url, args.out, args.timeout)
|
||||
return
|
||||
if args.cmd == "gen":
|
||||
cmd_gen(args)
|
||||
return
|
||||
if args.cmd == "recover":
|
||||
cmd_recover(args)
|
||||
return
|
||||
|
||||
parser.print_help()
|
||||
sys.exit(2)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
115
build-all-platforms.sh
Executable file
115
build-all-platforms.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Building wheels for all platforms using Python 3.12..."
|
||||
echo ""
|
||||
|
||||
# Check Docker is running
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo "ERROR: Docker is not running. Please start Docker Desktop."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. macOS ARM64 (via Docker with Python 3.12)
|
||||
echo "==> Building macOS ARM64 wheels (Docker + Python 3.12)..."
|
||||
docker run --rm \
|
||||
--platform linux/arm64 \
|
||||
-v $(pwd):/work \
|
||||
-w /work \
|
||||
python:3.12-slim \
|
||||
bash -c "
|
||||
apt-get update -qq && apt-get install -y -qq gcc g++ make libffi-dev > /dev/null
|
||||
pip install --upgrade pip > /dev/null
|
||||
mkdir -p vendor/macos-arm64-docker
|
||||
pip download --dest vendor/macos-arm64-docker -r requirements.txt
|
||||
pip wheel --wheel-dir vendor/macos-arm64-docker --no-deps vendor/macos-arm64-docker/*.tar.gz 2>/dev/null || true
|
||||
rm -f vendor/macos-arm64-docker/*.tar.gz
|
||||
"
|
||||
echo "✓ macOS ARM64: $(ls vendor/macos-arm64-docker/*.whl 2>/dev/null | wc -l | xargs) wheels"
|
||||
|
||||
# Actually, let's use native Mac Python 3.12 if available, otherwise use what we have
|
||||
echo ""
|
||||
echo "==> Building macOS ARM64 wheels (native - using your .venv312)..."
|
||||
mkdir -p vendor/macos-arm64
|
||||
|
||||
# Use the Python from your working venv
|
||||
if [ -f ".venv312/bin/pip" ]; then
|
||||
echo "Using Python from .venv312..."
|
||||
.venv312/bin/pip download --dest vendor/macos-arm64 -r requirements.txt
|
||||
echo "✓ macOS ARM64: $(ls vendor/macos-arm64/*.whl 2>/dev/null | wc -l | xargs) wheels"
|
||||
else
|
||||
echo "ERROR: .venv312 not found!"
|
||||
echo "Please activate your working venv first:"
|
||||
echo " source .venv312/bin/activate"
|
||||
echo " pip download --dest vendor/macos-arm64 -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Linux x86_64 (via Docker)
|
||||
echo ""
|
||||
echo "==> Building Linux x86_64 wheels (Docker + Python 3.12)..."
|
||||
docker run --rm \
|
||||
-v $(pwd):/work \
|
||||
-w /work \
|
||||
python:3.12-slim \
|
||||
bash -c "
|
||||
apt-get update -qq && apt-get install -y -qq gcc g++ make libffi-dev > /dev/null
|
||||
pip install --upgrade pip > /dev/null
|
||||
mkdir -p vendor/linux-x86_64
|
||||
pip download --dest vendor/linux-x86_64 -r requirements.txt
|
||||
pip wheel --wheel-dir vendor/linux-x86_64 --no-deps vendor/linux-x86_64/*.tar.gz 2>/dev/null || true
|
||||
rm -f vendor/linux-x86_64/*.tar.gz
|
||||
"
|
||||
echo "✓ Linux x86_64: $(ls vendor/linux-x86_64/*.whl 2>/dev/null | wc -l | xargs) wheels"
|
||||
|
||||
# 3. Generate SHA256 checksums
|
||||
echo ""
|
||||
echo "==> Generating checksums..."
|
||||
for platform in macos-arm64 linux-x86_64; do
|
||||
if [ -d "vendor/$platform" ] && [ "$(ls -A vendor/$platform/*.whl 2>/dev/null)" ]; then
|
||||
(cd vendor/$platform && shasum -a 256 *.whl > SHA256SUMS)
|
||||
echo "✓ Checksums for $platform"
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. Create provenance file
|
||||
echo ""
|
||||
echo "==> Creating provenance documentation..."
|
||||
cat > vendor/PROVENANCE.md << EOF
|
||||
# Dependency Provenance
|
||||
|
||||
All wheels generated on: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
Python version: 3.12
|
||||
Build machine: $(uname -s) $(uname -m)
|
||||
Docker used: Yes (python:3.12-slim)
|
||||
|
||||
## Platforms
|
||||
- macOS ARM64: Built using .venv312 (Python 3.12)
|
||||
- Linux x86_64: Built using Docker python:3.12-slim
|
||||
|
||||
## Package Versions
|
||||
$(cat requirements.txt | head -20)
|
||||
|
||||
## Integrity Verification
|
||||
Each platform directory contains SHA256SUMS for verification:
|
||||
\`\`\`bash
|
||||
cd vendor/linux-x86_64
|
||||
shasum -a 256 -c SHA256SUMS
|
||||
\`\`\`
|
||||
|
||||
Last updated: $(date -u +"%Y-%m-%d")
|
||||
Built by: $(git config user.name) <$(git config user.email)>
|
||||
Commit: $(git rev-parse --short HEAD 2>/dev/null || echo "not in git")
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "✓ All platforms built successfully!"
|
||||
echo ""
|
||||
echo "Summary:"
|
||||
echo " macOS ARM64: vendor/macos-arm64/ ($(ls vendor/macos-arm64/*.whl 2>/dev/null | wc -l | xargs) wheels)"
|
||||
echo " Linux x86_64: vendor/linux-x86_64/ ($(ls vendor/linux-x86_64/*.whl 2>/dev/null | wc -l | xargs) wheels)"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Verify: ls vendor/*/SHA256SUMS"
|
||||
echo " 2. Test: ./install_offline.sh"
|
||||
echo " 3. Commit: git add vendor/ && git commit -m 'vendor: add multi-platform wheels'"
|
||||
117
install_offline.sh
Executable file
117
install_offline.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Detect platform
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
PLATFORM="linux-x86_64"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
ARCH=$(uname -m)
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
PLATFORM="macos-arm64"
|
||||
else
|
||||
echo "ERROR: Only macOS ARM64 is supported"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "ERROR: Unsupported platform: $OSTYPE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Detected platform: $PLATFORM"
|
||||
|
||||
# Check if vendor directory exists
|
||||
if [ ! -d "vendor/$PLATFORM" ]; then
|
||||
echo "ERROR: vendor/$PLATFORM not found!"
|
||||
echo "Run ./build-vendors-simple.sh to generate wheels"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for Python 3.12
|
||||
if ! command -v python3.12 &> /dev/null; then
|
||||
echo "ERROR: Python 3.12 not found!"
|
||||
echo "Please install Python 3.12:"
|
||||
echo " macOS: brew install python@3.12"
|
||||
echo " Linux: apt-get install python3.12 python3.12-venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if already in a venv
|
||||
if [ -n "$VIRTUAL_ENV" ]; then
|
||||
CURRENT_PYTHON=$(python --version 2>&1 | grep -oE '3\.[0-9]+')
|
||||
if [[ "$CURRENT_PYTHON" != "3.12" ]]; then
|
||||
echo "WARNING: You are in a venv with Python $CURRENT_PYTHON"
|
||||
echo "Deactivating and creating new Python 3.12 venv..."
|
||||
deactivate || true
|
||||
else
|
||||
echo "Already in Python 3.12 venv, using it..."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create or use existing venv
|
||||
VENV_DIR=".venv"
|
||||
if [ ! -d "$VENV_DIR" ] || [ ! -f "$VENV_DIR/bin/python3.12" ]; then
|
||||
echo "Creating Python 3.12 virtual environment..."
|
||||
python3.12 -m venv "$VENV_DIR"
|
||||
echo "✓ Virtual environment created: $VENV_DIR"
|
||||
else
|
||||
echo "Using existing venv: $VENV_DIR"
|
||||
fi
|
||||
|
||||
# Activate venv
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
# Verify Python version
|
||||
PYTHON_VERSION=$(python --version 2>&1 | grep -oE '3\.[0-9]+')
|
||||
echo "Using Python $PYTHON_VERSION from: $(which python)"
|
||||
|
||||
if [[ "$PYTHON_VERSION" != "3.12" ]]; then
|
||||
echo "ERROR: Expected Python 3.12, got $PYTHON_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify checksums
|
||||
if [ -f "vendor/$PLATFORM/SHA256SUMS" ]; then
|
||||
echo ""
|
||||
echo "Verifying checksums..."
|
||||
(cd vendor/$PLATFORM && shasum -a 256 -c SHA256SUMS --quiet) || {
|
||||
echo "ERROR: Checksum verification failed!"
|
||||
exit 1
|
||||
}
|
||||
echo "✓ Checksums verified"
|
||||
fi
|
||||
|
||||
# Install
|
||||
echo ""
|
||||
echo "Installing from vendor/$PLATFORM..."
|
||||
pip install --upgrade pip --quiet
|
||||
pip install --no-index --find-links=vendor/$PLATFORM -r requirements.txt
|
||||
|
||||
# Test
|
||||
echo ""
|
||||
echo "Running tests..."
|
||||
python -m pytest -v tests/test_vectors.py || {
|
||||
echo "ERROR: Tests failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "Running built-in smoke test..."
|
||||
python src/pyhdwallet.py test || {
|
||||
echo "ERROR: Smoke test failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✓ Installation complete and verified!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Virtual environment: $VENV_DIR"
|
||||
echo "Python version: $(python --version)"
|
||||
echo ""
|
||||
echo "To activate this environment:"
|
||||
echo " source $VENV_DIR/bin/activate"
|
||||
echo ""
|
||||
echo "Generate a wallet with:"
|
||||
echo " python src/pyhdwallet.py gen --off-screen --file"
|
||||
echo ""
|
||||
1000
playbook.md
1000
playbook.md
File diff suppressed because it is too large
Load Diff
7
pytest.ini
Normal file
7
pytest.ini
Normal file
@@ -0,0 +1,7 @@
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning:pgpy.constants
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
2
requirements-dev.in
Normal file
2
requirements-dev.in
Normal file
@@ -0,0 +1,2 @@
|
||||
-r requirements.txt # Include production deps
|
||||
pytest>=9.0.0
|
||||
81
requirements-dev.txt
Normal file
81
requirements-dev.txt
Normal file
@@ -0,0 +1,81 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile requirements-dev.in
|
||||
#
|
||||
base58==2.1.1
|
||||
# via -r requirements.txt
|
||||
bip-utils==2.10.0
|
||||
# via -r requirements.txt
|
||||
cbor2==5.8.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
cffi==2.0.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# cryptography
|
||||
# pynacl
|
||||
coincurve==21.0.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
crcmod==1.7
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
cryptography==46.0.3
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# pgpy
|
||||
ecdsa==0.19.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
ed25519-blake2b==1.4.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
iniconfig==2.3.0
|
||||
# via pytest
|
||||
packaging==25.0
|
||||
# via pytest
|
||||
pgpy==0.6.0
|
||||
# via -r requirements.txt
|
||||
pluggy==1.6.0
|
||||
# via pytest
|
||||
py-sr25519-bindings==0.2.3
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
pyasn1==0.6.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# pgpy
|
||||
pycparser==2.23
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# cffi
|
||||
pycryptodome==3.23.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
pycryptodomex==3.23.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# pyzipper
|
||||
pygments==2.19.2
|
||||
# via pytest
|
||||
pynacl==1.6.2
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
pytest==9.0.2
|
||||
# via -r requirements-dev.in
|
||||
pyzipper==0.3.6
|
||||
# via -r requirements.txt
|
||||
six==1.17.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# ecdsa
|
||||
@@ -1,2 +1,8 @@
|
||||
PGPy
|
||||
bip-utils
|
||||
# Core crypto libraries
|
||||
bip-utils>=2.9.0
|
||||
PGPy>=0.6.0
|
||||
pynacl>=1.5.0
|
||||
base58>=2.1.1
|
||||
|
||||
# ZIP encryption
|
||||
pyzipper>=0.3.6
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.11
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile
|
||||
# pip-compile requirements.in
|
||||
#
|
||||
base58==2.1.1
|
||||
# via -r requirements.in
|
||||
bip-utils==2.10.0
|
||||
# via -r requirements.in
|
||||
cbor2==5.8.0
|
||||
@@ -32,7 +34,13 @@ pycparser==2.23
|
||||
# via cffi
|
||||
pycryptodome==3.23.0
|
||||
# via bip-utils
|
||||
pycryptodomex==3.23.0
|
||||
# via pyzipper
|
||||
pynacl==1.6.2
|
||||
# via bip-utils
|
||||
# via
|
||||
# -r requirements.in
|
||||
# bip-utils
|
||||
pyzipper==0.3.6
|
||||
# via -r requirements.in
|
||||
six==1.17.0
|
||||
# via ecdsa
|
||||
|
||||
1238
src/pyhdwallet.py
1238
src/pyhdwallet.py
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"master_fingerprint": "DD1449B7",
|
||||
"passphrase_used": false,
|
||||
"passphrase_hint": "",
|
||||
"dice_rolls_used": false,
|
||||
"solana_profile": "phantom_bip44change",
|
||||
"addresses": {
|
||||
"ethereum": [
|
||||
{
|
||||
"index": 0,
|
||||
"path": "m/44'/60'/0'/0/0",
|
||||
"address": "0x9d3e3540f4C507ca992035607326798130051e03"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
104
tests/bootstrap_vectors.py
Normal file
104
tests/bootstrap_vectors.py
Normal 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()
|
||||
91
tests/generate_bip85_vectors.py
Executable file
91
tests/generate_bip85_vectors.py
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate BIP85 test vectors for vectors.json
|
||||
|
||||
Run this from the repo root:
|
||||
python3 tests/generate_bip85_vectors.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add src to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||
|
||||
import pyhdwallet
|
||||
from bip_utils import Bip39SeedGenerator
|
||||
import json
|
||||
|
||||
# Test cases to generate
|
||||
test_cases = [
|
||||
{
|
||||
"description": "BIP85 test case 1: 12-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 12,
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 2: 18-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 18,
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 3: 24-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 24,
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 4: 12-word child, WITH passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "TREZOR",
|
||||
"child_words": 12,
|
||||
"index": 0,
|
||||
},
|
||||
]
|
||||
|
||||
print("Generating BIP85 test vectors...\n")
|
||||
print("=" * 80)
|
||||
|
||||
vectors = []
|
||||
|
||||
for case in test_cases:
|
||||
print(f"\n{case['description']}")
|
||||
print("-" * 80)
|
||||
|
||||
# Generate master seed
|
||||
master_seed = Bip39SeedGenerator(case["master_mnemonic"]).Generate(case["master_passphrase"])
|
||||
|
||||
# Derive child using BIP85
|
||||
path, child_mnemonic, entropy64, truncated_entropy = pyhdwallet.bip85_derive_child_mnemonic(
|
||||
master_seed,
|
||||
case["child_words"],
|
||||
case["index"]
|
||||
)
|
||||
|
||||
vector = {
|
||||
"description": case["description"],
|
||||
"master_mnemonic": case["master_mnemonic"],
|
||||
"master_passphrase": case["master_passphrase"],
|
||||
"child_words": case["child_words"],
|
||||
"index": case["index"],
|
||||
"bip85_path": path,
|
||||
"expected_entropy64_hex": entropy64.hex(),
|
||||
"expected_entropy_truncated_hex": truncated_entropy.hex(),
|
||||
"expected_child_mnemonic": child_mnemonic,
|
||||
}
|
||||
|
||||
vectors.append(vector)
|
||||
|
||||
print(f"Path: {path}")
|
||||
print(f"Entropy64: {entropy64.hex()}")
|
||||
print(f"Truncated: {truncated_entropy.hex()}")
|
||||
print(f"Child mnemonic: {child_mnemonic}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("\nJSON output for vectors.json:\n")
|
||||
print(json.dumps(vectors, indent=2))
|
||||
221
tests/test_vectors.py
Normal file
221
tests/test_vectors.py
Normal file
@@ -0,0 +1,221 @@
|
||||
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_bip85_derivation(vectors):
|
||||
"""
|
||||
Verifies BIP85 child mnemonic derivation matches expected test vectors.
|
||||
|
||||
Tests:
|
||||
- Path construction
|
||||
- Entropy64 derivation (HMAC-SHA512)
|
||||
- Entropy truncation
|
||||
- Child mnemonic generation
|
||||
"""
|
||||
from bip_utils import Bip39SeedGenerator
|
||||
|
||||
for case in vectors["bip85"]:
|
||||
master_mnemonic = case["master_mnemonic"]
|
||||
master_passphrase = case["master_passphrase"]
|
||||
child_words = case["child_words"]
|
||||
index = case["index"]
|
||||
|
||||
# Generate master seed
|
||||
master_seed = Bip39SeedGenerator(master_mnemonic).Generate(master_passphrase)
|
||||
|
||||
# Derive child using BIP85
|
||||
path, child_mnemonic, entropy64, truncated_entropy = pyhdwallet.bip85_derive_child_mnemonic(
|
||||
master_seed,
|
||||
child_words,
|
||||
index
|
||||
)
|
||||
|
||||
# Assert path
|
||||
assert path == case["bip85_path"], f"Path mismatch: {path} != {case['bip85_path']}"
|
||||
|
||||
# Assert entropy64 (full 64-byte HMAC output)
|
||||
assert entropy64.hex() == case["expected_entropy64_hex"], \
|
||||
f"Entropy64 mismatch for {case['description']}"
|
||||
|
||||
# Assert truncated entropy
|
||||
assert truncated_entropy.hex() == case["expected_entropy_truncated_hex"], \
|
||||
f"Truncated entropy mismatch for {case['description']}"
|
||||
|
||||
# Assert child mnemonic
|
||||
assert child_mnemonic == case["expected_child_mnemonic"], \
|
||||
f"Child mnemonic mismatch for {case['description']}"
|
||||
|
||||
|
||||
def test_cli_gen_child_smoke(tmp_path, vectors):
|
||||
"""
|
||||
CLI smoke test for gen-child command.
|
||||
|
||||
Verifies:
|
||||
- Command runs without error
|
||||
- ZIP file is created
|
||||
- Off-screen mode works
|
||||
- File output works
|
||||
"""
|
||||
vector = vectors["bip85"][0] # Use first BIP85 test case
|
||||
master_mnemonic = vector["master_mnemonic"]
|
||||
|
||||
cmd = [
|
||||
sys.executable, "src/pyhdwallet.py", "gen-child",
|
||||
"--mnemonic-stdin",
|
||||
"--index", "0",
|
||||
"--words", "12",
|
||||
"--off-screen",
|
||||
"--file",
|
||||
"--zip-password-mode", "auto",
|
||||
"--wallet-location", str(tmp_path)
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
input=master_mnemonic.encode("utf-8"),
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"CLI gen-child failed: {result.stderr.decode()}"
|
||||
|
||||
# Check ZIP file created
|
||||
zips = list(tmp_path.glob("*.zip"))
|
||||
assert len(zips) == 1, f"Expected exactly one zip file, found {len(zips)}"
|
||||
|
||||
# Check filename pattern
|
||||
zip_name = zips[0].name
|
||||
assert zip_name.startswith("bip85_child_"), f"Unexpected zip name: {zip_name}"
|
||||
|
||||
|
||||
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
|
||||
528
tests/vectors.json
Normal file
528
tests/vectors.json
Normal file
@@ -0,0 +1,528 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"bip85": [
|
||||
{
|
||||
"description": "BIP85 test case 1: 12-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 12,
|
||||
"index": 0,
|
||||
"bip85_path": "m/83696968'/39'/0'/12'/0'",
|
||||
"expected_entropy64_hex": "ac98dac5d4f4ebad6056682ac95eb9ad9ba94fb68e96848264dad0b4357d002e41b3dd7a4c6f4ebc234be6938495840a73f59e9ba0e8e5c5208c94e6df2d7709",
|
||||
"expected_entropy_truncated_hex": "ac98dac5d4f4ebad6056682ac95eb9ad",
|
||||
"expected_child_mnemonic": "prosper short ramp prepare exchange stove life snack client enough purpose fold"
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 2: 18-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 18,
|
||||
"index": 0,
|
||||
"bip85_path": "m/83696968'/39'/0'/18'/0'",
|
||||
"expected_entropy64_hex": "fc039f51d67ed7dfd01552f27de28887cf3e58655153e44b023d37578321f7083241970730e522d3f20b38a5296c5e51e57e0429546629704a09c6d1e2d10829",
|
||||
"expected_entropy_truncated_hex": "fc039f51d67ed7dfd01552f27de28887cf3e58655153e44b",
|
||||
"expected_child_mnemonic": "winter brother stamp provide uniform useful doctor prevent venue upper peasant auto view club next clerk tone fox"
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 3: 24-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 24,
|
||||
"index": 0,
|
||||
"bip85_path": "m/83696968'/39'/0'/24'/0'",
|
||||
"expected_entropy64_hex": "d5a9cb46670566c4246b6e7af22e1dfc3668744ed831afea7ce2beea44e34e23e348e86091f24394f4be6253a7d5d24b91b1c4e0863b296e9e541e8018288897",
|
||||
"expected_entropy_truncated_hex": "d5a9cb46670566c4246b6e7af22e1dfc3668744ed831afea7ce2beea44e34e23",
|
||||
"expected_child_mnemonic": "stick exact spice sock filter ginger museum horse kit multiply manual wear grief demand derive alert quiz fault december lava picture immune decade jaguar"
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 4: 12-word child, WITH passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "TREZOR",
|
||||
"child_words": 12,
|
||||
"index": 0,
|
||||
"bip85_path": "m/83696968'/39'/0'/12'/0'",
|
||||
"expected_entropy64_hex": "2b1d7c4f311137fa95f6302e64cdb88584d52b51b57d0430ee68e148b82baa3f8c40316397eb404f4573bc0c8e5c4bc14e4aa5f0f472a9d3587f494f1f7b3684",
|
||||
"expected_entropy_truncated_hex": "2b1d7c4f311137fa95f6302e64cdb885",
|
||||
"expected_child_mnemonic": "climb typical because giraffe beach wool fit ship common chapter hotel arm"
|
||||
}
|
||||
]
|
||||
}
|
||||
43
vendor/PROVENANCE.md
vendored
Normal file
43
vendor/PROVENANCE.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# Dependency Provenance
|
||||
|
||||
All wheels generated on: 2026-01-07 17:06:51 UTC
|
||||
Python version: 3.12
|
||||
Build machine: Darwin arm64
|
||||
Docker used: Yes (python:3.12-slim)
|
||||
|
||||
## Platforms
|
||||
- macOS ARM64: Built using .venv312 (Python 3.12)
|
||||
- Linux x86_64: Built using Docker python:3.12-slim
|
||||
|
||||
## Package Versions
|
||||
base58==2.1.1
|
||||
bip_utils==2.10.0
|
||||
build==1.3.0
|
||||
cbor2==5.8.0
|
||||
cffi==2.0.0
|
||||
click==8.3.1
|
||||
coincurve==21.0.0
|
||||
crcmod==1.7
|
||||
cryptography==46.0.3
|
||||
ecdsa==0.19.1
|
||||
ed25519-blake2b==1.4.1
|
||||
iniconfig==2.3.0
|
||||
packaging==25.0
|
||||
PGPy==0.6.0
|
||||
pip-chill==1.0.3
|
||||
pip-tools==7.5.2
|
||||
pluggy==1.6.0
|
||||
py-sr25519-bindings==0.2.3
|
||||
pyasn1==0.6.1
|
||||
pycparser==2.23
|
||||
|
||||
## Integrity Verification
|
||||
Each platform directory contains SHA256SUMS for verification:
|
||||
```bash
|
||||
cd vendor/linux-x86_64
|
||||
shasum -a 256 -c SHA256SUMS
|
||||
```
|
||||
|
||||
Last updated: 2026-01-07
|
||||
Built by: LC mac <leochan@hkjin.com>
|
||||
Commit: 2807982
|
||||
31
vendor/linux-aarch64/SHA256SUMS
vendored
Normal file
31
vendor/linux-aarch64/SHA256SUMS
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 base58-2.1.1-py3-none-any.whl
|
||||
33792674bda552a071a539b6590b2986aa8c08d0c9c30c2566d7cb323173310d bip_utils-2.10.0-py3-none-any.whl
|
||||
7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 build-1.3.0-py3-none-any.whl
|
||||
518c118a5e00001854adb51f3164e647aa99b6a9877d2a733a28cb5c0a4d6857 cbor2-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
|
||||
b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
|
||||
981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 click-8.3.1-py3-none-any.whl
|
||||
5a366c314df7217e3357bb8c7d2cda540b0bce180705f7a0ce2d1d9e28f62ad4 coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
6a6b11d6c42a450359f0d7f2312cb1fe69493ae8314dc3f6674b95cefed469a2 crcmod-1.7-cp312-cp312-linux_aarch64.whl
|
||||
549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91 cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
|
||||
30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 ecdsa-0.19.1-py2.py3-none-any.whl
|
||||
75c8d36691348abdd395b22f2c2ac5dd5c153c653550e6d18da5b0e433d7ce84 ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
|
||||
f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 iniconfig-2.3.0-py3-none-any.whl
|
||||
29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 packaging-25.0-py3-none-any.whl
|
||||
8d6f2b1a217ccefd933c7ec8bf69927e90d4630093d0af2404fe4e33703dcf0f pgpy-0.6.0-py3-none-any.whl
|
||||
9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd pip-25.3-py3-none-any.whl
|
||||
452a38edbcdfc333301c438c26ba00a0762d2034fe26a235d8587134453ccdb1 pip_chill-1.0.3-py2.py3-none-any.whl
|
||||
2fe16db727bbe5bf28765aeb581e792e61be51fc275545ef6725374ad720a1ce pip_tools-7.5.2-py3-none-any.whl
|
||||
e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 pluggy-1.6.0-py3-none-any.whl
|
||||
a3929c291408e67a1a11566f251b9f7d06c3fb3ae240caec44b9181de09e3fc9 py_sr25519_bindings-0.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 pyasn1-0.6.1-py3-none-any.whl
|
||||
e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 pycparser-2.23-py3-none-any.whl
|
||||
67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490 pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587 pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b pygments-2.19.2-py3-none-any.whl
|
||||
26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130 pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
|
||||
9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 pyproject_hooks-1.2.0-py3-none-any.whl
|
||||
711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b pytest-9.0.2-py3-none-any.whl
|
||||
6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87 pyzipper-0.3.6-py2.py3-none-any.whl
|
||||
062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 setuptools-80.9.0-py3-none-any.whl
|
||||
4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 six-1.17.0-py2.py3-none-any.whl
|
||||
708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 wheel-0.45.1-py3-none-any.whl
|
||||
BIN
vendor/linux-aarch64/base58-2.1.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/base58-2.1.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/build-1.3.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/build-1.3.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
vendor/linux-aarch64/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/click-8.3.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/click-8.3.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/crcmod-1.7-cp312-cp312-linux_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/crcmod-1.7-cp312-cp312-linux_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/iniconfig-2.3.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/iniconfig-2.3.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/packaging-25.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/packaging-25.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pgpy-0.6.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pgpy-0.6.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pip-25.3-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pip-25.3-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pip_chill-1.0.3-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pip_chill-1.0.3-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pip_tools-7.5.2-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pip_tools-7.5.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pluggy-1.6.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pluggy-1.6.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
vendor/linux-aarch64/pyasn1-0.6.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pyasn1-0.6.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pycparser-2.23-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pycparser-2.23-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pygments-2.19.2-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pygments-2.19.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pyproject_hooks-1.2.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pyproject_hooks-1.2.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pytest-9.0.2-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pytest-9.0.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pyzipper-0.3.6-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pyzipper-0.3.6-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/setuptools-80.9.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/setuptools-80.9.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/six-1.17.0-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/six-1.17.0-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/wheel-0.45.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/wheel-0.45.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
31
vendor/linux-x86_64/SHA256SUMS
vendored
Normal file
31
vendor/linux-x86_64/SHA256SUMS
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 base58-2.1.1-py3-none-any.whl
|
||||
33792674bda552a071a539b6590b2986aa8c08d0c9c30c2566d7cb323173310d bip_utils-2.10.0-py3-none-any.whl
|
||||
7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 build-1.3.0-py3-none-any.whl
|
||||
518c118a5e00001854adb51f3164e647aa99b6a9877d2a733a28cb5c0a4d6857 cbor2-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
|
||||
b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
|
||||
981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 click-8.3.1-py3-none-any.whl
|
||||
5a366c314df7217e3357bb8c7d2cda540b0bce180705f7a0ce2d1d9e28f62ad4 coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
82d740f4e923b88981b6ca8b854dceb270f319c81153211ccbec6ee523c7ccee crcmod-1.7-cp312-cp312-linux_aarch64.whl
|
||||
549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91 cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
|
||||
30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 ecdsa-0.19.1-py2.py3-none-any.whl
|
||||
96427209d130354f399ef20acc444c2ac76dd05b13cd270dea79909c39cdf6ec ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
|
||||
f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 iniconfig-2.3.0-py3-none-any.whl
|
||||
29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 packaging-25.0-py3-none-any.whl
|
||||
52a584525a9032726a8e28fc02ced9cec896f54aa5c6f5552035000821525f2d pgpy-0.6.0-py3-none-any.whl
|
||||
452a38edbcdfc333301c438c26ba00a0762d2034fe26a235d8587134453ccdb1 pip_chill-1.0.3-py2.py3-none-any.whl
|
||||
2fe16db727bbe5bf28765aeb581e792e61be51fc275545ef6725374ad720a1ce pip_tools-7.5.2-py3-none-any.whl
|
||||
9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd pip-25.3-py3-none-any.whl
|
||||
e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 pluggy-1.6.0-py3-none-any.whl
|
||||
a3929c291408e67a1a11566f251b9f7d06c3fb3ae240caec44b9181de09e3fc9 py_sr25519_bindings-0.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 pyasn1-0.6.1-py3-none-any.whl
|
||||
e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 pycparser-2.23-py3-none-any.whl
|
||||
67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490 pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587 pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b pygments-2.19.2-py3-none-any.whl
|
||||
26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130 pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
|
||||
9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 pyproject_hooks-1.2.0-py3-none-any.whl
|
||||
711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b pytest-9.0.2-py3-none-any.whl
|
||||
6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87 pyzipper-0.3.6-py2.py3-none-any.whl
|
||||
062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 setuptools-80.9.0-py3-none-any.whl
|
||||
4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 six-1.17.0-py2.py3-none-any.whl
|
||||
708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 wheel-0.45.1-py3-none-any.whl
|
||||
BIN
vendor/linux-x86_64/base58-2.1.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/base58-2.1.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/build-1.3.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/build-1.3.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
vendor/linux-x86_64/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/click-8.3.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/click-8.3.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/crcmod-1.7-cp312-cp312-linux_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/crcmod-1.7-cp312-cp312-linux_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/iniconfig-2.3.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/iniconfig-2.3.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/packaging-25.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/packaging-25.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pgpy-0.6.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pgpy-0.6.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pip-25.3-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pip-25.3-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pip_chill-1.0.3-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pip_chill-1.0.3-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pip_tools-7.5.2-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pip_tools-7.5.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pluggy-1.6.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pluggy-1.6.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
vendor/linux-x86_64/pyasn1-0.6.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pyasn1-0.6.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pycparser-2.23-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pycparser-2.23-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pygments-2.19.2-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pygments-2.19.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pyproject_hooks-1.2.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pyproject_hooks-1.2.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pytest-9.0.2-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pytest-9.0.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pyzipper-0.3.6-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pyzipper-0.3.6-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/setuptools-80.9.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/setuptools-80.9.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/six-1.17.0-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/six-1.17.0-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/wheel-0.45.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-x86_64/wheel-0.45.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/base58-2.1.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/base58-2.1.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/build-1.3.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/build-1.3.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
vendor/macos-arm64-docker/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/click-8.3.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/click-8.3.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
vendor/macos-arm64-docker/crcmod-1.7-cp312-cp312-linux_aarch64.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/crcmod-1.7-cp312-cp312-linux_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/iniconfig-2.3.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/iniconfig-2.3.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/packaging-25.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/packaging-25.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/pgpy-0.6.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/pgpy-0.6.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64-docker/pip-25.3-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64-docker/pip-25.3-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user