Skill: manyfestation/kaspa (version 1.0.11)

--- name: kaspa-wallet description: > Simple wallet for Kaspa blockchain. Send KAS, check balances, generate payment URIs. Self-custody CLI wallet with JSON output for automation. keywords: - kaspa - kas - wallet - payments - transfer - balance - cryptocurrency - blockdag allowed-tools: - Bash - Read - Write - Glob - Grep - Task user-invocable: true --- # Kaspa Wallet Skill Simple self-custody wallet for Kaspa blockchain. ## Overview ``` ┌─────────────────────────────────────────────────────────┐ │ KASPA WALLET │ ├─────────────────────────────────────────────────────────┤ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │ Balance │ │ Send │ │ Payment URIs │ │ │ │ Check │ │ KAS │ │ Generator │ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ │ │ │ │ │ └────────────────┴───────────────────┘ │ │ │ │ │ ┌──────────▼──────────┐ │ │ │ Kaspa Python SDK │ │ │ │ (wRPC Client) │ │ │ └─────────────────────┘ │ │ │ │ │ ┌───────────────┼───────────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Mainnet │ │ Testnet │ │ Custom │ │ │ │ wRPC │ │ wRPC │ │ RPC │ │ │ └─────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ## Features | Feature | Description | |---------|-------------| | **Send KAS** | Transfer KAS to any Kaspa address | | **Balance Check** | Check balance of any address | | **Payment URIs** | Generate `kaspa:` payment request URIs | | **Fee Estimates** | Get current network fee tiers | | **Network Info** | Check node sync status and blocks | | **Wallet Generation** | Generate new mnemonic phrases | ## Quick Start ### Installation ```bash python3 install.py ``` **Requirements:** Python 3.8+ with pip. Works on macOS, Linux, Windows. **Troubleshooting install:** - If pip fails: `pip install kaspa` manually, or try `KASPA_PYTHON=python3.12 python3 install.py` - If venv missing: `sudo apt install python3-venv` (Ubuntu/Debian) - To reinstall: `rm -rf .venv && python3 install.py` ### CLI Usage ```bash # Check balance ./kaswallet.sh balance ./kaswallet.sh balance kaspa:qrc8y... # Send payment ./kaswallet.sh send kaspa:qrc8y... 0.5 ./kaswallet.sh send kaspa:qrc8y... max # Generate payment URI ./kaswallet.sh uri kaspa:q... 1.5 "coffee payment" # Network info ./kaswallet.sh info # Fee estimates ./kaswallet.sh fees # Generate new wallet ./kaswallet.sh generate-mnemonic ``` ### Payment URI Format ## Architecture ``` kaspa-wallet/ ├── SKILL.md ├── README.md ├── install.py # Auto-installer with venv ├── kaswallet.sh # CLI wrapper script ├── requirements.txt └── scripts/ └── kaswallet.py # Main wallet logic ``` ## Configuration ```bash # Environment variables (one required) export KASPA_PRIVATE_KEY="64-character-hex-string" # OR export KASPA_MNEMONIC="your twelve or twenty four word seed phrase" # Optional export KASPA_NETWORK="mainnet" # mainnet (default), testnet-10 export KASPA_RPC_URL="wss://..." # Custom RPC endpoint export KASPA_RPC_CONNECT_TIMEOUT_MS="30000" # Connection timeout (default: 15000) ``` ## Core Functions ### Check Balance ```bash ./kaswallet.sh balance # Your wallet balance ./kaswallet.sh balance kaspa:qrc8y... # Any address balance ``` **Output:** ```json {"address": "kaspa:q...", "balance": "1.5", "sompi": "150000000", "network": "mainnet"} ``` ### Send KAS ```bash ./kaswallet.sh send <address> <amount> # Send specific amount ./kaswallet.sh send <address> max # Send entire balance ./kaswallet.sh send <address> <amount> priority # Priority fee tier ``` **Output (success):** ```json {"status": "sent", "txid": "abc123...", "from": "kaspa:q...", "to": "kaspa:q...", "amount": "0.5", "fee": "0.0002"} ``` **Output (error):** ```json {"error": "Storage mass exceeds maximum", "errorCode": "STORAGE_MASS_EXCEEDED", "hint": "...", "action": "consolidate_utxos"} ``` ### Network Info ```bash ./kaswallet.sh info ``` **Output:** ```json {"network": "mainnet", "url": "wss://...", "blocks": 12345678, "synced": true, "version": "1.0.0"} ``` ### Fee Estimates ```bash ./kaswallet.sh fees ``` **Output:** ```json {"network": "mainnet", "low": {"feerate": 1.0, "estimatedSeconds": 60}, "economic": {...}, "priority": {...}} ``` ### Generate New Wallet ```bash ./kaswallet.sh generate-mnemonic ``` **Output:** ```json {"mnemonic": "word1 word2 word3 ... word24"} ``` ### Payment URI ```bash ./kaswallet.sh uri # Your address ./kaswallet.sh uri kaspa:q... 1.5 "payment" # With amount and message ``` ## Error Handling | Error | Cause | Solution | |-------|-------|----------| | `STORAGE_MASS_EXCEEDED` | Amount too small for current UTXOs | Send `max` to yourself first to consolidate | | `NO_UTXOS` | No spendable outputs | Wait for confirmations or fund wallet | | `INSUFFICIENT_FUNDS` | Balance too low | Check balance, reduce amount | | `RPC_TIMEOUT` | Network slow | Retry or increase timeout | | `NO_CREDENTIALS` | Missing wallet key | Set KASPA_PRIVATE_KEY or KASPA_MNEMONIC | | `SDK_NOT_INSTALLED` | Kaspa SDK missing | Run `python3 install.py` | ## Payment Flow ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Sender │ │ Kaspa Wallet│ │ Recipient │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ 1. Initiate │ │ │──────────────────▶│ │ │ │ │ │ │ 2. Execute │ │ │ KAS Transfer │ │ │─────────────────▶│ │ │ │ │ │ 3. Confirm │ │ │◀──────────────────│ │ 4. Success │ │ │◀──────────────────│ │ │ │ │ ``` ## Common Workflows ### Consolidate UTXOs (Fix Storage Mass Error) When sending fails with `STORAGE_MASS_EXCEEDED`: ```bash # 1. Get your address ./kaswallet.sh balance # Returns: {"address": "kaspa:qYOUR_ADDRESS...", ...} # 2. Send max to yourself (consolidates UTXOs) ./kaswallet.sh send kaspa:qYOUR_ADDRESS... max # 3. Now send the original amount (will work) ./kaswallet.sh send kaspa:qRECIPIENT... 0.5 ``` ### Check Transaction Status After sending, use the `txid` to verify on a block explorer: - Mainnet: `https://explorer.kaspa.org/txs/{txid}` - Testnet: `https://explorer-tn10.kaspa.org/txs/{txid}` ### Switch Networks ```bash # Testnet export KASPA_NETWORK="testnet-10" ./kaswallet.sh info # Back to mainnet export KASPA_NETWORK="mainnet" ./kaswallet.sh info ``` ## Units - **KAS**: Human-readable unit (e.g., 1.5 KAS) - **sompi**: Smallest unit, 1 KAS = 100,000,000 sompi All command inputs accept KAS. Outputs include both KAS and sompi where relevant. ## Security Notes - **Private keys**: Never expose in logs or error messages - **Mnemonics**: Passed via environment variables only - **No disk storage**: Wallet does not store credentials - **Fresh connections**: Each command establishes new RPC connection - **Address format**: Validate Kaspa addresses (`kaspa:q...` format) ## Comparison with Traditional Wallets | Feature | Traditional Wallet | Kaspa Wallet CLI | |---------|-------------------|------------------| | Setup | GUI install | `python3 install.py` | | Interface | Desktop app | CLI + JSON output | | Automation | Limited | Full (JSON parsing) | | Custody | Varies | Self-custody | | Agent-friendly | No | Yes | ## Roadmap - [ ] QR code generation for addresses - [ ] Payment links - [ ] Transaction history - [ ] Multi-address support - [ ] Batch payments - [ ] Telegram bot integration ## Resources - [Kaspa Docs](https://docs.kaspa.org/) - [Kaspa Explorer](https://explorer.kaspa.org/) - [kaspa-py SDK](https://github.com/aspect-build/kaspa-py)

path: requirements.txt
content: kaspa


path: kaswallet.sh
content: #!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"

export PYTHONPYCACHEPREFIX="${PYTHONPYCACHEPREFIX:-$ROOT_DIR/.pycache}"

run_wallet() {
  exec "$ROOT_DIR/.venv/bin/python" "$ROOT_DIR/scripts/kaswallet.py" "$@"
}

if [[ -x "$ROOT_DIR/.venv/bin/python" ]]; then
  run_wallet "$@"
fi

find_python() {
  if [[ -n "${KASPA_PYTHON:-}" ]]; then
    if [[ -x "$KASPA_PYTHON" ]]; then
      echo "$KASPA_PYTHON"
      return 0
    fi
    if command -v "$KASPA_PYTHON" >/dev/null 2>&1; then
      echo "$KASPA_PYTHON"
      return 0
    fi
  fi

  for exe in python3 python; do
    if command -v "$exe" >/dev/null 2>&1; then
      echo "$exe"
      return 0
    fi
  done
  return 1
}

PY="$(find_python || true)"
if [[ -z "$PY" ]]; then
  echo "No Python found. Install Python 3 and re-run: python3 install.py" >&2
  exit 1
fi

echo "Python venv not found. Bootstrapping with: $PY install.py" >&2
"$PY" install.py >&2

if [[ -x "$ROOT_DIR/.venv/bin/python" ]]; then
  run_wallet "$@"
fi

echo "Install failed (no .venv created). Try running manually:" >&2
echo "  $PY install.py" >&2
exit 1


path: README.md
content: # Kaspa Wallet CLI

A standalone command-line wallet for the Kaspa blockchain. Pure Python, no Node.js required.

## Quick Start

```bash
# Install
python3 install.py

# Configure wallet
export KASPA_PRIVATE_KEY="your-64-char-hex-key"

# Check balance
./kaswallet.sh balance

# Send KAS
./kaswallet.sh send kaspa:recipient... 1.0
```

## Requirements

- Python 3.8+
- pip (included with Python)
- Internet connection

## Documentation

See [SKILL.md](SKILL.md) for full documentation including:
- All commands and options
- Error codes and troubleshooting
- Common workflows
- Examples for automated agents

## Commands

| Command | Description |
|---------|-------------|
| `balance [address]` | Check wallet or any address balance |
| `send <to> <amount\|max>` | Send KAS to an address |
| `info` | Network status and connectivity |
| `fees` | Current fee estimates |
| `generate-mnemonic` | Create new 24-word seed phrase |
| `uri [address] [amount]` | Generate payment URI |

## Environment Variables

```bash
# Required (one of):
KASPA_PRIVATE_KEY="hex"           # 64-char private key
KASPA_MNEMONIC="words..."         # 12/24-word seed phrase

# Optional:
KASPA_NETWORK="mainnet"           # mainnet, testnet-10
KASPA_RPC_CONNECT_TIMEOUT_MS=30000
```

## License

MIT


path: install.py
content: #!/usr/bin/env python3
"""
Kaspa Wallet Installer

Installs the Kaspa Python SDK with maximum compatibility.
Works on macOS, Linux, Windows - even minimal Docker images.

Usage:
    python3 install.py

Fallback cascade:
    1. Use existing .venv if present
    2. Standard venv creation
    3. venv --without-pip + get-pip.py bootstrap
    4. pip install --user (no venv)
    5. virtualenv (if available)
"""
from __future__ import annotations

import os
import platform
import shutil
import signal
import subprocess
import sys
from datetime import datetime
from pathlib import Path

ROOT = Path(__file__).resolve().parent
VENV_DIR = ROOT / ".venv"
REQ_FILE = ROOT / "requirements.txt"
LOG_FILE = ROOT / "install.log"
MIN_PYTHON = (3, 8)

# Installation mode flags
USE_VENV = True  # Will be set False if all venv methods fail


def log(msg: str) -> None:
    """Log to both console and file."""
    line = f"[kaspa-wallet] {msg}"
    print(line)
    try:
        with open(LOG_FILE, "a") as f:
            f.write(f"{datetime.now().isoformat()} {line}\n")
    except:
        pass


def error(msg: str) -> None:
    """Log error to stderr and file."""
    line = f"[kaspa-wallet] ERROR: {msg}"
    print(line, file=sys.stderr)
    try:
        with open(LOG_FILE, "a") as f:
            f.write(f"{datetime.now().isoformat()} {line}\n")
    except:
        pass


def init_log() -> None:
    """Initialize log file with system info."""
    try:
        with open(LOG_FILE, "w") as f:
            f.write(f"=== Kaspa Wallet Install Log ===\n")
            f.write(f"Time: {datetime.now().isoformat()}\n")
            f.write(f"Python: {sys.version}\n")
            f.write(f"Platform: {platform.system()} {platform.machine()}\n")
            f.write(f"Executable: {sys.executable}\n")
            f.write(f"================================\n\n")
    except:
        pass


def get_python_info() -> dict:
    """Get detailed Python environment info for diagnostics."""
    return {
        "version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
        "executable": sys.executable,
        "platform": platform.system(),
        "arch": platform.machine(),
        "implementation": platform.python_implementation(),
    }


def check_python_version() -> bool:
    """Ensure Python version meets minimum requirements."""
    if sys.version_info < MIN_PYTHON:
        error(f"Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+ required, found {sys.version_info.major}.{sys.version_info.minor}")
        error("Please install a newer Python version:")
        error("  macOS:   brew install python@3.12")
        error("  Ubuntu:  sudo apt install python3.12")
        error("  Windows: Download from python.org")
        return False
    return True


def find_best_python() -> str:
    """Find the best available Python executable."""
    # Allow override via environment
    custom = os.environ.get("KASPA_PYTHON")
    if custom:
        if shutil.which(custom) or os.path.isfile(custom):
            return custom
        error(f"KASPA_PYTHON={custom} not found, falling back to default")

    # Try newer versions first
    for ver in ("3.13", "3.12", "3.11", "3.10", "3.9", "3.8"):
        exe = shutil.which(f"python{ver}")
        if exe:
            return exe

    # Fallback to generic python3/python
    for name in ("python3", "python"):
        exe = shutil.which(name)
        if exe:
            return exe

    return sys.executable


def run_command(
    cmd: list[str],
    capture: bool = False,
    timeout: int | None = 300,
    env: dict | None = None,
) -> subprocess.CompletedProcess:
    """Run a command with proper error handling and signal detection."""
    cmd_str = " ".join(cmd)
    log(f"Running: {cmd_str}")
    
    run_env = os.environ.copy()
    if env:
        run_env.update(env)
    
    try:
        result = subprocess.run(
            cmd,
            cwd=str(ROOT),
            capture_output=capture,
            text=True,
            timeout=timeout,
            env=run_env,
        )
        
        # Check for signal termination (negative return code)
        if result.returncode < 0:
            sig = -result.returncode
            sig_name = signal.Signals(sig).name if sig in signal.Signals._value2member_map_ else f"signal {sig}"
            
            if sig == signal.SIGKILL:
                error(f"Process killed by {sig_name} (likely OOM or watchdog)")
                error("")
                error("This usually means the system ran out of memory.")
                error("Try one of:")
                error("  1. pip install --no-cache-dir kaspa")
                error("  2. Run on a machine with more RAM (>= 1GB)")
                error("  3. Check: dmesg | tail -20  (for OOM killer logs)")
                raise subprocess.CalledProcessError(result.returncode, cmd)
            else:
                error(f"Process killed by {sig_name}")
                raise subprocess.CalledProcessError(result.returncode, cmd)
        
        if result.returncode != 0:
            raise subprocess.CalledProcessError(result.returncode, cmd, result.stdout, result.stderr)
            
        return result
        
    except subprocess.TimeoutExpired:
        error(f"Command timed out after {timeout}s: {cmd_str}")
        raise
    except subprocess.CalledProcessError as e:
        if capture and e.stderr:
            error(f"Command failed: {cmd_str}")
            error(f"Output: {e.stderr[:500]}")
        raise


# =============================================================================
# VENV CREATION STRATEGIES
# =============================================================================

def try_venv_standard(python_exe: str) -> bool:
    """Strategy 1: Standard venv module."""
    log("Trying: python -m venv (standard)")
    try:
        run_command([python_exe, "-m", "venv", str(VENV_DIR)], capture=True)
        return True
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
        shutil.rmtree(VENV_DIR, ignore_errors=True)
        return False


def try_venv_without_pip(python_exe: str) -> bool:
    """Strategy 2: venv without pip, then bootstrap with get-pip.py."""
    log("Trying: python -m venv --without-pip + get-pip.py")
    try:
        run_command([python_exe, "-m", "venv", "--without-pip", str(VENV_DIR)], capture=True)
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
        shutil.rmtree(VENV_DIR, ignore_errors=True)
        return False
    
    # Get venv python path
    if os.name == "nt":
        venv_python = VENV_DIR / "Scripts" / "python.exe"
    else:
        venv_python = VENV_DIR / "bin" / "python"
    
    if not venv_python.exists():
        log("Warning: venv created but python not found inside")
        shutil.rmtree(VENV_DIR, ignore_errors=True)
        return False
    
    # Bootstrap pip with get-pip.py
    if not bootstrap_pip(venv_python):
        shutil.rmtree(VENV_DIR, ignore_errors=True)
        return False
    
    return True


def try_virtualenv(python_exe: str) -> bool:
    """Strategy 3: Use virtualenv if available."""
    virtualenv = shutil.which("virtualenv")
    if not virtualenv:
        return False
    
    log("Trying: virtualenv")
    try:
        run_command([virtualenv, "-p", python_exe, str(VENV_DIR)], capture=True)
        return True
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
        shutil.rmtree(VENV_DIR, ignore_errors=True)
        return False


def bootstrap_pip(venv_python: Path) -> bool:
    """Bootstrap pip using get-pip.py."""
    import tempfile
    import urllib.request
    
    log("Downloading get-pip.py...")
    get_pip_url = "https://bootstrap.pypa.io/get-pip.py"
    
    try:
        with tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False) as f:
            with urllib.request.urlopen(get_pip_url, timeout=60) as response:
                f.write(response.read())
            get_pip_path = f.name
        
        log("Installing pip via get-pip.py...")
        run_command([str(venv_python), get_pip_path], capture=True)
        
        os.unlink(get_pip_path)
        return True
        
    except Exception as e:
        log(f"Warning: get-pip.py failed: {e}")
        try:
            os.unlink(get_pip_path)
        except:
            pass
        return False


def create_venv(python_exe: str) -> bool:
    """Create virtual environment using fallback strategies.
    
    Returns True if venv was created, False if should use --user install.
    """
    global USE_VENV
    
    if VENV_DIR.exists():
        log(f"Using existing venv: {VENV_DIR}")
        return True

    log(f"Creating virtual environment...")
    
    # Strategy 1: Standard venv
    if try_venv_standard(python_exe):
        log("✓ Created venv (standard)")
        return True
    
    # Strategy 2: venv --without-pip + get-pip.py
    if try_venv_without_pip(python_exe):
        log("✓ Created venv (--without-pip + get-pip.py)")
        return True
    
    # Strategy 3: virtualenv
    if try_virtualenv(python_exe):
        log("✓ Created venv (virtualenv)")
        return True
    
    # All venv strategies failed - fall back to --user install
    log("")
    log("All venv methods failed. Falling back to pip install --user")
    log("(This installs to your user site-packages instead of a venv)")
    USE_VENV = False
    return False


def get_pip_command() -> list[str]:
    """Get the pip command to use based on install mode."""
    if USE_VENV:
        if os.name == "nt":
            venv_python = VENV_DIR / "Scripts" / "python.exe"
        else:
            venv_python = VENV_DIR / "bin" / "python"
        return [str(venv_python), "-m", "pip"]
    else:
        # Use system python with --user
        return [sys.executable, "-m", "pip"]


def install_dependencies() -> None:
    """Install dependencies from requirements.txt."""
    if not REQ_FILE.exists():
        error(f"requirements.txt not found at {REQ_FILE}")
        raise FileNotFoundError(str(REQ_FILE))

    pip_cmd = get_pip_command()
    
    # Set pip cache directory (use local to avoid permission issues)
    pip_cache = ROOT / ".pip-cache"
    env = {"PIP_CACHE_DIR": str(pip_cache)}

    # Upgrade pip first (ignore failure)
    log("Upgrading pip...")
    try:
        run_command(pip_cmd + ["install", "--upgrade", "pip"], capture=True, env=env)
    except:
        log("Warning: Could not upgrade pip, continuing...")

    # Install requirements
    log("Installing kaspa SDK...")
    install_cmd = pip_cmd + ["install", "-r", str(REQ_FILE)]
    
    if not USE_VENV:
        install_cmd.append("--user")
    
    # Add --no-cache-dir if low memory environment suspected
    # (can be forced via environment variable)
    if os.environ.get("KASPA_LOW_MEMORY"):
        install_cmd.append("--no-cache-dir")
        log("Using --no-cache-dir for low memory environment")

    try:
        run_command(install_cmd, env=env)
    except subprocess.CalledProcessError as e:
        error("Failed to install dependencies")
        info = get_python_info()
        error(f"Python: {info['version']} ({info['implementation']})")
        error(f"Platform: {info['platform']} {info['arch']}")
        error("")
        error("Common fixes:")
        error("  1. Check internet connection")
        error("  2. Try: KASPA_LOW_MEMORY=1 python3 install.py")
        error("  3. Try: pip install --user kaspa")
        error("  4. Check platform support: https://pypi.org/project/kaspa/")
        raise


def verify_installation() -> bool:
    """Verify kaspa SDK is properly installed."""
    log("Verifying installation...")

    test_script = """
import sys
try:
    import kaspa
    version = getattr(kaspa, '__version__', 'unknown')
    for name in ['PrivateKey', 'Address', 'RpcClient', 'Generator']:
        if not hasattr(kaspa, name):
            print(f"MISSING:{name}", file=sys.stderr)
            sys.exit(1)
    print(f"OK:{version}")
except ImportError as e:
    print(f"IMPORT_ERROR:{e}", file=sys.stderr)
    sys.exit(1)
except Exception as e:
    print(f"ERROR:{e}", file=sys.stderr)
    sys.exit(1)
"""

    # Determine which python to test with
    if USE_VENV:
        if os.name == "nt":
            test_python = str(VENV_DIR / "Scripts" / "python.exe")
        else:
            test_python = str(VENV_DIR / "bin" / "python")
    else:
        test_python = sys.executable

    try:
        result = run_command([test_python, "-c", test_script], capture=True)
        if result.stdout.startswith("OK:"):
            version = result.stdout.split(":")[1].strip()
            log(f"✓ Kaspa SDK version: {version}")
            return True
    except subprocess.CalledProcessError as e:
        stderr = e.stderr or ""
        if "IMPORT_ERROR" in stderr:
            error("Kaspa SDK import failed")
            if not USE_VENV:
                error("Try: python3 -c 'import kaspa' to debug")
        elif "MISSING" in stderr:
            error("Kaspa SDK is incomplete")
            error("Try: rm -rf .venv && python3 install.py")
        else:
            error(f"Verification failed: {stderr}")
        return False

    return False


def print_success() -> None:
    """Print success message with usage instructions."""
    log("")
    log("=" * 50)
    log("✓ Installation complete!")
    log("=" * 50)
    log("")
    log("Quick test:")
    log("  ./kaswallet.sh help")
    log("")
    log("Set wallet credentials:")
    log("  export KASPA_PRIVATE_KEY='your-64-char-hex-key'")
    log("  # or")
    log("  export KASPA_MNEMONIC='your 12-24 word phrase'")
    log("")
    log("Then check balance:")
    log("  ./kaswallet.sh balance")
    
    if not USE_VENV:
        log("")
        log("Note: Installed to user site-packages (no venv)")
        log("      kaspa module available system-wide for this user")


def main() -> int:
    init_log()
    
    log("Kaspa Wallet Installer")
    log("=" * 50)

    # Check Python version
    if not check_python_version():
        return 1

    python_exe = find_best_python()
    info = get_python_info()
    log(f"Python: {info['version']} on {info['platform']} ({info['arch']})")
    log(f"Executable: {python_exe}")

    try:
        # Create venv (or fall back to --user mode)
        create_venv(python_exe)

        # Verify venv python exists (if using venv)
        if USE_VENV:
            if os.name == "nt":
                venv_python = VENV_DIR / "Scripts" / "python.exe"
            else:
                venv_python = VENV_DIR / "bin" / "python"
            
            if not venv_python.exists():
                error(f"Venv Python not found at {venv_python}")
                error("Try: rm -rf .venv && python3 install.py")
                return 1

        # Install dependencies
        install_dependencies()

        # Verify installation
        if not verify_installation():
            return 1

        print_success()
        return 0

    except KeyboardInterrupt:
        error("Installation cancelled by user")
        return 130
    except Exception as e:
        error(f"Installation failed: {e}")
        error("")
        error(f"See {LOG_FILE} for details")
        return 1


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/kaswallet.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import asyncio
import inspect
import json
import os
import re
import sys
from typing import Any, Awaitable, Optional


NETWORK = os.environ.get("KASPA_NETWORK", "mainnet")
MNEMONIC = os.environ.get("KASPA_MNEMONIC")
PRIVATE_KEY = os.environ.get("KASPA_PRIVATE_KEY")
RPC_URL = os.environ.get("KASPA_RPC_URL")
RPC_CONNECT_TIMEOUT_MS = int(os.environ.get("KASPA_RPC_CONNECT_TIMEOUT_MS", "15000"))


def _json(obj: Any) -> None:
    def default(v: Any) -> Any:
        if isinstance(v, (bytes, bytearray)):
            return v.hex()
        try:
            import decimal

            if isinstance(v, decimal.Decimal):
                return str(v)
        except Exception:
            pass
        return str(v)

    print(json.dumps(obj, default=default, indent=2))

def get_any(obj: Any, *names: str, default: Any = None) -> Any:
    for name in names:
        if obj is None:
            continue
        if isinstance(obj, dict):
            if name in obj:
                return obj[name]
        else:
            if hasattr(obj, name):
                return getattr(obj, name)
    return default


def sompi_to_kas(sompi: int) -> str:
    return ("%0.8f" % (sompi / 1e8)).rstrip("0").rstrip(".")


def kas_to_sompi(kas: str) -> int:
    return int(round(float(kas) * 1e8))

_ADDRESS_WITH_PREFIX_RE = re.compile(r"(kaspa(?:test|dev|sim)?):[0-9a-z]+", re.IGNORECASE)
_ADDRESS_PAYLOAD_RE = re.compile(r"\b[0-9a-z]{20,}\b", re.IGNORECASE)


def _sdk_network_id() -> str:
    n = (NETWORK or "").lower()
    if n == "testnet-10":
        return "testnet"
    return n


def _network_address_prefix() -> str:
    n = (NETWORK or "").lower()
    if n.startswith("testnet"):
        return "kaspatest"
    if n.startswith("devnet"):
        return "kaspadev"
    if n.startswith("simnet"):
        return "kaspasim"
    return "kaspa"


def normalize_address(value: str) -> str:
    s = (value or "").strip()
    if not s:
        return s
    if "?" in s:
        s = s.split("?", 1)[0]
    s = s.strip()
    if ":" not in s:
        s = f"{_network_address_prefix()}:{s}"
    return s


def sanitize_address_input(value: Any) -> str:
    """
    The Python `kaspa` binding sometimes renders address objects as debug-y strings
    like `<Address ...>`; extract the real address and ensure it has the correct prefix.
    """
    # First, try to get string from Address objects using to_string() method
    if not isinstance(value, str) and value is not None:
        for name in ("to_string", "toString", "as_string", "asString"):
            fn = getattr(value, name, None)
            if fn:
                try:
                    s = fn()
                    if isinstance(s, str) and s and not s.startswith("<"):
                        return normalize_address(s.strip())
                except Exception:
                    pass

    s = (value or "").strip() if isinstance(value, str) else str(value).strip()

    # First, try to extract a full prefixed address.
    m = _ADDRESS_WITH_PREFIX_RE.search(s)
    if m:
        return normalize_address(m.group(0))

    # Next, try to extract a payload-only address.
    m = _ADDRESS_PAYLOAD_RE.search(s)
    if m:
        return normalize_address(m.group(0))

    # Last resort: strip common wrappers and normalize.
    s = s.strip("<> \t\r\n")
    return normalize_address(s)


def make_address(kaspa: Any, value: str) -> Any:
    Address = getattr(kaspa, "Address", None)
    if not Address:
        raise RuntimeError("kaspa SDK missing Address")

    s = sanitize_address_input(value)

    for name in ("try_from", "tryFrom", "from_string", "fromString", "parse"):
        fn = getattr(Address, name, None)
        if fn:
            return fn(s)

    try:
        return Address(s)
    except Exception as e:
        # Some bindings may expect payload without the `kaspa:` prefix. Try that too.
        if ":" in s:
            payload = s.split(":", 1)[1]
            try:
                return Address(payload)
            except Exception:
                pass
        raise e


def make_payment_output(kaspa: Any, to: str, amount: int) -> Any:
    PaymentOutput = getattr(kaspa, "PaymentOutput", None)
    if not PaymentOutput:
        raise RuntimeError("kaspa SDK missing PaymentOutput")

    addr_str = sanitize_address_input(to)
    try:
        dest = make_address(kaspa, addr_str)
        return PaymentOutput(dest, amount)
    except Exception:
        # Fallback: try passing raw address string if supported by the binding
        return PaymentOutput(addr_str, amount)


async def maybe_await(value: Any) -> Any:
    if inspect.isawaitable(value):
        return await value
    return value


async def _with_timeout(awaitable: Awaitable[Any], label: str) -> Any:
    if RPC_CONNECT_TIMEOUT_MS <= 0:
        return await awaitable
    try:
        return await asyncio.wait_for(awaitable, timeout=RPC_CONNECT_TIMEOUT_MS / 1000)
    except asyncio.TimeoutError as e:
        raise RuntimeError(f"{label} timed out after {RPC_CONNECT_TIMEOUT_MS}ms") from e


def _require_wallet_secret() -> None:
    if not MNEMONIC and not PRIVATE_KEY:
        raise RuntimeError(
            "No wallet credentials. Set one of:\n"
            "  export KASPA_PRIVATE_KEY='your-64-char-hex-key'\n"
            "  export KASPA_MNEMONIC='your 12 or 24 word seed phrase'"
        )


def load_kaspa():
    try:
        import kaspa  # type: ignore

        return kaspa
    except Exception as e:
        raise RuntimeError("Missing Python dependency 'kaspa'. Run: python3 install.py") from e


def address_to_string(kaspa: Any, addr_obj: Any) -> str:
    for name in ("to_string", "toString", "as_string", "asString"):
        fn = getattr(addr_obj, name, None)
        if fn:
            try:
                return sanitize_address_input(fn())
            except Exception:
                pass
    return sanitize_address_input(addr_obj)


async def rpc_client():
    kaspa = load_kaspa()
    Resolver = getattr(kaspa, "Resolver", None)
    RpcClient = getattr(kaspa, "RpcClient", None)
    if not Resolver or not RpcClient:
        raise RuntimeError("kaspa SDK missing Resolver/RpcClient")

    if RPC_URL:
        client = RpcClient(url=RPC_URL, network_id=NETWORK)
    else:
        client = RpcClient(resolver=Resolver(), network_id=NETWORK)

    await _with_timeout(client.connect(), "RPC connect")
    return client


async def wallet():
    _require_wallet_secret()
    kaspa = load_kaspa()

    def call_any(obj: Any, names: list[str], *args: Any) -> Any:
        for name in names:
            fn = getattr(obj, name, None)
            if fn:
                return fn(*args)
        raise AttributeError(f"Missing method(s): {', '.join(names)}")

    if MNEMONIC:
        Mnemonic = getattr(kaspa, "Mnemonic", None)
        XPrv = getattr(kaspa, "XPrv", None)
        PrivateKeyGenerator = getattr(kaspa, "PrivateKeyGenerator", None)
        if not (Mnemonic and XPrv and PrivateKeyGenerator):
            raise RuntimeError("kaspa SDK missing Mnemonic/XPrv/PrivateKeyGenerator")

        m = Mnemonic(MNEMONIC)
        seed = call_any(m, ["to_seed", "toSeed"])
        x = XPrv(seed)
        x_str = call_any(x, ["to_string", "toString"])
        gen = PrivateKeyGenerator(x_str, False, 0)
        key = call_any(gen, ["receive_key", "receiveKey"], 0)
    else:
        PrivateKey = getattr(kaspa, "PrivateKey", None)
        if not PrivateKey:
            raise RuntimeError("kaspa SDK missing PrivateKey")
        key = PrivateKey(PRIVATE_KEY)

    pub = call_any(key, ["to_public_key", "toPublicKey"])
    addr = call_any(pub, ["to_address", "toAddress"], _sdk_network_id())
    return key, addr


def _pick_feerate(estimate: Any, tier: str) -> tuple[float, float]:
    # estimate: { priority_bucket, normal_buckets, low_buckets } (names vary slightly)
    if not estimate:
        return 1.0, 0.0

    priority = getattr(estimate, "priority_bucket", None) or estimate.get("priority_bucket") or estimate.get("priorityBucket")
    normal = getattr(estimate, "normal_buckets", None) or estimate.get("normal_buckets") or estimate.get("normalBuckets")
    low = getattr(estimate, "low_buckets", None) or estimate.get("low_buckets") or estimate.get("lowBuckets")

    def bucket_value(b: Any) -> tuple[float, float]:
        if b is None:
            return 1.0, 0.0
        feerate = getattr(b, "feerate", None)
        secs = getattr(b, "estimated_seconds", None)
        if feerate is None and isinstance(b, dict):
            feerate = b.get("feerate")
        if secs is None and isinstance(b, dict):
            secs = b.get("estimated_seconds") or b.get("estimatedSeconds")
        return float(feerate or 1.0), float(secs or 0.0)

    if tier == "priority":
        return bucket_value(priority)
    if tier == "economic":
        if normal and len(normal) > 0:
            return bucket_value(normal[0])
        return bucket_value(priority)
    if tier == "low":
        if low and len(low) > 0:
            return bucket_value(low[0])
        if normal and len(normal) > 0:
            return bucket_value(normal[0])
        return bucket_value(priority)

    return 1.0, 0.0


async def cmd_info(_: argparse.Namespace) -> None:
    c = await rpc_client()
    try:
        dag = await c.get_block_dag_info()
        srv = await c.get_server_info()
        blocks = get_any(dag, "block_count", "blockCount")
        synced = get_any(srv, "is_synced", "isSynced")
        version = get_any(srv, "server_version", "serverVersion")
        _json({"network": NETWORK, "url": getattr(c, "url", None), "blocks": blocks, "synced": synced, "version": version})
    finally:
        await maybe_await(c.disconnect())


async def cmd_balance(args: argparse.Namespace) -> None:
    c = await rpc_client()
    try:
        kaspa = load_kaspa()
        address = args.address
        if not address:
            _, addr = await wallet()
            address = address_to_string(kaspa, addr)
        address = sanitize_address_input(address)
        r = await c.get_balance_by_address({"address": address})
        balance = int(get_any(r, "balance", default=0))
        _json({"address": address, "balance": sompi_to_kas(balance), "sompi": str(balance), "network": NETWORK})
    finally:
        await maybe_await(c.disconnect())


async def cmd_fees(_: argparse.Namespace) -> None:
    c = await rpc_client()
    try:
        resp = await c.get_fee_estimate()
        estimate = resp.estimate if hasattr(resp, "estimate") else resp.get("estimate")
        tiers = {}
        for t in ("low", "economic", "priority"):
            feerate, secs = _pick_feerate(estimate, t)
            tiers[t] = {"feerate": feerate, "estimatedSeconds": secs}
        _json({"network": NETWORK, **tiers})
    finally:
        await maybe_await(c.disconnect())


async def _get_utxos(c: Any, address: str) -> list[Any]:
    resp = await c.get_utxos_by_addresses({"addresses": [address]})
    # The response can be an object with .entries or a dict with "entries"
    entries = getattr(resp, "entries", None)
    if entries is None and isinstance(resp, dict):
        entries = resp.get("entries")
    return list(entries or [])


def _create_generator(kaspa: Any, *, entries: list[Any], change_address: Any, outputs: list[Any], priority_fee: int = 0) -> Any:
    """Create a Generator instance for transaction building/estimation."""
    Generator = getattr(kaspa, "Generator", None)
    if not Generator:
        raise RuntimeError("kaspa SDK missing Generator")

    return Generator(
        network_id=NETWORK,
        entries=entries,
        change_address=change_address,
        outputs=outputs,
        priority_fee=priority_fee,
    )


async def _estimate_max_amount(kaspa: Any, *, utxos: list[Any], to: str, balance: int, change_address: Any, fee_rate: float, priority_fee: int) -> dict[str, Any]:
    """Estimate the maximum sendable amount after fees."""

    def estimate_for(amount: int) -> dict[str, Any]:
        outputs = [make_payment_output(kaspa, to, amount)]
        gen = _create_generator(kaspa, entries=utxos, change_address=change_address, outputs=outputs, priority_fee=priority_fee)
        summary = gen.estimate()
        return {"fees": int(summary.fees), "mass": int(summary.mass), "transactions": int(summary.transactions)}

    # Probe with minimal amount to get base fee
    probe = estimate_for(1)
    fees = probe["fees"]
    amount = balance - fees
    if amount <= 0:
        raise RuntimeError("Insufficient funds (balance does not cover transaction fees)")

    # Iterate to converge on final amount
    for _ in range(3):
        est = estimate_for(amount)
        new_fees = est["fees"]
        new_amount = balance - new_fees
        if new_amount == amount:
            return {"amount": amount, "fees": new_fees, "mass": est["mass"], "transactions": est["transactions"]}
        amount, fees = new_amount, new_fees
        if amount <= 0:
            raise RuntimeError("Insufficient funds (balance does not cover transaction fees)")

    est = estimate_for(amount)
    return {"amount": amount, "fees": est["fees"], "mass": est["mass"], "transactions": est["transactions"]}


async def _estimate_for_amount(kaspa: Any, *, utxos: list[Any], to: str, amount: int, change_address: Any, fee_rate: float, priority_fee: int) -> Any:
    """Estimate fees for a specific amount."""
    outputs = [make_payment_output(kaspa, to, amount)]
    gen = _create_generator(kaspa, entries=utxos, change_address=change_address, outputs=outputs, priority_fee=priority_fee)
    return gen.estimate()


async def cmd_estimate(args: argparse.Namespace) -> None:
    kaspa = load_kaspa()
    _, addr = await wallet()
    c = await rpc_client()
    try:
        from_addr = address_to_string(kaspa, addr)
        utxos = await _get_utxos(c, from_addr)
        if not utxos:
            raise RuntimeError("No UTXOs")

        bal_resp = await c.get_balance_by_address({"address": from_addr})
        bal = int(get_any(bal_resp, "balance", default=0))
        priority_fee = kas_to_sompi(args.priority_fee) if args.priority_fee else 0
        resp = await c.get_fee_estimate()
        estimate = resp.estimate if hasattr(resp, "estimate") else resp.get("estimate")

        tiers = [args.tier] if args.tier else ["low", "economic", "priority"]
        results = []
        to_addr = sanitize_address_input(args.to)
        for t in tiers:
            feerate, secs = _pick_feerate(estimate, t)
            if args.amount == "max":
                max_info = await _estimate_max_amount(
                    kaspa,
                    utxos=utxos,
                    to=to_addr,
                    balance=bal,
                    change_address=addr,
                    fee_rate=feerate,
                    priority_fee=priority_fee,
                )
                results.append(
                    {
                        "tier": t,
                        "feerate": feerate,
                        "estimatedSeconds": secs,
                        "mass": str(max_info["mass"]),
                        "feesSompi": str(max_info["fees"]),
                        "feesKas": sompi_to_kas(max_info["fees"]),
                        "sendAmountKas": sompi_to_kas(max_info["amount"]),
                    }
                )
            else:
                amount = kas_to_sompi(args.amount)
                est = await _estimate_for_amount(
                    kaspa,
                    utxos=utxos,
                    to=to_addr,
                    amount=amount,
                    change_address=addr,
                    fee_rate=feerate,
                    priority_fee=priority_fee,
                )
                results.append(
                    {
                        "tier": t,
                        "feerate": feerate,
                        "estimatedSeconds": secs,
                        "mass": str(int(est.mass)),
                        "feesSompi": str(int(est.fees)),
                        "feesKas": sompi_to_kas(int(est.fees)),
                        "sendAmountKas": args.amount,
                    }
                )

        _json(
            {
                "network": NETWORK,
                "from": from_addr,
                "to": to_addr,
                "balanceKas": sompi_to_kas(bal),
                "amountArg": args.amount,
                "priorityFeeKas": sompi_to_kas(priority_fee),
                "estimates": results,
            }
        )
    finally:
        await maybe_await(c.disconnect())


async def cmd_send(args: argparse.Namespace) -> None:
    kaspa = load_kaspa()
    key, addr = await wallet()
    c = await rpc_client()
    try:
        from_addr = address_to_string(kaspa, addr)
        utxos = await _get_utxos(c, from_addr)
        if not utxos:
            raise RuntimeError("No UTXOs")

        bal_resp = await c.get_balance_by_address({"address": from_addr})
        bal = int(get_any(bal_resp, "balance", default=0))
        priority_fee = kas_to_sompi(args.priority_fee) if args.priority_fee else 0

        fee_rate = 1.0
        tier = args.tier or "economic"
        try:
            resp = await c.get_fee_estimate()
            estimate = resp.estimate if hasattr(resp, "estimate") else resp.get("estimate")
            fee_rate, _ = _pick_feerate(estimate, tier)
        except Exception:
            fee_rate = 1.0

        to_addr = sanitize_address_input(args.to)

        if args.amount == "max":
            max_info = await _estimate_max_amount(
                kaspa,
                utxos=utxos,
                to=to_addr,
                balance=bal,
                change_address=addr,
                fee_rate=fee_rate,
                priority_fee=priority_fee,
            )
            amount = int(max_info["amount"])
        else:
            amount = kas_to_sompi(args.amount)

        if amount <= 0:
            raise RuntimeError(f"Insufficient balance: {sompi_to_kas(bal)} KAS")

        # Create outputs and generator
        outputs = [make_payment_output(kaspa, to_addr, amount)]
        gen = _create_generator(kaspa, entries=utxos, change_address=addr, outputs=outputs, priority_fee=priority_fee)

        # Process transactions from generator
        txids = []
        total_fees = 0
        for pending_tx in gen:
            # Sign and submit
            pending_tx.sign([key])
            txid = await maybe_await(pending_tx.submit(c))
            txids.append(str(txid))
            total_fees += int(pending_tx.fee_amount) if hasattr(pending_tx, "fee_amount") else 0

        # Get summary if available
        summary = gen.summary() if hasattr(gen, "summary") else None
        fees = int(summary.fees) if summary and hasattr(summary, "fees") else total_fees

        _json(
            {
                "status": "sent",
                "txid": txids[0] if txids else None,
                "from": from_addr,
                "to": to_addr,
                "amount": sompi_to_kas(amount),
                "tier": tier,
                "feeRate": fee_rate,
                "priorityFee": sompi_to_kas(priority_fee),
                "fee": sompi_to_kas(fees) if fees else None,
            }
        )
    finally:
        await maybe_await(c.disconnect())


async def cmd_uri(args: argparse.Namespace) -> None:
    address = args.address
    if not address:
        _, addr = await wallet()
        address = str(addr)

    qs = []
    if args.amount:
        qs.append(f"amount={args.amount}")
    if args.message:
        qs.append(f"message={args.message}")
    suffix = ("?" + "&".join(qs)) if qs else ""
    _json({"uri": f"{normalize_address(address)}{suffix}"})


async def cmd_generate_mnemonic(_: argparse.Namespace) -> None:
    kaspa = load_kaspa()
    Mnemonic = getattr(kaspa, "Mnemonic", None)
    if not Mnemonic:
        raise RuntimeError("kaspa SDK missing Mnemonic")
    m = Mnemonic.random()
    phrase = getattr(m, "phrase", None) or (m.get("phrase") if isinstance(m, dict) else None) or str(m)
    _json({"mnemonic": phrase})


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="kaswallet", add_help=False)
    sub = p.add_subparsers(dest="cmd", required=False)

    help_p = sub.add_parser("help")

    info_p = sub.add_parser("info")
    info_p.set_defaults(func=cmd_info)

    bal_p = sub.add_parser("balance")
    bal_p.add_argument("address", nargs="?")
    bal_p.set_defaults(func=cmd_balance)

    fees_p = sub.add_parser("fees")
    fees_p.set_defaults(func=cmd_fees)

    est_p = sub.add_parser("estimate")
    est_p.add_argument("to")
    est_p.add_argument("amount", help="KAS amount or 'max'")
    est_p.add_argument("tier", nargs="?", choices=["low", "economic", "priority"])
    est_p.add_argument("priority_fee", nargs="?", help="Optional priority fee (KAS)")
    est_p.set_defaults(func=cmd_estimate)

    send_p = sub.add_parser("send")
    send_p.add_argument("to")
    send_p.add_argument("amount", help="KAS amount or 'max'")
    send_p.add_argument("tier", nargs="?", choices=["low", "economic", "priority"])
    send_p.add_argument("priority_fee", nargs="?", help="Optional priority fee (KAS)")
    send_p.set_defaults(func=cmd_send)

    uri_p = sub.add_parser("uri")
    uri_p.add_argument("address", nargs="?")
    uri_p.add_argument("amount", nargs="?")
    uri_p.add_argument("message", nargs="?")
    uri_p.set_defaults(func=cmd_uri)

    gen_p = sub.add_parser("generate-mnemonic")
    gen_p.set_defaults(func=cmd_generate_mnemonic)

    p.add_argument("-h", "--help", action="store_true")
    return p


def print_help() -> None:
    print(
        """Kaspa Wallet CLI (PyPI kaspa SDK)

Commands:
  balance [address]                    Check balance (uses wallet if no address)
  fees                                 Show fee tiers from node
  info                                 Network info
  estimate <to> <amount|max> [tier]    Estimate fees (defaults: low/economic/priority)
  send <to> <amount|max> [tier]        Send KAS (tier: low|economic|priority; optional priorityFee as last arg)
  uri [address] [amount] [msg]         Generate payment URI
  generate-mnemonic                    Generate new 24-word mnemonic

Environment:
  KASPA_MNEMONIC      Wallet seed phrase
  KASPA_PRIVATE_KEY   Or private key hex
  KASPA_NETWORK       mainnet (default), testnet-10
  KASPA_RPC_URL       Optional direct wRPC url (ws:// or wss://)
  KASPA_RPC_CONNECT_TIMEOUT_MS  RPC connect timeout (ms)
"""
    )


def _format_error(e: Exception) -> dict[str, Any]:
    """Format error with helpful context for agents."""
    msg = str(e)
    result: dict[str, Any] = {"error": msg}

    # Add actionable hints for common errors
    if "Storage mass exceeds maximum" in msg:
        result["errorCode"] = "STORAGE_MASS_EXCEEDED"
        result["hint"] = (
            "Amount too small relative to UTXOs. Fix: send 'max' to your own address first to consolidate UTXOs, then retry."
        )
        result["action"] = "consolidate_utxos"
    elif "No UTXOs" in msg:
        result["errorCode"] = "NO_UTXOS"
        result["hint"] = "Wallet has no spendable outputs. Either empty or funds are unconfirmed."
    elif "Insufficient" in msg:
        result["errorCode"] = "INSUFFICIENT_FUNDS"
        result["hint"] = "Not enough balance for this transaction including fees."
    elif "timed out" in msg.lower():
        result["errorCode"] = "RPC_TIMEOUT"
        result["hint"] = "Network connection slow. Retry or set KASPA_RPC_CONNECT_TIMEOUT_MS=60000"
    elif "KASPA_MNEMONIC" in msg or "KASPA_PRIVATE_KEY" in msg:
        result["errorCode"] = "NO_CREDENTIALS"
        result["hint"] = "Set KASPA_PRIVATE_KEY or KASPA_MNEMONIC environment variable."
    elif "kaspa" in msg.lower() and ("import" in msg.lower() or "missing" in msg.lower()):
        result["errorCode"] = "SDK_NOT_INSTALLED"
        result["hint"] = "Run: python3 install.py"

    return result


async def main_async(argv: list[str]) -> int:
    parser = build_parser()
    ns, _ = parser.parse_known_args(argv)

    if ns.help or ns.cmd in (None, "help"):
        print_help()
        return 0

    func = getattr(ns, "func", None)
    if not func:
        print_help()
        return 1

    try:
        await func(ns)
        return 0
    except Exception as e:
        _json(_format_error(e))
        return 1


def main() -> int:
    return asyncio.run(main_async(sys.argv[1:]))


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)