diff --git a/devenv.nix b/devenv.nix index b9e5faf..f8523fd 100644 --- a/devenv.nix +++ b/devenv.nix @@ -1,8 +1,7 @@ { pkgs, lib, config, ... }: { - # Project metadata - name = "bitspire-next"; + name = "bitspire"; # ============================================ # Languages @@ -19,9 +18,11 @@ languages.typescript.enable = true; - # Use nixpkgs rust (simpler, no overlay needed) packages = with pkgs; [ - # Rust toolchain from nixpkgs + # Rust toolchain — only the orphaned Rust HAL under packages/hal/src + # uses it (not built; see CLAUDE.md → Hardware drivers). Kept so the + # rustfmt git hook and `cargo check` still work until that crate is + # removed outright. rustc cargo clippy @@ -33,42 +34,22 @@ openssl openssl.dev - # Electron dependencies (Linux) - # electron # Use npm-installed electron instead - - # Python for node-gyp (native module builds) + # Python for node-gyp (native module builds: serialport, better-sqlite3) (python3.withPackages (ps: [ ps.setuptools ])) # Hardware access libusb1 udev - # Serial port access + # Serial port access (talk to a validator / dispenser by hand) picocom # Development utilities - just # Task runner jq # JSON processing - websocat # WebSocket client + websocat # WebSocket client — relay-test below qrencode # QR code generation for terminal - - # Database tools - pgcli - - # Container tools (for Lightning.Pub, strfry) - docker-compose ]; - # ============================================ - # Services - # ============================================ - - services.postgres = { - enable = true; - initialDatabases = [{ name = "bitspire_dev"; }]; - listen_addresses = "127.0.0.1"; - }; - # ============================================ # Git Hooks (formerly pre-commit) # ============================================ @@ -97,21 +78,24 @@ env = { RUST_BACKTRACE = "1"; - DATABASE_URL = "postgresql://localhost/bitspire_dev"; - # Nostr development relay - NOSTR_RELAY_URL = "ws://localhost:7777"; + # The dev relay is LNbits's bundled nostrrelay extension on the native + # instance (see CLAUDE.md → Environment variables); there is no + # in-repo relay container any more. Override per shell as needed. + NOSTR_RELAY_URL = "ws://localhost:5001/nostrrelay/test"; - # Lightning.Pub development - LIGHTNING_PUB_URL = "http://localhost:1776"; - - # For Tauri PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; }; # ============================================ # Scripts (available as commands in shell) # ============================================ + # + # The Lightning.Pub-era regtest stack (docker/ + the infra-*/lncli/btccli/ + # mine-blocks/setup-channel/fund-atm/test-* commands that drove it) was + # removed 2026-10-09. Development runs against LNbits: FakeWallet needs + # nothing; bohm's native instance answers on :5001; the shared regtest + # stack lives at ~/dev/local/docker/regtest (not in this repo). scripts = { dev.exec = "pnpm turbo dev"; @@ -119,418 +103,13 @@ test.exec = "pnpm turbo test"; lint.exec = "pnpm turbo lint"; - # Start infrastructure - infra-up.exec = '' - echo "Starting development infrastructure..." - docker compose -f docker/docker-compose.dev.yml up -d - echo "" - echo "Waiting for services to be healthy..." - echo "(This may take 30-60 seconds on first run)" - echo "" - - # Wait for strfry - echo -n "strfry relay: " - until docker exec bitspire-relay nc -z localhost 7777 2>/dev/null; do - echo -n "." - sleep 2 - done - echo " ready" - - # Wait for bitcoind - echo -n "bitcoind: " - until docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire getblockchaininfo >/dev/null 2>&1; do # pragma: allowlist secret - echo -n "." - sleep 2 - done - echo " ready" - - # Wait for LND - echo -n "LND: " - until docker exec bitspire-lnd lncli --network=regtest getinfo >/dev/null 2>&1; do - echo -n "." - sleep 3 - done - echo " ready" - - # Wait for Alice's LND - echo -n "LND (Alice): " - until docker exec bitspire-lnd-alice lncli --network=regtest getinfo >/dev/null 2>&1; do - echo -n "." - sleep 3 - done - echo " ready" - - echo "" - echo "Infrastructure ready!" - echo " - Nostr relay: ws://localhost:7777" - echo " - Bitcoin RPC: localhost:18443 (regtest)" - echo " - LND gRPC: localhost:10009" - echo " - LND Alice gRPC: localhost:10010" - echo " - Lightning.Pub: http://localhost:1776" - echo " - PostgreSQL: localhost:5432" - echo "" - echo "Next steps:" - echo " 1. mine-blocks 101 # Fund the wallet (if first run)" - echo " 2. setup-channel # Open channel between Alice and LND" - echo "" - echo "Use 'infra-logs' to view logs, 'infra-status' to check status" - ''; - - infra-down.exec = '' - echo "Stopping development infrastructure..." - docker compose -f docker/docker-compose.dev.yml down - echo "Infrastructure stopped" - ''; - - infra-logs.exec = '' - docker compose -f docker/docker-compose.dev.yml logs -f "$@" - ''; - - infra-status.exec = '' - echo "Infrastructure Status:" - echo "" - docker compose -f docker/docker-compose.dev.yml ps - ''; - - # Mine regtest blocks (useful for testing) - mine-blocks.exec = '' - BLOCKS=''${1:-1} - echo "Mining $BLOCKS regtest block(s)..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate "$BLOCKS" # pragma: allowlist secret - ''; - - # Start auto-miner (mines 1 block every 30 seconds) - auto-mine.exec = '' - echo "Starting auto-miner (1 block every 30 seconds)..." - docker compose -f docker/docker-compose.dev.yml --profile mining up -d miner - echo "" - echo "Auto-miner started. This keeps LND in sync." - echo "Use 'auto-mine-stop' to stop it." - ''; - - # Stop auto-miner - auto-mine-stop.exec = '' - echo "Stopping auto-miner..." - docker stop bitspire-miner 2>/dev/null || true - docker rm bitspire-miner 2>/dev/null || true - echo "Auto-miner stopped." - ''; - - # Connect to LND CLI - lncli.exec = '' - docker exec -it bitspire-lnd lncli --network=regtest "$@" - ''; - - # Connect to Bitcoin CLI - btccli.exec = '' - docker exec -it bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire "$@" # pragma: allowlist secret - ''; - - # Test relay connection + # Test the dev relay connection relay-test.exec = '' - echo "Testing Nostr relay connection..." - echo '["REQ", "test", {"kinds": [0], "limit": 1}]' | websocat ws://localhost:7777 - ''; - - # Connect to Alice's LND CLI (second node for testing payments) - lncli-alice.exec = '' - docker exec -it bitspire-lnd-alice lncli --network=regtest "$@" - ''; - - # Setup Lightning channel between Alice and the main LND node - setup-channel.exec = '' - echo "Setting up Lightning channel between Alice and LND..." - echo "" - - # Get LND's pubkey and address - LND_INFO=$(docker exec bitspire-lnd lncli --network=regtest getinfo 2>/dev/null) - LND_PUBKEY=$(echo "$LND_INFO" | jq -r '.identity_pubkey') - echo "LND pubkey: $LND_PUBKEY" - - # Connect Alice to LND - echo "Connecting Alice to LND..." - docker exec bitspire-lnd-alice lncli --network=regtest connect "$LND_PUBKEY@lnd:9735" 2>/dev/null || true - - # Check if Alice has enough funds - ALICE_BALANCE=$(docker exec bitspire-lnd-alice lncli --network=regtest walletbalance 2>/dev/null | jq -r '.confirmed_balance') - echo "Alice's on-chain balance: $ALICE_BALANCE sats" - - if [ "$ALICE_BALANCE" -lt 1000000 ]; then - echo "" - echo "Alice needs funds. Getting new address..." - ALICE_ADDR=$(docker exec bitspire-lnd-alice lncli --network=regtest newaddress p2wkh | jq -r '.address') - echo "Alice's address: $ALICE_ADDR" - echo "" - echo "Sending 5 BTC to Alice..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire sendtoaddress "$ALICE_ADDR" 5 # pragma: allowlist secret - echo "Mining 6 blocks for confirmation..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate 6 >/dev/null # pragma: allowlist secret - sleep 2 - ALICE_BALANCE=$(docker exec bitspire-lnd-alice lncli --network=regtest walletbalance 2>/dev/null | jq -r '.confirmed_balance') - echo "Alice's new balance: $ALICE_BALANCE sats" - fi - - # Open channel from Alice to LND (1M sats) - echo "" - echo "Opening 1M sat channel from Alice to LND..." - docker exec bitspire-lnd-alice lncli --network=regtest openchannel --node_key="$LND_PUBKEY" --local_amt=1000000 - - echo "" - echo "Mining 6 blocks to confirm channel..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate 6 >/dev/null # pragma: allowlist secret - - sleep 3 - echo "" - echo "Channel status:" - docker exec bitspire-lnd-alice lncli --network=regtest listchannels | jq '.channels[] | {remote_pubkey, capacity, local_balance, remote_balance, active}' - echo "" - echo "Channel setup complete! Alice can now pay invoices to Lightning.Pub." - ''; - - # Pay an invoice from Alice's node - alice-pay.exec = '' - if [ -z "$1" ]; then - echo "Usage: alice-pay " - exit 1 - fi - echo "Paying invoice from Alice's node..." - docker exec bitspire-lnd-alice lncli --network=regtest payinvoice --force "$1" - ''; - - # Create invoice on Alice's node (for testing ATM payouts) - alice-invoice.exec = '' - AMOUNT=''${1:-1000} - MEMO=''${2:-"Test invoice"} - docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt="$AMOUNT" --memo="$MEMO" | jq -r '.payment_request' - ''; - - # Fund ATM account in Lightning.Pub - fund-atm.exec = '' - AMOUNT=''${1:-100000} - echo "Funding ATM account with $AMOUNT sats..." - echo "" - - # Run the funding script from nostr-client package - cd packages/nostr-client - FUND_AMOUNT=$AMOUNT node fund-dev.mjs 2>&1 | tee /tmp/fund-atm-output.txt - - # Extract the invoice - INVOICE=$(grep -o 'lnbcrt[a-zA-Z0-9]*' /tmp/fund-atm-output.txt | head -1) - - if [ -z "$INVOICE" ]; then - echo "Failed to create invoice. Check the output above." - exit 1 - fi - - echo "" - echo "Invoice created. Paying from Alice..." - docker exec bitspire-lnd-alice lncli --network=regtest payinvoice --force "$INVOICE" - - echo "" - echo "ATM account funded with $AMOUNT sats!" - ''; - - # Validate entire test setup - test-setup.exec = '' - echo "" - echo "═══════════════════════════════════════════════════════════" - echo " bitSpire - Test Setup Validation" - echo "═══════════════════════════════════════════════════════════" - echo "" - - # Mine a block to wake up LND sync (regtest quirk: LND reports - # "not synced" when no blocks mined recently) - echo "Mining block to sync nodes..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate 1 >/dev/null 2>&1 # pragma: allowlist secret - sleep 1 - echo "" - - PASSED=0 - FAILED=0 - - check() { - if [ $1 -eq 0 ]; then - echo " ✓ $2" - PASSED=$((PASSED + 1)) - else - echo " ✗ $2" - FAILED=$((FAILED + 1)) - fi - } - - echo "1. Services" - echo "───────────────────────────────────────────────────────────" - - # Check containers - docker ps --format '{{.Names}}' | grep -q bitspire-relay - check $? "strfry relay running" - - docker ps --format '{{.Names}}' | grep -q bitspire-bitcoind - check $? "bitcoind running" - - docker ps --format '{{.Names}}' | grep -q bitspire-lnd - check $? "LND running" - - docker ps --format '{{.Names}}' | grep -q bitspire-lnd-alice - check $? "LND Alice running" - - docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub - check $? "Lightning.Pub running" - - echo "" - echo "2. Connectivity" - echo "───────────────────────────────────────────────────────────" - - # Check relay port is open - timeout 2 bash -c 'echo > /dev/tcp/localhost/7777' 2>/dev/null - check $? "Nostr relay port open" - - # Check bitcoind RPC - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire getblockchaininfo >/dev/null 2>&1 # pragma: allowlist secret - check $? "Bitcoin RPC responding" - - # Check LND - docker exec bitspire-lnd lncli --network=regtest getinfo >/dev/null 2>&1 - check $? "LND RPC responding" - - # Check Alice - docker exec bitspire-lnd-alice lncli --network=regtest getinfo >/dev/null 2>&1 - check $? "LND Alice RPC responding" - - echo "" - echo "3. Blockchain State" - echo "───────────────────────────────────────────────────────────" - - BLOCKS=$(docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire getblockcount 2>/dev/null) # pragma: allowlist secret - [ "$BLOCKS" -ge 100 ] - check $? "Block height >= 100 (current: $BLOCKS)" - - # Check LND synced - LND_SYNCED=$(docker exec bitspire-lnd lncli --network=regtest getinfo 2>/dev/null | jq -r '.synced_to_chain') - [ "$LND_SYNCED" = "true" ] - check $? "LND synced to chain" - - # Check Alice synced - ALICE_SYNCED=$(docker exec bitspire-lnd-alice lncli --network=regtest getinfo 2>/dev/null | jq -r '.synced_to_chain') - [ "$ALICE_SYNCED" = "true" ] - check $? "LND Alice synced to chain" - - echo "" - echo "4. Lightning Channels" - echo "───────────────────────────────────────────────────────────" - - # Check Alice has active channels - ALICE_CHANNELS=$(docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels | length') - [ "$ALICE_CHANNELS" -ge 1 ] - check $? "Alice has active channels (count: $ALICE_CHANNELS)" - - # Check channel is active - ACTIVE_CHANNEL=$(docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels[0].active') - [ "$ACTIVE_CHANNEL" = "true" ] - check $? "Channel is active" - - # Check Alice has outbound capacity - ALICE_LOCAL=$(docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq -r '.channels[0].local_balance // 0') - [ "$ALICE_LOCAL" -ge 10000 ] - check $? "Alice has outbound capacity ($ALICE_LOCAL sats)" - - echo "" - echo "5. Payment Test" - echo "───────────────────────────────────────────────────────────" - - # Create test invoice on LND and pay from Alice - TEST_INVOICE=$(docker exec bitspire-lnd lncli --network=regtest addinvoice --amt=100 --memo="test-setup validation" 2>/dev/null | jq -r '.payment_request') - if [ -n "$TEST_INVOICE" ]; then - check 0 "Created 100 sat test invoice" - - # Pay it - PAY_RESULT=$(docker exec bitspire-lnd-alice lncli --network=regtest payinvoice --force "$TEST_INVOICE" 2>&1) - if echo "$PAY_RESULT" | grep -q "SUCCEEDED"; then - check 0 "Payment Alice → LND succeeded" - else - check 1 "Payment Alice → LND failed" - fi - else - check 1 "Failed to create test invoice" - check 1 "Payment test skipped" - fi - - echo "" - echo "═══════════════════════════════════════════════════════════" - echo " Results: $PASSED passed, $FAILED failed" - echo "═══════════════════════════════════════════════════════════" - echo "" - - if [ $FAILED -gt 0 ]; then - echo "Some checks failed. Run 'infra-up' and 'setup-channel' first." - exit 1 - else - echo "All checks passed! Ready for testing." - exit 0 - fi - ''; - - # Quick e2e payment test (ATM pays customer invoice) - test-payment.exec = '' - AMOUNT=''${1:-1000} - echo "Testing e2e payment flow ($AMOUNT sats)..." - echo "" - - # Create invoice on Alice (simulating customer's wallet) - echo "1. Creating invoice on Alice's wallet..." - INVOICE=$(docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt="$AMOUNT" --memo="E2E test" | jq -r '.payment_request') - echo " Invoice: ''${INVOICE:0:40}..." - - # Use test-pay.mjs to pay via Lightning.Pub - echo "" - echo "2. Paying via Lightning.Pub (ATM flow)..." - cd packages/nostr-client - node test-pay.mjs "$INVOICE" - - echo "" - echo "3. Verifying payment on Alice..." - sleep 2 - PAID=$(docker exec bitspire-lnd-alice lncli --network=regtest listinvoices 2>/dev/null | jq '.invoices[-1].settled') - if [ "$PAID" = "true" ]; then - echo " ✓ Payment received!" - else - echo " ✗ Payment not received" - exit 1 - fi - ''; - - # Show node info - node-info.exec = '' - echo "" - echo "Node Information" - echo "════════════════════════════════════════════════════════" - echo "" - - echo "LND (Lightning.Pub's node):" - docker exec bitspire-lnd lncli --network=regtest getinfo 2>/dev/null | jq '{pubkey: .identity_pubkey, alias: .alias, channels: .num_active_channels, peers: .num_peers}' - - echo "" - echo "LND Alice (Payment source):" - docker exec bitspire-lnd-alice lncli --network=regtest getinfo 2>/dev/null | jq '{pubkey: .identity_pubkey, alias: .alias, channels: .num_active_channels, peers: .num_peers}' - - echo "" - echo "Channel Details:" - docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels[] | {peer: .remote_pubkey[0:16], capacity, local: .local_balance, remote: .remote_balance, active}' - - echo "" - echo "Lightning.Pub Nostr pubkey:" - echo " f454c5eec4ec4474128e19b57b45e49c3d0851d01eea960b39ce391cdba76fc6" - - echo "" - echo "ATM Dev Identity:" - echo " 4646ae5047316b4230d0086c8acec687f00b1cd9d1dc634f6cb358ac0a9a8fff" + echo "Testing Nostr relay connection to $NOSTR_RELAY_URL ..." + echo '["REQ", "test", {"kinds": [0], "limit": 1}]' | websocat "$NOSTR_RELAY_URL" ''; }; - # ============================================ - # Shell Hook - # ============================================ - enterShell = '' echo "" echo " ⚡ bitSpire - Nostr-Native Lightning ATM" @@ -543,30 +122,8 @@ echo " dev Start development servers" echo " build Build all packages" echo " test Run tests" - echo "" - echo " Infrastructure:" - echo " infra-up Start Docker services (strfry, bitcoind, LND, Lightning.Pub)" - echo " infra-down Stop Docker services" - echo " infra-status Show service status" - echo " infra-logs Follow service logs" - echo "" - echo " Bitcoin/Lightning:" - echo " btccli Bitcoin CLI (regtest)" - echo " lncli LND CLI (Lightning.Pub's node)" - echo " lncli-alice LND CLI (Alice's node for testing payments)" - echo " mine-blocks Mine regtest blocks (default: 1)" - echo " auto-mine Start auto-miner (1 block/30s, keeps LND synced)" - echo " auto-mine-stop Stop auto-miner" - echo " setup-channel Setup channel between Alice and LND" - echo " alice-pay Pay invoice from Alice's node" - echo " relay-test Test Nostr relay connection" - echo "" - echo " Testing (E2E):" - echo " test-setup Validate test environment (services, channels, payments)" - echo " test-payment Quick e2e payment test (ATM → customer)" - echo " fund-atm Fund ATM account (default: 100k sats)" - echo " alice-invoice Create invoice on Alice's node" - echo " node-info Show node pubkeys and channel info" + echo " lint Lint all packages" + echo " relay-test Test the dev relay ($NOSTR_RELAY_URL)" echo "" ''; } diff --git a/docker/dev.sh b/docker/dev.sh deleted file mode 100755 index f47d42f..0000000 --- a/docker/dev.sh +++ /dev/null @@ -1,1087 +0,0 @@ -#!/bin/bash -# -# bitSpire Development Environment -# -# Single command to manage the complete development stack: -# - Regtest Bitcoin/Lightning network (from ~/dev/local/docker/regtest) -# - Lightning.Pub with configurable image/worktree -# - Auto-configured ATM application -# -# Usage: -# ./dev.sh up # Start (uses shared regtest) -# ./dev.sh up --standalone # Start self-contained (own bitcoind/lnd) -# ./dev.sh up --fund # Start and auto-fund ATM with 100k sats -# ./dev.sh up --fund=50000 # Start and auto-fund with specific amount -# ./dev.sh up --machine # Start and launch ATM app -# ./dev.sh up --fund --machine # Start, fund, and launch ATM -# ./dev.sh up --worktree ~/path/to/lp # Build Lightning.Pub from worktree -# ./dev.sh up --image myimage:tag # Use specific Docker image -# ./dev.sh down # Stop everything -# ./dev.sh atm # Launch ATM application -# ./dev.sh status # Show connection info -# ./dev.sh logs [service] # Follow logs -# ./dev.sh fund [amount] # Fund ATM (default: 100000 sats) -# ./dev.sh mine [blocks] # Mine blocks manually -# ./dev.sh reset # Reset all state (fresh start) -# - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}" -DEFAULT_IMAGE="lightning-pub-withdraw:latest" -DEFAULT_FUNDING_SATS=100000 - -# State files -STATE_DIR="$SCRIPT_DIR/.state" -PUBKEY_FILE="$STATE_DIR/lightning-pub-pubkey" -NPROFILE_FILE="$STATE_DIR/lightning-pub-nprofile" -APP_ID_FILE="$STATE_DIR/atm-app-id" -MODE_FILE="$STATE_DIR/compose-mode" # "standalone" or "unified" - -# Get current compose file based on last used mode -get_compose_file() { - local mode=$(cat "$MODE_FILE" 2>/dev/null || echo "unified") - if [[ "$mode" == "standalone" ]]; then - echo "docker-compose.dev.yml" - else - echo "docker-compose.regtest.yml" - fi -} - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -CYAN='\033[0;36m' -BOLD='\033[1m' -DIM='\033[2m' -NC='\033[0m' - -log() { echo -e "${GREEN}►${NC} $1"; } -warn() { echo -e "${YELLOW}⚠${NC} $1"; } -error() { echo -e "${RED}✗${NC} $1"; } -success() { echo -e "${GREEN}✓${NC} $1"; } - -# Ensure state directory exists -mkdir -p "$STATE_DIR" - -# Get ATM pubkey from its private key # pragma: allowlist secret -get_atm_pubkey() { - local env_file="$PROJECT_DIR/apps/machine/.env" - if [[ ! -f "$env_file" ]]; then - return 1 - fi - - local privkey=$(grep "VITE_ATM_PRIVATE_KEY=" "$env_file" | cut -d= -f2) - if [[ -z "$privkey" ]]; then - return 1 - fi - - # Use node with @noble/curves to derive pubkey from privkey - cd "$PROJECT_DIR" && node -e " - const { secp256k1 } = require('./node_modules/.pnpm/@noble+curves@2.0.1/node_modules/@noble/curves/secp256k1.js'); - const privkeyHex = '$privkey'; - const privkey = Uint8Array.from(Buffer.from(privkeyHex, 'hex')); - const pubkeyFull = secp256k1.getPublicKey(privkey, true); - const pubkey = Buffer.from(pubkeyFull.slice(1)).toString('hex'); - console.log(pubkey); - " 2>/dev/null -} - -############################################################################# -# Helper Functions -############################################################################# - -get_local_ip() { - # Allow explicit override via env var - if [[ -n "${BITSPIRE_HOST_IP:-}" ]]; then - echo "$BITSPIRE_HOST_IP" - return - fi - - # Linux: use ip route - if command -v ip &>/dev/null; then - local ip=$(ip route get 1 2>/dev/null | awk '{print $7; exit}') - if [[ -n "$ip" ]]; then - echo "$ip" - return - fi - fi - - # macOS: use route + ifconfig - if [[ "$(uname)" == "Darwin" ]]; then - local iface=$(route get default 2>/dev/null | awk '/interface:/ {print $2}') - if [[ -n "$iface" ]]; then - local ip=$(ifconfig "$iface" 2>/dev/null | awk '/inet / {print $2}') - if [[ -n "$ip" ]]; then - echo "$ip" - return - fi - fi - fi - - # Fallback: hostname -I (Linux) or hostname (macOS) - hostname -I 2>/dev/null | awk '{print $1}' || hostname 2>/dev/null -} - -is_regtest_running() { - # Check if lnd-4 container is actually running (not just network exists) - docker ps --filter "name=lnbits-lnd-4-1" --format "{{.Names}}" 2>/dev/null | grep -q lnbits-lnd-4-1 -} - -is_lnd4_ready() { - # Use lnd-4 hostname (not localhost) since lnd binds to container IP - docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 getinfo &>/dev/null 2>&1 -} - -lnd4_has_channels() { - local channel_count=$(docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 listchannels 2>/dev/null | grep -c '"chan_id"' || echo "0") - [[ "$channel_count" -gt 0 ]] -} - -lnd4_has_active_channels() { - docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 listchannels 2>/dev/null | grep -q '"active": true' -} - -is_lightning_pub_ready() { - docker logs bitspire-lightning-pub 2>&1 | grep -q "LightningPub listening" -} - -get_lnd4_balance() { - docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 walletbalance 2>/dev/null | grep -oP '"total_balance":\s*"\K[0-9]+' || echo "0" -} - -get_lnd4_channel_balance() { - docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 channelbalance 2>/dev/null | grep -oP '"local_balance"[^}]*"sat":\s*"\K[0-9]+' || echo "0" -} - -############################################################################# -# Regtest Management -############################################################################# - -is_lnd1_ready() { - # Check if lnd-1 is fully ready (can make RPC calls) - docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 getinfo &>/dev/null 2>&1 -} - -is_lnd1_wallet_ready() { - # Check if lnd-1 wallet is ready (can query balance - needed for channel operations) - docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 walletbalance &>/dev/null 2>&1 -} - -wait_for_lnd1_wallet() { - local attempts=0 - while ! is_lnd1_wallet_ready && [[ $attempts -lt 30 ]]; do - sleep 2 - attempts=$((attempts + 1)) - done - is_lnd1_wallet_ready -} - -wait_for_lnd1() { - log "Waiting for lnd-1 to be ready..." - local attempts=0 - while ! is_lnd1_ready && [[ $attempts -lt 30 ]]; do - sleep 2 - attempts=$((attempts + 1)) - done - is_lnd1_ready -} - -ensure_lnd4_channel() { - # Ensure lnd-4 has a channel so it can create invoices - # Also ensure lnd-3 has a channel so it can pay invoices (for funding) - # Opens channels from lnd-1 (hub) to both lnd-3 and lnd-4 - - if lnd4_has_active_channels; then - log "lnd-4 has active channels" - return 0 - fi - - log "Setting up Lightning channels for ATM..." - - # Wait for lnd-1 to be ready (it's the funding hub) - if ! wait_for_lnd1; then - error "lnd-1 failed to start" - return 1 - fi - success "lnd-1 is ready" - - # Wait for wallet to be fully initialized (needed for channel operations) - log "Waiting for lnd-1 wallet..." - if ! wait_for_lnd1_wallet; then - error "lnd-1 wallet failed to initialize" - return 1 - fi - success "lnd-1 wallet is ready" - - # From a clean slate, bitcoind may have 0 blocks. Mine some for LND to sync. - local block_count=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getblockcount 2>/dev/null || echo "0") - if [[ "$block_count" -lt 101 ]]; then - log "Mining initial blocks for chain sync..." - local init_addr=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 newaddress p2wkh 2>/dev/null | grep -oP '"address":\s*"\K[^"]+') - if [[ -n "$init_addr" ]]; then - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 110 "$init_addr" &>/dev/null - sleep 3 - success "Initial blocks mined" - fi - fi - - # Wait for LND to sync to chain - log "Waiting for lnd-1 to sync..." - local sync_attempts=0 - while [[ $sync_attempts -lt 30 ]]; do - local synced=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 getinfo 2>/dev/null | grep -oP '"synced_to_chain":\s*\K(true|false)') - if [[ "$synced" == "true" ]]; then - break - fi - sleep 2 - sync_attempts=$((sync_attempts + 1)) - done - success "lnd-1 synced to chain" - - # Check if lnd-1 has funds, if not fund it - local lnd1_balance=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 walletbalance 2>/dev/null | grep -oP '"confirmed_balance":\s*"\K[0-9]+' | head -1 || echo "0") - lnd1_balance="${lnd1_balance:-0}" - if [[ "$lnd1_balance" -lt 100000000 ]]; then - log "Funding lnd-1 with mining rewards..." - local lnd1_addr=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 newaddress p2wkh 2>/dev/null | grep -oP '"address":\s*"\K[^"]+') - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 110 "$lnd1_addr" &>/dev/null - sleep 3 - success "lnd-1 funded" - fi - - # Get pubkeys - local lnd4_pubkey=$(docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 getinfo 2>/dev/null | grep -oP '"identity_pubkey":\s*"\K[^"]+') - local lnd3_pubkey=$(docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 getinfo 2>/dev/null | grep -oP '"identity_pubkey":\s*"\K[^"]+') - - if [[ -z "$lnd4_pubkey" ]] || [[ -z "$lnd3_pubkey" ]]; then - error "Could not get LND pubkeys" - return 1 - fi - - # Connect lnd-1 to lnd-3 and lnd-4 - log "Connecting nodes..." - docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \ - connect "${lnd3_pubkey}@lnd-3:9735" &>/dev/null || true - docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \ - connect "${lnd4_pubkey}@lnd-4:9735" &>/dev/null || true - - # Retry loop for opening channels (server may still be initializing) - # From a clean slate, LND needs ~30-60s after getinfo works to fully initialize - local result3="" result4="" attempts=0 - while [[ $attempts -lt 10 ]]; do - # Open channel to lnd-3: 10M sats, push 5M so lnd-3 has sending capacity - if [[ -z "$result3" ]] || echo "$result3" | grep -q "still in the process of starting"; then - result3=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \ - openchannel --node_key="$lnd3_pubkey" --local_amt=10000000 --push_amt=5000000 2>&1) - fi - - # Open channel to lnd-4: 10M sats, push 5M so lnd-4 has receiving capacity - if [[ -z "$result4" ]] || echo "$result4" | grep -q "still in the process of starting"; then - result4=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \ - openchannel --node_key="$lnd4_pubkey" --local_amt=10000000 --push_amt=5000000 2>&1) - fi - - # Check if both succeeded - if echo "$result3" | grep -q "funding_txid" && echo "$result4" | grep -q "funding_txid"; then - break - fi - - attempts=$((attempts + 1)) - if [[ $attempts -lt 10 ]]; then - log "Waiting for LND to be fully ready... (attempt $attempts/10)" - sleep 5 - fi - done - - log "Opening channel lnd-1 -> lnd-3..." - log "Opening channel lnd-1 -> lnd-4..." - - local channels_opened=false - if echo "$result3" | grep -q "funding_txid" || echo "$result4" | grep -q "funding_txid"; then - channels_opened=true - fi - - if [[ "$channels_opened" == "true" ]]; then - log "Mining blocks to confirm channels..." - - # From clean slate, bitcoind may need wallet. Ensure it has one. - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest createwallet "regtest" &>/dev/null 2>&1 || true - - # Mine 10 blocks to confirm channels - local addr=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getnewaddress 2>/dev/null) - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 10 "$addr" &>/dev/null - - # Wait for channels to become active (may take up to 60s) - local attempts=0 - while ! lnd4_has_active_channels && [[ $attempts -lt 30 ]]; do - sleep 2 - attempts=$((attempts + 1)) - done - - if lnd4_has_active_channels; then - # Wait for graph propagation (critical for routing payments) - log "Waiting for network graph to sync..." - # Mine more blocks to trigger channel announcements - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 6 "$addr" &>/dev/null - sleep 5 - - # Verify graph has propagated by checking if lnd-3 can route to lnd-4 - local route_attempts=0 - while [[ $route_attempts -lt 15 ]]; do - local route=$(docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 \ - queryroutes --dest "$lnd4_pubkey" --amt 10000 2>&1) - if echo "$route" | grep -q '"total_amt"'; then - # Route found - wait a bit more for full sync before payments - sleep 10 - break - fi - sleep 5 - route_attempts=$((route_attempts + 1)) - done - - success "Channels active - lnd-3 and lnd-4 ready" - return 0 - else - warn "Channels opened but not yet active, may need more time" - return 0 - fi - else - error "Failed to open channels" - echo "lnd-3 result: $result3" - echo "lnd-4 result: $result4" - return 1 - fi -} - -start_regtest() { - if is_regtest_running; then - log "Regtest network already running" - return 0 - fi - - if [[ ! -d "$REGTEST_DIR" ]]; then - error "Regtest directory not found: $REGTEST_DIR" - echo " Clone it from: https://github.com/your-org/regtest-env" - exit 1 - fi - - log "Starting regtest environment..." - # Use project name "lnbits" to match the expected network name (lnbits_default) - (cd "$REGTEST_DIR" && docker compose -p lnbits up -d) - - # Wait for lnd-4 to be ready - log "Waiting for lnd-4 to be ready..." - local attempts=0 - while ! is_lnd4_ready && [[ $attempts -lt 30 ]]; do - sleep 2 - attempts=$((attempts + 1)) - done - - if is_lnd4_ready; then - success "lnd-4 is ready" - else - error "lnd-4 failed to start" - return 1 - fi - - # Ensure lnd-4 has a channel for invoices - ensure_lnd4_channel || warn "Could not setup lnd-4 channel (funding may fail)" -} - -stop_regtest() { - if [[ -d "$REGTEST_DIR" ]]; then - log "Stopping regtest environment..." - (cd "$REGTEST_DIR" && docker compose down) - fi -} - -############################################################################# -# Lightning.Pub Management -############################################################################# - -build_from_worktree() { - local worktree="$1" - local image_name="lightning-pub-dev:latest" - - if [[ ! -d "$worktree" ]]; then - error "Worktree not found: $worktree" - exit 1 - fi - - log "Building Lightning.Pub from $worktree..." - docker build -t "$image_name" "$worktree" - echo "$image_name" -} - -wait_for_lightning_pub() { - log "Waiting for Lightning.Pub..." - local attempts=0 - while ! is_lightning_pub_ready && [[ $attempts -lt 45 ]]; do - # Check for errors - if docker logs bitspire-lightning-pub 2>&1 | grep -q "Error:"; then - local err=$(docker logs bitspire-lightning-pub 2>&1 | grep "Error:" | tail -1) - error "Lightning.Pub error: $err" - return 1 - fi - sleep 2 - attempts=$((attempts + 1)) - done - - if is_lightning_pub_ready; then - sleep 2 # Extra time for Nostr middleware - success "Lightning.Pub is ready" - return 0 - else - error "Lightning.Pub failed to start (timeout)" - docker logs bitspire-lightning-pub 2>&1 | tail -10 - return 1 - fi -} - -extract_lightning_pub_info() { - local pubkey=$(docker logs bitspire-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1) - local nprofile=$(docker logs bitspire-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1) - - echo "$pubkey" > "$PUBKEY_FILE" - echo "$nprofile" > "$NPROFILE_FILE" - - echo "$pubkey" -} - -############################################################################# -# ATM Configuration -############################################################################# - -update_atm_env() { - local pubkey="$1" - local env_file="$PROJECT_DIR/apps/machine/.env" - - if [[ ! -f "$env_file" ]]; then - warn "ATM .env not found, creating..." - cat > "$env_file" << EOF -# Lightning.Pub connection -VITE_RELAY_URL=ws://localhost:7777 -VITE_LIGHTNING_PUB_PUBKEY=$pubkey -VITE_LIGHTNING_PUB_API_URL=http://localhost:1776 -VITE_ADMIN_TOKEN=bitspire-dev-admin-token -VITE_ATM_PRIVATE_KEY=f391a2c3fc734f443b0f685688a0441b5fb9805853c0023f570c5a3c6412b136 - -# Extension API (for LNURL-withdraw) -VITE_EXTENSION_API_URL=http://localhost:1777 -EOF - else - # Update existing pubkey - if grep -q "VITE_LIGHTNING_PUB_PUBKEY" "$env_file"; then - sed -i "s/VITE_LIGHTNING_PUB_PUBKEY=.*/VITE_LIGHTNING_PUB_PUBKEY=$pubkey/" "$env_file" - else - echo "VITE_LIGHTNING_PUB_PUBKEY=$pubkey" >> "$env_file" - fi - fi - success "Updated ATM .env with pubkey" -} - -setup_atm_app() { - # Use a fixed app name so we reuse the same app across restarts - local app_name="bitspire-atm-dev" - - # Check if we already have valid app state - if [[ -f "$APP_ID_FILE" ]]; then - local existing_app_id=$(cat "$APP_ID_FILE") - if [[ -n "$existing_app_id" ]]; then - log "Using existing ATM app: ${existing_app_id:0:16}..." - return 0 - fi - fi - - log "Creating ATM app..." - - local response=$(curl -s -X POST http://localhost:1776/api/admin/app/add \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer bitspire-dev-admin-token" \ - -d "{\"name\":\"$app_name\",\"allow_user_creation\":true}" 2>/dev/null) - - if echo "$response" | grep -q '"status":"OK"'; then - local app_id=$(echo "$response" | grep -oP '"id":"\K[^"]+') - - echo "$app_id" > "$APP_ID_FILE" - - # Update ATM .env with app ID - local env_file="$PROJECT_DIR/apps/machine/.env" - if [[ -f "$env_file" ]]; then - if grep -q "VITE_APP_ID" "$env_file"; then - sed -i "s/VITE_APP_ID=.*/VITE_APP_ID=$app_id/" "$env_file" - else - echo "" >> "$env_file" - echo "# ATM App ID" >> "$env_file" - echo "VITE_APP_ID=$app_id" >> "$env_file" - fi - fi - - success "Created ATM app: ${app_id:0:16}..." - return 0 - else - warn "Failed to create ATM app (may already exist)" - if [[ -f "$APP_ID_FILE" ]]; then - log "Using existing app ID from state" - return 0 - fi - return 1 - fi -} - -############################################################################# -# Zeus Connection -############################################################################# - -generate_lndconnect() { - local local_ip=$(get_local_ip) - local rest_port=8081 # lnd-3 REST API port - - # Read certs directly from lnd-3 container (host files are root-owned) - local cert_b64=$(docker exec lnbits-lnd-3-1 base64 -w0 /root/.lnd/tls.cert 2>/dev/null | tr '+/' '-_' | tr -d '=') - local mac_b64=$(docker exec lnbits-lnd-3-1 base64 -w0 /root/.lnd/data/chain/bitcoin/regtest/admin.macaroon 2>/dev/null | tr '+/' '-_' | tr -d '=') - - if [[ -n "$cert_b64" ]] && [[ -n "$mac_b64" ]]; then - echo "lndconnect://${local_ip}:${rest_port}?cert=${cert_b64}&macaroon=${mac_b64}" - return 0 - fi - return 1 -} - -############################################################################# -# Display Functions -############################################################################# - -get_atm_balance() { - local app_id=$(cat "$APP_ID_FILE" 2>/dev/null) - if [[ -z "$app_id" ]]; then - echo "not configured" - return - fi - echo "configured (use './dev.sh fund' to add sats)" -} - -show_status() { - local pubkey=$(cat "$PUBKEY_FILE" 2>/dev/null || docker logs bitspire-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1) - local nprofile=$(cat "$NPROFILE_FILE" 2>/dev/null || docker logs bitspire-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1) - local local_ip=$(get_local_ip) - local lndconnect=$(generate_lndconnect 2>/dev/null || echo "") - local lnd4_balance=$(get_lnd4_balance) - local app_id=$(cat "$APP_ID_FILE" 2>/dev/null || echo "not set") - - echo "" - echo -e "${BOLD}╔═══════════════════════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}║ BITSPIRE - DEVELOPMENT ENVIRONMENT ║${NC}" - echo -e "${BOLD}╚═══════════════════════════════════════════════════════════════════╝${NC}" - echo "" - - echo -e "${CYAN}Lightning.Pub${NC}" - echo -e " Pubkey: ${GREEN}$pubkey${NC}" - echo -e " nprofile: ${GREEN}$nprofile${NC}" - echo "" - - echo -e "${CYAN}Service URLs (local)${NC}" - echo " Nostr Relay: ws://localhost:7777" - echo " Lightning.Pub: http://localhost:1776" - echo " Withdraw API: http://localhost:1777" - echo "" - - echo -e "${CYAN}External Access (LAN: $local_ip)${NC}" - echo " Nostr Relay: ws://${local_ip}:7777" - echo " Withdraw API: http://${local_ip}:1777" - echo "" - - local lnd4_channel_balance=$(get_lnd4_channel_balance) - echo -e "${CYAN}lnd-4 (Lightning.Pub backend)${NC}" - echo " Channel Balance: ${lnd4_channel_balance} sats" - echo " Wallet Balance: ${lnd4_balance} sats" - echo "" - - echo -e "${CYAN}ATM App${NC}" - if [[ "$app_id" != "not set" ]]; then - echo " App ID: ${app_id:0:16}..." - echo " Status: $(get_atm_balance)" - else - echo " Status: not configured" - fi - echo "" - - if [[ -n "$lndconnect" ]]; then - echo -e "${CYAN}Zeus Wallet (connect to lnd-3 for testing)${NC}" - echo -e " ${DIM}${lndconnect:0:60}...${NC}" - echo -e " ${DIM}To display QR: ./dev.sh zeus${NC}" - echo "" - fi - - echo -e "${CYAN}Quick Commands${NC}" - echo " ./dev.sh logs lightning-pub # View Lightning.Pub logs" - echo " ./dev.sh fund 100000 # Fund ATM with 100k sats" - echo " ./dev.sh status # Show this info" - echo "" - echo -e "${BOLD}═══════════════════════════════════════════════════════════════════${NC}" -} - -############################################################################# -# Main Commands -############################################################################# - -cmd_up() { - local image="$DEFAULT_IMAGE" - local worktree="" - local skip_regtest=false - local auto_fund=false - local fund_amount="$DEFAULT_FUNDING_SATS" - local launch_machine=false - local standalone=false - local compose_file="docker-compose.regtest.yml" - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --image) image="$2"; shift 2 ;; - --worktree) worktree="$2"; shift 2 ;; - --skip-regtest) skip_regtest=true; shift ;; - --standalone) standalone=true; shift ;; - --fund) auto_fund=true; shift ;; - --fund=*) auto_fund=true; fund_amount="${1#*=}"; shift ;; - --machine|--atm) launch_machine=true; shift ;; - *) shift ;; - esac - done - - # Determine compose file - if [[ "$standalone" == "true" ]]; then - compose_file="docker-compose.dev.yml" - skip_regtest=true # Standalone mode has its own bitcoind/lnd - log "Starting in STANDALONE mode (self-contained bitcoind/lnd)" - else - log "Starting in UNIFIED mode (shared regtest)" - fi - - echo "" - log "Starting bitSpire development environment..." - echo "" - - # 1. Start shared regtest if needed (not in standalone mode) - if [[ "$standalone" != "true" ]] && [[ "$skip_regtest" != "true" ]]; then - start_regtest || exit 1 - elif [[ "$standalone" != "true" ]] && ! is_regtest_running; then - error "Shared regtest not running." - echo " Start with: regtest-start" - echo " Or use: ./dev.sh up --standalone" - exit 1 - elif [[ "$standalone" != "true" ]]; then - # Regtest already running, but ensure lnd-4 has channels - ensure_lnd4_channel || warn "Could not setup lnd-4 channel (funding may fail)" - fi - - # 2. Build from worktree if specified - if [[ -n "$worktree" ]]; then - image=$(build_from_worktree "$worktree") - fi - - # 3. Check image exists - if ! docker image inspect "$image" &>/dev/null; then - error "Docker image not found: $image" - echo "" - echo "Options:" - echo " 1. Build from worktree: ./dev.sh up --worktree ~/path/to/lightning-pub" - echo " 2. Build manually: docker build -t $image ~/path/to/lightning-pub" - exit 1 - fi - - log "Using Lightning.Pub image: $image" - - # 4. Start bitspire services - export REGTEST_DATA_DIR="$REGTEST_DIR/data" - export LIGHTNING_PUB_IMAGE="$image" - export HOST_IP=$(get_local_ip) - - # Save mode for other commands - if [[ "$standalone" == "true" ]]; then - echo "standalone" > "$MODE_FILE" - else - echo "unified" > "$MODE_FILE" - fi - - log "Starting bitspire services..." - docker compose -f "$SCRIPT_DIR/$compose_file" up -d - - # 5. Wait for Lightning.Pub - if ! wait_for_lightning_pub; then - error "Failed to start. Check: ./dev.sh logs lightning-pub" - exit 1 - fi - - # 6. Extract and save Lightning.Pub info - local pubkey=$(extract_lightning_pub_info) - - # 7. Update ATM .env - update_atm_env "$pubkey" - - # 8. Setup ATM app - setup_atm_app || true - - # 9. Auto-fund if requested - if [[ "$auto_fund" == "true" ]]; then - echo "" - log "Auto-funding ATM with $fund_amount sats..." - sleep 2 # Give Lightning.Pub a moment to settle - cmd_fund "$fund_amount" || warn "Auto-funding failed. Run './dev.sh fund' manually." - fi - - # 10. Show status - show_status - - # 11. Launch ATM if requested (in background so status stays visible) - if [[ "$launch_machine" == "true" ]]; then - echo "" - cmd_atm --background - fi -} - -cmd_down() { - local compose_file=$(get_compose_file) - log "Stopping bitspire services..." - docker compose -f "$SCRIPT_DIR/$compose_file" down 2>/dev/null || true - - if [[ "$1" == "--all" ]]; then - stop_regtest - fi - - success "Services stopped" -} - -cmd_reset() { - warn "This will delete all bitspire state (Lightning.Pub identity, ATM app, etc.)" - read -p "Continue? [y/N] " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Aborted" - exit 0 - fi - - local compose_file=$(get_compose_file) - log "Stopping services..." - docker compose -f "$SCRIPT_DIR/$compose_file" down -v 2>/dev/null || true - - log "Removing state files..." - rm -rf "$STATE_DIR" - mkdir -p "$STATE_DIR" - - success "Reset complete. Run './dev.sh up' for fresh start." -} - -cmd_logs() { - local compose_file=$(get_compose_file) - docker compose -f "$SCRIPT_DIR/$compose_file" logs -f "$@" -} - -cmd_status() { - if ! docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub; then - error "Services not running. Start with: ./dev.sh up" - exit 1 - fi - show_status -} - -cmd_fund() { - local amount="${1:-$DEFAULT_FUNDING_SATS}" - - if ! is_regtest_running; then - error "Regtest not running" - exit 1 - fi - - # Check for Lightning.Pub pubkey - if [[ ! -f "$PUBKEY_FILE" ]]; then - error "Lightning.Pub not configured. Run './dev.sh up' first." - exit 1 - fi - - local lp_pubkey=$(cat "$PUBKEY_FILE") - local env_file="$PROJECT_DIR/apps/machine/.env" - - # Get ATM private key from .env # pragma: allowlist secret - if [[ ! -f "$env_file" ]]; then - error "ATM not configured. Run './dev.sh up' first." - exit 1 - fi - - local atm_privkey=$(grep "VITE_ATM_PRIVATE_KEY=" "$env_file" | cut -d= -f2) - if [[ -z "$atm_privkey" ]]; then - error "VITE_ATM_PRIVATE_KEY not found in .env" - exit 1 - fi - - # Get app ID - required to ensure funds go to the same user as LNURL-withdraw - local app_id="" - if [[ -f "$APP_ID_FILE" ]]; then - app_id=$(cat "$APP_ID_FILE") - else - app_id=$(grep "VITE_APP_ID=" "$env_file" | cut -d= -f2) - fi - - if [[ -z "$app_id" ]]; then - error "No app ID found. Run './dev.sh up' first to create the ATM app." - exit 1 - fi - - log "Creating invoice via Nostr RPC for $amount sats..." - log "Using app ID: ${app_id:0:16}..." - - # Use the Nostr-based funding script (the Lightning.Pub way) - # This creates an invoice for the ATM's Nostr user under the correct app, - # ensuring balance is shared with LNURL-withdraw (Extension API) - local invoice=$(cd "$PROJECT_DIR/packages/nostr-client" && \ - VITE_ATM_PRIVATE_KEY="$atm_privkey" \ - VITE_LIGHTNING_PUB_PUBKEY="$lp_pubkey" \ - VITE_RELAY_URL="ws://localhost:7777" \ - VITE_APP_ID="$app_id" \ - node dev/fund-dev.mjs "$amount" 2>&1) - - # Extract just the invoice (last line, starts with lnbc) - local bolt11=$(echo "$invoice" | grep -E "^lnbc") - - if [[ -z "$bolt11" ]]; then - error "Failed to create invoice via Nostr" - echo "Output: $invoice" - exit 1 - fi - - log "Got invoice: ${bolt11:0:30}..." - - log "Paying invoice from lnd-3..." - - # Source regtest helpers and pay - if [[ -f "$REGTEST_DIR/docker-scripts.sh" ]]; then - ( - cd "$REGTEST_DIR" - source docker-scripts.sh 2>/dev/null - lncli-sim 3 payinvoice --force "$bolt11" - ) - if [[ $? -eq 0 ]]; then - success "Funded ATM with $amount sats" - else - error "Payment failed" - exit 1 - fi - else - # Fallback: try direct docker exec - docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 payinvoice --force "$bolt11" - if [[ $? -eq 0 ]]; then - success "Funded ATM with $amount sats" - else - error "Payment failed. Make sure lnd-3 has funds and channels." - exit 1 - fi - fi -} - -cmd_mine() { - local blocks="${1:-1}" - if ! is_regtest_running; then - error "Regtest not running" - exit 1 - fi - log "Mining $blocks block(s)..." - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest -generate "$blocks" > /dev/null - local height=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getblockcount) - success "Mined $blocks block(s) (height: $height)" -} - -cmd_zeus() { - if ! is_regtest_running; then - error "Regtest not running. Start with: ./dev.sh up" - exit 1 - fi - - local lndconnect=$(generate_lndconnect 2>/dev/null) - if [[ -z "$lndconnect" ]]; then - error "Could not generate lndconnect string" - exit 1 - fi - - echo "" - echo -e "${CYAN}Zeus Wallet Connection (lnd-3)${NC}" - echo "" - - if command -v qrencode &> /dev/null; then - local qr_file="$STATE_DIR/zeus-lndconnect.png" - qrencode -o "$qr_file" -s 6 -l L "$lndconnect" - echo -e "QR code saved to: ${GREEN}$qr_file${NC}" - echo "" - - # Try to open the image - if command -v xdg-open &> /dev/null; then - xdg-open "$qr_file" 2>/dev/null & - echo -e "${DIM}Opening QR code image...${NC}" - elif command -v open &> /dev/null; then - open "$qr_file" 2>/dev/null & - echo -e "${DIM}Opening QR code image...${NC}" - else - echo -e "${DIM}Open the PNG file to scan with Zeus${NC}" - fi - else - echo -e "${DIM}$lndconnect${NC}" - echo "" - echo -e "${YELLOW}Install qrencode for QR display: nix-shell -p qrencode${NC}" - fi - echo "" -} - -cmd_atm() { - local atm_dir="$PROJECT_DIR/apps/machine" - local background=false - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --background|-b) background=true; shift ;; - *) shift ;; - esac - done - - if [[ ! -d "$atm_dir" ]]; then - error "ATM app not found at $atm_dir" - exit 1 - fi - - # Check if services are running - if ! docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub; then - warn "Services not running. Start with: ./dev.sh up" - read -p "Start services first? [Y/n] " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Nn]$ ]]; then - cmd_up - fi - fi - - # Auto-generate lndconnect URI for Zeus QR in mock UI - local lndconnect=$(generate_lndconnect 2>/dev/null || echo "") - if [[ -n "$lndconnect" ]]; then - export VITE_LNDCONNECT_URL="$lndconnect" - log "Zeus lndconnect injected into ATM UI" - fi - - if [[ "$background" == "true" ]]; then - local log_file="$STATE_DIR/atm.log" - log "Starting ATM application in background..." - log "Logs: $log_file" - (cd "$atm_dir" && nohup pnpm dev > "$log_file" 2>&1 &) - sleep 2 - if pgrep -f "electron.*machine" > /dev/null; then - success "ATM app started (PID: $(pgrep -f 'electron.*machine' | head -1))" - else - warn "ATM may still be starting. Check logs: tail -f $log_file" - fi - else - log "Starting ATM application..." - echo "" - echo -e "${CYAN}ATM Mock Mode:${NC}" - echo " - Press 'b' to insert a bill (simulates cash insertion)" - echo " - Use the UI to complete transactions" - if [[ -n "$lndconnect" ]]; then - echo -e " - ${GREEN}Zeus QR available on idle screen${NC}" - fi - echo "" - cd "$atm_dir" && pnpm dev - fi -} - -############################################################################# -# Entry Point -############################################################################# - -case "${1:-help}" in - up|start) - shift - cmd_up "$@" - ;; - down|stop) - shift - cmd_down "$@" - ;; - reset) - cmd_reset - ;; - logs) - shift - cmd_logs "$@" - ;; - # Internal command for external orchestration (called by regtest-start --with-bitspire) - _setup_after_start) - if ! is_lightning_pub_ready; then - wait_for_lightning_pub || exit 1 - fi - local pubkey=$(extract_lightning_pub_info) - update_atm_env "$pubkey" - setup_atm_app || true - echo "ATM setup complete" - ;; - status|info) - cmd_status - ;; - fund) - shift - cmd_fund "$@" - ;; - mine) - shift - cmd_mine "$@" - ;; - atm) - cmd_atm - ;; - zeus) - cmd_zeus - ;; - *) - echo "bitSpire Development Environment" - echo "" - echo "Usage: $0 [options]" - echo "" - echo "Commands:" - echo " up [options] Start development environment" - echo " down [--all] Stop services (--all includes regtest)" - echo " atm Launch ATM application (Electron)" - echo " zeus Show Zeus wallet connection QR code" - echo " status Show connection info" - echo " logs [service] Follow service logs" - echo " fund [sats] Fund ATM account" - echo " mine [blocks] Mine blocks manually (default: 1)" - echo " reset Delete all state and start fresh" - echo "" - echo "Note: Auto-miner runs in background (1 block/2min). Check with:" - echo " ./dev.sh logs miner" - echo "" - echo "Options for 'up':" - echo " --standalone Use self-contained mode (own bitcoind/lnd)" - echo " --worktree Build Lightning.Pub from git worktree" - echo " --image Use specific Docker image" - echo " --skip-regtest Don't auto-start shared regtest" - echo " --fund Auto-fund ATM with 100k sats after startup" - echo " --fund= Auto-fund ATM with specific amount" - echo " --machine Launch ATM app after startup" - echo "" - echo "Environment variables:" - echo " BITSPIRE_HOST_IP Override auto-detected LAN IP (for VPN/multi-NIC)" - echo "" - echo "Examples:" - echo " $0 up # Start (uses shared regtest)" - echo " $0 up --standalone # Start self-contained" - echo " $0 up --fund # Start and fund ATM" - echo " $0 up --fund --machine # Start, fund, and launch ATM" - echo " $0 up --fund=50000 # Start and fund with 50k sats" - echo " $0 up --worktree ~/dev/lightning-pub/withdraw" - echo " $0 atm # Launch ATM app" - echo " $0 fund 200000 # Add 200k more sats" - echo " $0 logs lightning-pub" - echo " $0 reset && $0 up --fund # Fresh start with funding" - echo " BITSPIRE_HOST_IP=192.168.1.50 $0 up # Override LAN IP" - ;; -esac diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml deleted file mode 100644 index fa9860e..0000000 --- a/docker/docker-compose.dev.yml +++ /dev/null @@ -1,226 +0,0 @@ -# bitSpire Development Infrastructure -# Usage: docker compose -f docker-compose.dev.yml up -d - -services: - # Private Nostr relay for ATM communication - strfry: - image: ghcr.io/hoytech/strfry:latest - container_name: bitspire-relay - ports: - - '7777:7777' - volumes: - - ./strfry.conf:/etc/strfry.conf:ro - - strfry-data:/app/strfry-db - ulimits: - nofile: - soft: 524288 - hard: 524288 - healthcheck: - test: ['CMD', 'nc', '-z', 'localhost', '7777'] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - - # Bitcoin Core in regtest mode - bitcoind: - image: lncm/bitcoind:v27.0 - container_name: bitspire-bitcoind - volumes: - - bitcoind-data:/data/.bitcoin - environment: - BITCOIN_NETWORK: regtest - command: - - -regtest - - -server - - -rpcuser=bitspire - - -rpcpassword=bitspire # pragma: allowlist secret - - -rpcallowip=0.0.0.0/0 - - -rpcbind=0.0.0.0 - - -zmqpubrawblock=tcp://0.0.0.0:28332 - - -zmqpubrawtx=tcp://0.0.0.0:28333 - - -fallbackfee=0.00001 - - -txindex=1 - ports: - - '18443:18443' # RPC - - '28332:28332' # ZMQ blocks - - '28333:28333' # ZMQ tx - healthcheck: - test: - [ - 'CMD', - 'bitcoin-cli', - '-regtest', - '-rpcuser=bitspire', - '-rpcpassword=bitspire', # pragma: allowlist secret - 'getblockchaininfo', - ] - interval: 10s - timeout: 5s - retries: 10 - restart: unless-stopped - - # LND Lightning node - lnd: - image: lightninglabs/lnd:v0.18.0-beta - container_name: bitspire-lnd - depends_on: - bitcoind: - condition: service_healthy - volumes: - - lnd-data:/root/.lnd - environment: - - NETWORK=regtest - command: - - --bitcoin.active - - --bitcoin.regtest - - --bitcoin.node=bitcoind - - --bitcoind.rpchost=bitcoind:18443 - - --bitcoind.rpcuser=bitspire - - --bitcoind.rpcpass=bitspire # pragma: allowlist secret - - --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 - - --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 - - --rpclisten=0.0.0.0:10009 - - --restlisten=0.0.0.0:8080 - - --tlsextradomain=lnd - - --tlsextraip=0.0.0.0 - - --noseedbackup - - --accept-keysend - - --accept-amp - ports: - - '10009:10009' # gRPC - - '8080:8080' # REST - - '9735:9735' # P2P - healthcheck: - test: ['CMD', 'lncli', '--network=regtest', 'getinfo'] - interval: 10s - timeout: 10s - retries: 30 - start_period: 30s - restart: unless-stopped - - # Lightning.Pub - Nostr-native Lightning account system - # Note: Lightning.Pub connects to LND for actual Lightning operations - # Using patched image with Kind 0 profile publishing support - lightning-pub: - image: lightning-pub-patched:latest - container_name: bitspire-lightning-pub - depends_on: - lnd: - condition: service_healthy - extra_hosts: - - 'host.docker.internal:host-gateway' # Enable host.docker.internal on Linux - ports: - - '1776:1776' - volumes: - - lightning-pub-data:/root/lightning_pub - - lnd-data:/root/.lnd:ro # Read-only access to LND data for macaroons/certs - environment: - - NETWORK=regtest - - LND_ADDRESS=lnd:10009 - - LND_CERT_PATH=/root/.lnd/tls.cert - - LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon - # Relay URLs (space-separated). Docker-internal relay FIRST for publishing, - # then backup. Mock ATM rewrites ndebit relay for browser access. - - NOSTR_RELAYS=ws://strfry:7777 - # Disable external liquidity provider for regtest - - DISABLE_LIQUIDITY_PROVIDER=true - # Admin token for HTTP API access (development only) - - ADMIN_TOKEN=bitspire-dev-admin-token - restart: unless-stopped - - # Second LND node (Alice) for payment testing - # This node can pay invoices to Lightning.Pub's LND - lnd-alice: - image: lightninglabs/lnd:v0.18.0-beta - container_name: bitspire-lnd-alice - depends_on: - bitcoind: - condition: service_healthy - volumes: - - lnd-alice-data:/root/.lnd - environment: - - NETWORK=regtest - command: - - --bitcoin.active - - --bitcoin.regtest - - --bitcoin.node=bitcoind - - --bitcoind.rpchost=bitcoind:18443 - - --bitcoind.rpcuser=bitspire - - --bitcoind.rpcpass=bitspire # pragma: allowlist secret - - --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 - - --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 - - --rpclisten=0.0.0.0:10009 - - --restlisten=0.0.0.0:8080 - - --tlsextradomain=lnd-alice - - --tlsextraip=0.0.0.0 - - --noseedbackup - - --accept-keysend - - --accept-amp - - --alias=alice - ports: - - '10010:10009' # gRPC (different host port) - - '8081:8080' # REST (different host port) - - '9736:9735' # P2P (different host port) - healthcheck: - test: ['CMD', 'lncli', '--network=regtest', 'getinfo'] - interval: 10s - timeout: 10s - retries: 30 - start_period: 30s - restart: unless-stopped - - # Automatic block miner for regtest (keeps LND synced) - # Mines 1 block every 30 seconds - miner: - image: alpine:latest - container_name: bitspire-miner - depends_on: - bitcoind: - condition: service_healthy - entrypoint: /bin/sh - command: - - -c - - | - apk add --no-cache curl jq - echo "Starting auto-miner (1 block every 30 seconds)..." - while true; do - # Create wallet if not exists - curl -s --user bitspire:bitspire --data-binary '{"jsonrpc":"1.0","method":"createwallet","params":["miner"]}' http://bitcoind:18443/ > /dev/null 2>&1 - # Mine a block - ADDR=$$(curl -s --user bitspire:bitspire --data-binary '{"jsonrpc":"1.0","method":"getnewaddress","params":[]}' http://bitcoind:18443/ | jq -r '.result // empty') - if [ -n "$$ADDR" ]; then - curl -s --user bitspire:bitspire --data-binary "{\"jsonrpc\":\"1.0\",\"method\":\"generatetoaddress\",\"params\":[1,\"$$ADDR\"]}" http://bitcoind:18443/ > /dev/null - fi - sleep 30 - done - restart: unless-stopped - profiles: - - mining # Only starts with: docker compose --profile mining up -d - - # PostgreSQL for optional server-side state - postgres: - image: postgres:16-alpine - container_name: bitspire-postgres - ports: - - '5432:5432' - environment: - POSTGRES_DB: bitspire_dev - POSTGRES_USER: bitspire - POSTGRES_PASSWORD: bitspire_dev_password # pragma: allowlist secret - volumes: - - postgres-data:/var/lib/postgresql/data - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U bitspire -d bitspire_dev'] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - -volumes: - strfry-data: - bitcoind-data: - lnd-data: - lnd-alice-data: - lightning-pub-data: - postgres-data: diff --git a/docker/docker-compose.regtest.yml b/docker/docker-compose.regtest.yml deleted file mode 100644 index 43ca8a0..0000000 --- a/docker/docker-compose.regtest.yml +++ /dev/null @@ -1,166 +0,0 @@ -# bitSpire - Regtest Integration -# -# This overlay connects bitspire-next services to the comprehensive regtest -# environment at ~/dev/local/docker/regtest -# -# Usage: -# 1. Start regtest (minimal — only what LP needs): -# cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4 -# -# 2. Start bitspire services: -# cd docker && docker compose -f docker-compose.regtest.yml up -d -# -# 3. Bootstrap (funds LND, opens channels, creates LP app): -# cd docker && ./regtest-bootstrap.sh -# -# 4. Configure apps/machine/.env: -# VITE_RELAY_URL=ws://localhost:7777 -# VITE_EXTENSION_API_URL=http://localhost:1777 -# -# Phone wallet testing (LNURL callbacks need LAN IP): -# HOST_IP=192.168.1.100 docker compose -f docker-compose.regtest.yml up -d -# -# Services: -# - strfry: Private Nostr relay (port 7777) -# - lightning-pub: Nostr-native Lightning account system (port 1776) -# - Uses lnd-4 from regtest as backend -# - Withdraw extension on port 1777 -# - miner: Auto-mines blocks to keep Lightning channels active -# - -services: - # Private Nostr relay for ATM communication - strfry: - image: ghcr.io/hoytech/strfry:latest - container_name: bitspire-relay - ports: - - '7777:7777' - volumes: - - ./strfry.conf:/etc/strfry.conf:ro - - strfry-data:/app/strfry-db - ulimits: - nofile: - soft: 524288 - hard: 524288 - healthcheck: - # Check port 7777 (0x1E61) is listening via procfs — BusyBox nc lacks -z flag - test: ['CMD-SHELL', 'grep -q ":1E61 " /proc/net/tcp'] - interval: 5s - timeout: 3s - retries: 10 - restart: unless-stopped - networks: - - regtest - - # Lightning.Pub - Nostr-native Lightning account system - # Connects to lnd-4 from the regtest environment - # Use LIGHTNING_PUB_IMAGE env var to specify image (default: lightning-pub-withdraw) - lightning-pub: - image: ${LIGHTNING_PUB_IMAGE:-lightning-pub-withdraw:latest} - container_name: bitspire-lightning-pub - extra_hosts: - - 'host.docker.internal:host-gateway' - ports: - - '1776:1776' - - '1777:1777' # Withdraw extension HTTP API - volumes: - - lightning-pub-data:/root/lightning_pub - # Override Dockerfile's anonymous /app/data volume with named volume - - lightning-pub-appdata:/app/data - # Mount lnd-4 data from regtest for macaroons/certs - - ${REGTEST_DATA_DIR:-/home/padreug/dev/local/docker/regtest/data}/lnd-4:/root/.lnd:ro - environment: - - NETWORK=regtest - # lnd-4 is accessible via Docker network - - LND_ADDRESS=lnd-4:10009 - - LND_CERT_PATH=/root/.lnd/tls.cert - - LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon - # Use strfry from this compose - - NOSTR_RELAYS=ws://strfry:7777 - # Disable external liquidity provider for regtest - - DISABLE_LIQUIDITY_PROVIDER=true - # Admin token for HTTP API access (development only) - - ADMIN_TOKEN=bitspire-dev-admin-token - # Extension HTTP API URL (for LNURL callbacks from external wallets) - # Use HOST_IP env var for your machine's LAN IP (required for phone wallets) - - EXTENSION_SERVICE_URL=http://${HOST_IP:-localhost}:1777 - depends_on: - strfry: - condition: service_healthy - healthcheck: - test: ['CMD-SHELL', 'curl -so /dev/null -w "%{http_code}" http://localhost:1776 | grep -q .'] - interval: 10s - timeout: 5s - retries: 30 - start_period: 30s - restart: unless-stopped - networks: - - regtest - - # Auto-miner for regtest (mines 1 block every MINE_INTERVAL seconds) - # Keeps Lightning channels active during development - miner: - image: boltz/bitcoin-core:25.0 - container_name: bitspire-miner - entrypoint: /bin/sh - command: - - -c - - | - echo "Auto-miner started (interval: $${MINE_INTERVAL}s)" - # Wait for bitcoind to be ready - while ! bitcoin-cli -regtest -rpcconnect=bitcoind getblockchaininfo > /dev/null 2>&1; do - echo "Waiting for bitcoind..." - sleep 5 - done - # Ensure a wallet is loaded (-generate requires one) - bitcoin-cli -regtest -rpcconnect=bitcoind createwallet default 2>/dev/null \ - || bitcoin-cli -regtest -rpcconnect=bitcoind loadwallet default 2>/dev/null \ - || true - echo "bitcoind ready, starting mining loop" - while true; do - bitcoin-cli -regtest -rpcconnect=bitcoind -generate 1 > /dev/null 2>&1 && echo "Mined block $(bitcoin-cli -regtest -rpcconnect=bitcoind getblockcount)" - sleep $${MINE_INTERVAL} - done - environment: - - MINE_INTERVAL=${MINE_INTERVAL:-30} - volumes: - - bitcoin-data:/root/.bitcoin - restart: unless-stopped - networks: - - regtest - - # PostgreSQL for optional server-side state - postgres: - image: postgres:16-alpine - container_name: bitspire-postgres - ports: - - '5432:5432' - environment: - POSTGRES_DB: bitspire_dev - POSTGRES_USER: bitspire - POSTGRES_PASSWORD: bitspire_dev_password # pragma: allowlist secret - volumes: - - postgres-data:/var/lib/postgresql/data - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U bitspire -d bitspire_dev'] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - networks: - - regtest - -volumes: - strfry-data: - lightning-pub-data: - lightning-pub-appdata: - postgres-data: - # Mount the bitcoin-data volume from the regtest environment - bitcoin-data: - external: true - name: regtest_bitcoin-data - -networks: - regtest: - name: regtest_default - external: true diff --git a/docker/regtest-bootstrap.sh b/docker/regtest-bootstrap.sh deleted file mode 100755 index ec1af77..0000000 --- a/docker/regtest-bootstrap.sh +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/env bash -# regtest-bootstrap.sh — Bootstrap a working LP+LND regtest environment -# -# Prerequisites: -# 1. Regtest running (minimal — only needs bitcoind + lnd-1 + lnd-4): -# cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4 -# 2. bitSpire services running: -# cd docker && docker compose -f docker-compose.regtest.yml up -d -# -# What this script does: -# 1. Waits for bitcoind and LND nodes to be synced -# 2. Funds LND-1 (1 BTC from bitcoind) -# 3. Opens a 5M sat channel from LND-1 → LND-4 (LP's backend) -# 4. Mines blocks to confirm and announce the channel -# 5. Waits for LP to be healthy -# 6. Creates an LP app and funds it (10k sats via invoice from LND-1) -# 7. Prints summary with pubkeys, app token, and channel status - -set -euo pipefail - -# -- Colors -- -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -info() { echo -e "${CYAN}[INFO]${NC} $*"; } -ok() { echo -e "${GREEN}[OK]${NC} $*"; } -warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -err() { echo -e "${RED}[ERROR]${NC} $*"; } - -# -- Auto-detect container name prefix -- -# The regtest compose uses project name "lnbits" (via start-regtest) or "regtest" (direct). -# Detect which prefix the RUNNING bitcoind container has (filter=running avoids stale containers). -if docker ps -f name=lnbits-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then - PREFIX="lnbits" -elif docker ps -f name=regtest-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then - PREFIX="regtest" -else - err "No running bitcoind container found (tried lnbits-bitcoind-1 and regtest-bitcoind-1)" - err "Start regtest first: cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4" - exit 1 -fi -info "Using container prefix: ${PREFIX}" - -# -- Container helpers -- -bitcoin_cli() { - docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest "$@" -} - -lncli_1() { - docker exec "${PREFIX}-lnd-1-1" lncli --network regtest --rpcserver=lnd-1:10009 "$@" -} - -lncli_4() { - docker exec "${PREFIX}-lnd-4-1" lncli --network regtest --rpcserver=lnd-4:10009 "$@" -} - -LP_URL="http://localhost:1776" -LP_EXT_URL="http://localhost:1777" -ADMIN_TOKEN="bitspire-dev-admin-token" -CHANNEL_SIZE=5000000 # 5M sats -FUND_AMOUNT=10000 # 10k sats for LP app - -# -- Step 1: Wait for bitcoind -- -info "Waiting for bitcoind..." -for i in $(seq 1 60); do - if bitcoin_cli getblockchaininfo > /dev/null 2>&1; then - ok "bitcoind ready (block $(bitcoin_cli getblockcount))" - break - fi - if [ "$i" -eq 60 ]; then - err "bitcoind not ready after 60s — is regtest running?" - exit 1 - fi - sleep 1 -done - -# Ensure a wallet exists -bitcoin_cli createwallet default 2>/dev/null \ - || bitcoin_cli loadwallet default 2>/dev/null \ - || true - -# Mine initial blocks if needed (coinbase needs 100 confirmations to be spendable) -BLOCKS=$(bitcoin_cli getblockcount) -if [ "$BLOCKS" -lt 101 ]; then - NEEDED=$((101 - BLOCKS)) - info "Mining ${NEEDED} initial blocks (coinbase needs 100 confirmations)..." - bitcoin_cli -generate "$NEEDED" > /dev/null - ok "Mined to block $(bitcoin_cli getblockcount)" -fi - -# -- Step 2: Wait for LND-1 and LND-4 to sync -- -wait_lnd_sync() { - local name=$1 - local cli_fn=$2 - info "Waiting for ${name} to sync..." - for i in $(seq 1 120); do - if [ "$($cli_fn getinfo 2>/dev/null | jq -r '.synced_to_chain' 2>/dev/null)" = "true" ]; then - ok "${name} synced" - return 0 - fi - if [ "$i" -eq 120 ]; then - err "${name} not synced after 120s" - exit 1 - fi - sleep 1 - done -} - -wait_lnd_sync "LND-1" lncli_1 -wait_lnd_sync "LND-4" lncli_4 - -# -- Step 3: Fund LND-1 -- -info "Funding LND-1..." -LND1_ADDR=$(lncli_1 newaddress p2wkh | jq -r '.address') -bitcoin_cli -named sendtoaddress address="$LND1_ADDR" amount=1 fee_rate=100 > /dev/null -info "Mining 6 blocks to confirm funding..." -bitcoin_cli -generate 6 > /dev/null - -# Wait for LND-1 to see the confirmed balance -for i in $(seq 1 30); do - BALANCE=$(lncli_1 walletbalance | jq -r '.confirmed_balance') - if [ "$BALANCE" != "0" ] && [ -n "$BALANCE" ]; then - ok "LND-1 funded: ${BALANCE} sats" - break - fi - sleep 1 -done - -# -- Step 4: Connect LND-1 → LND-4 and open channel -- -LND4_PUBKEY=$(lncli_4 getinfo | jq -r '.identity_pubkey') -info "Connecting LND-1 → LND-4 (${LND4_PUBKEY:0:16}...)..." -lncli_1 connect "${LND4_PUBKEY}@${PREFIX}-lnd-4-1:9735" > /dev/null 2>&1 || true - -# Check if channel already exists -EXISTING=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq '.channels | length') -if [ "${EXISTING:-0}" -gt 0 ]; then - warn "Channel to LND-4 already exists, skipping open" -else - info "Opening ${CHANNEL_SIZE} sat channel LND-1 → LND-4..." - lncli_1 openchannel "$LND4_PUBKEY" "$CHANNEL_SIZE" > /dev/null - - info "Mining 6 blocks to confirm channel..." - bitcoin_cli -generate 6 > /dev/null - - # Wait for channel to leave pending - for i in $(seq 1 60); do - PENDING=$(lncli_1 pendingchannels | jq '.pending_open_channels | length') - if [ "$PENDING" = "0" ]; then - break - fi - sleep 1 - done -fi - -# -- Step 5: Verify channel is active and in graph -- -info "Waiting for channel to become active..." -for i in $(seq 1 60); do - ACTIVE=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq '[.channels[] | select(.active == true)] | length') - if [ "${ACTIVE:-0}" -gt 0 ]; then - ok "Channel active" - break - fi - if [ "$i" -eq 60 ]; then - warn "Channel not active after 60s — may need more blocks" - fi - sleep 1 -done - -# Mine a few more blocks to ensure graph announcement propagates -bitcoin_cli -generate 3 > /dev/null - -info "Checking graph..." -for i in $(seq 1 30); do - GRAPH_NODES=$(lncli_1 describegraph 2>/dev/null | jq '.nodes | length') - GRAPH_EDGES=$(lncli_1 describegraph 2>/dev/null | jq '.edges | length') - if [ "${GRAPH_EDGES:-0}" -gt 0 ]; then - ok "Graph: ${GRAPH_NODES} nodes, ${GRAPH_EDGES} edges" - break - fi - if [ "$i" -eq 30 ]; then - warn "Graph has no edges yet — channel may not be announced" - fi - sleep 2 -done - -# -- Step 6: Wait for Lightning.Pub to be healthy -- -info "Waiting for Lightning.Pub..." -for i in $(seq 1 120); do - if curl -so /dev/null "${LP_URL}" 2>/dev/null; then - ok "Lightning.Pub ready" - break - fi - if [ "$i" -eq 120 ]; then - err "Lightning.Pub not ready after 120s" - err "Check: docker logs bitspire-lightning-pub" - exit 1 - fi - sleep 1 -done - -# Small delay to ensure LP has fully initialized its LND connection -sleep 5 - -# -- Step 7: Create LP app -- -info "Creating LP app 'regtest-atm'..." -APP_RESPONSE=$(curl -sf "${LP_URL}/api/admin/app/add" \ - -H "Authorization: Bearer ${ADMIN_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"name": "regtest-atm", "allow_user_creation": true}') - -if [ -z "$APP_RESPONSE" ]; then - err "Failed to create LP app — check LP logs" - exit 1 -fi - -APP_ID=$(echo "$APP_RESPONSE" | jq -r '.app.id') -APP_NPUB=$(echo "$APP_RESPONSE" | jq -r '.app.npub') -APP_TOKEN=$(echo "$APP_RESPONSE" | jq -r '.auth_token') - -if [ "$APP_ID" = "null" ] || [ -z "$APP_ID" ]; then - err "App creation returned unexpected response:" - echo "$APP_RESPONSE" | jq . - exit 1 -fi - -ok "App created: id=${APP_ID}" - -# -- Step 8: Fund the app (create invoice via LP, pay from LND-1) -- -info "Funding app with ${FUND_AMOUNT} sats..." -INVOICE_RESPONSE=$(curl -sf "${LP_URL}/api/app/add/invoice" \ - -H "Authorization: Bearer ${APP_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"payer_identifier\": \"bootstrap\", \"http_callback_url\": \"\", \"invoice_req\": {\"amountSats\": ${FUND_AMOUNT}, \"memo\": \"regtest bootstrap\"}}") - -INVOICE=$(echo "$INVOICE_RESPONSE" | jq -r '.invoice') -if [ "$INVOICE" = "null" ] || [ -z "$INVOICE" ]; then - warn "Failed to create invoice — app has 0 balance" - warn "Response: $INVOICE_RESPONSE" -else - # Pay from LND-1 - PAY_RESULT=$(lncli_1 payinvoice --force "$INVOICE" 2>&1) || true - PAY_STATUS=$(echo "$PAY_RESULT" | jq -r '.status' 2>/dev/null || echo "") - if [ "$PAY_STATUS" = "SUCCEEDED" ] || echo "$PAY_RESULT" | grep -q "SUCCEEDED"; then - ok "App funded with ${FUND_AMOUNT} sats" - else - warn "Payment may have failed — check manually" - warn "Result: $(echo "$PAY_RESULT" | tail -3)" - fi -fi - -# -- Summary -- -# LP uses LND-4 as its backend, so LP pubkey = LND-4 pubkey -echo "" -echo -e "${GREEN}========================================${NC}" -echo -e "${GREEN} Regtest Bootstrap Complete${NC}" -echo -e "${GREEN}========================================${NC}" -echo "" -echo -e " ${CYAN}LND-1 pubkey:${NC} $(lncli_1 getinfo | jq -r '.identity_pubkey')" -echo -e " ${CYAN}LP (LND-4):${NC} ${LND4_PUBKEY}" -echo "" -echo -e " ${CYAN}LP app ID:${NC} ${APP_ID}" -echo -e " ${CYAN}LP app token:${NC} ${APP_TOKEN}" -echo "" -echo -e " ${CYAN}LP URL:${NC} ${LP_URL}" -echo -e " ${CYAN}Extension URL:${NC} ${LP_EXT_URL}" -echo -e " ${CYAN}Relay URL:${NC} ws://localhost:7777" -echo "" -CHAN_INFO=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq -r '.channels[0] | "\(.local_balance) / \(.capacity) sats (active: \(.active))"' 2>/dev/null || echo "unknown") -echo -e " ${CYAN}Channel:${NC} LND-1 → LND-4: ${CHAN_INFO}" -echo "" diff --git a/docker/regtest.sh b/docker/regtest.sh deleted file mode 100755 index 1c198a0..0000000 --- a/docker/regtest.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env bash -# regtest.sh — Manage the LP+LND regtest development environment -# -# Usage: -# ./regtest.sh up Start everything + bootstrap -# ./regtest.sh down Stop everything -# ./regtest.sh reset Full teardown (volumes + data) and fresh start -# ./regtest.sh status Show container status -# ./regtest.sh logs [svc] Tail logs (lp, relay, miner, lnd1, lnd4, bitcoind) -# ./regtest.sh lncli N Run lncli on LND-N (e.g. ./regtest.sh lncli 1 getinfo) -# ./regtest.sh bitcoin Run bitcoin-cli (e.g. ./regtest.sh bitcoin getblockcount) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}" -COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.regtest.yml" - -RED='\033[0;31m' -GREEN='\033[0;32m' -CYAN='\033[0;36m' -NC='\033[0m' - -# -- Detect container prefix (regtest- or lnbits-) -- -detect_prefix() { - if docker ps -f name=regtest-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then - echo "regtest" - elif docker ps -f name=lnbits-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then - echo "lnbits" - else - echo "" - fi -} - -cmd_up() { - echo -e "${CYAN}Starting regtest (bitcoind + lnd-1 + lnd-4)...${NC}" - cd "$REGTEST_DIR" && docker compose up -d bitcoind lnd-1 lnd-4 - - echo -e "${CYAN}Starting bitspire services...${NC}" - docker compose -f "$COMPOSE_FILE" up -d - - echo -e "${CYAN}Running bootstrap...${NC}" - "$SCRIPT_DIR/regtest-bootstrap.sh" -} - -cmd_down() { - echo -e "${CYAN}Stopping bitspire services...${NC}" - docker compose -f "$COMPOSE_FILE" down 2>/dev/null || true - - echo -e "${CYAN}Stopping regtest...${NC}" - cd "$REGTEST_DIR" && docker compose down 2>/dev/null || true - - echo -e "${GREEN}All stopped.${NC}" -} - -cmd_reset() { - echo -e "${CYAN}Full reset: tearing down everything + wiping data...${NC}" - - docker compose -f "$COMPOSE_FILE" down -v 2>/dev/null || true - cd "$REGTEST_DIR" && docker compose down -v 2>/dev/null || true - - # Clean LND data (root-owned from containers) - docker run --rm -v "$REGTEST_DIR/data:/data" alpine sh -c 'rm -rf /data/lnd-1/* /data/lnd-4/*' 2>/dev/null || true - - echo -e "${GREEN}Clean. Starting fresh...${NC}" - cmd_up -} - -cmd_status() { - echo -e "${CYAN}Containers:${NC}" - docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'regtest|bitspire|NAMES' | sort - - PREFIX=$(detect_prefix) - if [ -n "$PREFIX" ]; then - echo "" - BLOCK=$(docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest getblockcount 2>/dev/null || echo "?") - echo -e " ${CYAN}Block height:${NC} ${BLOCK}" - - for N in 1 4; do - SYNCED=$(docker exec "${PREFIX}-lnd-${N}-1" lncli --network regtest --rpcserver="lnd-${N}:10009" getinfo 2>/dev/null | jq -r '.synced_to_chain' 2>/dev/null || echo "?") - echo -e " ${CYAN}LND-${N} synced:${NC} ${SYNCED}" - done - - LP_STATUS=$(curl -so /dev/null -w "%{http_code}" http://localhost:1776 2>/dev/null || echo "down") - echo -e " ${CYAN}LP status:${NC} ${LP_STATUS}" - - EXT_STATUS=$(curl -so /dev/null -w "%{http_code}" http://localhost:1777 2>/dev/null || echo "down") - echo -e " ${CYAN}Extension:${NC} ${EXT_STATUS}" - fi -} - -cmd_logs() { - local svc="${1:-lp}" - case "$svc" in - lp|lightning-pub) docker logs -f --tail 50 bitspire-lightning-pub ;; - relay|strfry) docker logs -f --tail 50 bitspire-relay ;; - miner) docker logs -f --tail 50 bitspire-miner ;; - lnd1|lnd-1) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-1-1" ;; - lnd4|lnd-4) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-4-1" ;; - bitcoind) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-bitcoind-1" ;; - *) echo "Unknown service: $svc (try: lp, relay, miner, lnd1, lnd4, bitcoind)"; exit 1 ;; - esac -} - -cmd_lncli() { - local N="$1"; shift - PREFIX=$(detect_prefix) - if [ -z "$PREFIX" ]; then - echo -e "${RED}No running regtest found${NC}"; exit 1 - fi - docker exec "${PREFIX}-lnd-${N}-1" lncli --network regtest --rpcserver="lnd-${N}:10009" "$@" -} - -cmd_bitcoin() { - PREFIX=$(detect_prefix) - if [ -z "$PREFIX" ]; then - echo -e "${RED}No running regtest found${NC}"; exit 1 - fi - docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest "$@" -} - -# -- Main -- -case "${1:-help}" in - up) cmd_up ;; - down) cmd_down ;; - reset) cmd_reset ;; - status) cmd_status ;; - logs) shift; cmd_logs "${1:-lp}" ;; - lncli) shift; cmd_lncli "$@" ;; - bitcoin) shift; cmd_bitcoin "$@" ;; - *) - echo "Usage: $0 {up|down|reset|status|logs|lncli|bitcoin}" - echo "" - echo " up Start everything + bootstrap" - echo " down Stop everything" - echo " reset Full teardown + fresh start" - echo " status Show container & service status" - echo " logs [s] Tail logs (lp, relay, miner, lnd1, lnd4, bitcoind)" - echo " lncli N Run lncli on LND-N (e.g. $0 lncli 1 getinfo)" - echo " bitcoin Run bitcoin-cli (e.g. $0 bitcoin getblockcount)" - ;; -esac diff --git a/docker/start-with-regtest.sh b/docker/start-with-regtest.sh deleted file mode 100755 index 93aff63..0000000 --- a/docker/start-with-regtest.sh +++ /dev/null @@ -1,335 +0,0 @@ -#!/bin/bash -# -# Start bitspire-next development environment with comprehensive regtest -# -# This script: -# 1. Connects to the regtest environment at ~/dev/local/docker/regtest -# 2. Starts Lightning.Pub with the specified image/worktree -# 3. Creates and funds an ATM account -# 4. Displays connection info for Zeus wallet and Lightning.Pub nprofile -# -# Prerequisites: -# ~/dev/local/docker/regtest must be running: -# cd ~/dev/local/docker/regtest && ./start-regtest -# -# Usage: -# ./start-with-regtest.sh # Start with default image -# ./start-with-regtest.sh --image myimage # Use specific Docker image -# ./start-with-regtest.sh --worktree ~/path # Build from worktree -# ./start-with-regtest.sh down # Stop services -# ./start-with-regtest.sh logs # Follow logs -# ./start-with-regtest.sh status # Show connection info -# - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}" -DEFAULT_IMAGE="lightning-pub-withdraw:latest" -ATM_FUNDING_SATS=100000 - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log() { echo -e "${GREEN}[bitspire]${NC} $1"; } -warn() { echo -e "${YELLOW}[bitspire]${NC} $1"; } -error() { echo -e "${RED}[bitspire]${NC} $1"; } -info() { echo -e "${CYAN}[bitspire]${NC} $1"; } - -# Get local IP for external wallet access -get_local_ip() { - ip route get 1 2>/dev/null | awk '{print $7; exit}' || hostname -I | awk '{print $1}' -} - -# Check if regtest is running -check_regtest() { - if ! docker network inspect lnbits_default &>/dev/null; then - error "Regtest network not found!" - echo "" - echo "Start the regtest environment first:" - echo " cd $REGTEST_DIR && ./start-regtest" - exit 1 - fi - - # Check if lnd-4 is running (Lightning.Pub's backend) - if ! docker exec lnbits-lnd-4-1 lncli --network=regtest getinfo &>/dev/null 2>&1; then - warn "lnd-4 not responding. Waiting..." - sleep 5 - fi -} - -# Build Lightning.Pub from worktree -build_from_worktree() { - local worktree="$1" - local image_name="lightning-pub-custom:latest" - - if [[ ! -d "$worktree" ]]; then - error "Worktree not found: $worktree" - exit 1 - fi - - log "Building Lightning.Pub from $worktree..." - docker build -t "$image_name" "$worktree" - echo "$image_name" -} - -# Wait for Lightning.Pub to be ready and get nprofile -wait_for_lightning_pub() { - log "Waiting for Lightning.Pub to start..." - local max_attempts=30 - local attempt=0 - - while [[ $attempt -lt $max_attempts ]]; do - if docker logs bitspire-lightning-pub 2>&1 | grep -q "LightningPub listening"; then - sleep 2 # Extra time for Nostr middleware - return 0 - fi - attempt=$((attempt + 1)) - sleep 2 - done - - error "Lightning.Pub failed to start within 60 seconds" - docker logs bitspire-lightning-pub 2>&1 | tail -20 - return 1 -} - -# Get Lightning.Pub nprofile from logs -get_nprofile() { - docker logs bitspire-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1 -} - -# Get Lightning.Pub pubkey from logs -get_pubkey() { - docker logs bitspire-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1 -} - -# Generate lndconnect URL for Zeus (using lnd-3 which has REST exposed) -generate_lndconnect() { - local lnd_container="lnbits-lnd-3-1" - local lnd_data="$REGTEST_DIR/data/lnd-3" - local local_ip=$(get_local_ip) - local rest_port=8082 # lnd-3's REST port - - # Get cert and macaroon - local cert_path="$lnd_data/tls.cert" - local mac_path="$lnd_data/data/chain/bitcoin/regtest/admin.macaroon" - - if [[ ! -f "$cert_path" ]] || [[ ! -f "$mac_path" ]]; then - warn "lnd-3 credentials not found. Zeus connection string unavailable." - return 1 - fi - - # Base64url encode (replace + with -, / with _, remove =) - local cert_b64=$(base64 -w0 "$cert_path" | tr '+/' '-_' | tr -d '=') - local mac_b64=$(base64 -w0 "$mac_path" | tr '+/' '-_' | tr -d '=') - - echo "lndconnect://${local_ip}:${rest_port}?cert=${cert_b64}&macaroon=${mac_b64}" -} - -# Create and fund ATM account -setup_atm_account() { - log "Setting up ATM account..." - - # Create app - local app_response=$(curl -s -X POST http://localhost:1776/api/admin/app/add \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer bitspire-dev-admin-token" \ - -d '{"name":"atm-app","allow_user_creation":true}' 2>/dev/null) - - if echo "$app_response" | grep -q '"status":"OK"'; then - local app_id=$(echo "$app_response" | grep -oP '"id":"\K[^"]+') - local app_token=$(echo "$app_response" | grep -oP '"auth_token":"\K[^"]+') - log "Created ATM app: $app_id" - - # Store app token for later use - echo "$app_token" > "$SCRIPT_DIR/.atm-app-token" - echo "$app_id" > "$SCRIPT_DIR/.atm-app-id" - - # TODO: Fund the app by creating an invoice and paying from lnd-3 - # This requires the app to support invoice creation via HTTP API - # For now, the app starts with 0 balance - - return 0 - else - warn "Failed to create ATM app: $app_response" - return 1 - fi -} - -# Display connection information -show_connection_info() { - local nprofile=$(get_nprofile) - local pubkey=$(get_pubkey) - local local_ip=$(get_local_ip) - local lndconnect=$(generate_lndconnect 2>/dev/null || echo "") - - echo "" - echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" - echo -e "${BOLD} BITSPIRE REGTEST ENVIRONMENT ${NC}" - echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" - echo "" - - echo -e "${CYAN}Lightning.Pub:${NC}" - echo -e " Pubkey: ${GREEN}$pubkey${NC}" - echo -e " nprofile: ${GREEN}$nprofile${NC}" - echo "" - - echo -e "${CYAN}Service URLs:${NC}" - echo " Nostr Relay: ws://localhost:7777" - echo " Lightning.Pub: http://localhost:1776" - echo " Withdraw API: http://localhost:1777" - echo " PostgreSQL: localhost:5432" - echo "" - - echo -e "${CYAN}External Access (from phone/other devices):${NC}" - echo " Nostr Relay: ws://${local_ip}:7777" - echo " Withdraw API: http://${local_ip}:1777" - echo "" - - if [[ -n "$lndconnect" ]]; then - echo -e "${CYAN}Zeus Wallet Connection (lnd-3):${NC}" - echo -e " ${GREEN}$lndconnect${NC}" - echo "" - echo " Scan this with Zeus to connect to lnd-3 for testing payments." - echo "" - fi - - echo -e "${CYAN}ATM Configuration (apps/machine/.env):${NC}" - echo " VITE_RELAY_URL=ws://localhost:7777" - echo " VITE_LIGHTNING_PUB_PUBKEY=$pubkey" - echo " VITE_EXTENSION_API_URL=http://localhost:1777" - echo "" - - echo -e "${CYAN}CLI Helpers:${NC}" - echo " source $REGTEST_DIR/docker-scripts.sh" - echo " bitcoin-cli-sim -generate 1 # Mine blocks" - echo " lncli-sim 4 getinfo # lnd-4 (Lightning.Pub)" - echo " lncli-sim 3 getinfo # lnd-3 (Zeus wallet)" - echo "" - echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" -} - -# Start services -start() { - local image="$DEFAULT_IMAGE" - local worktree="" - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --image) - image="$2" - shift 2 - ;; - --worktree) - worktree="$2" - shift 2 - ;; - *) - shift - ;; - esac - done - - log "Checking prerequisites..." - check_regtest - - # Build from worktree if specified - if [[ -n "$worktree" ]]; then - image=$(build_from_worktree "$worktree") - fi - - # Check if image exists - if ! docker image inspect "$image" &>/dev/null; then - error "Docker image not found: $image" - echo "" - echo "Either:" - echo " 1. Build the image: docker build -t $image " - echo " 2. Use --worktree: ./start-with-regtest.sh --worktree ~/path/to/lightning-pub" - exit 1 - fi - - log "Using Lightning.Pub image: $image" - - # Export environment variables - export REGTEST_DATA_DIR="$REGTEST_DIR/data" - export LIGHTNING_PUB_IMAGE="$image" - export HOST_IP=$(get_local_ip) - - log "Starting bitspire services..." - docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" up -d - - # Wait for Lightning.Pub and setup - if wait_for_lightning_pub; then - setup_atm_account || true - show_connection_info - else - error "Failed to start Lightning.Pub. Check logs with: $0 logs lightning-pub" - exit 1 - fi -} - -# Stop services -stop() { - log "Stopping bitspire services..." - docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" down - rm -f "$SCRIPT_DIR/.atm-app-token" "$SCRIPT_DIR/.atm-app-id" - log "Services stopped." -} - -# Show logs -logs() { - docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" logs -f "$@" -} - -# Show status/connection info -status() { - if ! docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub; then - error "Services not running. Start with: $0 start" - exit 1 - fi - show_connection_info -} - -# Main -case "${1:-start}" in - start) - shift || true - start "$@" - ;; - stop|down) - stop - ;; - logs) - shift - logs "$@" - ;; - status|info) - status - ;; - *) - echo "Usage: $0 [command] [options]" - echo "" - echo "Commands:" - echo " start Start services (default)" - echo " stop, down Stop services" - echo " logs [service] Follow logs" - echo " status, info Show connection info" - echo "" - echo "Options for 'start':" - echo " --image Use specific Docker image" - echo " --worktree Build from Lightning.Pub worktree" - echo "" - echo "Examples:" - echo " $0 # Start with default image" - echo " $0 --worktree ~/dev/lightning-pub/withdraw # Build and use worktree" - echo " $0 --image lightning-pub-custom:v1 # Use specific image" - exit 1 - ;; -esac diff --git a/docker/strfry.conf b/docker/strfry.conf deleted file mode 100644 index 30e6dc0..0000000 --- a/docker/strfry.conf +++ /dev/null @@ -1,65 +0,0 @@ -## -## strfry configuration for bitSpire ATM development -## - -relay { - bind = "0.0.0.0" - port = 7777 - - # Set to 0 to skip configuring nofiles limit (avoids container ulimit issues) - nofiles = 0 - - info { - name = "bitSpire Dev Relay" - description = "Private Nostr relay for ATM development and testing" - pubkey = "" - contact = "" - } - - # Maximum message size in bytes - maxWebsocketPayloadSize = 131072 - - # Connection limits - maxWebsockets = 100 - maxConnections = 1000 -} - -# Event policies -events { - # Maximum event size - maxEventSize = 65536 - - # Rate limiting - rejectEventsNewerThanSeconds = 900 - rejectEventsOlderThanSeconds = 94608000 - - # Event kinds we care about - # 14: Private DMs (NIP-17) - # 21000: Lightning.Pub RPC (generic request/response) - # 21001-21003: CLINK events (Offer, Debit, Manage) - # 22242: NIP-42 auth - # 30078: Service Beacon (replaceable, service discovery) - # 30079: Transaction records (replaceable) - - # TODO: Review if these ephemeral event settings actually do anything useful. - # CLINK events (21000-21003) are in the ephemeral range but need to persist - # long enough for request/response flows. These settings may not be recognized - # by strfry - verify against strfry documentation. - # Allow ephemeral events up to 5 minutes old to be accepted - rejectEphemeralEventsOlderThanSeconds = 300 - # Keep ephemeral events for 5 minutes before deletion - ephemeralEventsLifetimeSeconds = 300 -} - -# Negentropy sync (for relay federation) -negentropy { - enabled = true - syncOnConnect = false -} - -# Plugins (for NIP-42 auth in production) -# plugins { -# authRequired = true -# writePolicy = "accept" -# readPolicy = "accept" -# } diff --git a/docs/ndebit-cash-in-flow.md b/docs/ndebit-cash-in-flow.md index 2119bf0..c8d5034 100644 --- a/docs/ndebit-cash-in-flow.md +++ b/docs/ndebit-cash-in-flow.md @@ -9,6 +9,11 @@ > debit-approval listener and all kind-21002 handling were removed in > commit `3c14eea` (the 3b.4 cleanup of LP-paired infrastructure). > +> The Lightning.Pub regtest tooling this doc leans on — the `docker/` +> compose stack, the `fund-atm` / `lncli` devenv commands, and the +> `packages/nostr-client/dev/` agent scripts — was removed on 2026-10-09. +> Commands quoted below no longer exist in this repo. +> > This doc is retained because it explains *why* the previous flow > existed and what the ndebit/CLINK protocol surface looks like — useful > if the project ever wants to reintroduce nostr-native cash-in that diff --git a/packages/nostr-client/dev/atm-debit-agent.mjs b/packages/nostr-client/dev/atm-debit-agent.mjs deleted file mode 100644 index c40c586..0000000 --- a/packages/nostr-client/dev/atm-debit-agent.mjs +++ /dev/null @@ -1,270 +0,0 @@ -/** - * ATM Debit Authorization Agent - * - * This script demonstrates how an ATM can act as the authorization authority - * for CLINK debit requests, similar to an "admin macaroon" for Lightning. - * - * Flow: - * 1. Generate keypair for ATM (or load from secure storage) - * 2. Link keypair to Lightning.Pub user account - * 3. Subscribe to live debit requests - * 4. Auto-approve requests (within configured limits) - * - * Prerequisites: - * - Get a linking token from Lightning.Pub HTTP API - * - Run: curl -X POST "http://localhost:1776/api/app/user/npub/token/reset" \ - * -H "Authorization: Bearer $APP_TOKEN" \ - * -H "Content-Type: application/json" \ - * -d '{"user_identifier": "YOUR_USER_IDENTIFIER"}' - */ - -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools' -import * as nip44v1 from './nip44v1.mjs' - -// Configuration -const LINKING_TOKEN = process.argv[2] -const LIGHTNING_PUB_PUBKEY = '6c59284e3da31b776cb1c06324c25f4a0b0308177af9f8aec5ebef07b44c3fdf' -const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:7777' - -// Generate ATM keypair (in production, this would be stored securely) -const ATM_PRIVATE_KEY = generateSecretKey() -const ATM_PUBLIC_KEY = getPublicKey(ATM_PRIVATE_KEY) -const ATM_PRIVATE_KEY_HEX = Buffer.from(ATM_PRIVATE_KEY).toString('hex') - -if (!LINKING_TOKEN) { - console.log('ATM Debit Authorization Agent') - console.log('==============================') - console.log('') - console.log('Usage: node atm-debit-agent.mjs ') - console.log('') - console.log('Get a linking token:') - console.log(' curl -X POST "http://localhost:1776/api/app/user/npub/token/reset" \\') - console.log(' -H "Authorization: Bearer $APP_TOKEN" \\') - console.log(' -H "Content-Type: application/json" \\') - console.log(' -d \'{"user_identifier": "YOUR_USER_IDENTIFIER"}\'') - process.exit(1) -} - -console.log('=== ATM Debit Authorization Agent ===') -console.log('') -console.log('ATM Pubkey:', ATM_PUBLIC_KEY) -console.log('Lightning.Pub Pubkey:', LIGHTNING_PUB_PUBKEY) -console.log('Linking Token:', LINKING_TOKEN.substring(0, 16) + '...') -console.log('') - -async function main() { - // Connect to relay - console.log('Connecting to relay...') - const relay = await Relay.connect(RELAY_URL) - console.log('Connected!') - console.log('') - - // Create conversation key for NIP-44 v1 encryption (used by Kind 21000 RPC) - const conversationKey = nip44v1.getConversationKey(ATM_PRIVATE_KEY_HEX, LIGHTNING_PUB_PUBKEY) - - // Step 1: Link NPub through token - console.log('Step 1: Linking ATM keypair to user account...') - - const linkRequest = { - rpcName: 'LinkNPubThroughToken', - authIdentifier: ATM_PUBLIC_KEY, - body: { - token: LINKING_TOKEN, - }, - } - - const linkEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(linkRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - // Subscribe for response - let linkingComplete = false - const linkSub = relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - '#p': [ATM_PUBLIC_KEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - try { - const decrypted = nip44v1.decrypt(evt.content, conversationKey) - const response = JSON.parse(decrypted) - console.log('Link response:', JSON.stringify(response)) - if (response.status === 'OK') { - linkingComplete = true - console.log('Keypair linked successfully!') - } - } catch (err) { - console.log('Failed to decrypt link response:', err.message) - } - }, - } - ) - - await relay.publish(linkEvent) - console.log( - 'Link request sent (event id:', - linkEvent.id.substring(0, 16) + '...), waiting for confirmation...' - ) - - // Wait for linking to complete - for (let i = 0; i < 10 && !linkingComplete; i++) { - await new Promise((r) => setTimeout(r, 1000)) - if (i % 3 === 2) console.log('Still waiting for link confirmation...') - } - linkSub.close() - - if (!linkingComplete) { - console.log('Warning: Did not receive linking confirmation, continuing anyway...') - } - console.log('') - - // Step 2: Subscribe to live debit requests - console.log('Step 2: Subscribing to live debit requests...') - - const subscribeRequest = { - rpcName: 'GetLiveDebitRequests', - authIdentifier: ATM_PUBLIC_KEY, - body: {}, - } - - const subEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(subscribeRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - // Subscribe for debit requests and responses - console.log('Listening for debit requests...') - console.log('(Scan the ndebit QR code with ShockWallet to test)') - console.log('') - - const debitSub = relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - '#p': [ATM_PUBLIC_KEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - async onevent(evt) { - try { - const decrypted = nip44v1.decrypt(evt.content, conversationKey) - const message = JSON.parse(decrypted) - - // Check if this is a debit request (has request_id and debit fields) - if (message.requestId === 'GetLiveDebitRequests' && message.debit) { - console.log('') - console.log('========================================') - console.log('Received debit request!') - console.log(' Request ID:', message.request_id) - console.log(' From npub:', message.npub) - console.log(' Debit type:', message.debit.type) - - if (message.debit.type === 'invoice' && message.debit.invoice) { - console.log(' Invoice:', message.debit.invoice.substring(0, 50) + '...') - - // Auto-approve by responding with INVOICE type - console.log('') - console.log('Auto-approving debit request...') - - const approveRequest = { - rpcName: 'RespondToDebit', - authIdentifier: ATM_PUBLIC_KEY, - body: { - npub: message.npub, - request_id: message.request_id, - response: { - type: 'invoice', - invoice: message.debit.invoice, - }, - }, - } - - const approveEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(approveRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - await relay.publish(approveEvent) - console.log('Approval sent! (event id:', approveEvent.id.substring(0, 16) + '...)') - } else if (message.debit.type === 'budget') { - console.log(' Budget request - ignoring for now') - } else if (message.debit.type === 'fullAccess') { - console.log(' Full access request - ignoring for now') - } - - console.log('========================================') - console.log('') - } else if (message.rpcName) { - // This is a response to our RPC request - console.log('RPC response:', message.rpcName, ':', message.status || 'received') - } else { - // Log other messages for debugging - console.log('Message received:', JSON.stringify(message).substring(0, 100)) - } - } catch (err) { - // Ignore decryption failures (may be messages for other clients) - if (!err.message.includes('Unsupported')) { - console.log('Error processing message:', err.message) - } - } - }, - } - ) - - await relay.publish(subEvent) - console.log('Subscription request sent (event id:', subEvent.id.substring(0, 16) + '...)') - console.log('') - - // Keep running - console.log('ATM Debit Agent running. Press Ctrl+C to exit.') - console.log('') - - // Handle graceful shutdown - process.on('SIGINT', () => { - console.log('\nShutting down...') - debitSub.close() - relay.close() - process.exit(0) - }) - - // Keep alive with heartbeat - let heartbeatCount = 0 - while (true) { - await new Promise((r) => setTimeout(r, 10000)) - heartbeatCount++ - if (heartbeatCount % 6 === 0) { - // Every minute - console.log('Still listening... (' + heartbeatCount * 10 + 's)') - } - } -} - -main().catch((err) => { - console.error('Error:', err.message) - console.error(err.stack) - process.exit(1) -}) diff --git a/packages/nostr-client/dev/fund-dev.mjs b/packages/nostr-client/dev/fund-dev.mjs deleted file mode 100644 index d804067..0000000 --- a/packages/nostr-client/dev/fund-dev.mjs +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env node -/** - * Fund the ATM's Lightning.Pub account via Nostr RPC - * - * This creates an invoice for the ATM user (authenticated via Nostr), - * so the funds go directly to the user that will be queried for balance. - * - * Usage: - * VITE_ATM_PRIVATE_KEY=xxx VITE_LIGHTNING_PUB_PUBKEY=yyy node fund-dev.mjs [amount] - */ -import { NostrClient, loadIdentityFromHex, encryptContent, decryptJSON } from '../dist/index.js' -import { finalizeEvent } from 'nostr-tools' -import { randomUUID } from 'crypto' - -const ATM_PRIVATE_KEY = process.env.VITE_ATM_PRIVATE_KEY -const LIGHTNING_PUB_PUBKEY = process.env.VITE_LIGHTNING_PUB_PUBKEY -const RELAY_URL = process.env.VITE_RELAY_URL || 'ws://localhost:7777' -const APP_ID = process.env.VITE_APP_ID || '' -const FUND_AMOUNT = parseInt(process.argv[2] || process.env.FUND_AMOUNT || '100000', 10) - -if (!ATM_PRIVATE_KEY) { - console.error('Error: VITE_ATM_PRIVATE_KEY environment variable required') - process.exit(1) -} - -if (!LIGHTNING_PUB_PUBKEY) { - console.error('Error: VITE_LIGHTNING_PUB_PUBKEY environment variable required') - process.exit(1) -} - -if (!APP_ID) { - console.error('Error: VITE_APP_ID environment variable required') - console.error('This ensures funds go to the same user as LNURL-withdraw uses') - process.exit(1) -} - -async function main() { - const identity = loadIdentityFromHex(ATM_PRIVATE_KEY) - console.error('[fund-dev] ATM pubkey:', identity.publicKey) - console.error('[fund-dev] Lightning.Pub pubkey:', LIGHTNING_PUB_PUBKEY) - console.error('[fund-dev] Creating invoice for', FUND_AMOUNT, 'sats') - - const client = new NostrClient({ - relays: [{ url: RELAY_URL }], - identity, - }) - - await client.connect() - console.error('[fund-dev] Connected to relay:', RELAY_URL) - - const requestId = randomUUID() - - console.error('[fund-dev] App ID:', APP_ID) - - // Correct RPC structure per Lightning.Pub documentation - // appId ensures the Nostr user is created under the same app as HTTP API users - const rpcRequest = { - rpcName: 'NewInvoice', - params: {}, - query: {}, - body: { - amountSats: FUND_AMOUNT, - memo: `ATM funding (${FUND_AMOUNT} sats)`, - }, - authIdentifier: identity.publicKey, - requestId, - appId: APP_ID, - } - - const encryptedContent = encryptContent(identity, LIGHTNING_PUB_PUBKEY, rpcRequest) - - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - identity.privateKey - ) - - // Subscribe to responses before publishing - let invoice = null - const subId = client.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - '#p': [identity.publicKey], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onEvent: (evt) => { - try { - const response = decryptJSON(identity, LIGHTNING_PUB_PUBKEY, evt.content) - if (response.requestId === requestId && response.invoice) { - invoice = response.invoice - console.error('[fund-dev] Got invoice!') - } - } catch (err) { - // Ignore decrypt errors for other messages - } - }, - } - ) - - await client.publish(event) - console.error('[fund-dev] Request published, waiting for invoice...') - - // Wait up to 15 seconds for response - for (let i = 0; i < 30; i++) { - await new Promise((resolve) => setTimeout(resolve, 500)) - if (invoice) break - } - - client.unsubscribe(subId) - client.disconnect() - - if (!invoice) { - console.error('[fund-dev] Error: No invoice received within timeout') - process.exit(1) - } - - // Output just the invoice to stdout (for piping to payment command) - console.log(invoice) - process.exit(0) -} - -main().catch((err) => { - console.error('[fund-dev] Error:', err.message) - process.exit(1) -}) diff --git a/packages/nostr-client/dev/generate-ndebit.mjs b/packages/nostr-client/dev/generate-ndebit.mjs deleted file mode 100644 index d344dac..0000000 --- a/packages/nostr-client/dev/generate-ndebit.mjs +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Generate an ndebit string for ShockWallet to scan - * - * Usage: node generate-ndebit.mjs [pointer] - * - * The ndebit allows ShockWallet to send an invoice that Lightning.Pub will pay. - * This is the LNURL-withdraw equivalent for CLINK. - */ - -import { bech32 } from '@scure/base' - -const LIGHTNING_PUB_PUBKEY = - process.env.LIGHTNING_PUB_PUBKEY || - '4a72e400a254bf74a70cc711ab97e461b8d4fd9738b1ac3b5194cdeb3192ab91' -const RELAY_URL = process.env.NOSTR_RELAY_URL || 'wss://strfry.shock.network' -const POINTER = process.argv[2] || 'atm-cashin-' + Date.now() - -function hexToBytes(hex) { - const bytes = new Uint8Array(hex.length / 2) - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16) - } - return bytes -} - -function encodeTLV(tlv) { - const entries = [] - Object.entries(tlv) - .reverse() - .forEach(([t, vs]) => { - vs.forEach((v) => { - const entry = new Uint8Array(v.length + 2) - entry.set([parseInt(t)], 0) - entry.set([v.length], 1) - entry.set(v, 2) - entries.push(entry) - }) - }) - - // Concatenate all entries - const totalLength = entries.reduce((sum, e) => sum + e.length, 0) - const result = new Uint8Array(totalLength) - let offset = 0 - for (const entry of entries) { - result.set(entry, offset) - offset += entry.length - } - return result -} - -function ndebitEncode(debit) { - const encoder = new TextEncoder() - - const tlv = { - 0: [hexToBytes(debit.pubkey)], - 1: [encoder.encode(debit.relay)], - } - - if (debit.pointer) { - tlv[2] = [encoder.encode(debit.pointer)] - } - - const data = encodeTLV(tlv) - const words = bech32.toWords(data) - return bech32.encode('ndebit', words, 5000) -} - -// Generate ndebit -const ndebit = ndebitEncode({ - pubkey: LIGHTNING_PUB_PUBKEY, - relay: RELAY_URL, - pointer: POINTER, -}) - -console.log('=== NDEBIT for ShockWallet ===') -console.log('') -console.log('Pubkey:', LIGHTNING_PUB_PUBKEY) -console.log('Relay:', RELAY_URL) -console.log('Pointer:', POINTER) -console.log('') -console.log('ndebit string:') -console.log(ndebit) -console.log('') -console.log('ShockWallet should scan this QR code to receive sats from the ATM.') diff --git a/packages/nostr-client/dev/mock-machine.mjs b/packages/nostr-client/dev/mock-machine.mjs deleted file mode 100644 index 152e30b..0000000 --- a/packages/nostr-client/dev/mock-machine.mjs +++ /dev/null @@ -1,833 +0,0 @@ -#!/usr/bin/env node -/** - * Mock ATM Machine - Simulates the ATM cash-in flow with CLINK - * - * Usage: node mock-machine.mjs - * - * This creates a web server that: - * 1. Displays the ndebit QR code for customers to scan - * 2. Runs the ATM debit agent to authorize payments - * 3. Shows real-time status updates - */ - -import http from 'http' -import { WebSocketServer } from 'ws' -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools' -import { getConversationKey, encrypt, decrypt } from './nip44v1.mjs' -import { randomUUID } from 'crypto' -import QRCode from 'qrcode' -import { decodeBech32, ndebitEncode } from '@shocknet/clink-sdk' - -const PORT = 3456 -const RELAY_URL = 'ws://localhost:7777' -// Relay URL for browser access (different from Docker internal strfry:7777) -const BROWSER_RELAY_URL = 'ws://localhost:7777' -const LIGHTNING_PUB_HTTP = 'http://localhost:1776' -const ADMIN_TOKEN = 'bitspire-dev-admin-token' - -// Lightning.Pub pubkey - fetched dynamically from the ATM user's ndebit -let LIGHTNING_PUB_PUBKEY = null - -// ATM keypair (persistent for this session) -const ATM_PRIVATE_KEY = generateSecretKey() -const ATM_PUBLIC_KEY = getPublicKey(ATM_PRIVATE_KEY) - -// Fake exchange rate: sats per USD (approximately $100k/BTC) -const SATS_PER_USD = 1000 - -// ATM states -const ATM_STATE = { - IDLE: 'idle', - CASH_INSERTED: 'cash_inserted', - WAITING_FOR_SCAN: 'waiting_for_scan', - PROCESSING: 'processing', - COMPLETE: 'complete', -} - -// State -let relay = null -let appToken = null -let ndebit = null -let ndebitQR = null -let atmBalance = 0 -let wsClients = [] -let isLinked = false -let withdrawAmount = 0 // Amount in sats (calculated from cash) -let cashInserted = 0 // Amount in USD -let atmState = ATM_STATE.IDLE - -// Rewrite ndebit relay for browser access (strfry:7777 -> localhost:7777) -function rewriteNdebitRelay(ndebitString) { - try { - const decoded = decodeBech32(ndebitString) - if (decoded.type !== 'ndebit') return ndebitString - - // Replace Docker internal relay with browser-accessible relay - const data = { - pubkey: decoded.data.pubkey, - relay: BROWSER_RELAY_URL, - pointer: decoded.data.pointer, - } - return ndebitEncode(data) - } catch (e) { - console.error('Failed to rewrite ndebit relay:', e) - return ndebitString - } -} - -// Format ndebit with clink: prefix and amount parameter -// Using clink: instead of lightning: because CLINK is protocol-agnostic -// (could work with Cashu/Fedimint, not just Lightning) -function formatNdebitUri(ndebitString, amount) { - const rewritten = rewriteNdebitRelay(ndebitString) - return `clink:${rewritten}?amount=${amount}` -} - -function broadcast(type, data) { - const msg = JSON.stringify({ type, ...data }) - wsClients.forEach((ws) => { - if (ws.readyState === 1) ws.send(msg) - }) -} - -function log(message) { - const timestamp = new Date().toLocaleTimeString() - console.log(`[${timestamp}] ${message}`) - broadcast('log', { message: `[${timestamp}] ${message}` }) -} - -async function getAppToken() { - const res = await fetch(`${LIGHTNING_PUB_HTTP}/api/admin/app/auth`, { - method: 'POST', - headers: { - Authorization: `Bearer ${ADMIN_TOKEN}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name: 'wallet' }), - }) - const data = await res.json() - return data.auth_token -} - -async function getAtmUser() { - const res = await fetch(`${LIGHTNING_PUB_HTTP}/api/app/user/get`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ user_identifier: 'atm' }), - }) - return res.json() -} - -async function getLinkingToken() { - const res = await fetch(`${LIGHTNING_PUB_HTTP}/api/app/user/npub/token/reset`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ user_identifier: 'atm' }), - }) - const data = await res.json() - return data.token -} - -async function sendRPC(rpcName, body) { - const requestId = randomUUID() - const request = { - rpcName, - authIdentifier: ATM_PUBLIC_KEY, - body, - } - - const conversationKey = getConversationKey(ATM_PRIVATE_KEY, LIGHTNING_PUB_PUBKEY) - const encryptedContent = encrypt(JSON.stringify(request), conversationKey) - - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - ATM_PRIVATE_KEY - ) - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('RPC timeout')), 30000) - - const sub = relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - const pTags = evt.tags.filter((t) => t[0] === 'p') - if (!pTags.some((t) => t[1] === ATM_PUBLIC_KEY)) return - - try { - const response = JSON.parse(decrypt(evt.content, conversationKey)) - clearTimeout(timeout) - sub.close() - resolve(response) - } catch (e) {} - }, - } - ) - - relay.publish(event) - }) -} - -async function linkKeypair(token) { - log('Linking ATM keypair to user account...') - const response = await sendRPC('LinkNPubThroughToken', { token }) - if (response.status === 'OK') { - log('[OK] Keypair linked successfully!') - isLinked = true - return true - } else { - log(`[ERROR] Link failed: ${response.reason}`) - return false - } -} - -async function subscribeToDebitRequests() { - log('Subscribing to debit requests...') - - const conversationKey = getConversationKey(ATM_PRIVATE_KEY, LIGHTNING_PUB_PUBKEY) - - // Subscribe to Kind 21000 messages from Lightning.Pub - relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - const pTags = evt.tags.filter((t) => t[0] === 'p') - if (!pTags.some((t) => t[1] === ATM_PUBLIC_KEY)) return - - try { - const message = JSON.parse(decrypt(evt.content, conversationKey)) - - if (message.debit) { - handleDebitRequest(message, conversationKey) - } - } catch (e) {} - }, - } - ) - - // Send GetLiveDebitRequests to start the stream - const request = { - rpcName: 'GetLiveDebitRequests', - authIdentifier: ATM_PUBLIC_KEY, - body: {}, - } - - const encryptedContent = encrypt(JSON.stringify(request), conversationKey) - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - ATM_PRIVATE_KEY - ) - - await relay.publish(event) - log('[OK] Listening for debit requests...') -} - -async function handleDebitRequest(message, conversationKey) { - const { debit } = message - atmState = ATM_STATE.PROCESSING - broadcastState() - - log(`[ALERT] DEBIT REQUEST RECEIVED!`) - log(` Amount: ${debit.amount || 'invoice amount'} sats`) - log(` Type: ${debit.type}`) - - broadcast('debit_request', { debit }) - - // Auto-approve after 1 second - setTimeout(async () => { - log('[OK] Auto-approving debit request...') - - const approval = { - rpcName: 'RespondToDebit', - authIdentifier: ATM_PUBLIC_KEY, - body: { - npub: message.npub, - request_id: message.request_id, - response: { - type: 'invoice', - invoice: debit.invoice, - }, - }, - } - - const encryptedContent = encrypt(JSON.stringify(approval), conversationKey) - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - ATM_PRIVATE_KEY - ) - - await relay.publish(event) - log('[OK] Approval sent! Payment complete.') - - // Mark as complete - atmState = ATM_STATE.COMPLETE - broadcastState() - - // Update balance and reset after delay - setTimeout(async () => { - await updateBalance() - // Auto-reset after showing success - setTimeout(resetAtm, 3000) - }, 2000) - }, 1000) -} - -async function updateBalance() { - const user = await getAtmUser() - if (user.status === 'OK') { - atmBalance = user.info.balance - ndebit = user.info.ndebit - broadcastState() - log(`Balance updated: ${atmBalance} sats`) - } -} - -async function regenerateQR() { - if (ndebit) { - const uri = formatNdebitUri(ndebit, withdrawAmount) - ndebitQR = await QRCode.toDataURL(uri, { width: 300, margin: 2 }) - log(`QR code generated for ${withdrawAmount} sats`) - } -} - -function setWithdrawAmount(amount) { - withdrawAmount = amount - regenerateQR() - broadcastState() - log(`Withdrawal amount set to ${amount} sats`) -} - -async function insertCash(usdAmount) { - cashInserted = usdAmount - withdrawAmount = usdAmount * SATS_PER_USD - atmState = ATM_STATE.CASH_INSERTED - log(`[CASH] $${usdAmount} inserted → ${withdrawAmount.toLocaleString()} sats`) - - // Brief delay then show QR - await new Promise((r) => setTimeout(r, 500)) - atmState = ATM_STATE.WAITING_FOR_SCAN - await regenerateQR() - broadcastState() - log(`[QR] Scan to receive ${withdrawAmount.toLocaleString()} sats`) -} - -function resetAtm() { - atmState = ATM_STATE.IDLE - cashInserted = 0 - withdrawAmount = 0 - ndebitQR = null - broadcastState() - log('[RESET] ATM ready for next customer') -} - -function broadcastState() { - broadcast('status', { - balance: atmBalance, - ndebit, - ndebitQR, - ndebitUri: withdrawAmount > 0 ? formatNdebitUri(ndebit, withdrawAmount) : null, - withdrawAmount, - cashInserted, - atmState, - satsPerUsd: SATS_PER_USD, - }) -} - -async function initialize() { - log('Starting Mock ATM Machine...') - - // Get app token - appToken = await getAppToken() - log('Got app token') - - // Get ATM user info - const user = await getAtmUser() - if (user.status === 'OK') { - atmBalance = user.info.balance - ndebit = user.info.ndebit - - // Extract Lightning.Pub pubkey from ndebit - const decoded = decodeBech32(ndebit) - LIGHTNING_PUB_PUBKEY = decoded.data.pubkey - log(`ATM user found: ${atmBalance} sats balance`) - log(`Lightning.Pub pubkey: ${LIGHTNING_PUB_PUBKEY.substring(0, 16)}...`) - } else { - log('ATM user not found - please create one first') - return - } - - // Connect to relay - log('Connecting to relay...') - relay = await Relay.connect(RELAY_URL) - log('Connected to relay') - - // Get linking token and link keypair - const token = await getLinkingToken() - await linkKeypair(token) - - // Subscribe to debit requests - await subscribeToDebitRequests() - - // Start in IDLE state (no QR until cash inserted) - atmState = ATM_STATE.IDLE - broadcastState() - - log('[READY] Mock ATM Machine ready!') - log(` Open http://localhost:${PORT} to use the ATM`) -} - -// HTML page -const html = ` - - - Mock ATM Machine - - - -
-

Mock ATM

-

Simulated Bitcoin ATM for testing

- -
- ATM Balance: - 0 sats -
- -
-
- -
-
Insert Cash
-
Select amount to withdraw as Bitcoin
-
- - - - -
-

Rate: 1,000 sats/$

-
- - - - - - - - - -
-
- -
-

Activity Log

-
-
-
- - - -` - -// Create HTTP server -const server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }) - res.end(html) -}) - -// Create WebSocket server -const wss = new WebSocketServer({ server }) - -wss.on('connection', (ws) => { - wsClients.push(ws) - ws.on('close', () => { - wsClients = wsClients.filter((c) => c !== ws) - }) - - ws.on('message', (data) => { - try { - const msg = JSON.parse(data) - if (msg.type === 'insert_cash' && typeof msg.amount === 'number') { - insertCash(msg.amount) - } else if (msg.type === 'reset') { - resetAtm() - } - } catch (e) {} - }) - - // Send current state - if (ndebit) { - ws.send( - JSON.stringify({ - type: 'status', - balance: atmBalance, - ndebit, - ndebitQR, - ndebitUri: withdrawAmount > 0 ? formatNdebitUri(ndebit, withdrawAmount) : null, - withdrawAmount, - cashInserted, - atmState, - satsPerUsd: SATS_PER_USD, - isLinked, - }) - ) - } -}) - -// Start server -server.listen(PORT, async () => { - console.log(`Mock ATM Machine running at http://localhost:${PORT}`) - await initialize() -}) diff --git a/packages/nostr-client/dev/nip44v1.mjs b/packages/nostr-client/dev/nip44v1.mjs deleted file mode 100644 index da908d5..0000000 --- a/packages/nostr-client/dev/nip44v1.mjs +++ /dev/null @@ -1,111 +0,0 @@ -/** - * NIP-44 v1 Implementation - * - * This is the XChaCha20-based encryption used by Lightning.Pub for Kind 21000 RPC events. - * It differs from standard NIP-44 v2 (used in nostr-tools) which uses ChaCha20-Poly1305. - */ - -import { base64 } from '@scure/base' -import { randomBytes } from '@noble/hashes/utils.js' -import { streamXOR as xchacha20 } from '@stablelib/xchacha20' -import { secp256k1 } from '@noble/curves/secp256k1.js' -import { sha256 } from '@noble/hashes/sha2.js' - -const XCHACHA20_VERSION = 1 - -/** - * Convert hex string to Uint8Array - * @param {string} hex - Hex string - * @returns {Uint8Array} - */ -function hexToBytes(hex) { - const bytes = new Uint8Array(hex.length / 2) - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16) - } - return bytes -} - -/** - * Get shared secret for NIP-44 v1 encryption - * @param {Uint8Array|string} privateKey - Private key (32 bytes or hex string) - * @param {string} publicKey - Public key (32 bytes hex string, no prefix) - * @returns {Uint8Array} - 32-byte shared secret - */ -export function getConversationKey(privateKey, publicKey) { - // Convert private key to Uint8Array if it's a hex string - const privKeyBytes = typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey - - // Convert public key (with 02 prefix) to Uint8Array - const pubKeyBytes = hexToBytes('02' + publicKey) - - // Get ECDH shared point - const sharedPoint = secp256k1.getSharedSecret(privKeyBytes, pubKeyBytes) - - // Hash the x-coordinate of the shared point - return sha256(sharedPoint.slice(1, 33)) -} - -/** - * Encrypt content using NIP-44 v1 (XChaCha20) - * @param {string} content - Plaintext content to encrypt - * @param {Uint8Array} conversationKey - 32-byte conversation key from getConversationKey - * @returns {string} - Base64-encoded encrypted payload - */ -export function encrypt(content, conversationKey) { - const nonce = randomBytes(24) - const plaintext = new TextEncoder().encode(content) - - // XChaCha20 stream cipher - encrypts in place - const ciphertext = new Uint8Array(plaintext.length) - xchacha20(conversationKey, nonce, plaintext, ciphertext) - - // Encode: version byte + nonce + ciphertext - return base64.encode(new Uint8Array([XCHACHA20_VERSION, ...nonce, ...ciphertext])) -} - -/** - * Decrypt content using NIP-44 v1 (XChaCha20) - * @param {string} content - Base64-encoded encrypted payload - * @param {Uint8Array} conversationKey - 32-byte conversation key from getConversationKey - * @returns {string} - Decrypted plaintext - */ -export function decrypt(content, conversationKey) { - const payload = decodePayload(content) - - // XChaCha20 stream cipher - decrypts in place - const plaintext = new Uint8Array(payload.ciphertext.length) - xchacha20(conversationKey, payload.nonce, payload.ciphertext, plaintext) - - return new TextDecoder().decode(plaintext) -} - -/** - * Decode encrypted payload (supports both formats) - * @param {string} content - Base64-encoded or JSON-encoded payload - * @returns {{nonce: Uint8Array, ciphertext: Uint8Array}} - */ -function decodePayload(content) { - // Check for JSON format - if (content.startsWith('{') && content.endsWith('}')) { - const parsed = JSON.parse(content) - if (parsed.v !== XCHACHA20_VERSION) { - throw new Error(`Unsupported encryption version: ${parsed.v}`) - } - return { - nonce: base64.decode(parsed.nonce), - ciphertext: base64.decode(parsed.ciphertext), - } - } - - // Binary format: version byte + nonce (24 bytes) + ciphertext - const buf = base64.decode(content) - if (buf[0] !== XCHACHA20_VERSION) { - throw new Error(`Unsupported encryption version: ${buf[0]}`) - } - - return { - nonce: buf.subarray(1, 25), - ciphertext: buf.subarray(25), - } -} diff --git a/packages/nostr-client/dev/run-debit-agent.mjs b/packages/nostr-client/dev/run-debit-agent.mjs deleted file mode 100644 index 8dd87c9..0000000 --- a/packages/nostr-client/dev/run-debit-agent.mjs +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env node -/** - * ATM Debit Approval Agent (TESTING ONLY) - * - * ⚠️ WARNING: This script auto-approves ALL debit requests without validation! - * ⚠️ DO NOT use in production - use the integrated debit approval service instead. - * - * Purpose: - * - Standalone debugging tool for testing the GetLiveDebitRequests subscription - * - Helps diagnose relay connectivity and message decryption issues - * - Useful when the integrated service isn't receiving events - * - * Usage: - * # Set environment variables (or create .env file in apps/machine/) - * export ATM_PRIVATE_KEY= - * export LIGHTNING_PUB_PUBKEY= - * export RELAY_URL=ws://localhost:7777 - * - * # Run the script - * node run-debit-agent.mjs - * - * Production alternative: - * The ATM app (apps/machine) includes an integrated debit approval service - * with session-based single-use protection. See: - * - apps/machine/src/services/lightning.ts (startDebitApprovalService) - * - docs/ndebit-cash-in-flow.md - */ - -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent, getPublicKey } from 'nostr-tools' -import * as nip44v1 from './nip44v1.mjs' -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -// Load .env file from apps/machine if it exists -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const envPath = path.join(__dirname, '../../apps/machine/.env') -if (fs.existsSync(envPath)) { - const envContent = fs.readFileSync(envPath, 'utf-8') - for (const line of envContent.split('\n')) { - const trimmed = line.trim() - if (trimmed && !trimmed.startsWith('#')) { - const [key, ...valueParts] = trimmed.split('=') - if (key && valueParts.length > 0) { - // Map VITE_ prefixed vars to non-prefixed - const envKey = key.replace(/^VITE_/, '') - process.env[envKey] = valueParts.join('=') - } - } - } - console.log('[Config] Loaded .env from:', envPath) -} - -// Configuration from environment -const ATM_PRIVATE_KEY_HEX = process.env.ATM_PRIVATE_KEY -const LIGHTNING_PUB_PUBKEY = process.env.LIGHTNING_PUB_PUBKEY -const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:7777' - -// Validate required config -if (!ATM_PRIVATE_KEY_HEX) { - console.error('ERROR: ATM_PRIVATE_KEY environment variable is required') - console.error('Set it directly or create apps/machine/.env with VITE_ATM_PRIVATE_KEY') - process.exit(1) -} - -if (!LIGHTNING_PUB_PUBKEY) { - console.error('ERROR: LIGHTNING_PUB_PUBKEY environment variable is required') - console.error('Set it directly or create apps/machine/.env with VITE_LIGHTNING_PUB_PUBKEY') - process.exit(1) -} - -const ATM_PRIVATE_KEY = Uint8Array.from(Buffer.from(ATM_PRIVATE_KEY_HEX, 'hex')) -const ATM_PUBLIC_KEY = getPublicKey(ATM_PRIVATE_KEY) - -console.log('') -console.log('='.repeat(60)) -console.log(' ATM Debit Approval Agent (TESTING ONLY)') -console.log('='.repeat(60)) -console.log('') -console.log('⚠️ WARNING: Auto-approves ALL requests without validation!') -console.log('⚠️ For production, use the integrated service in apps/machine') -console.log('') -console.log('ATM Pubkey:', ATM_PUBLIC_KEY) -console.log('Lightning.Pub Pubkey:', LIGHTNING_PUB_PUBKEY) -console.log('Relay URL:', RELAY_URL) -console.log('') - -async function main() { - // Connect to relay - console.log('Connecting to relay...') - const relay = await Relay.connect(RELAY_URL) - console.log('Connected!') - console.log('') - - // Create conversation key for NIP-44 v1 encryption - const conversationKey = nip44v1.getConversationKey(ATM_PRIVATE_KEY_HEX, LIGHTNING_PUB_PUBKEY) - - // Subscribe to GetLiveDebitRequests - console.log('Subscribing to live debit requests...') - - const subscribeRequest = { - rpcName: 'GetLiveDebitRequests', - authIdentifier: ATM_PUBLIC_KEY, - body: {}, - } - - const subEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(subscribeRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - // Listen for debit requests - console.log('Listening for debit requests...') - console.log('(Test by scanning ndebit QR with ShockWallet)') - console.log('') - - const debitSub = relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - '#p': [ATM_PUBLIC_KEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - async onevent(evt) { - try { - const decrypted = nip44v1.decrypt(evt.content, conversationKey) - const message = JSON.parse(decrypted) - - // Check if this is a live debit request - if (message.requestId === 'GetLiveDebitRequests' && message.debit) { - console.log('') - console.log('========================================') - console.log('DEBIT REQUEST RECEIVED!') - console.log(' Request ID:', message.request_id) - console.log(' From npub:', message.npub?.substring(0, 16) + '...') - console.log(' Debit type:', message.debit.type) - - if (message.debit.invoice) { - console.log(' Invoice:', message.debit.invoice.substring(0, 50) + '...') - - // Auto-approve by responding with INVOICE type - console.log('') - console.log('⚠️ AUTO-APPROVING debit request (NO VALIDATION)...') - - const approveRequest = { - rpcName: 'RespondToDebit', - authIdentifier: ATM_PUBLIC_KEY, - body: { - npub: message.npub, - request_id: message.request_id, - response: { - type: 'invoice', - invoice: message.debit.invoice, - }, - }, - } - - const approveEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(approveRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - await relay.publish(approveEvent) - console.log('APPROVED! (event id:', approveEvent.id.substring(0, 16) + '...)') - } - - console.log('========================================') - console.log('') - } else if (message.rpcName) { - console.log('RPC response:', message.rpcName, ':', message.status || 'received') - } else if (message.requestId) { - console.log('Live subscription active:', message.status) - } - } catch (err) { - // Ignore decryption failures for events not meant for us - if (!err.message?.includes('Unsupported')) { - // Uncomment for debugging: - // console.log('Decryption error:', err.message) - } - } - }, - } - ) - - await relay.publish(subEvent) - console.log('Subscription sent, waiting for debit requests...') - console.log('') - - // Keep running - process.on('SIGINT', () => { - console.log('\nShutting down...') - debitSub.close() - relay.close() - process.exit(0) - }) - - // Heartbeat - while (true) { - await new Promise((r) => setTimeout(r, 30000)) - console.log('Still listening...') - } -} - -main().catch((err) => { - console.error('Error:', err.message) - process.exit(1) -}) diff --git a/packages/nostr-client/dev/test-debit.mjs b/packages/nostr-client/dev/test-debit.mjs deleted file mode 100644 index c1861ac..0000000 --- a/packages/nostr-client/dev/test-debit.mjs +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Test CLINK Debit flow - * - * This simulates what ShockWallet does when scanning an ndebit: - * 1. Decode the ndebit to get pubkey, relay, pointer - * 2. Create a bolt11 invoice (we'll get one from Alice) - * 3. Send Kind 21002 debit request to Lightning.Pub - * 4. Wait for payment response - */ - -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent, getPublicKey } from 'nostr-tools' -import { nip44 } from 'nostr-tools' -import { bech32 } from '@scure/base' -import { randomBytes } from 'crypto' - -// Generate a random keypair for this test (simulates ShockWallet) -const WALLET_PRIVATE_KEY = randomBytes(32) -const WALLET_PUBLIC_KEY = getPublicKey(WALLET_PRIVATE_KEY) - -// The ndebit to test -const NDEBIT = process.argv[2] -// The bolt11 invoice to be paid -const BOLT11 = process.argv[3] - -if (!NDEBIT || !BOLT11) { - console.log('Usage: node test-debit.mjs ') - console.log('') - console.log('Example:') - console.log(' # First create an invoice on Alice:') - console.log(' docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt 1000') - console.log('') - console.log(' # Then test the debit:') - console.log(' node test-debit.mjs ndebit1... lnbcrt...') - process.exit(1) -} - -function decodeNdebit(ndebit) { - const { prefix, words } = bech32.decode(ndebit, 5000) - if (prefix !== 'ndebit') throw new Error('Invalid ndebit prefix') - - const data = new Uint8Array(bech32.fromWords(words)) - - let pubkey, relay, pointer - let offset = 0 - - while (offset < data.length) { - const type = data[offset] - const length = data[offset + 1] - const value = data.slice(offset + 2, offset + 2 + length) - - switch (type) { - case 0: - pubkey = Buffer.from(value).toString('hex') - break - case 1: - relay = new TextDecoder().decode(value) - break - case 2: - pointer = new TextDecoder().decode(value) - break - } - - offset += 2 + length - } - - return { pubkey, relay, pointer } -} - -async function main() { - console.log('=== CLINK Debit Test ===') - console.log('') - console.log('Test wallet pubkey:', WALLET_PUBLIC_KEY) - console.log('') - - // Decode ndebit - const debit = decodeNdebit(NDEBIT) - console.log('Decoded ndebit:') - console.log(' Pubkey:', debit.pubkey) - console.log(' Relay:', debit.relay) - console.log(' Pointer:', debit.pointer || '(none)') - console.log('') - - // Connect to relay (override Docker internal hostnames with localhost for local testing) - const relayUrl = debit.relay - .replace('host.docker.internal', 'localhost') - .replace('ws://strfry:', 'ws://localhost:') - console.log('Connecting to relay:', relayUrl) - const relay = await Relay.connect(relayUrl) - console.log('Connected!') - console.log('') - - // Build debit request payload - const requestPayload = { - pointer: debit.pointer, - bolt11: BOLT11, - } - - console.log('Request payload:', JSON.stringify(requestPayload, null, 2)) - console.log('') - - // Encrypt with NIP-44 - const conversationKey = nip44.getConversationKey(WALLET_PRIVATE_KEY, debit.pubkey) - const encryptedContent = nip44.encrypt(JSON.stringify(requestPayload), conversationKey) - - // Create Kind 21002 event - const event = finalizeEvent( - { - kind: 21002, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['p', debit.pubkey], - ['clink_version', '1'], - ], - content: encryptedContent, - }, - WALLET_PRIVATE_KEY - ) - - console.log('Publishing debit request (event id:', event.id.substring(0, 16) + '...)...') - - // Subscribe to responses - let responseReceived = false - const sub = relay.subscribe( - [ - { - kinds: [21002], - authors: [debit.pubkey], - '#p': [WALLET_PUBLIC_KEY], - '#e': [event.id], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - console.log('') - console.log('Got response event:', evt.id.substring(0, 16) + '...') - try { - const decrypted = nip44.decrypt(evt.content, conversationKey) - const response = JSON.parse(decrypted) - console.log('Response:', JSON.stringify(response, null, 2)) - - if (response.res === 'ok') { - console.log('') - console.log('✅ DEBIT SUCCESS!') - if (response.preimage) { - console.log('Preimage:', response.preimage) - } - } else if (response.res === 'GFY') { - console.log('') - console.log('❌ DEBIT FAILED:', response.error) - } - - responseReceived = true - } catch (err) { - console.log('Failed to decrypt:', err.message) - } - }, - } - ) - - // Publish request - await relay.publish(event) - console.log('Request published, waiting for response...') - - // Wait for response - for (let i = 0; i < 30; i++) { - await new Promise((r) => setTimeout(r, 1000)) - if (responseReceived) break - if (i % 5 === 4) console.log('Still waiting... (' + (i + 1) + 's)') - } - - if (!responseReceived) { - console.log('') - console.log('❌ No response received within timeout') - } - - sub.close() - relay.close() -} - -main().catch(console.error) diff --git a/packages/nostr-client/dev/test-ndebit.mjs b/packages/nostr-client/dev/test-ndebit.mjs deleted file mode 100644 index bcddaf4..0000000 --- a/packages/nostr-client/dev/test-ndebit.mjs +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env node -/** - * Test script to simulate a wallet sending an ndebit claim request - * This tests whether Lightning.Pub sends Kind 21002 responses after the fix - */ - -import { Relay } from 'nostr-tools/relay' -import { nip44, finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools' -import { decodeBech32 } from '@shocknet/clink-sdk' - -const { getConversationKey, encrypt, decrypt } = nip44 - -const NDEBIT = - 'ndebit1qgpkzardqyg8wue69uhhxarjvee8jw3hxumnwqpqf05wyqarxsdm9d62fh9lsa6wqc2r0a37cgd00mp3gnydpf5w9uusavytcn' -const RELAY_URL = 'ws://localhost:7777' -const AMOUNT_SATS = 5000 // Small test amount - -// Generate a wallet keypair for this test -const WALLET_PRIVATE_KEY = generateSecretKey() -const WALLET_PUBLIC_KEY = getPublicKey(WALLET_PRIVATE_KEY) - -async function main() { - console.log('🔧 Test: ndebit claim flow (NIP-44 v2)') - console.log('='.repeat(50)) - - // Decode ndebit to get Lightning.Pub pubkey and pointer - const decoded = decodeBech32(NDEBIT) - const LPUB_PUBKEY = decoded.data.pubkey - const POINTER = decoded.data.pointer - console.log(`\n📍 Lightning.Pub pubkey: ${LPUB_PUBKEY.slice(0, 16)}...`) - console.log(`🔑 Pointer (user ID): ${POINTER.slice(0, 16)}...`) - console.log(`👛 Test wallet pubkey: ${WALLET_PUBLIC_KEY.slice(0, 16)}...`) - console.log(`💰 Amount: ${AMOUNT_SATS} sats`) - - // Connect to relay - console.log(`\n🔌 Connecting to relay: ${RELAY_URL}`) - const relay = await Relay.connect(RELAY_URL) - console.log('✅ Connected!') - - // Build the debit request data (NdebitData format) - // Using newNdebitFullAccessRequest format with amount - const debitData = { - amount_sats: AMOUNT_SATS, - pointer: POINTER, - } - - // Encrypt using NIP-44 v2 - const conversationKey = getConversationKey(WALLET_PRIVATE_KEY, LPUB_PUBKEY) - const encryptedContent = encrypt(JSON.stringify(debitData), conversationKey) - - // Build event with correct tags (including clink_version) - const event = finalizeEvent( - { - kind: 21002, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['p', LPUB_PUBKEY], - ['clink_version', '1'], - ], - content: encryptedContent, - }, - WALLET_PRIVATE_KEY - ) - - console.log(`\n📤 Sending Kind 21002 debit request`) - console.log(` Event ID: ${event.id.slice(0, 16)}...`) - console.log(` Content length: ${encryptedContent.length} chars`) - - // Subscribe for responses BEFORE sending the request - let responseReceived = false - const startTime = Date.now() - - // Filter for Kind 21002 responses from Lightning.Pub that reference our event - const sub = relay.subscribe( - [ - { - kinds: [21002], - authors: [LPUB_PUBKEY], - '#p': [WALLET_PUBLIC_KEY], - '#e': [event.id], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - console.log(`\n📥 Received Kind 21002 response!`) - console.log(` Event ID: ${evt.id.slice(0, 16)}...`) - console.log(` Author: ${evt.pubkey.slice(0, 16)}...`) - - // Check #e tag (should reference our original event) - const eTag = evt.tags.find((t) => t[0] === 'e') - if (eTag) { - console.log(` #e tag: ${eTag[1].slice(0, 16)}...`) - if (eTag[1] === event.id) { - console.log(' ✅ Correctly references our original event!') - } - } else { - console.log(' ⚠️ No #e tag found') - } - - try { - const response = JSON.parse(decrypt(evt.content, conversationKey)) - console.log(`\n📋 Response content:`) - console.log(JSON.stringify(response, null, 2)) - - if (response.res === 'OK') { - console.log('\n✅✅✅ SUCCESS! Lightning.Pub sent Kind 21002 response correctly!') - console.log(' The fix is working!') - } else if (response.res === 'GFY' || response.error) { - console.log(`\n⚠️ Response indicates error: ${response.error || 'unknown'}`) - console.log(' (Expected if payment denied or auth required)') - } - } catch (e) { - console.log(' ❌ Could not decrypt response:', e.message) - } - - responseReceived = true - }, - } - ) - - // Publish the debit request - await relay.publish(event) - console.log('✅ Request published!') - - // Wait for response with timeout - console.log('\n⏳ Waiting for Kind 21002 response (30s timeout)...') - const timeout = 30000 - const checkInterval = 1000 - - while (!responseReceived && Date.now() - startTime < timeout) { - await new Promise((r) => setTimeout(r, checkInterval)) - const elapsed = Math.floor((Date.now() - startTime) / 1000) - process.stdout.write(`\r ${elapsed}s elapsed...`) - } - - console.log('') - - if (!responseReceived) { - console.log('\n❌❌❌ TIMEOUT! No Kind 21002 response received.') - console.log(' This means the fix did NOT work or there was another issue.') - - // Let's check what Kind 21002 events exist - console.log('\n🔍 Checking for any Kind 21002 events on relay...') - let foundEvents = 0 - const allDebitSub = relay.subscribe( - [ - { - kinds: [21002], - limit: 10, - }, - ], - { - onevent(evt) { - foundEvents++ - const pTags = evt.tags.filter((t) => t[0] === 'p').map((t) => t[1].slice(0, 8) + '...') - const eTags = evt.tags.filter((t) => t[0] === 'e').map((t) => t[1].slice(0, 8) + '...') - console.log( - ` ${foundEvents}. id=${evt.id.slice(0, 12)}... by=${evt.pubkey.slice(0, 8)}... #p=${pTags.join(',')} #e=${eTags.join(',')}` - ) - }, - oneose() { - console.log(` (Found ${foundEvents} Kind 21002 events total)`) - }, - } - ) - - await new Promise((r) => setTimeout(r, 3000)) - allDebitSub.close() - } - - sub.close() - relay.close() - console.log('\n🏁 Test complete') - process.exit(responseReceived ? 0 : 1) -} - -main().catch((e) => { - console.error('Fatal error:', e) - process.exit(1) -}) diff --git a/packages/nostr-client/dev/test-pay.mjs b/packages/nostr-client/dev/test-pay.mjs deleted file mode 100644 index 4385322..0000000 --- a/packages/nostr-client/dev/test-pay.mjs +++ /dev/null @@ -1,116 +0,0 @@ -import { NostrClient, loadIdentityFromHex, encryptContent, decryptJSON } from '../dist/index.js' -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent } from 'nostr-tools' -import { randomUUID } from 'crypto' - -const DEV_PRIVATE_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' -const LIGHTNING_PUB_PUBKEY = - process.env.LIGHTNING_PUB_PUBKEY || - '4a72e400a254bf74a70cc711ab97e461b8d4fd9738b1ac3b5194cdeb3192ab91' -const RELAY_URL = process.env.NOSTR_RELAY_URL || 'ws://localhost:7777' - -// Get a fresh invoice from Alice first: -// docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt 1000 -const TEST_INVOICE = process.argv[2] - -if (!TEST_INVOICE) { - console.log('Usage: node test-pay.mjs ') - console.log( - 'Generate invoice: docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt 1000' - ) - process.exit(1) -} - -async function main() { - const identity = loadIdentityFromHex(DEV_PRIVATE_KEY) - console.log('Using identity:', identity.publicKey) - - console.log('Connecting to relay...') - const relay = await Relay.connect(RELAY_URL) - console.log('Connected!') - - const requestId = randomUUID() - - // Check if invoice has amount (look for pattern before '1' separator) - const amountMatch = TEST_INVOICE.toLowerCase().match(/ln(?:bc|tb|bcrt)(\d+)?([munp])?1/) - const hasAmount = amountMatch && amountMatch[1] - console.log('Invoice amount match:', amountMatch ? amountMatch.slice(0, 3) : null) - console.log('Has embedded amount:', hasAmount) - - // Build body - amount is always required (use 0 for invoices with embedded amounts) - const body = { - invoice: TEST_INVOICE, - amount: hasAmount ? 0 : 2000, // 0 means "use invoice amount" - } - console.log('Body amount:', body.amount, hasAmount ? '(use invoice amount)' : '(explicit amount)') - - const rpcRequest = { - rpcName: 'PayInvoice', - params: {}, - query: {}, - body, - authIdentifier: identity.publicKey, - requestId, - } - - console.log('\nRequest structure:', JSON.stringify(rpcRequest, null, 2)) - - const encryptedContent = encryptContent(identity, LIGHTNING_PUB_PUBKEY, rpcRequest) - - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - identity.privateKey - ) - - console.log('\nPublishing PayInvoice request (event id:', event.id.substring(0, 16) + '...)...') - - // Subscribe to responses - const filter = { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - since: Math.floor(Date.now() / 1000) - 5, - } - - let responseReceived = false - const sub = relay.subscribe([filter], { - onevent(evt) { - // Check if for us - const pTags = evt.tags.filter((t) => t[0] === 'p') - if (!pTags.some((t) => t[1] === identity.publicKey)) return - - console.log('\nGot response event:', evt.id.substring(0, 16) + '...') - try { - const response = decryptJSON(identity, LIGHTNING_PUB_PUBKEY, evt.content) - console.log('Response:', JSON.stringify(response, null, 2)) - if (response.requestId === requestId) { - responseReceived = true - } - } catch (err) { - console.log('Failed to decrypt:', err.message) - } - }, - }) - - await relay.publish(event) - console.log('Request published, waiting for response...') - - // Wait for response - for (let i = 0; i < 20; i++) { - await new Promise((r) => setTimeout(r, 500)) - if (responseReceived) break - } - - if (!responseReceived) { - console.log('\nNo response received for our requestId within timeout') - } - - sub.close() - relay.close() -} - -main().catch(console.error) diff --git a/packages/nostr-client/package.json b/packages/nostr-client/package.json index e235660..ef5ff2b 100644 --- a/packages/nostr-client/package.json +++ b/packages/nostr-client/package.json @@ -21,20 +21,14 @@ "validate-schemas": "tsx scripts/validate-schemas.ts" }, "dependencies": { - "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", - "@scure/base": "^1.2.6", - "@shocknet/clink-sdk": "^1.5.4", - "@stablelib/xchacha20": "^2.0.1", "nostr-tools": "^2.10.0" }, "devDependencies": { "@types/node": "^22.19.7", - "qrcode": "^1.5.4", "tsx": "^4.19.0", "typescript": "^5.7.0", - "vitest": "^2.1.0", - "ws": "^8.19.0" + "vitest": "^2.1.0" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f91de1..0fe5e88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,21 +211,9 @@ importers: packages/nostr-client: dependencies: - '@noble/curves': - specifier: ^2.0.1 - version: 2.0.1 '@noble/hashes': specifier: ^2.0.1 version: 2.0.1 - '@scure/base': - specifier: ^1.2.6 - version: 1.2.6 - '@shocknet/clink-sdk': - specifier: ^1.5.4 - version: 1.5.4 - '@stablelib/xchacha20': - specifier: ^2.0.1 - version: 2.0.1 nostr-tools: specifier: ^2.10.0 version: 2.19.4(typescript@5.9.3) @@ -233,9 +221,6 @@ importers: '@types/node': specifier: ^22.19.7 version: 22.19.7 - qrcode: - specifier: ^1.5.4 - version: 1.5.4 tsx: specifier: ^4.19.0 version: 4.21.0 @@ -245,9 +230,6 @@ importers: vitest: specifier: ^2.1.0 version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2) - ws: - specifier: ^8.19.0 - version: 8.19.0 packages/state-machine: dependencies: @@ -1107,28 +1089,10 @@ packages: resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} engines: {node: '>=12.0.0'} - '@shocknet/clink-sdk@1.5.4': - resolution: {integrity: sha512-YrKR7oFjzUmBhm4p8pT6mP3YC7UdWbIlh5PMXHSwSLfJhL8Ddx91NLIfOHUTcQa1cDf27uvillMFXYrVUMCRuQ==} - '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} - '@stablelib/binary@2.0.1': - resolution: {integrity: sha512-U9iAO8lXgEDONsA0zPPSgcf3HUBNAqHiJmSHgZz62OvC3Hi2Bhc5kTnQ3S1/L+sthDTHtCMhcEiklmIly6uQ3w==} - - '@stablelib/chacha@2.0.1': - resolution: {integrity: sha512-lS1FqtNqofxe2vLkRsLli2m3x/XanUyAYRphLhdHumKeIsLbjbCXdCq3Pf/eWiO7G3QlSG5ViqnoVjktzfLWMg==} - - '@stablelib/int@2.0.1': - resolution: {integrity: sha512-Ht63fQp3wz/F8U4AlXEPb7hfJOIILs8Lq55jgtD7KueWtyjhVuzcsGLSTAWtZs3XJDZYdF1WcSKn+kBtbzupww==} - - '@stablelib/wipe@2.0.1': - resolution: {integrity: sha512-1eU2K9EgOcV4qc9jcP6G72xxZxEm5PfeI5H55l08W95b4oRJaqhmlWRc4xZAm6IVSKhVNxMi66V67hCzzuMTAg==} - - '@stablelib/xchacha20@2.0.1': - resolution: {integrity: sha512-k55pNv7gIM4mUPU00+nJYTxKiUVNwAtsgrridC0aIU5cVbw9u6qP99x8ENu5eiwOEhZUNg+p3tTOLooCeAOJQA==} - '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} @@ -1590,10 +1554,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1639,9 +1599,6 @@ packages: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} - cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1744,10 +1701,6 @@ packages: supports-color: optional: true - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -1792,9 +1745,6 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - dijkstrajs@1.0.3: - resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} - dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} @@ -1965,10 +1915,6 @@ packages: filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -2043,10 +1989,6 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} - engines: {node: 20 || >=22} - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -2317,10 +2259,6 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} @@ -2356,10 +2294,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} - engines: {node: 20 || >=22} - lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -2543,14 +2477,6 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} - nostr-tools@2.15.1: - resolution: {integrity: sha512-LpetHDR9ltnkpJDkva/SONgyKBbsoV+5yLB8DWc0/U3lCWGtoWJw6Nbc2vR2Ai67RIQYrBQeZLyMlhwVZRK/9A==} - peerDependencies: - typescript: '>=5.0.0' - peerDependenciesMeta: - typescript: - optional: true - nostr-tools@2.19.4: resolution: {integrity: sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==} peerDependencies: @@ -2589,36 +2515,20 @@ packages: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -2631,10 +2541,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} - pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -2669,10 +2575,6 @@ packages: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} - pngjs@5.0.0: - resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} - engines: {node: '>=10.13.0'} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2723,11 +2625,6 @@ packages: peerDependencies: vue: ^3.0.0 - qrcode@1.5.4: - resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} - engines: {node: '>=10.13.0'} - hasBin: true - quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -2759,9 +2656,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - resedit@1.7.2: resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} engines: {node: '>=12', npm: '>=6'} @@ -2788,11 +2682,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@6.1.2: - resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==} - engines: {node: 20 || >=22} - hasBin: true - roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -3245,9 +3134,6 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3261,10 +3147,6 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3276,18 +3158,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -3295,9 +3165,6 @@ packages: xstate@5.25.1: resolution: {integrity: sha512-oyvsNH5pF2qkHmiHEMdWqc3OjDtoZOH2MTAI35r01f/ZQWOD+VLOiYqo65UgQET0XMA5s9eRm8fnsIo+82biEw==} - y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3305,18 +3172,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} - yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -3927,35 +3786,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@shocknet/clink-sdk@1.5.4': - dependencies: - '@noble/hashes': 1.8.0 - '@scure/base': 1.2.6 - nostr-tools: 2.15.1(typescript@5.9.3) - rimraf: 6.1.2 - typescript: 5.9.3 - '@sindresorhus/is@4.6.0': {} - '@stablelib/binary@2.0.1': - dependencies: - '@stablelib/int': 2.0.1 - - '@stablelib/chacha@2.0.1': - dependencies: - '@stablelib/binary': 2.0.1 - '@stablelib/wipe': 2.0.1 - - '@stablelib/int@2.0.1': {} - - '@stablelib/wipe@2.0.1': {} - - '@stablelib/xchacha20@2.0.1': - dependencies: - '@stablelib/binary': 2.0.1 - '@stablelib/chacha': 2.0.1 - '@stablelib/wipe': 2.0.1 - '@swc/helpers@0.5.18': dependencies: tslib: 2.8.1 @@ -4539,8 +4371,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - camelcase@5.3.1: {} - chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -4582,12 +4412,6 @@ snapshots: string-width: 4.2.3 optional: true - cliui@6.0.0: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -4678,8 +4502,6 @@ snapshots: dependencies: ms: 2.1.3 - decamelize@1.2.0: {} - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -4719,8 +4541,6 @@ snapshots: detect-node@2.1.0: optional: true - dijkstrajs@1.0.3: {} - dir-compare@4.2.0: dependencies: minimatch: 3.1.2 @@ -4995,11 +4815,6 @@ snapshots: dependencies: minimatch: 5.1.6 - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -5101,12 +4916,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@13.0.0: - dependencies: - minimatch: 10.1.1 - minipass: 7.1.2 - path-scurry: 2.0.1 - glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -5368,10 +5177,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - lodash-es@4.17.23: {} lodash.defaults@4.2.0: {} @@ -5397,8 +5202,6 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.4: {} - lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -5575,18 +5378,6 @@ snapshots: normalize-url@6.1.0: {} - nostr-tools@2.15.1(typescript@5.9.3): - dependencies: - '@noble/ciphers': 0.5.3 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.1 - '@scure/base': 1.1.1 - '@scure/bip32': 1.3.1 - '@scure/bip39': 1.2.1 - nostr-wasm: 0.1.0 - optionalDependencies: - typescript: 5.9.3 - nostr-tools@2.19.4(typescript@5.9.3): dependencies: '@noble/ciphers': 0.5.3 @@ -5635,30 +5426,18 @@ snapshots: p-cancelable@2.1.1: {} - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - p-map@4.0.0: dependencies: aggregate-error: 3.1.0 - p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} path-browserify@1.0.1: {} - path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -5668,11 +5447,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-scurry@2.0.1: - dependencies: - lru-cache: 11.2.4 - minipass: 7.1.2 - pathe@1.1.2: {} pathval@2.0.1: {} @@ -5701,8 +5475,6 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 - pngjs@5.0.0: {} - postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -5750,12 +5522,6 @@ snapshots: dependencies: vue: 3.5.27(typescript@5.9.3) - qrcode@1.5.4: - dependencies: - dijkstrajs: 1.0.3 - pngjs: 5.0.0 - yargs: 15.4.1 - quick-lru@5.1.1: {} rc@1.2.8: @@ -5810,8 +5576,6 @@ snapshots: require-directory@2.1.1: {} - require-main-filename@2.0.0: {} - resedit@1.7.2: dependencies: pe-library: 0.4.1 @@ -5835,11 +5599,6 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@6.1.2: - dependencies: - glob: 13.0.0 - package-json-from-dist: 1.0.1 - roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -6291,8 +6050,6 @@ snapshots: dependencies: defaults: 1.0.4 - which-module@2.0.1: {} - which@2.0.2: dependencies: isexe: 2.0.0 @@ -6306,12 +6063,6 @@ snapshots: dependencies: string-width: 4.2.3 - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -6326,39 +6077,16 @@ snapshots: wrappy@1.0.2: {} - ws@8.19.0: {} - xmlbuilder@15.1.1: {} xstate@5.25.1: {} - y18n@4.0.3: {} - y18n@5.0.8: {} yallist@4.0.0: {} - yargs-parser@18.1.3: - dependencies: - camelcase: 5.3.1 - decamelize: 1.2.0 - yargs-parser@21.1.1: {} - yargs@15.4.1: - dependencies: - cliui: 6.0.0 - decamelize: 1.2.0 - find-up: 4.1.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - require-main-filename: 2.0.0 - set-blocking: 2.0.0 - string-width: 4.2.3 - which-module: 2.0.1 - y18n: 4.0.3 - yargs-parser: 18.1.3 - yargs@17.7.2: dependencies: cliui: 8.0.1