Compare commits

...

20 Commits

Author SHA1 Message Date
LC mac
a6c84f81ee chore: bump version to v1.1.0 2026-01-09 19:30:10 +08:00
109829f1f5 Update playbook.md 2026-01-09 19:20:22 +08:00
LC mac
2f7433b704 add wheels and update docs 2026-01-09 19:16:28 +08:00
LC mac
6457ec2cee chore: recompile requirements.txt 2026-01-09 18:55:57 +08:00
LC mac
0949fe9792 feat(bip85): add gen-child command (v1.0.6)
Implements BIP85 child mnemonic derivation with full interoperability.

Features:
- Derives child BIP39 mnemonics (12/15/18/21/24 words) from master mnemonic
- BIP85 path: m/83696968'/39'/0'/{words}'/{index}'
- Supports optional master BIP39 passphrase
- Reuses existing input modes (--interactive, --mnemonic-stdin)
- Follows existing UX patterns (--off-screen, --file, PGP encryption)
- Offline-first with NetworkGuard protection

Testing:
- Adds deterministic regression tests for BIP85 spec compliance
- Verified against official BIP85 test vectors
- CLI smoke tests for end-to-end validation

Interoperability:
- Produces mnemonics compatible with Coldcard, Ian Coleman tool, etc.
- Test vector verified: 'girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose'

Version bumped to v1.0.6
2026-01-09 18:46:19 +08:00
21b9389591 add vendor folder for linux-aarch64 2026-01-09 02:58:31 +08:00
LC mac
369c8595a1 Refs: offline install via vendored wheels and test verification workflow (see README/playbook updates) 2026-01-08 01:45:25 +08:00
LC mac
2807982209 vendor: add macOS ARM64 wheels for offline installation (Python 3.12) 2026-01-08 00:42:22 +08:00
LC mac
84953dbe5a add BestPractices.md 2026-01-08 00:12:37 +08:00
LC mac
6fd7cd4e79 modified playbook.md 2026-01-08 00:05:27 +08:00
LC mac
5a52eb0954 test: add offline integrity test suite with frozen vectors 2026-01-07 23:54:25 +08:00
LC mac
28f01a613a format the README.md 2026-01-07 23:14:05 +08:00
a9af9d33af add Makefile Dockerfile README.md 2026-01-07 21:03:00 +08:00
129b09fcd9 adding wheelhouse 2026-01-07 20:53:37 +08:00
LC mac
d02e1d872e with figerprint match option when encryption with asc file, extra assurance the file is the target asc file 2026-01-07 02:21:03 +08:00
LC mac
875fa17d6c patch the recover logic with interactive mode 2026-01-07 01:45:35 +08:00
LC mac
ccd070dc56 modified README 2026-01-07 00:53:39 +08:00
LC mac
ce26b3560a modified doc to v1.0.5 2026-01-07 00:40:47 +08:00
LC
4cf32f9ba0 Remove venv from previous commit 2026-01-06 15:48:15 +00:00
LC
94fcb993db Fix: ignore venv and commit project files 2026-01-06 15:25:11 +00:00
173 changed files with 3598 additions and 479 deletions

4
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.venv/
.venv312/
__pycache__/
*.pyc
.vscode/
@@ -23,3 +24,6 @@ build/
*.sqlite3
*.db
*.asc
.venv/
.wallet/
.potentialfix.md

18
Dockerfile Normal file
View 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
View 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
View File

@@ -1,31 +1,203 @@
# pyhdwallet v1.0.4
# 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)
---

115
build-all-platforms.sh Executable file
View 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
View 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 ""

File diff suppressed because it is too large Load Diff

7
pytest.ini Normal file
View 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
View File

@@ -0,0 +1,2 @@
-r requirements.txt # Include production deps
pytest>=9.0.0

81
requirements-dev.txt Normal file
View 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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
Bootstrap script to generate tests/vectors.json.
Run this ONCE (online allowed if needed, though this logic is offline)
to freeze the expected outputs of the current pyhdwallet implementation.
"""
import sys
import json
import os
from pathlib import Path
# Add src to path so we can import pyhdwallet
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
import pyhdwallet
from bip_utils import Bip39SeedGenerator
def main():
print("Bootstrapping test vectors...")
vectors = {}
# 1. PGP Fingerprint Vector
recipient_path = Path(__file__).parent / "data" / "recipient.asc"
if not recipient_path.exists():
print(f"Error: {recipient_path} not found. Please create it first.")
sys.exit(1)
with open(recipient_path, "r", encoding="utf-8") as f:
armored = f.read()
# Calculate expected fingerprint using current code logic
expected_fpr = pyhdwallet.pgp_fingerprint(armored)
vectors["pgp"] = {
"recipient_file": "recipient.asc",
"expected_fingerprint": expected_fpr
}
print(f"PGP: Pinned fingerprint {expected_fpr}")
# 2. BIP39 & Derivation Vectors
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
vectors["bip39"] = []
# Case A: No Passphrase
seed_bytes = Bip39SeedGenerator(mnemonic).Generate("")
seed_hex = seed_bytes.hex()
# Generate addresses for all supported chains/profiles
chains = ["ethereum", "bitcoin", "solana"]
# Fixed profile names to use underscores instead of hyphens
sol_profiles = ["phantom_bip44change", "phantom_bip44", "solana_bip39_first32"]
derived_data = {}
# Run derivation for each sol profile logic
for profile in sol_profiles:
res = pyhdwallet.derive_all(
seed_bytes,
chains,
count=5,
sol_profile=profile,
export_private=False
)
derived_data[profile] = res["addresses"]
vectors["bip39"].append({
"mnemonic": mnemonic,
"passphrase": "",
"expected_seed_hex": seed_hex,
"derived_addresses": derived_data
})
# Case B: With Passphrase (Regression test)
passphrase = "TREZOR"
seed_bytes_p = Bip39SeedGenerator(mnemonic).Generate(passphrase)
seed_hex_p = seed_bytes_p.hex()
res_p = pyhdwallet.derive_all(
seed_bytes_p,
chains,
count=1,
sol_profile="phantom_bip44change",
export_private=False
)
vectors["bip39"].append({
"mnemonic": mnemonic,
"passphrase": passphrase,
"expected_seed_hex": seed_hex_p,
"derived_addresses": {"phantom_bip44change": res_p["addresses"]}
})
print("Derivation: Generated vectors for empty and non-empty passphrases.")
# 3. Save
out_path = Path(__file__).parent / "vectors.json"
with open(out_path, "w", encoding="utf-8") as f:
json.dump(vectors, f, indent=2)
print(f"Success! Vectors written to {out_path}")
if __name__ == "__main__":
main()

91
tests/generate_bip85_vectors.py Executable file
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

31
vendor/linux-x86_64/SHA256SUMS vendored Normal file
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More