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})
+
| Peer | Capacity | Local | Remote | Status |
${channelRows}
+
+
+
+
Recent Invoices
+
| Memo | Amount | Status | Created |
${invoiceRows}
+
+
+
+
Recent Payments
+
| Hash | Amount | Fee | Status |
${paymentRows}
+
+
+
+
Forwarding History
+
| In Channel | Out Channel | Amount | Fee | Time |
+${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 `| ${inPeer} | ${outPeer} | ${f.amt_in.toLocaleString()} sats | ${f.fee} sats | ${new Date(f.timestamp_ns/1e6).toLocaleString()} |
`
+}).join('')}
+
+
+
+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
-