chore(dev): remove the Lightning.Pub-era regtest tooling

docker/ (two compose stacks, dev.sh, regtest.sh, start-with-regtest.sh,
regtest-bootstrap.sh, strfry.conf), packages/nostr-client/dev/ (nine
agent and test scripts), and the seventeen devenv commands that drove
them — infra-*, lncli, btccli, mine-blocks, auto-mine, setup-channel,
alice-*, fund-atm, test-setup, test-payment, node-info — along with the
devenv postgres service, DATABASE_URL, LIGHTNING_PUB_URL, pgcli,
docker-compose and the `just` runner (no justfile exists).

None of it could talk to the app on `dev`. Every piece was built around
Lightning.Pub (a `lightning-pub` service in both compose files, 37
references in dev.sh, LIGHTNING_PUB_PUBKEY and the :1776 API in every
dev script, a NIP-44 v1 implementation the project forbids), and the
last substantive change predates the LNbits cutover that deleted
packages/lightning. No container under either name exists on any
machine. Development runs against LNbits: FakeWallet needs nothing,
bohm's native instance answers on :5001, and the shared regtest stack
lives at ~/dev/local/docker/regtest, outside this repo.

devenv.nix keeps the toolchain, the hardware/serial utilities, the git
hooks and `relay-test`, now pointed at LNbits's bundled nostrrelay. The
Rust toolchain stays for the orphaned crate until that is removed on its
own. nostr-client drops the four dependencies and two devDependencies
only the dead scripts imported (@noble/curves, @scure/base,
@shocknet/clink-sdk, @stablelib/xchacha20, qrcode, ws); lockfile
regenerated, −272 lines.

Verified: devenv.nix parses; nostr-client 43/43 + tsc; clink 11/11 + tsc;
machine app vue-tsc clean. The ndebit-cash-in-flow doc, kept as CLINK
design history, now says the commands it quotes no longer exist here.
This commit is contained in:
Padreug 2026-10-09 22:25:07 +02:00
commit 763817b9f3
20 changed files with 29 additions and 5170 deletions

View file

@ -1,8 +1,7 @@
{ pkgs, lib, config, ... }: { pkgs, lib, config, ... }:
{ {
# Project metadata name = "bitspire";
name = "bitspire-next";
# ============================================ # ============================================
# Languages # Languages
@ -19,9 +18,11 @@
languages.typescript.enable = true; languages.typescript.enable = true;
# Use nixpkgs rust (simpler, no overlay needed)
packages = with pkgs; [ 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 rustc
cargo cargo
clippy clippy
@ -33,42 +34,22 @@
openssl openssl
openssl.dev openssl.dev
# Electron dependencies (Linux) # Python for node-gyp (native module builds: serialport, better-sqlite3)
# electron # Use npm-installed electron instead
# Python for node-gyp (native module builds)
(python3.withPackages (ps: [ ps.setuptools ])) (python3.withPackages (ps: [ ps.setuptools ]))
# Hardware access # Hardware access
libusb1 libusb1
udev udev
# Serial port access # Serial port access (talk to a validator / dispenser by hand)
picocom picocom
# Development utilities # Development utilities
just # Task runner
jq # JSON processing jq # JSON processing
websocat # WebSocket client websocat # WebSocket client — relay-test below
qrencode # QR code generation for terminal 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) # Git Hooks (formerly pre-commit)
# ============================================ # ============================================
@ -97,21 +78,24 @@
env = { env = {
RUST_BACKTRACE = "1"; RUST_BACKTRACE = "1";
DATABASE_URL = "postgresql://localhost/bitspire_dev";
# Nostr development relay # The dev relay is LNbits's bundled nostrrelay extension on the native
NOSTR_RELAY_URL = "ws://localhost:7777"; # 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"; PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
}; };
# ============================================ # ============================================
# Scripts (available as commands in shell) # 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 = { scripts = {
dev.exec = "pnpm turbo dev"; dev.exec = "pnpm turbo dev";
@ -119,418 +103,13 @@
test.exec = "pnpm turbo test"; test.exec = "pnpm turbo test";
lint.exec = "pnpm turbo lint"; lint.exec = "pnpm turbo lint";
# Start infrastructure # Test the dev relay connection
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
relay-test.exec = '' relay-test.exec = ''
echo "Testing Nostr relay connection..." echo "Testing Nostr relay connection to $NOSTR_RELAY_URL ..."
echo '["REQ", "test", {"kinds": [0], "limit": 1}]' | websocat ws://localhost:7777 echo '["REQ", "test", {"kinds": [0], "limit": 1}]' | websocat "$NOSTR_RELAY_URL"
'';
# 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 <invoice>"
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"
''; '';
}; };
# ============================================
# Shell Hook
# ============================================
enterShell = '' enterShell = ''
echo "" echo ""
echo " ⚡ bitSpire - Nostr-Native Lightning ATM" echo " ⚡ bitSpire - Nostr-Native Lightning ATM"
@ -543,30 +122,8 @@
echo " dev Start development servers" echo " dev Start development servers"
echo " build Build all packages" echo " build Build all packages"
echo " test Run tests" echo " test Run tests"
echo "" echo " lint Lint all packages"
echo " Infrastructure:" echo " relay-test Test the dev relay ($NOSTR_RELAY_URL)"
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 "" echo ""
''; '';
} }

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <path>"
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 <name> Use specific Docker image"
echo " --worktree <path> 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

View file

@ -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"
# }

View file

@ -9,6 +9,11 @@
> debit-approval listener and all kind-21002 handling were removed in > debit-approval listener and all kind-21002 handling were removed in
> commit `3c14eea` (the 3b.4 cleanup of LP-paired infrastructure). > 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 > This doc is retained because it explains *why* the previous flow
> existed and what the ndebit/CLINK protocol surface looks like — useful > existed and what the ndebit/CLINK protocol surface looks like — useful
> if the project ever wants to reintroduce nostr-native cash-in that > if the project ever wants to reintroduce nostr-native cash-in that

View file

@ -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 <linking-token>')
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)
})

View file

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

View file

@ -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.')

View file

@ -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 = `<!DOCTYPE html>
<html>
<head>
<title>Mock ATM Machine</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
min-height: 100vh;
padding: 20px;
}
.container { max-width: 500px; margin: 0 auto; }
h1 { text-align: center; margin-bottom: 8px; color: #00d4ff; font-size: 28px; }
.subtitle { text-align: center; opacity: 0.6; margin-bottom: 20px; font-size: 14px; }
.card {
background: rgba(255,255,255,0.1);
border-radius: 16px;
padding: 24px;
margin-bottom: 16px;
backdrop-filter: blur(10px);
}
.balance-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(0,0,0,0.2);
border-radius: 8px;
margin-bottom: 16px;
font-size: 14px;
}
.balance-bar .label { opacity: 0.7; }
.balance-bar .value { color: #00ff88; font-weight: bold; }
.exchange-rate { font-size: 12px; opacity: 0.5; }
/* ATM Screen */
.atm-screen {
min-height: 400px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.screen-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 8px;
}
.screen-subtitle {
opacity: 0.7;
margin-bottom: 24px;
}
/* Cash buttons */
.cash-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
width: 100%;
max-width: 320px;
}
.cash-btn {
background: linear-gradient(135deg, #2d5a27 0%, #1e3d1a 100%);
border: 2px solid #3d7a35;
border-radius: 12px;
padding: 24px 16px;
color: #fff;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.cash-btn:hover {
transform: scale(1.05);
border-color: #00ff88;
box-shadow: 0 4px 20px rgba(0,255,136,0.3);
}
.cash-btn:active {
transform: scale(0.98);
}
.cash-btn .amount {
font-size: 32px;
font-weight: bold;
color: #00ff88;
}
.cash-btn .sats {
font-size: 14px;
opacity: 0.8;
margin-top: 4px;
}
/* QR Display */
#qr-container {
display: flex;
justify-content: center;
padding: 20px;
background: #fff;
border-radius: 12px;
margin: 20px 0;
}
#qr-container img { max-width: 250px; }
.amount-display {
font-size: 36px;
font-weight: bold;
color: #00ff88;
margin-bottom: 8px;
}
.amount-usd {
font-size: 18px;
opacity: 0.7;
margin-bottom: 16px;
}
.ndebit-code {
font-family: monospace;
font-size: 9px;
word-break: break-all;
background: rgba(0,0,0,0.3);
padding: 12px;
border-radius: 8px;
margin-top: 16px;
max-width: 100%;
}
.cancel-btn {
background: transparent;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 8px;
padding: 12px 32px;
color: #fff;
cursor: pointer;
margin-top: 16px;
font-size: 14px;
}
.cancel-btn:hover {
border-color: #ff6b6b;
color: #ff6b6b;
}
/* Processing */
.spinner {
width: 60px;
height: 60px;
border: 4px solid rgba(255,255,255,0.2);
border-top-color: #00d4ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Success */
.success-icon {
width: 80px;
height: 80px;
background: #00ff88;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
font-size: 40px;
}
/* Logs */
.logs-card { margin-top: 8px; }
.logs-card h3 { font-size: 14px; margin-bottom: 8px; opacity: 0.7; }
.logs {
background: rgba(0,0,0,0.3);
border-radius: 8px;
padding: 12px;
height: 150px;
overflow-y: auto;
font-family: monospace;
font-size: 11px;
}
.log-entry { margin: 4px 0; }
.log-entry.alert { color: #00d4ff; }
.log-entry.success { color: #00ff88; }
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="container">
<h1>Mock ATM</h1>
<p class="subtitle">Simulated Bitcoin ATM for testing</p>
<div class="balance-bar">
<span class="label">ATM Balance:</span>
<span class="value"><span id="balance">0</span> sats</span>
</div>
<div class="card">
<div class="atm-screen">
<!-- IDLE State -->
<div id="screen-idle">
<div class="screen-title">Insert Cash</div>
<div class="screen-subtitle">Select amount to withdraw as Bitcoin</div>
<div class="cash-buttons">
<button class="cash-btn" data-usd="20">
<div class="amount">$20</div>
<div class="sats" id="sats-20">20,000 sats</div>
</button>
<button class="cash-btn" data-usd="50">
<div class="amount">$50</div>
<div class="sats" id="sats-50">50,000 sats</div>
</button>
<button class="cash-btn" data-usd="100">
<div class="amount">$100</div>
<div class="sats" id="sats-100">100,000 sats</div>
</button>
<button class="cash-btn" data-usd="200">
<div class="amount">$200</div>
<div class="sats" id="sats-200">200,000 sats</div>
</button>
</div>
<p class="exchange-rate">Rate: <span id="rate">1,000</span> sats/$</p>
</div>
<!-- WAITING_FOR_SCAN State -->
<div id="screen-scan" class="hidden">
<div class="screen-title">Scan QR Code</div>
<div class="screen-subtitle">Open your wallet and scan to receive</div>
<div class="amount-display"><span id="display-sats">0</span> sats</div>
<div class="amount-usd">($<span id="display-usd">0</span>)</div>
<div id="qr-container">
<img id="qr" alt="QR Code" />
</div>
<div class="ndebit-code" id="ndebit-code"></div>
<button class="cancel-btn" id="cancel-btn">Cancel Transaction</button>
</div>
<!-- PROCESSING State -->
<div id="screen-processing" class="hidden">
<div class="spinner"></div>
<div class="screen-title">Processing...</div>
<div class="screen-subtitle">Verifying payment request</div>
</div>
<!-- COMPLETE State -->
<div id="screen-complete" class="hidden">
<div class="success-icon">✓</div>
<div class="screen-title">Success!</div>
<div class="screen-subtitle">
<span id="complete-sats">0</span> sats sent to your wallet
</div>
</div>
</div>
</div>
<div class="card logs-card">
<h3>Activity Log</h3>
<div class="logs" id="logs"></div>
</div>
</div>
<script>
const ws = new WebSocket('ws://localhost:3456')
const logs = document.getElementById('logs')
// Screen elements
const screens = {
idle: document.getElementById('screen-idle'),
scan: document.getElementById('screen-scan'),
processing: document.getElementById('screen-processing'),
complete: document.getElementById('screen-complete'),
}
function showScreen(name) {
Object.values(screens).forEach(s => s.classList.add('hidden'))
if (screens[name]) screens[name].classList.remove('hidden')
}
// Cash buttons
document.querySelectorAll('.cash-btn').forEach(btn => {
btn.onclick = () => {
const usd = parseInt(btn.dataset.usd, 10)
ws.send(JSON.stringify({ type: 'insert_cash', amount: usd }))
}
})
// Cancel button
document.getElementById('cancel-btn').onclick = () => {
ws.send(JSON.stringify({ type: 'reset' }))
}
function addLog(message, type = '') {
const entry = document.createElement('div')
entry.className = 'log-entry' + (type ? ' ' + type : '')
entry.textContent = message
logs.appendChild(entry)
logs.scrollTop = logs.scrollHeight
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'log') {
const isAlert = data.message.includes('[ALERT]')
const isSuccess = data.message.includes('[OK]') || data.message.includes('Success')
addLog(data.message, isAlert ? 'alert' : isSuccess ? 'success' : '')
}
if (data.type === 'status') {
// Update balance
document.getElementById('balance').textContent = data.balance.toLocaleString()
// Update exchange rate display
if (data.satsPerUsd) {
document.getElementById('rate').textContent = data.satsPerUsd.toLocaleString()
document.getElementById('sats-20').textContent = (20 * data.satsPerUsd).toLocaleString() + ' sats'
document.getElementById('sats-50').textContent = (50 * data.satsPerUsd).toLocaleString() + ' sats'
document.getElementById('sats-100').textContent = (100 * data.satsPerUsd).toLocaleString() + ' sats'
document.getElementById('sats-200').textContent = (200 * data.satsPerUsd).toLocaleString() + ' sats'
}
// Update amounts
if (data.withdrawAmount !== undefined) {
document.getElementById('display-sats').textContent = data.withdrawAmount.toLocaleString()
document.getElementById('complete-sats').textContent = data.withdrawAmount.toLocaleString()
}
if (data.cashInserted !== undefined) {
document.getElementById('display-usd').textContent = data.cashInserted
}
// Update QR
if (data.ndebitQR) {
document.getElementById('qr').src = data.ndebitQR
}
if (data.ndebitUri) {
document.getElementById('ndebit-code').textContent = data.ndebitUri
}
// Show correct screen based on state
switch (data.atmState) {
case 'idle':
showScreen('idle')
break
case 'cash_inserted':
case 'waiting_for_scan':
showScreen('scan')
break
case 'processing':
showScreen('processing')
break
case 'complete':
showScreen('complete')
break
}
}
if (data.type === 'debit_request') {
addLog('[DEBIT] Request received from wallet', 'alert')
}
}
ws.onopen = () => {
addLog('Connected to ATM server')
}
ws.onerror = () => {
addLog('Connection error - is the server running?')
}
</script>
</body>
</html>`
// 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()
})

View file

@ -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),
}
}

View file

@ -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=<hex-private-key>
* export LIGHTNING_PUB_PUBKEY=<hex-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)
})

View file

@ -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 <ndebit> <bolt11>')
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)

View file

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

View file

@ -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 <invoice>')
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)

View file

@ -21,20 +21,14 @@
"validate-schemas": "tsx scripts/validate-schemas.ts" "validate-schemas": "tsx scripts/validate-schemas.ts"
}, },
"dependencies": { "dependencies": {
"@noble/curves": "^2.0.1",
"@noble/hashes": "^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" "nostr-tools": "^2.10.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.19.7", "@types/node": "^22.19.7",
"qrcode": "^1.5.4",
"tsx": "^4.19.0", "tsx": "^4.19.0",
"typescript": "^5.7.0", "typescript": "^5.7.0",
"vitest": "^2.1.0", "vitest": "^2.1.0"
"ws": "^8.19.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0"

272
pnpm-lock.yaml generated
View file

@ -211,21 +211,9 @@ importers:
packages/nostr-client: packages/nostr-client:
dependencies: dependencies:
'@noble/curves':
specifier: ^2.0.1
version: 2.0.1
'@noble/hashes': '@noble/hashes':
specifier: ^2.0.1 specifier: ^2.0.1
version: 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: nostr-tools:
specifier: ^2.10.0 specifier: ^2.10.0
version: 2.19.4(typescript@5.9.3) version: 2.19.4(typescript@5.9.3)
@ -233,9 +221,6 @@ importers:
'@types/node': '@types/node':
specifier: ^22.19.7 specifier: ^22.19.7
version: 22.19.7 version: 22.19.7
qrcode:
specifier: ^1.5.4
version: 1.5.4
tsx: tsx:
specifier: ^4.19.0 specifier: ^4.19.0
version: 4.21.0 version: 4.21.0
@ -245,9 +230,6 @@ importers:
vitest: vitest:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2) 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: packages/state-machine:
dependencies: dependencies:
@ -1107,28 +1089,10 @@ packages:
resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
'@shocknet/clink-sdk@1.5.4':
resolution: {integrity: sha512-YrKR7oFjzUmBhm4p8pT6mP3YC7UdWbIlh5PMXHSwSLfJhL8Ddx91NLIfOHUTcQa1cDf27uvillMFXYrVUMCRuQ==}
'@sindresorhus/is@4.6.0': '@sindresorhus/is@4.6.0':
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'} 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': '@swc/helpers@0.5.18':
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
@ -1590,10 +1554,6 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
chai@5.3.3: chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1639,9 +1599,6 @@ packages:
resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
engines: {node: '>=8'} engines: {node: '>=8'}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
cliui@8.0.1: cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1744,10 +1701,6 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decompress-response@6.0.0: decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1792,9 +1745,6 @@ packages:
detect-node@2.1.0: detect-node@2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
dir-compare@4.2.0: dir-compare@4.2.0:
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
@ -1965,10 +1915,6 @@ packages:
filelist@1.0.4: filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} 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: foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} 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 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 hasBin: true
glob@13.0.0:
resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
engines: {node: 20 || >=22}
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 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 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==} resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
lodash-es@4.17.23: lodash-es@4.17.23:
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
@ -2356,10 +2294,6 @@ packages:
lru-cache@10.4.3: lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 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: lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2543,14 +2477,6 @@ packages:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'} 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: nostr-tools@2.19.4:
resolution: {integrity: sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==} resolution: {integrity: sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==}
peerDependencies: peerDependencies:
@ -2589,36 +2515,20 @@ packages:
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
engines: {node: '>=8'} engines: {node: '>=8'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-limit@3.1.0: p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-map@4.0.0: p-map@4.0.0:
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
engines: {node: '>=10'} 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: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-browserify@1.0.1: path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} 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: path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2631,10 +2541,6 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'} 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: pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
@ -2669,10 +2575,6 @@ packages:
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
engines: {node: '>=10.4.0'} engines: {node: '>=10.4.0'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
postcss@8.5.6: postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@ -2723,11 +2625,6 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.0.0 vue: ^3.0.0
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
quick-lru@5.1.1: quick-lru@5.1.1:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2759,9 +2656,6 @@ packages:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
resedit@1.7.2: resedit@1.7.2:
resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
engines: {node: '>=12', npm: '>=6'} engines: {node: '>=12', npm: '>=6'}
@ -2788,11 +2682,6 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true hasBin: true
rimraf@6.1.2:
resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==}
engines: {node: 20 || >=22}
hasBin: true
roarr@2.15.4: roarr@2.15.4:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@ -3245,9 +3134,6 @@ packages:
wcwidth@1.0.1: wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -3261,10 +3147,6 @@ packages:
wide-align@1.1.5: wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} 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: wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3276,18 +3158,6 @@ packages:
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 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: xmlbuilder@15.1.1:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@ -3295,9 +3165,6 @@ packages:
xstate@5.25.1: xstate@5.25.1:
resolution: {integrity: sha512-oyvsNH5pF2qkHmiHEMdWqc3OjDtoZOH2MTAI35r01f/ZQWOD+VLOiYqo65UgQET0XMA5s9eRm8fnsIo+82biEw==} resolution: {integrity: sha512-oyvsNH5pF2qkHmiHEMdWqc3OjDtoZOH2MTAI35r01f/ZQWOD+VLOiYqo65UgQET0XMA5s9eRm8fnsIo+82biEw==}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
y18n@5.0.8: y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3305,18 +3172,10 @@ packages:
yallist@4.0.0: yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs-parser@21.1.1: yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'} engines: {node: '>=12'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yargs@17.7.2: yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -3927,35 +3786,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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': {} '@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': '@swc/helpers@0.5.18':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@ -4539,8 +4371,6 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
function-bind: 1.1.2 function-bind: 1.1.2
camelcase@5.3.1: {}
chai@5.3.3: chai@5.3.3:
dependencies: dependencies:
assertion-error: 2.0.1 assertion-error: 2.0.1
@ -4582,12 +4412,6 @@ snapshots:
string-width: 4.2.3 string-width: 4.2.3
optional: true 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: cliui@8.0.1:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
@ -4678,8 +4502,6 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
decompress-response@6.0.0: decompress-response@6.0.0:
dependencies: dependencies:
mimic-response: 3.1.0 mimic-response: 3.1.0
@ -4719,8 +4541,6 @@ snapshots:
detect-node@2.1.0: detect-node@2.1.0:
optional: true optional: true
dijkstrajs@1.0.3: {}
dir-compare@4.2.0: dir-compare@4.2.0:
dependencies: dependencies:
minimatch: 3.1.2 minimatch: 3.1.2
@ -4995,11 +4815,6 @@ snapshots:
dependencies: dependencies:
minimatch: 5.1.6 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: foreground-child@3.3.1:
dependencies: dependencies:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
@ -5101,12 +4916,6 @@ snapshots:
package-json-from-dist: 1.0.1 package-json-from-dist: 1.0.1
path-scurry: 1.11.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: glob@7.2.3:
dependencies: dependencies:
fs.realpath: 1.0.0 fs.realpath: 1.0.0
@ -5368,10 +5177,6 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-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-es@4.17.23: {}
lodash.defaults@4.2.0: {} lodash.defaults@4.2.0: {}
@ -5397,8 +5202,6 @@ snapshots:
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lru-cache@11.2.4: {}
lru-cache@6.0.0: lru-cache@6.0.0:
dependencies: dependencies:
yallist: 4.0.0 yallist: 4.0.0
@ -5575,18 +5378,6 @@ snapshots:
normalize-url@6.1.0: {} 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): nostr-tools@2.19.4(typescript@5.9.3):
dependencies: dependencies:
'@noble/ciphers': 0.5.3 '@noble/ciphers': 0.5.3
@ -5635,30 +5426,18 @@ snapshots:
p-cancelable@2.1.1: {} p-cancelable@2.1.1: {}
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-limit@3.1.0: p-limit@3.1.0:
dependencies: dependencies:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-map@4.0.0: p-map@4.0.0:
dependencies: dependencies:
aggregate-error: 3.1.0 aggregate-error: 3.1.0
p-try@2.2.0: {}
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
path-browserify@1.0.1: {} path-browserify@1.0.1: {}
path-exists@4.0.0: {}
path-is-absolute@1.0.1: {} path-is-absolute@1.0.1: {}
path-key@3.1.1: {} path-key@3.1.1: {}
@ -5668,11 +5447,6 @@ snapshots:
lru-cache: 10.4.3 lru-cache: 10.4.3
minipass: 7.1.2 minipass: 7.1.2
path-scurry@2.0.1:
dependencies:
lru-cache: 11.2.4
minipass: 7.1.2
pathe@1.1.2: {} pathe@1.1.2: {}
pathval@2.0.1: {} pathval@2.0.1: {}
@ -5701,8 +5475,6 @@ snapshots:
base64-js: 1.5.1 base64-js: 1.5.1
xmlbuilder: 15.1.1 xmlbuilder: 15.1.1
pngjs@5.0.0: {}
postcss@8.5.6: postcss@8.5.6:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
@ -5750,12 +5522,6 @@ snapshots:
dependencies: dependencies:
vue: 3.5.27(typescript@5.9.3) 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: {} quick-lru@5.1.1: {}
rc@1.2.8: rc@1.2.8:
@ -5810,8 +5576,6 @@ snapshots:
require-directory@2.1.1: {} require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
resedit@1.7.2: resedit@1.7.2:
dependencies: dependencies:
pe-library: 0.4.1 pe-library: 0.4.1
@ -5835,11 +5599,6 @@ snapshots:
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3
rimraf@6.1.2:
dependencies:
glob: 13.0.0
package-json-from-dist: 1.0.1
roarr@2.15.4: roarr@2.15.4:
dependencies: dependencies:
boolean: 3.2.0 boolean: 3.2.0
@ -6291,8 +6050,6 @@ snapshots:
dependencies: dependencies:
defaults: 1.0.4 defaults: 1.0.4
which-module@2.0.1: {}
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
@ -6306,12 +6063,6 @@ snapshots:
dependencies: dependencies:
string-width: 4.2.3 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: wrap-ansi@7.0.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
@ -6326,39 +6077,16 @@ snapshots:
wrappy@1.0.2: {} wrappy@1.0.2: {}
ws@8.19.0: {}
xmlbuilder@15.1.1: {} xmlbuilder@15.1.1: {}
xstate@5.25.1: {} xstate@5.25.1: {}
y18n@4.0.3: {}
y18n@5.0.8: {} y18n@5.0.8: {}
yallist@4.0.0: {} yallist@4.0.0: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs-parser@21.1.1: {} 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: yargs@17.7.2:
dependencies: dependencies:
cliui: 8.0.1 cliui: 8.0.1