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:
parent
723553522c
commit
763817b9f3
20 changed files with 29 additions and 5170 deletions
489
devenv.nix
489
devenv.nix
|
|
@ -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 ""
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1087
docker/dev.sh
1087
docker/dev.sh
File diff suppressed because it is too large
Load diff
|
|
@ -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:
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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 ""
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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"
|
|
||||||
# }
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
})
|
|
||||||
|
|
@ -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)
|
|
||||||
})
|
|
||||||
|
|
@ -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.')
|
|
||||||
|
|
@ -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()
|
|
||||||
})
|
|
||||||
|
|
@ -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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
})
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
})
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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
272
pnpm-lock.yaml
generated
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue