From db2ad27340b70eea1134685225fcd45239cd2b69 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 18 Mar 2026 21:06:14 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20dev=20environment=20=E2=80=94=20signet?= =?UTF-8?q?=20testnet=20stack,=20mock=20LND=20RPCs,=20faucet=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch docker-compose from regtest to signet, add standalone testnet stack (docker-compose.testnet.yml) with Bitcoin+LND+ThunderHub+Fedimint. Mock backend now auto-detects Podman/Docker sockets and includes full LND/Lightning RPC mocks. Dev scripts refactored with boot mode, testnet option, and macOS EAGAIN fix for port cleanup. Added dev faucet button to Home.vue. Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.testnet.yml | 147 ++++++++++ docker-compose.yml | 59 ++-- neode-ui/dev-dist/sw.js | 2 +- neode-ui/mock-backend.js | 496 ++++++++++++++++++++++++++++++++- neode-ui/package.json | 4 +- neode-ui/src/views/Home.vue | 16 +- neode-ui/start-dev.sh | 16 +- scripts/dev-start.sh | 340 +++++++++++----------- testnet/README.md | 88 ++++++ testnet/thunderhub-config.yaml | 8 + 10 files changed, 958 insertions(+), 218 deletions(-) create mode 100644 docker-compose.testnet.yml create mode 100644 testnet/README.md create mode 100644 testnet/thunderhub-config.yaml diff --git a/docker-compose.testnet.yml b/docker-compose.testnet.yml new file mode 100644 index 00000000..c0950908 --- /dev/null +++ b/docker-compose.testnet.yml @@ -0,0 +1,147 @@ +# Archipelago Lightning Testnet Stack (Signet) +# Real Bitcoin signet + LND + ThunderHub for testing Lightning features +# +# Start: docker compose -f docker-compose.testnet.yml up -d +# Stop: docker compose -f docker-compose.testnet.yml down +# Logs: docker compose -f docker-compose.testnet.yml logs -f +# +# First run: signet blockchain syncs in ~10 minutes (~200MB) +# LND wallet auto-created with --noseedbackup (dev only!) +# +# Access: +# ThunderHub: http://localhost:3010 (password: thunderhub) +# LND REST: http://localhost:8080 +# LND gRPC: localhost:10009 +# Bitcoin RPC: localhost:38332 (user: bitcoin, pass: bitcoinpass) +# +# Get signet coins: https://signetfaucet.com or https://alt.signetfaucet.com + +services: + # Bitcoin Core — signet mode (lightweight testnet, ~200MB sync) + bitcoind-signet: + image: lncm/bitcoind:v27.0 + container_name: archy-bitcoind-signet + ports: + - "38332:38332" # RPC + - "38333:38333" # P2P + volumes: + - signet-bitcoin-data:/data/.bitcoin + command: | + -signet + -server + -rpcuser=bitcoin + -rpcpassword=bitcoinpass + -rpcallowip=0.0.0.0/0 + -rpcbind=0.0.0.0 + -rpcport=38332 + -txindex=1 + -zmqpubrawblock=tcp://0.0.0.0:28332 + -zmqpubrawtx=tcp://0.0.0.0:28333 + restart: unless-stopped + healthcheck: + test: ["CMD", "bitcoin-cli", "-signet", "-rpcuser=bitcoin", "-rpcpassword=bitcoinpass", "-rpcport=38332", "getblockchaininfo"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + networks: + - signet-net + + # LND — connected to signet bitcoind + lnd-signet: + image: lightninglabs/lnd:v0.17.4-beta + container_name: archy-lnd-signet + ports: + - "9735:9735" # P2P (Lightning) + - "8080:8080" # REST API + - "10009:10009" # gRPC + volumes: + - signet-lnd-data:/root/.lnd + command: | + --bitcoin.active + --bitcoin.signet + --bitcoin.node=bitcoind + --bitcoind.rpchost=bitcoind-signet:38332 + --bitcoind.rpcuser=bitcoin + --bitcoind.rpcpass=bitcoinpass + --bitcoind.zmqpubrawblock=tcp://bitcoind-signet:28332 + --bitcoind.zmqpubrawtx=tcp://bitcoind-signet:28333 + --debuglevel=info + --rpclisten=0.0.0.0:10009 + --restlisten=0.0.0.0:8080 + --listen=0.0.0.0:9735 + --alias=archy-signet + --color=#f7931a + --noseedbackup + --accept-keysend + --gc-canceled-invoices-on-startup + depends_on: + bitcoind-signet: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "lncli", "--network=signet", "getinfo"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + networks: + - signet-net + + # ThunderHub — Lightning node management UI + thunderhub-signet: + image: apotdevin/thunderhub:v0.13.31 + container_name: archy-thunderhub-signet + ports: + - "3010:3000" + volumes: + - signet-lnd-data:/lnd-data:ro + - ./testnet/thunderhub-config.yaml:/data/thubConfig.yaml:ro + environment: + ACCOUNT_CONFIG_PATH: /data/thubConfig.yaml + LOG_LEVEL: info + THEME: dark + CURRENCY: BTC + FETCH_PRICES: "false" + FETCH_FEES: "true" + depends_on: + lnd-signet: + condition: service_healthy + restart: unless-stopped + networks: + - signet-net + + # Fedimint — signet mode (optional, for ecash testing) + fedimint-signet: + image: fedimint/fedimintd:v0.10.0 + container_name: archy-fedimint-signet + platform: linux/amd64 + ports: + - "18173:8173" # P2P + - "18174:8174" # API + - "18175:8175" # Guardian UI + volumes: + - signet-fedimint-data:/data + environment: + FM_BITCOIND_URL: http://bitcoind-signet:38332 + FM_BITCOIND_USERNAME: bitcoin + FM_BITCOIND_PASSWORD: bitcoinpass + FM_BITCOIN_NETWORK: signet + FM_BIND_P2P: 0.0.0.0:8173 + FM_BIND_API: 0.0.0.0:8174 + FM_BIND_UI: 0.0.0.0:8175 + depends_on: + bitcoind-signet: + condition: service_healthy + restart: unless-stopped + networks: + - signet-net + +volumes: + signet-bitcoin-data: + signet-lnd-data: + signet-fedimint-data: + +networks: + signet-net: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 6f014b3a..85fc186d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,21 @@ services: - # Bitcoin Core - regtest mode (no blockchain sync) + # Bitcoin Core - signet mode (lightweight testnet, ~200MB sync) bitcoin: image: lncm/bitcoind:v27.0 container_name: archy-bitcoin ports: - - "18443:18443" # RPC - - "18444:18444" # P2P + - "38332:38332" # RPC + - "38333:38333" # P2P volumes: - bitcoin-data:/data/.bitcoin command: | - -regtest + -signet -server -rpcuser=bitcoin -rpcpassword=bitcoinpass -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0 + -rpcport=38332 -txindex=1 -zmqpubrawblock=tcp://0.0.0.0:28332 -zmqpubrawtx=tcp://0.0.0.0:28333 @@ -22,18 +23,6 @@ services: networks: - archy-net - # Bitcoin Core UI - Web interface - bitcoin-ui: - image: nginx:alpine - container_name: archy-bitcoin-ui - ports: - - "18445:80" - volumes: - - ./docker/bitcoin-ui:/usr/share/nginx/html:ro - restart: unless-stopped - networks: - - archy-net - # BTCPay Server btcpay: image: btcpayserver/btcpayserver:1.13.5 @@ -45,7 +34,7 @@ services: BTCPAY_HOST: localhost:14142 BTCPAY_CHAINS: btc BTCPAY_BTCEXPLORERURL: http://mempool:4080 - BTCPAY_BTCRPCURL: http://bitcoin:18443 + BTCPAY_BTCRPCURL: http://bitcoin:38332 BTCPAY_BTCRPCUSER: bitcoin BTCPAY_BTCRPCPASSWORD: bitcoinpass depends_on: @@ -109,10 +98,10 @@ services: volumes: - fedimint-data:/data environment: - FM_BITCOIND_URL: http://bitcoin:18443 + FM_BITCOIND_URL: http://bitcoin:38332 FM_BITCOIND_USERNAME: bitcoin FM_BITCOIND_PASSWORD: bitcoinpass - FM_BITCOIN_NETWORK: regtest + FM_BITCOIN_NETWORK: signet FM_BIND_P2P: 0.0.0.0:8173 FM_BIND_API: 0.0.0.0:8174 FM_BIND_UI: 0.0.0.0:8175 @@ -134,9 +123,9 @@ services: - lnd-data:/root/.lnd command: | --bitcoin.active - --bitcoin.regtest + --bitcoin.signet --bitcoin.node=bitcoind - --bitcoind.rpchost=bitcoin:18443 + --bitcoind.rpchost=bitcoin:38332 --bitcoind.rpcuser=bitcoin --bitcoind.rpcpass=bitcoinpass --bitcoind.zmqpubrawblock=tcp://bitcoin:28332 @@ -144,21 +133,35 @@ services: --debuglevel=info --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 + --listen=0.0.0.0:9735 + --alias=archy-dev + --color=#f7931a --noseedbackup + --accept-keysend depends_on: - bitcoin restart: unless-stopped networks: - archy-net - # LND UI - Web interface - lnd-ui: - image: nginx:alpine - container_name: archy-lnd-ui + # ThunderHub - Lightning node management UI + thunderhub: + image: apotdevin/thunderhub:v0.13.31 + container_name: archy-thunderhub ports: - - "8085:80" + - "3010:3000" volumes: - - ./docker/lnd-ui:/usr/share/nginx/html:ro + - lnd-data:/lnd-data:ro + - ./testnet/thunderhub-config.yaml:/data/thubConfig.yaml:ro + environment: + ACCOUNT_CONFIG_PATH: /data/thubConfig.yaml + LOG_LEVEL: info + THEME: dark + CURRENCY: BTC + FETCH_PRICES: "false" + FETCH_FEES: "true" + depends_on: + - lnd restart: unless-stopped networks: - archy-net @@ -187,7 +190,7 @@ services: ELECTRUM_PORT: 50001 ELECTRUM_TLS_ENABLED: "false" CORE_RPC_HOST: bitcoin - CORE_RPC_PORT: 18443 + CORE_RPC_PORT: 38332 CORE_RPC_USERNAME: bitcoin CORE_RPC_PASSWORD: bitcoinpass DATABASE_ENABLED: "true" diff --git a/neode-ui/dev-dist/sw.js b/neode-ui/dev-dist/sw.js index a0bb757e..940f5937 100644 --- a/neode-ui/dev-dist/sw.js +++ b/neode-ui/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.qj72pfa74qs" + "revision": "0.g6vfn35hb3c" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 4ed92b1d..ef2cdd8d 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -22,7 +22,38 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const execPromise = promisify(exec) -const docker = new Docker() + +// Find container socket: Podman (macOS/Linux) or Docker +import { existsSync } from 'fs' + +function findContainerSocket() { + // DOCKER_HOST env var (set by podman machine start) + if (process.env.DOCKER_HOST) { + const p = process.env.DOCKER_HOST.replace('unix://', '') + if (existsSync(p)) return p + } + // Podman machine socket (macOS) — check TMPDIR-based path + if (process.env.TMPDIR) { + const podmanDir = path.join(path.dirname(process.env.TMPDIR), 'podman') + const sock = path.join(podmanDir, 'podman-machine-default-api.sock') + if (existsSync(sock)) return sock + } + // Docker socket + if (existsSync('/var/run/docker.sock')) return '/var/run/docker.sock' + // Linux podman rootless + const uid = process.getuid?.() || 1000 + const linuxSock = `/run/user/${uid}/podman/podman.sock` + if (existsSync(linuxSock)) return linuxSock + return null +} + +const containerSocket = findContainerSocket() +const docker = containerSocket ? new Docker({ socketPath: containerSocket }) : null +if (containerSocket) { + console.log(`[Container] Socket: ${containerSocket}`) +} else { + console.log('[Container] No socket found — simulation mode (no Docker/Podman)') +} const app = express() const PORT = 5959 @@ -176,6 +207,7 @@ let nextAutoPort = 8200 // Helper: Query real Docker containers async function getDockerContainers() { + if (!docker) return {} try { const containers = await docker.listContainers({ all: true }) @@ -1869,6 +1901,302 @@ app.post('/rpc/v1', (req, res) => { return res.json({ result: { mesh_only: meshOnly, configured: true } }) } + // ===================================================================== + // LND / Lightning + // ===================================================================== + case 'lnd.getinfo': { + return res.json({ + result: { + alias: 'archy-signet', + color: '#f7931a', + num_active_channels: 4, + num_inactive_channels: 1, + num_pending_channels: 1, + block_height: 892451, + synced_to_chain: true, + synced_to_graph: true, + version: '0.17.4-beta', + identity_pubkey: '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', + chains: [{ chain: 'bitcoin', network: 'signet' }], + // Balances (Home.vue reads these from getinfo) + balance_sats: 2_350_000, + channel_balance_sats: 8_250_000, + }, + }) + } + + case 'lnd.gettransactions': { + return res.json({ + result: { + transactions: [ + { tx_hash: 'ab12cd34ef5678901234567890abcdef12345678', amount_sats: 2_000_000, num_confirmations: 142, block_height: 892310, time_stamp: Math.floor(Date.now()/1000) - 86400, label: 'Channel funding' }, + { tx_hash: 'cd34ef5678901234567890abcdef1234567890ab', amount_sats: 250_000, num_confirmations: 28, block_height: 892420, time_stamp: Math.floor(Date.now()/1000) - 7200, label: 'Faucet deposit' }, + { tx_hash: 'ff99ee88dd7766554433221100aabbccddeeff00', amount_sats: 100_000, num_confirmations: 0, block_height: 0, time_stamp: Math.floor(Date.now()/1000) - 600, label: 'Incoming from faucet' }, + ], + incoming_pending_count: 1, + }, + }) + } + + case 'lnd.channelbalance': { + return res.json({ + result: { + local_balance: { sat: 8250000 }, + remote_balance: { sat: 11750000 }, + pending_open_local_balance: { sat: 500000 }, + }, + }) + } + + case 'lnd.walletbalance': { + return res.json({ + result: { + total_balance: 2450000, + confirmed_balance: 2350000, + unconfirmed_balance: 100000, + }, + }) + } + + case 'lnd.listchannels': { + return res.json({ + result: { + channels: [ + { chan_id: '840921088114688', remote_pubkey: '02778f4a', capacity: 5000000, local_balance: 2450000, remote_balance: 2550000, active: true, peer_alias: 'ACINQ Signet' }, + { chan_id: '840921088114689', remote_pubkey: '03abcdef', capacity: 2000000, local_balance: 1200000, remote_balance: 800000, active: true, peer_alias: 'WalletOfSatoshi' }, + { chan_id: '840921088114690', remote_pubkey: '02fedcba', capacity: 10000000, local_balance: 4500000, remote_balance: 5500000, active: true, peer_alias: 'Voltage' }, + { chan_id: '840921088114691', remote_pubkey: '03456789', capacity: 3000000, local_balance: 100000, remote_balance: 2900000, active: true, peer_alias: 'Kraken' }, + ], + }, + }) + } + + case 'lnd.newaddress': { + const addrType = params?.type || 'p2wkh' + const mockAddr = addrType === 'p2tr' + ? 'tb1p' + Array.from({length: 58}, () => '0123456789abcdef'[Math.floor(Math.random()*16)]).join('') + : 'tb1q' + Array.from({length: 38}, () => '0123456789abcdef'[Math.floor(Math.random()*16)]).join('') + return res.json({ result: { address: mockAddr } }) + } + + case 'lnd.addinvoice': + case 'lnd.createinvoice': { + const amt = params?.amt || params?.value || params?.amount_sats || 1000 + const memo = params?.memo || '' + const rHash = Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join('') + return res.json({ + result: { + r_hash: rHash, + payment_request: `lnsb${amt}n1pjmock${Date.now().toString(36)}qqqxqyz5vqsp5mock${rHash.slice(0,20)}`, + add_index: Date.now(), + payment_addr: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), + }, + }) + } + + case 'lnd.payinvoice': + case 'lnd.sendpayment': { + return res.json({ + result: { + payment_hash: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), + payment_preimage: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), + status: 'SUCCEEDED', + fee_sat: Math.floor(Math.random() * 10) + 1, + value_sat: params?.amt || params?.amount_sats || 1000, + }, + }) + } + + case 'lnd.sendcoins': { + const amt = params?.amount || params?.amt || 50000 + return res.json({ + result: { + txid: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), + amount: amt, + }, + }) + } + + case 'lnd.decodepayreq': { + return res.json({ + result: { + destination: '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', + num_satoshis: params?.pay_req?.match(/lnsb(\d+)/)?.[1] || '1000', + description: 'Mock decoded invoice', + expiry: 3600, + timestamp: Math.floor(Date.now() / 1000), + }, + }) + } + + case 'lnd.openchannel': { + return res.json({ + result: { + funding_txid: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), + output_index: 0, + }, + }) + } + + case 'lnd.closechannel': { + return res.json({ + result: { + closing_txid: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), + }, + }) + } + + case 'lnd.listinvoices': { + return res.json({ result: { invoices: MOCK_LND_DATA.invoices } }) + } + + case 'lnd.listpayments': { + return res.json({ result: { payments: MOCK_LND_DATA.payments } }) + } + + case 'lnd.create-psbt': + case 'lnd.finalize-psbt': + case 'lnd.create-raw-tx': { + return res.json({ + result: { + psbt: 'cHNidP8BAH0CAAAA...mockPSBT', + txid: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), + }, + }) + } + + // ===================================================================== + // Wallet / Ecash (Fedimint) + // ===================================================================== + case 'wallet.ecash-balance': { + return res.json({ + result: { + balance_sats: 250_000, + balance_msat: 250_000_000, + token_count: 12, + federations: [ + { federation_id: 'fed1-demo', name: 'Archy Signet Mint', balance_msat: 250_000_000, gateway_active: true }, + ], + }, + }) + } + + case 'wallet.ecash-send': { + const amt = params?.amount_sats || 1000 + return res.json({ + result: { + token: `cashuSend_mock_${amt}_${Date.now().toString(36)}`, + amount_sats: amt, + }, + }) + } + + case 'wallet.ecash-receive': { + return res.json({ + result: { + amount_sats: 5000, + federation_id: 'fed1-demo', + }, + }) + } + + case 'wallet.ecash-history': { + return res.json({ + result: { + transactions: [ + { type: 'receive', amount_sats: 50000, timestamp: new Date(Date.now() - 86400000).toISOString(), note: 'Minted from Lightning' }, + { type: 'send', amount_sats: 5000, timestamp: new Date(Date.now() - 43200000).toISOString(), note: 'Sent ecash token' }, + { type: 'receive', amount_sats: 10000, timestamp: new Date(Date.now() - 3600000).toISOString(), note: 'Redeemed token' }, + ], + }, + }) + } + + case 'wallet.networking-profits': { + return res.json({ + result: { + total_earned_sats: 42, + total_forwarded_sats: 35025, + forward_count: 3, + period_days: 30, + daily_avg_sats: 1.4, + }, + }) + } + + case 'dev.faucet': { + // Dev-only: add mock funds to all wallet types + const amount = params?.amount_sats || 1_000_000 + console.log(`[Dev Faucet] Adding ${amount} sats to all wallets`) + return res.json({ + result: { + onchain: { txid: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), amount_sats: amount }, + lightning: { payment_hash: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), amount_sats: amount }, + ecash: { token: `cashuSend_faucet_${amount}_${Date.now().toString(36)}`, amount_sats: Math.floor(amount / 10) }, + message: `Added ${amount} sats on-chain, ${amount} sats Lightning, ${Math.floor(amount / 10)} sats ecash`, + }, + }) + } + + case 'bitcoin.getinfo': { + return res.json({ + result: { + chain: 'signet', + blocks: 892451, + headers: 892451, + bestblockhash: 'a1b2c3d4e5f6' + '0'.repeat(58), + difficulty: 0.001126515290698186, + mediantime: Math.floor(Date.now() / 1000) - 300, + verificationprogress: 1.0, + chainwork: '000000000000000000000000000000000000000000000000000000000001a2b3', + size_on_disk: 210_000_000, + pruned: false, + network: 'signet', + }, + }) + } + + // ===================================================================== + // System / Network / Updates + // ===================================================================== + case 'system.stats': { + return res.json({ + result: { + cpu_percent: +(12 + Math.random() * 18).toFixed(1), + mem_used_bytes: 6_200_000_000 + Math.floor(Math.random() * 500_000_000), + mem_total_bytes: 16_000_000_000, + disk_used_bytes: 620_000_000_000 + Math.floor(Math.random() * 10_000_000_000), + disk_total_bytes: 1_800_000_000_000, + uptime_secs: Math.floor(process.uptime()) + 604800, + load_avg: [+(0.5 + Math.random() * 1.5).toFixed(2), +(0.8 + Math.random()).toFixed(2), +(0.6 + Math.random()).toFixed(2)], + net_rx_bytes: 12_400_000_000, + net_tx_bytes: 8_900_000_000, + }, + }) + } + + case 'update.status': { + return res.json({ + result: { + current_version: '0.1.0', + latest_version: '0.1.1', + update_available: true, + release_notes: 'Bug fixes and performance improvements.', + channel: 'stable', + }, + }) + } + + case 'network.list-requests': { + return res.json({ + result: { + requests: [ + { id: 'req-1', from_did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', from_name: 'archy-198', type: 'federation-join', status: 'pending', created_at: new Date(Date.now() - 3600000).toISOString() }, + ], + }, + }) + } + default: { console.log(`[RPC] Unknown method: ${method}`) return res.json({ @@ -2192,6 +2520,169 @@ app.all('/aiui/api/*', (req, res) => { res.status(404).json({ error: 'Not available in demo mode' }) }) +// ============================================================================= +// Mock ThunderHub UI + Lightning API (no Docker required) +// ============================================================================= + +const MOCK_LND_DATA = { + info: { + alias: 'archy-signet', + color: '#f7931a', + public_key: '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', + num_active_channels: 4, + num_inactive_channels: 1, + num_pending_channels: 1, + block_height: 892451, + synced_to_chain: true, + synced_to_graph: true, + version: '0.17.4-beta commit=v0.17.4-beta', + chains: [{ chain: 'bitcoin', network: 'signet' }], + uris: ['03a1b2c3@archy-signet.onion:9735'], + }, + balance: { + total_balance: 2_450_000, + confirmed_balance: 2_350_000, + unconfirmed_balance: 100_000, + }, + channelBalance: { + local_balance: { sat: 8_250_000 }, + remote_balance: { sat: 11_750_000 }, + pending_open_local_balance: { sat: 500_000 }, + pending_open_remote_balance: { sat: 0 }, + }, + channels: [ + { chan_id: '840921088114688', remote_pubkey: '02778f4a4e...acinq', capacity: 5_000_000, local_balance: 2_450_000, remote_balance: 2_550_000, active: true, peer_alias: 'ACINQ Signet', total_satoshis_sent: 850_000, total_satoshis_received: 1_200_000, uptime: 604800, lifetime: 2592000 }, + { chan_id: '840921088114689', remote_pubkey: '03abcdef12...wos', capacity: 2_000_000, local_balance: 1_200_000, remote_balance: 800_000, active: true, peer_alias: 'WalletOfSatoshi', total_satoshis_sent: 350_000, total_satoshis_received: 500_000, uptime: 259200, lifetime: 1296000 }, + { chan_id: '840921088114690', remote_pubkey: '02fedcba98...voltage', capacity: 10_000_000, local_balance: 4_500_000, remote_balance: 5_500_000, active: true, peer_alias: 'Voltage', total_satoshis_sent: 2_100_000, total_satoshis_received: 1_800_000, uptime: 518400, lifetime: 2592000 }, + { chan_id: '840921088114691', remote_pubkey: '03456789ab...kraken', capacity: 3_000_000, local_balance: 100_000, remote_balance: 2_900_000, active: true, peer_alias: 'Kraken', total_satoshis_sent: 50_000, total_satoshis_received: 120_000, uptime: 86400, lifetime: 604800 }, + { chan_id: '840921088114692', remote_pubkey: '02112233aa...old', capacity: 1_000_000, local_balance: 0, remote_balance: 1_000_000, active: false, peer_alias: 'OldPeer-Offline', total_satoshis_sent: 0, total_satoshis_received: 0, uptime: 0, lifetime: 5184000 }, + ], + pendingChannels: { + pending_open_channels: [ + { channel: { remote_node_pub: '03ffeeddcc...newpeer', capacity: 500_000, local_balance: 500_000, remote_balance: 0 }, confirmation_height: 892452 }, + ], + pending_closing_channels: [], + pending_force_closing_channels: [], + waiting_close_channels: [], + }, + invoices: [ + { memo: 'Channel opening fee', value: 50_000, settled: true, creation_date: Math.floor(Date.now()/1000) - 86400, settle_date: Math.floor(Date.now()/1000) - 85800, payment_request: 'lnsb500000n1pjtest...truncated', state: 'SETTLED', amt_paid_sat: 50_000, r_hash: Buffer.from('aabbccdd01').toString('hex') }, + { memo: 'Test payment', value: 1_000, settled: true, creation_date: Math.floor(Date.now()/1000) - 7200, settle_date: Math.floor(Date.now()/1000) - 7100, payment_request: 'lnsb10000n1pjtest2...truncated', state: 'SETTLED', amt_paid_sat: 1_000, r_hash: Buffer.from('aabbccdd02').toString('hex') }, + { memo: 'Coffee payment', value: 5_000, settled: true, creation_date: Math.floor(Date.now()/1000) - 3600, settle_date: Math.floor(Date.now()/1000) - 3500, payment_request: 'lnsb50000n1pjtest3...truncated', state: 'SETTLED', amt_paid_sat: 5_000, r_hash: Buffer.from('aabbccdd03').toString('hex') }, + { memo: 'Donation', value: 21_000, settled: false, creation_date: Math.floor(Date.now()/1000) - 600, settle_date: 0, payment_request: 'lnsb210000n1pjtest4...truncated', state: 'OPEN', amt_paid_sat: 0, r_hash: Buffer.from('aabbccdd04').toString('hex') }, + ], + payments: [ + { payment_hash: 'ff00112233', value_sat: 10_000, fee_sat: 3, status: 'SUCCEEDED', creation_date: Math.floor(Date.now()/1000) - 43200, payment_request: 'lnsb100000n1pjpay1...', failure_reason: 'FAILURE_REASON_NONE' }, + { payment_hash: 'ff00112234', value_sat: 100_000, fee_sat: 12, status: 'SUCCEEDED', creation_date: Math.floor(Date.now()/1000) - 21600, payment_request: 'lnsb1000000n1pjpay2...', failure_reason: 'FAILURE_REASON_NONE' }, + { payment_hash: 'ff00112235', value_sat: 500, fee_sat: 1, status: 'SUCCEEDED', creation_date: Math.floor(Date.now()/1000) - 1800, payment_request: 'lnsb5000n1pjpay3...', failure_reason: 'FAILURE_REASON_NONE' }, + ], + forwarding: [ + { chan_id_in: '840921088114688', chan_id_out: '840921088114690', amt_in: 10_012, amt_out: 10_000, fee: 12, timestamp_ns: (Date.now() - 7200000) * 1e6 }, + { chan_id_in: '840921088114690', chan_id_out: '840921088114689', amt_in: 5_005, amt_out: 5_000, fee: 5, timestamp_ns: (Date.now() - 3600000) * 1e6 }, + { chan_id_in: '840921088114689', chan_id_out: '840921088114691', amt_in: 25_025, amt_out: 25_000, fee: 25, timestamp_ns: (Date.now() - 1200000) * 1e6 }, + ], +} + +// ThunderHub mock web UI +app.get('/app/thunderhub/', (req, res) => { + const d = MOCK_LND_DATA + const totalCap = d.channels.reduce((s, c) => s + c.capacity, 0) + const totalLocal = d.channels.reduce((s, c) => s + c.local_balance, 0) + const totalRemote = d.channels.reduce((s, c) => s + c.remote_balance, 0) + const totalFees = d.forwarding.reduce((s, f) => s + f.fee, 0) + const channelRows = d.channels.map(c => ` + + ${c.peer_alias} + ${(c.capacity/1e6).toFixed(1)}M +
${(c.local_balance/1e3).toFixed(0)}k
+
${(c.remote_balance/1e3).toFixed(0)}k
+ ${c.active ? 'Active' : 'Offline'} + `).join('') + const invoiceRows = d.invoices.slice().reverse().map(inv => ` + + ${inv.memo} + ${inv.value.toLocaleString()} sats + ${inv.settled ? 'Settled' : 'Open'} + ${new Date(inv.creation_date * 1000).toLocaleString()} + `).join('') + const paymentRows = d.payments.map(p => ` + + ${p.payment_hash.slice(0,12)}... + ${p.value_sat.toLocaleString()} sats + ${p.fee_sat} sats + Succeeded + `).join('') + + res.type('html').send(` +ThunderHub — archy-signet + +

ThunderHubsignet

+
${d.info.alias} — block ${d.info.block_height.toLocaleString()} — ${d.info.num_active_channels} active channels
+ +
+
On-chain Balance
${(d.balance.confirmed_balance/1e6).toFixed(2)}M sats
+
Channel Capacity
${(totalCap/1e6).toFixed(1)}M sats
+
Local Balance
${(totalLocal/1e6).toFixed(1)}M sats
+
Remote Balance
${(totalRemote/1e6).toFixed(1)}M sats
+
Routing Fees Earned
${totalFees} sats
+
Payments Sent
${d.payments.length}
+
+ +
+

Channels (${d.channels.length})

+${channelRows}
PeerCapacityLocalRemoteStatus
+
+ +
+

Recent Invoices

+${invoiceRows}
MemoAmountStatusCreated
+
+ +
+

Recent Payments

+${paymentRows}
HashAmountFeeStatus
+
+ +
+

Forwarding History

+ +${d.forwarding.map(f => { + const inPeer = d.channels.find(c => c.chan_id === f.chan_id_in)?.peer_alias || f.chan_id_in + const outPeer = d.channels.find(c => c.chan_id === f.chan_id_out)?.peer_alias || f.chan_id_out + return `` +}).join('')} +
In ChannelOut ChannelAmountFeeTime
${inPeer}${outPeer}${f.amt_in.toLocaleString()} sats${f.fee} sats${new Date(f.timestamp_ns/1e6).toLocaleString()}
+
+ +

Mock ThunderHub — Archipelago Dev Mode — No Docker Required

+`) +}) + +// ThunderHub API stubs (for any programmatic access) +app.get('/app/thunderhub/api/info', (req, res) => res.json(MOCK_LND_DATA.info)) +app.get('/app/thunderhub/api/balance', (req, res) => res.json(MOCK_LND_DATA.balance)) +app.get('/app/thunderhub/api/channels', (req, res) => res.json(MOCK_LND_DATA.channels)) +app.get('/app/thunderhub/api/invoices', (req, res) => res.json(MOCK_LND_DATA.invoices)) +app.get('/app/thunderhub/api/payments', (req, res) => res.json(MOCK_LND_DATA.payments)) +app.get('/app/thunderhub/api/forwards', (req, res) => res.json(MOCK_LND_DATA.forwarding)) + // Health check app.get('/health', (req, res) => { res.status(200).send('healthy') @@ -2290,8 +2781,7 @@ server.listen(PORT, '0.0.0.0', async () => { ║ ║ ║ Mock Password: ${MOCK_PASSWORD.padEnd(40)}║ ║ ║ -║ Container Runtime: ${runtime.available ? `✅ ${runtime.runtime}`.padEnd(40) : '❌ Not available'.padEnd(40)}║ -║ Docker API: ✅ Connected ║ +║ Container Runtime: ${runtime.available ? `✅ ${runtime.runtime}`.padEnd(40) : '⏭️ Simulation mode'.padEnd(40)}║ ║ Claude API Key: ${process.env.ANTHROPIC_API_KEY ? '✅ Set (' + process.env.ANTHROPIC_API_KEY.slice(0, 12) + '...)' : '❌ Not set (chat disabled)'.padEnd(40)}║ ║ ║ ╚════════════════════════════════════════════════════════════╝ diff --git a/neode-ui/package.json b/neode-ui/package.json index fbafdc3c..7d579398 100644 --- a/neode-ui/package.json +++ b/neode-ui/package.json @@ -9,8 +9,8 @@ "test": "vitest run", "test:watch": "vitest", "dev": "vite", - "dev:mock": "concurrently \"node mock-backend.js\" \"VITE_AIUI_URL=http://localhost:5173 vite\" \"cd ../../AIUI && pnpm dev 2>/dev/null || echo '[AIUI] Not found at ../../AIUI — chat will show placeholder'\"", - "dev:boot": "VITE_DEV_MODE=boot concurrently \"VITE_DEV_MODE=boot node mock-backend.js\" \"VITE_DEV_MODE=boot vite\"", + "dev:mock": "concurrently --raw \"node mock-backend.js\" \"VITE_AIUI_URL=http://localhost:5173 vite\" \"cd ../../AIUI && pnpm dev 2>/dev/null || echo '[AIUI] Not found at ../../AIUI — chat will show placeholder'\"", + "dev:boot": "VITE_DEV_MODE=boot concurrently --raw \"VITE_DEV_MODE=boot node mock-backend.js\" \"VITE_DEV_MODE=boot vite\"", "dev:real": "echo 'Start backend: cd ../core && cargo run --release' && vite", "backend:mock": "node mock-backend.js", "backend:real": "cd ../core && cargo run --release", diff --git a/neode-ui/src/views/Home.vue b/neode-ui/src/views/Home.vue index a0732a81..46f16524 100644 --- a/neode-ui/src/views/Home.vue +++ b/neode-ui/src/views/Home.vue @@ -345,13 +345,16 @@ {{ walletEcash.toLocaleString() }} sats -
+
+ Web5 @@ -719,6 +722,17 @@ onMounted(async () => { const showSendModal = ref(false) const showReceiveModal = ref(false) +// Dev faucet — adds mock funds to all wallet types +async function devFaucet() { + try { + const res = await rpcClient.call<{ message: string }>({ method: 'dev.faucet', params: { amount_sats: 1_000_000 } }) + console.log('[Faucet]', res.message) + await loadWeb5Status() + } catch (err) { + console.error('[Faucet] Error:', err) + } +} + // Wallet balances and transactions (fetched from RPC) const walletConnected = ref(false) const walletOnchain = ref(0) diff --git a/neode-ui/start-dev.sh b/neode-ui/start-dev.sh index 2da4cefb..d415429c 100755 --- a/neode-ui/start-dev.sh +++ b/neode-ui/start-dev.sh @@ -25,12 +25,16 @@ check_port() { lsof -ti:$1 > /dev/null 2>&1 } -# Function to kill process on a port +# Function to kill process on a port (avoids xargs which causes EAGAIN on macOS) kill_port() { local port=$1 - if check_port $port; then - echo -e "${YELLOW}⚠️ Port $port is in use, cleaning up...${NC}" - lsof -ti:$port | xargs kill -9 2>/dev/null || true + local pids + pids=$(lsof -ti:"$port" 2>/dev/null) || true + if [ -n "$pids" ]; then + echo -e "${YELLOW} Port $port in use, cleaning up...${NC}" + echo "$pids" | while read -r pid; do + kill -9 "$pid" 2>/dev/null || true + done sleep 1 fi } @@ -85,7 +89,5 @@ echo -e "${BLUE}🚀 Starting servers...${NC}" echo "" # Use npm run dev:mock (includes AIUI dev server automatically) -npm run dev:mock - -# Note: The script will stay running until Ctrl+C +exec npm run dev:mock diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh index fb58842a..209e43f8 100755 --- a/scripts/dev-start.sh +++ b/scripts/dev-start.sh @@ -1,235 +1,223 @@ #!/bin/bash -set -euo pipefail # Archipelago Development Server Starter -# Pure Archipelago implementation - NO StartOS - -set +e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -echo "🚀 Archipelago Development Server Starter" -echo "" - -# Use the workspace directory FRONTEND_DIR="$PROJECT_ROOT/neode-ui" BACKEND_DIR="$PROJECT_ROOT/core" -# Verify the frontend directory exists +# Quietly kill a port — avoids EAGAIN by not piping through xargs +kill_port() { + local pids + pids=$(lsof -ti:"$1" 2>/dev/null) || true + if [ -n "$pids" ]; then + echo "$pids" | while read -r pid; do + kill -9 "$pid" 2>/dev/null || true + done + sleep 1 + fi +} + +cleanup_ports() { + kill_port 5959 + kill_port 8100 +} + +ensure_deps() { + cd "$FRONTEND_DIR" + if [ ! -d "node_modules" ]; then + echo " Installing dependencies..." + npm install + fi +} + if [ ! -d "$FRONTEND_DIR" ]; then - echo "❌ Frontend directory not found: $FRONTEND_DIR" + echo "Frontend directory not found: $FRONTEND_DIR" exit 1 fi -echo "Choose a development mode:" -echo " 1) Mock backend (UI development only - fastest)" -echo " 2) Full stack (Archipelago backend + frontend)" -echo " 3) Setup mode (first-time user setup flow - mock)" -echo " 4) Onboarding mode (onboarding flow - mock)" -echo " 5) Existing user (login with password - mock)" -echo " 6) Show manual instructions" echo "" -read -p "Enter choice [1-6]: " choice +echo "Archipelago Dev Server" +echo "" +echo " 1) Mock backend (UI dev — fastest, no Docker/Podman needed)" +echo " 2) Full stack (Rust backend + frontend)" +echo " 3) Setup mode (first-time password setup — mock)" +echo " 4) Onboarding mode (onboarding flow — mock)" +echo " 5) Existing user (login screen — mock)" +echo " 6) Boot mode (simulated 25s startup — mock)" +echo " 7) Testnet stack (signet Bitcoin + LND + ThunderHub via Podman)" +echo " 8) Manual instructions" +echo "" +read -p "Enter choice [1-8]: " choice case $choice in 1) echo "" - echo "🎨 Starting frontend with mock backend..." - cd "$FRONTEND_DIR" - - # Kill any existing processes - echo " 🧹 Cleaning up ports 5959 and 8100..." - lsof -ti:5959 | xargs kill -9 2>/dev/null || true - lsof -ti:8100 | xargs kill -9 2>/dev/null || true - sleep 1 - - # Check dependencies - if [ ! -d "node_modules" ]; then - echo "⚠️ Installing dependencies..." - npm install - fi - - echo " Running: npm run dev:mock" - echo " (Press Ctrl+C to stop)" - echo "" - npm run dev:mock + echo "Starting frontend with mock backend..." + cleanup_ports + ensure_deps + exec npm run dev:mock ;; 2) echo "" - echo "🔧 Starting FULL STACK (Archipelago backend + frontend + Docker apps)..." - - # Kill ports - echo " 🧹 Cleaning up ports 5959 and 8100..." - lsof -ti:5959 | xargs kill -9 2>/dev/null || true - lsof -ti:8100 | xargs kill -9 2>/dev/null || true - sleep 1 - - # Mock backend simulates apps — Docker containers optional - echo "" - echo " ℹ️ Mock backend will simulate apps (Docker containers optional)" - - echo "" - # Check if backend can build - echo " 🔨 Checking backend build..." + echo "Starting full stack (Rust backend + frontend)..." + cleanup_ports + if [ ! -d "$BACKEND_DIR" ]; then - echo "❌ Backend directory not found: $BACKEND_DIR" + echo "Backend directory not found: $BACKEND_DIR" exit 1 fi - + cd "$BACKEND_DIR" if ! cargo check --bin archipelago > /tmp/archipelago-backend-check.log 2>&1; then - echo "❌ Backend build check failed. See /tmp/archipelago-backend-check.log for details." - echo " Falling back to mock backend for UI development." - cd "$FRONTEND_DIR" - if [ ! -d "node_modules" ]; then - npm install - fi - npm run dev:mock - exit 0 + echo "Backend build check failed. See /tmp/archipelago-backend-check.log" + echo "Falling back to mock backend." + ensure_deps + exec npm run dev:mock fi - - echo " 🚀 Starting Archipelago backend..." - # Set development environment variables + + echo " Starting Rust backend..." export ARCHIPELAGO_DATA_DIR=/tmp/archipelago-dev export ARCHIPELAGO_DEV_DATA_DIR=/tmp/archipelago-dev export ARCHIPELAGO_DEV_MODE=true export ARCHIPELAGO_BIND=127.0.0.1:5959 export ARCHIPELAGO_LOG_LEVEL=debug - export ARCHIPELAGO_PORT_OFFSET=10000 export ARCHIPELAGO_BITCOIN_SIMULATION=mock - export ARCHIPELAGO_CONTAINER_RUNTIME=docker - + cargo run --bin archipelago > /tmp/archipelago-backend.log 2>&1 & BACKEND_PID=$! - echo " Backend PID: $BACKEND_PID" - echo " Logs: tail -f /tmp/archipelago-backend.log" - - # Wait for backend to start listening on port 5959 - echo " ⏳ Waiting for backend to start on port 5959..." - timeout=60 - count=0 - while ! lsof -ti:5959 > /dev/null 2>&1 && [ $count -lt $timeout ]; do + echo " Backend PID: $BACKEND_PID (logs: /tmp/archipelago-backend.log)" + + echo " Waiting for backend on port 5959..." + for i in $(seq 1 60); do + if lsof -ti:5959 >/dev/null 2>&1; then break; fi sleep 1 - count=$((count + 1)) done - - if ! lsof -ti:5959 > /dev/null 2>&1; then - echo "❌ Backend did not start on port 5959 within $timeout seconds." - echo " Killing backend process $BACKEND_PID." - kill $BACKEND_PID 2>/dev/null || true - echo " Falling back to mock backend for UI development." - cd "$FRONTEND_DIR" - if [ ! -d "node_modules" ]; then - npm install - fi - npm run dev:mock - exit 0 + + if ! lsof -ti:5959 >/dev/null 2>&1; then + echo "Backend did not start. Falling back to mock." + kill "$BACKEND_PID" 2>/dev/null || true + ensure_deps + exec npm run dev:mock fi - - echo " ✅ Backend is listening on port 5959" - - # Start frontend - echo " 🎨 Starting frontend..." - cd "$FRONTEND_DIR" - - if [ ! -d "node_modules" ]; then - echo " Installing frontend dependencies..." - npm install - fi - - # Trap to kill backend on exit (Docker containers keep running) + + echo " Backend ready." trap "kill $BACKEND_PID 2>/dev/null" EXIT - - echo "" - echo " (Press Ctrl+C to stop servers)" - echo " 💡 Docker containers will keep running. Use 'docker compose down' to stop them." - echo "" - npm run dev + + ensure_deps + exec npm run dev ;; 3) echo "" - echo "🔧 Starting in SETUP mode (mock backend)..." - cd "$FRONTEND_DIR" - - # Kill ports - lsof -ti:5959 | xargs kill -9 2>/dev/null || true - lsof -ti:8100 | xargs kill -9 2>/dev/null || true - sleep 1 - - if [ ! -d "node_modules" ]; then - npm install - fi - - echo " Starting setup flow..." - VITE_DEV_MODE=setup npm run dev:mock + echo "Starting setup mode..." + cleanup_ports + ensure_deps + VITE_DEV_MODE=setup exec npm run dev:mock ;; 4) echo "" - echo "📚 Starting in ONBOARDING mode (mock backend)..." - cd "$FRONTEND_DIR" - - # Kill ports - lsof -ti:5959 | xargs kill -9 2>/dev/null || true - lsof -ti:8100 | xargs kill -9 2>/dev/null || true - sleep 1 - - if [ ! -d "node_modules" ]; then - npm install - fi - - echo " Starting onboarding flow..." - VITE_DEV_MODE=onboarding npm run dev:mock + echo "Starting onboarding mode..." + cleanup_ports + ensure_deps + VITE_DEV_MODE=onboarding exec npm run dev:mock ;; 5) echo "" - echo "👤 Starting as EXISTING USER (mock backend)..." - cd "$FRONTEND_DIR" - - # Kill ports - lsof -ti:5959 | xargs kill -9 2>/dev/null || true - lsof -ti:8100 | xargs kill -9 2>/dev/null || true - sleep 1 - - if [ ! -d "node_modules" ]; then - npm install - fi - - echo " Starting with login screen..." - VITE_DEV_MODE=existing npm run dev:mock + echo "Starting existing user mode..." + cleanup_ports + ensure_deps + VITE_DEV_MODE=existing exec npm run dev:mock ;; 6) echo "" - echo "📋 Manual Instructions:" + echo "Starting boot mode (25s simulated startup)..." + cleanup_ports + ensure_deps + VITE_DEV_MODE=boot exec npm run dev:mock + ;; + 7) echo "" - echo "For UI development (mock backend):" + echo "Starting testnet stack (signet) via Podman/Docker..." + + # Check for a working container runtime (binary exists AND daemon responds) + RUNTIME="" + COMPOSE="" + if command -v docker &>/dev/null && docker ps &>/dev/null; then + RUNTIME="docker" + COMPOSE="docker compose" + elif command -v podman &>/dev/null && podman ps &>/dev/null; then + if command -v podman-compose &>/dev/null; then + RUNTIME="podman" + COMPOSE="podman-compose" + else + RUNTIME="podman" + COMPOSE="podman compose" + fi + fi + + if [ -z "$RUNTIME" ]; then + echo "" + echo "No working container runtime detected." + echo "" + if command -v podman &>/dev/null; then + echo "Podman is installed but the machine isn't running:" + echo " podman machine start" + elif command -v docker &>/dev/null; then + echo "Docker is installed but the daemon isn't running." + echo "Start Docker Desktop and try again." + else + echo "Install Docker Desktop or Podman:" + echo " brew install --cask docker" + echo " # or" + echo " brew install podman podman-compose" + echo " podman machine init && podman machine start" + fi + echo "" + exit 1 + fi + + echo " Using: $RUNTIME" + cd "$PROJECT_ROOT" + + echo " Starting signet Bitcoin + LND + ThunderHub + Fedimint..." + $COMPOSE -f docker-compose.testnet.yml up -d + + echo "" + echo " Testnet stack starting. Services:" + echo " ThunderHub: http://localhost:3010 (password: thunderhub)" + echo " Fedimint Guardian: http://localhost:18175" + echo " LND REST: http://localhost:8080" + echo " Bitcoin RPC: localhost:38332" + echo "" + echo " Get signet coins: https://signetfaucet.com" + echo "" + echo " Also starting mock frontend..." + cleanup_ports + ensure_deps + exec npm run dev:mock + ;; + 8) + echo "" + echo "Manual Instructions" + echo "" + echo "UI development (mock backend, no Docker):" echo " cd $FRONTEND_DIR" - echo " npm run dev:mock" + echo " npm install && npm run dev:mock" echo "" - echo "For full stack (Docker apps + Archipelago backend + frontend):" - echo " Terminal 1 (Docker Apps):" - echo " cd $PROJECT_ROOT" - echo " ./start-docker-apps.sh" + echo "Dev modes (prepend to command):" + echo " VITE_DEV_MODE=setup First-time setup flow" + echo " VITE_DEV_MODE=onboarding Onboarding flow" + echo " VITE_DEV_MODE=existing Login screen" + echo " VITE_DEV_MODE=boot Boot sequence" echo "" - echo " Terminal 2 (Backend):" - echo " cd $BACKEND_DIR" - echo " export ARCHIPELAGO_CONTAINER_RUNTIME=docker" - echo " export ARCHIPELAGO_DEV_MODE=true" - echo " cargo run --bin archipelago" + echo "Testnet stack (requires Podman or Docker):" + echo " podman compose -f docker-compose.testnet.yml up -d" echo "" - echo " Terminal 3 (Frontend):" - echo " cd $FRONTEND_DIR" - echo " npm run dev" + echo "Full stack (requires Rust toolchain):" + echo " Terminal 1: cd $BACKEND_DIR && cargo run --bin archipelago" + echo " Terminal 2: cd $FRONTEND_DIR && npm run dev" echo "" - echo "Then open: http://localhost:8100" - echo "" - echo "To stop Docker apps:" - echo " cd $PROJECT_ROOT" - echo " ./stop-docker-apps.sh" - echo "" - echo "Mock backend dev modes:" - echo " VITE_DEV_MODE=setup - First-time setup flow" - echo " VITE_DEV_MODE=onboarding - Onboarding flow" - echo " VITE_DEV_MODE=existing - Login screen" + echo "Access: http://localhost:8100 (password: password123)" ;; *) echo "Invalid choice" diff --git a/testnet/README.md b/testnet/README.md new file mode 100644 index 00000000..fac7002e --- /dev/null +++ b/testnet/README.md @@ -0,0 +1,88 @@ +# Lightning Testnet Stack (Signet) + +Real Bitcoin signet + LND + ThunderHub + Fedimint for testing Lightning features. + +## Quick Start + +```bash +docker compose -f docker-compose.testnet.yml up -d +``` + +First run takes ~10 minutes for signet blockchain sync (~200MB). + +## Access + +| Service | URL | Credentials | +|---------|-----|-------------| +| ThunderHub | http://localhost:3010 | password: `thunderhub` | +| Fedimint Guardian UI | http://localhost:18175 | — | +| LND REST API | http://localhost:8080 | — | +| Bitcoin RPC | localhost:38332 | user: `bitcoin` / pass: `bitcoinpass` | + +## Get Signet Coins + +1. Get a new address: `docker exec archy-lnd-signet lncli --network=signet newaddress p2wkh` +2. Visit https://signetfaucet.com and paste the address +3. Wait for 1 confirmation (~10 min) +4. Check balance: `docker exec archy-lnd-signet lncli --network=signet walletbalance` + +## Open a Lightning Channel + +```bash +# Connect to a signet peer (example: ACINQ signet node) +docker exec archy-lnd-signet lncli --network=signet connect 03...@signet-node:9735 + +# Open channel (amount in sats) +docker exec archy-lnd-signet lncli --network=signet openchannel --node_key=03... --local_amt=100000 +``` + +Or use ThunderHub's UI at http://localhost:3010 to manage channels visually. + +## Create & Pay Invoices + +```bash +# Create invoice +docker exec archy-lnd-signet lncli --network=signet addinvoice --amt=1000 --memo="test payment" + +# Pay invoice (from another node or ThunderHub) +docker exec archy-lnd-signet lncli --network=signet payinvoice +``` + +## Useful Commands + +```bash +# Node info +docker exec archy-lnd-signet lncli --network=signet getinfo + +# List channels +docker exec archy-lnd-signet lncli --network=signet listchannels + +# Check Bitcoin sync progress +docker exec archy-bitcoind-signet bitcoin-cli -signet -rpcuser=bitcoin -rpcpassword=bitcoinpass -rpcport=38332 getblockchaininfo + +# View logs +docker compose -f docker-compose.testnet.yml logs -f lnd-signet +docker compose -f docker-compose.testnet.yml logs -f thunderhub-signet + +# Stop everything +docker compose -f docker-compose.testnet.yml down + +# Reset all data (fresh start) +docker compose -f docker-compose.testnet.yml down -v +``` + +## Architecture + +``` +bitcoind-signet (port 38332) + ↓ RPC + ZMQ +lnd-signet (gRPC 10009, REST 8080, P2P 9735) + ↓ macaroon + TLS +thunderhub-signet (web UI on 3010) + +bitcoind-signet + ↓ RPC +fedimint-signet (Guardian UI on 18175, API on 18174) +``` + +All containers share the `signet-net` bridge network for internal DNS resolution. diff --git a/testnet/thunderhub-config.yaml b/testnet/thunderhub-config.yaml new file mode 100644 index 00000000..9965d35c --- /dev/null +++ b/testnet/thunderhub-config.yaml @@ -0,0 +1,8 @@ +# ThunderHub account config — connects to LND signet node +# The LND data volume is mounted read-only at /lnd-data +masterPassword: "thunderhub" +accounts: + - name: "Archy Signet" + serverUrl: "lnd-signet:10009" + macaroonPath: "/lnd-data/data/chain/bitcoin/signet/admin.macaroon" + certificatePath: "/lnd-data/tls.cert"