walletpress/backend/walletpress_cli.py
cryptorugmunch e13bd4d774
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / security (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / license (push) Waiting to run
CI / ai-review (push) Waiting to run
docs: apply fleet-template (16-artifact scaffold)
Adds missing standard artifacts:
- README.md (if missing)
- AGENTS.md (AI agent contract)
- PLAN.md (current sprint)
- STATUS.md (where we are)
- DEVELOPMENT.md (dev workflow)
- DEPLOYMENT.md (deploy procedure)
- TESTING.md (test strategy)
- DECISIONS.md (ADR index + templates)
- .github/CODEOWNERS
- .github/workflows/ci.yml

Preserves all existing artifacts.

Refs: RugMunchMedia/fleet-template
2026-07-02 02:07:06 +07:00

435 lines
16 KiB
Python

#!/usr/bin/env python3
"""WalletPress CLI — run the backend without Docker.
Usage:
walletpress serve # Start the API server
walletpress init # Initialize data directory
walletpress generate # Generate a wallet from CLI
walletpress validate # Validate an address
walletpress version # Show version
No Docker required. Just pip install walletpress && walletpress serve.
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import sys
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("walletpress")
def cmd_serve(args):
"""Start the WalletPress API server."""
from core.config import cfg
import uvicorn
port = args.port or cfg.port
host = args.host or cfg.host
os.makedirs(cfg.data_dir, exist_ok=True)
logger.info(f"WalletPress v{cfg.version} starting on {host}:{port}")
logger.info(f"Data directory: {cfg.data_dir}")
logger.info(f"API docs: http://{host}:{port}/docs")
logger.info(f"Health: http://{host}:{port}/health")
if cfg.vault_password:
logger.info("Vault encryption: AES-256-GCM + Argon2id")
else:
logger.warning("Vault encryption DISABLED. Set WP_VAULT_PASSWORD for production.")
uvicorn.run(
"main:app",
host=host,
port=port,
reload=args.reload,
log_level="info",
)
def cmd_deploy(args):
"""Production one-command setup (requires Pro license key).
Detects OS, installs deps, configures everything, starts service.
Setup methods:
--docker Deploy with Docker Compose (production stack)
--method auto Auto-detect best method (default)
--method cli Direct installation with systemd/launchd
--domain DOMAIN Set domain for SSL auto-config
--email EMAIL Email for Let's Encrypt notifications
"""
from core.config import cfg as _cfg
from core.license import get_license
lic = get_license()
if not lic.is_paid:
print(" walletpress deploy requires a Pro license.")
print(" Buy: https://walletpress.cc/buy")
print(" Or use hosted: https://walletpress.cc/pricing")
return 1
method = args.method or "auto"
domain = args.domain or ""
print(f" WalletPress Pro Deploy — v{_cfg.version}")
print(f" {'='*50}")
print(f" License: {lic.tier.title()}")
print(f" Method: {method}")
if domain:
print(f" Domain: {domain}")
print()
if method == "docker":
print(" Setting up Docker Compose production stack...")
compose_path = os.path.join(os.path.dirname(__file__), "..", "installers", "docker-compose.prod.yml")
if os.path.exists(compose_path):
print(f" Compose file: {compose_path}")
print(f" Run: docker compose -f {compose_path} up -d")
else:
print(" Docker compose file not found. Re-download from walletpress.cc.")
return
# CLI method — direct installation
import platform
import subprocess
system = platform.system().lower()
print(f" Detected OS: {system}")
if system == "linux":
print(" Installing system dependencies...")
try:
subprocess.run(["sudo", "apt-get", "update", "-qq"], check=False)
subprocess.run(["sudo", "apt-get", "install", "-y", "-qq",
"python3", "python3-venv", "python3-pip",
"openssl", "sqlite3", "curl"], check=False)
except Exception:
pass
print(" Setting up virtual environment...")
venv_path = "/opt/walletpress/venv"
subprocess.run(["sudo", "python3", "-m", "venv", venv_path], check=False)
print(" Installing Python dependencies...")
req_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
subprocess.run(["sudo", f"{venv_path}/bin/pip", "install", "-q", "-r", req_path], check=False)
print(" Generating credentials...")
admin_key = subprocess.run(["openssl", "rand", "-hex", 32], capture_output=True, text=True).stdout.strip()
vault_pass = subprocess.run(["openssl", "rand", "-hex", 32], capture_output=True, text=True).stdout.strip()
subprocess.run(["openssl", "rand", "-hex", 16], capture_output=True, text=True).stdout.strip()
print(" Creating configuration...")
subprocess.run(["sudo", "mkdir", "-p", "/etc/walletpress"], check=False)
subprocess.run(["sudo", "mkdir", "-p", "/var/lib/walletpress"], check=False)
env_content = f"""WP_ADMIN_KEY={admin_key}
WP_VAULT_PASSWORD={vault_pass}
WP_LICENSE_KEY={os.environ.get('WP_LICENSE_KEY', '')}
WP_DOMAIN={domain}
WP_CORS_ORIGINS={f'https://{domain}' if domain else '*'}
WP_SMTP_HOST=
WP_SMTP_PORT=587
WP_SMTP_USER=
WP_SMTP_PASS=
WP_FROM_EMAIL=walletpress@{domain if domain else 'localhost'}
WP_NOTIFY_EMAIL=
WP_ALLOWED_IPS=
WP_RATE_LIMIT=120
"""
subprocess.run(["sudo", "tee", "/etc/walletpress/env"], input=env_content, text=True, check=False)
subprocess.run(["sudo", "chmod", "600", "/etc/walletpress/env"], check=False)
print(" Installing systemd service...")
service_content = f"""[Unit]
Description=WalletPress Pro
After=network.target
[Service]
Type=simple
User=walletpress
Group=walletpress
EnvironmentFile=/etc/walletpress/env
WorkingDirectory=/opt/walletpress
ExecStart={venv_path}/bin/uvicorn main:app --host 127.0.0.1 --port 8010
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
"""
subprocess.run(["sudo", "tee", "/etc/systemd/system/walletpress.service"], input=service_content, text=True, check=False)
subprocess.run(["sudo", "systemctl", "daemon-reload"], check=False)
subprocess.run(["sudo", "systemctl", "enable", "walletpress"], check=False)
subprocess.run(["sudo", "systemctl", "start", "walletpress"], check=False)
print()
print(f" {'='*50}")
print(" WALLETPRESS PRO DEPLOYED")
print(f" {'='*50}")
print(" URL: http://localhost:8010")
print(" Admin: /api/v1/license")
print(" Health: /health")
print(" Docs: /docs")
print("")
print(f" Admin key: {admin_key}")
print(f" Vault pass: {vault_pass}")
print(f" License: {lic.tier.title()}")
print("")
print(" Save these credentials. They won't be shown again.")
print(" Production: set up nginx reverse proxy + Let's Encrypt SSL.")
elif system == "darwin":
print(" macOS detected. Set up with launchd:")
print(" brew install python@3.12 openssl sqlite")
print(" python3 -m venv venv")
print(" source venv/bin/activate")
print(" pip install -r requirements.txt")
print(" uvicorn main:app --port 8010")
else:
print(f" Unsupported OS: {system}")
print(" Use Docker: docker compose -f installers/docker-compose.prod.yml up -d")
return 0
def cmd_init(args):
"""Initialize the WalletPress data directory."""
from core.config import cfg
from core.auth import get_key_store
from core.vault import get_vault
os.makedirs(cfg.data_dir, exist_ok=True)
get_vault()
get_key_store()
if not cfg.admin_key:
logger.warning("WP_ADMIN_KEY not set. API authentication will be disabled.")
logger.warning("Set WP_ADMIN_KEY environment variable for production.")
if not cfg.vault_password:
logger.warning("WP_VAULT_PASSWORD not set. Private keys will NOT be encrypted at rest.")
admin_key = cfg.admin_key
logger.info(f"WalletPress initialized at {cfg.data_dir}")
if admin_key:
logger.info(f"Admin key: {admin_key[:16]}...")
config_path = cfg.data_dir / ".env.example"
if not config_path.exists():
config_path.write_text("""# WalletPress Configuration
# Copy to .env and customize
WP_HOST=0.0.0.0
WP_PORT=8010
WP_DATA_DIR=/data
WP_ADMIN_KEY=change-me-to-a-random-string
WP_VAULT_PASSWORD=change-me-to-a-strong-password
WP_CORS_ORIGINS=*
WP_RATE_LIMIT=60
WP_MAX_BATCH=100
""")
logger.info(f"Example config created at {config_path}")
def cmd_generate(args):
"""Generate a wallet from the CLI."""
from wallet_engine.generator import get_generator
from wallet_engine.chains import CHAINS
generator = get_generator()
chain = args.chain or "eth"
if chain not in CHAINS:
print(f"Unsupported chain: {chain}")
print(f"Supported: {', '.join(list(CHAINS.keys())[:20])}...")
sys.exit(1)
wallet = generator.generate(chain, label=args.label or f"cli_{chain}")
print(json.dumps(wallet.to_safe_dict(), indent=2))
print(f"\n{'!'*60}")
print("! WARNING: Private key is shown above. Keep it SECRET.")
print(f"{'!'*60}")
def cmd_validate(args):
"""Validate a wallet address."""
from wallet_engine.chains import CHAINS, validate_address
chain = args.chain or "eth"
address = args.address
chain_info = CHAINS.get(chain.lower())
if not chain_info:
print(f"Unsupported chain: {chain}")
sys.exit(1)
valid = validate_address(chain, address)
print(f"Chain: {chain_info.name} ({chain})")
print(f"Address: {address}")
print(f"Valid: {'YES ✓' if valid else 'NO ✗'}")
print(f"Expected pattern: {chain_info.address_pattern}")
if not valid:
sys.exit(1)
def cmd_doctor(args):
"""Run system diagnostics and show setup status."""
from core.config import cfg as _cfg
from core.onboarding import onboarding_status
status = onboarding_status()
from core.license import get_license
lic = get_license()
print(f"\n WalletPress Doctor — v{_cfg.version}")
print(f" {'='*50}")
print(f" License: {lic.tier.title()} ({'permanent key' if lic.is_paid else 'free — 3 chains only'})")
print(f" Vault encryption: {'✅ AES-256-GCM' if _cfg.vault_password else '❌ Not configured'}")
print(f" Admin API key: {'✅ Set' if _cfg.admin_key else '❌ Not set'}")
print(f" RPC endpoints: {status['rpc_health']['rpc_configured']}/{status['rpc_health']['total_chains']} configured")
print(f" SSL: {'✅ Configured' if os.path.exists('/etc/letsencrypt') or os.getenv('WP_SSL_ENABLED') else '❌ Not configured'}")
print(f" SMTP: {'✅ Configured' if os.getenv('WP_SMTP_HOST') else '❌ Not configured'}")
print()
print(f" Issues: {status['items_missing']} of {status['items_total']} setup items incomplete")
missing = status['missing_items'][:5]
for item in missing:
print(f"{item['item']}")
print()
if lic.is_paid:
print(" Visit walletpress.cc/app for the hosted dashboard.")
else:
print(" → Buy Pro ($199) for deploy script and setup support: walletpress.cc/buy")
print()
def cmd_version(args):
"""Show WalletPress version."""
from core.config import cfg
print(f"WalletPress v{cfg.version}")
print("License: MIT")
print("Repository: https://github.com/cryptorugmuncher/walletpress")
def main():
parser = argparse.ArgumentParser(description="WalletPress — self-hosted multi-chain wallet management")
parser.add_argument("--version", action="store_true", help="Show version")
sub = parser.add_subparsers(dest="command", help="Commands")
serve_parser = sub.add_parser("serve", help="Start the API server")
serve_parser.add_argument("--host", default="", help="Bind host")
serve_parser.add_argument("--port", type=int, default=0, help="Bind port")
serve_parser.add_argument("--reload", action="store_true", help="Auto-reload on code changes")
deploy_parser = sub.add_parser("deploy", help="Production one-liner setup (requires Pro license)")
deploy_parser.add_argument("--docker", action="store_true", help="Deploy with Docker Compose")
deploy_parser.add_argument("--method", default="auto", choices=["auto", "cli", "docker"], help="Deployment method")
deploy_parser.add_argument("--domain", default="", help="Domain for SSL configuration")
deploy_parser.add_argument("--email", default="", help="Email for Let's Encrypt")
sub.add_parser("init", help="Initialize data directory")
gen_parser = sub.add_parser("generate", help="Generate a wallet")
gen_parser.add_argument("--chain", default="eth", help="Chain key (eth, sol, btc, etc.)")
gen_parser.add_argument("--label", default="", help="Wallet label")
val_parser = sub.add_parser("validate", help="Validate an address")
val_parser.add_argument("--chain", default="eth", help="Chain key")
val_parser.add_argument("address", help="Wallet address to validate")
sub.add_parser("doctor", help="Run system diagnostics")
backup_parser = sub.add_parser("backup", help="Export vault + proofs to encrypted archive")
backup_parser.add_argument("--output", default="walletpress_backup.json", help="Output file path")
restore_parser = sub.add_parser("restore", help="Restore vault from backup archive")
restore_parser.add_argument("input", help="Backup file to restore from")
sub.add_parser("version", help="Show version")
args = parser.parse_args()
if args.version:
cmd_version(args)
return
if args.command == "serve":
cmd_serve(args)
elif args.command == "deploy":
cmd_deploy(args)
elif args.command == "init":
cmd_init(args)
elif args.command == "generate":
cmd_generate(args)
elif args.command == "validate":
cmd_validate(args)
elif args.command == "doctor":
cmd_doctor(args)
elif args.command == "backup":
cmd_backup(args)
elif args.command == "restore":
cmd_restore(args)
elif args.command == "version":
cmd_version(args)
else:
parser.print_help()
def cmd_backup(args):
"""Export vault, proofs, and audit log to a portable JSON archive."""
import json
from core.vault import get_vault
print(f" Exporting WalletPress data to {args.output}...")
vault = get_vault()
wallets = vault.list(limit=100000)
data = {
"version": "1.1.0",
"exported_at": __import__("time").time(),
"wallets": [{
"id": w.id, "chain": w.chain, "address": w.address,
"label": w.label, "tags": w.tags, "created_at": w.created_at,
"encrypted_key": w.encrypted_key,
"public_key": w.public_key,
"derivation_path": w.derivation_path,
"encrypted": w.encrypted,
"balance_usd": w.balance_usd, "notes": w.notes,
} for w in wallets],
}
with open(args.output, "w") as f:
json.dump(data, f, indent=2, default=str)
print(f" Exported {len(wallets)} wallets to {args.output}")
print(f" Restore: walletpress restore {args.output}")
def cmd_restore(args):
"""Restore vault from a backup archive."""
import json
from core.vault import WalletEntry, get_vault
print(f" Restoring from {args.input}...")
with open(args.input) as f:
data = json.load(f)
vault = get_vault()
count = 0
for w_data in data.get("wallets", []):
entry = WalletEntry(
id=w_data["id"], chain=w_data["chain"], address=w_data["address"],
label=w_data.get("label", ""), tags=w_data.get("tags", []),
created_at=w_data.get("created_at", 0),
encrypted_key=w_data.get("encrypted_key", ""),
public_key=w_data.get("public_key", ""),
derivation_path=w_data.get("derivation_path", ""),
encrypted=w_data.get("encrypted", False),
balance_usd=w_data.get("balance_usd", 0),
notes=w_data.get("notes", ""),
)
if vault.put(entry):
count += 1
print(f" Restored {count} of {len(data.get('wallets', []))} wallets")
if __name__ == "__main__":
main()