chore: dev environment — signet testnet stack, mock LND RPCs, faucet button
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) <noreply@anthropic.com>
This commit is contained in:
parent
e9da567116
commit
db2ad27340
147
docker-compose.testnet.yml
Normal file
147
docker-compose.testnet.yml
Normal file
@ -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
|
||||
@ -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"
|
||||
|
||||
@ -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"), {
|
||||
|
||||
@ -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 => `
|
||||
<tr>
|
||||
<td>${c.peer_alias}</td>
|
||||
<td>${(c.capacity/1e6).toFixed(1)}M</td>
|
||||
<td><div style="display:flex;gap:4px;align-items:center"><div style="background:#4ade80;height:8px;width:${Math.round(c.local_balance/c.capacity*100)}%;border-radius:4px"></div><span style="font-size:11px;opacity:.6">${(c.local_balance/1e3).toFixed(0)}k</span></div></td>
|
||||
<td><div style="display:flex;gap:4px;align-items:center"><div style="background:#3b82f6;height:8px;width:${Math.round(c.remote_balance/c.capacity*100)}%;border-radius:4px"></div><span style="font-size:11px;opacity:.6">${(c.remote_balance/1e3).toFixed(0)}k</span></div></td>
|
||||
<td><span style="color:${c.active ? '#4ade80' : '#ef4444'}">${c.active ? 'Active' : 'Offline'}</span></td>
|
||||
</tr>`).join('')
|
||||
const invoiceRows = d.invoices.slice().reverse().map(inv => `
|
||||
<tr>
|
||||
<td>${inv.memo}</td>
|
||||
<td>${inv.value.toLocaleString()} sats</td>
|
||||
<td><span style="color:${inv.settled ? '#4ade80' : '#fb923c'}">${inv.settled ? 'Settled' : 'Open'}</span></td>
|
||||
<td>${new Date(inv.creation_date * 1000).toLocaleString()}</td>
|
||||
</tr>`).join('')
|
||||
const paymentRows = d.payments.map(p => `
|
||||
<tr>
|
||||
<td>${p.payment_hash.slice(0,12)}...</td>
|
||||
<td>${p.value_sat.toLocaleString()} sats</td>
|
||||
<td>${p.fee_sat} sats</td>
|
||||
<td style="color:#4ade80">Succeeded</td>
|
||||
</tr>`).join('')
|
||||
|
||||
res.type('html').send(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>ThunderHub — archy-signet</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:#0f0f1a;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif;padding:24px}
|
||||
h1{font-size:22px;margin-bottom:4px;color:#fb923c}
|
||||
.subtitle{color:rgba(255,255,255,.5);font-size:13px;margin-bottom:24px}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:32px}
|
||||
.stat{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:16px}
|
||||
.stat .label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:rgba(255,255,255,.4);margin-bottom:4px}
|
||||
.stat .value{font-size:24px;font-weight:600}
|
||||
.stat .value.green{color:#4ade80}.stat .value.orange{color:#fb923c}.stat .value.blue{color:#3b82f6}
|
||||
h2{font-size:16px;margin:24px 0 12px;color:rgba(255,255,255,.8)}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
th{text-align:left;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.1);color:rgba(255,255,255,.4);font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.5px}
|
||||
td{padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.05)}
|
||||
tr:hover td{background:rgba(255,255,255,.02)}
|
||||
.section{background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:20px;margin-bottom:20px}
|
||||
.badge{display:inline-block;background:rgba(247,147,26,.15);color:#fb923c;padding:2px 8px;border-radius:6px;font-size:11px;margin-left:8px}
|
||||
</style></head><body>
|
||||
<h1>ThunderHub<span class="badge">signet</span></h1>
|
||||
<div class="subtitle">${d.info.alias} — block ${d.info.block_height.toLocaleString()} — ${d.info.num_active_channels} active channels</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="stat"><div class="label">On-chain Balance</div><div class="value orange">${(d.balance.confirmed_balance/1e6).toFixed(2)}M sats</div></div>
|
||||
<div class="stat"><div class="label">Channel Capacity</div><div class="value">${(totalCap/1e6).toFixed(1)}M sats</div></div>
|
||||
<div class="stat"><div class="label">Local Balance</div><div class="value green">${(totalLocal/1e6).toFixed(1)}M sats</div></div>
|
||||
<div class="stat"><div class="label">Remote Balance</div><div class="value blue">${(totalRemote/1e6).toFixed(1)}M sats</div></div>
|
||||
<div class="stat"><div class="label">Routing Fees Earned</div><div class="value green">${totalFees} sats</div></div>
|
||||
<div class="stat"><div class="label">Payments Sent</div><div class="value">${d.payments.length}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Channels (${d.channels.length})</h2>
|
||||
<table><thead><tr><th>Peer</th><th>Capacity</th><th>Local</th><th>Remote</th><th>Status</th></tr></thead><tbody>${channelRows}</tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Recent Invoices</h2>
|
||||
<table><thead><tr><th>Memo</th><th>Amount</th><th>Status</th><th>Created</th></tr></thead><tbody>${invoiceRows}</tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Recent Payments</h2>
|
||||
<table><thead><tr><th>Hash</th><th>Amount</th><th>Fee</th><th>Status</th></tr></thead><tbody>${paymentRows}</tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Forwarding History</h2>
|
||||
<table><thead><tr><th>In Channel</th><th>Out Channel</th><th>Amount</th><th>Fee</th><th>Time</th></tr></thead><tbody>
|
||||
${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 `<tr><td>${inPeer}</td><td>${outPeer}</td><td>${f.amt_in.toLocaleString()} sats</td><td>${f.fee} sats</td><td>${new Date(f.timestamp_ns/1e6).toLocaleString()}</td></tr>`
|
||||
}).join('')}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<p style="text-align:center;color:rgba(255,255,255,.25);font-size:12px;margin-top:32px">Mock ThunderHub — Archipelago Dev Mode — No Docker Required</p>
|
||||
</body></html>`)
|
||||
})
|
||||
|
||||
// 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)}║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -345,13 +345,16 @@
|
||||
<span class="text-purple-400 text-sm font-medium">{{ walletEcash.toLocaleString() }} sats</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-card-buttons grid grid-cols-3 gap-2 mt-auto pt-4 shrink-0">
|
||||
<div class="home-card-buttons grid grid-cols-4 gap-2 mt-auto pt-4 shrink-0">
|
||||
<button @click="showSendModal = true" class="home-card-btn px-3 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
||||
{{ t('common.send') }}
|
||||
</button>
|
||||
<button @click="showReceiveModal = true" class="home-card-btn px-3 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
||||
{{ t('web5.receiveBitcoin') }}
|
||||
</button>
|
||||
<button @click="devFaucet" class="home-card-btn px-3 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors text-green-400">
|
||||
Faucet
|
||||
</button>
|
||||
<RouterLink to="/dashboard/web5" class="home-card-btn px-3 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
||||
Web5
|
||||
</RouterLink>
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
88
testnet/README.md
Normal file
88
testnet/README.md
Normal file
@ -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 <bolt11>
|
||||
```
|
||||
|
||||
## 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.
|
||||
8
testnet/thunderhub-config.yaml
Normal file
8
testnet/thunderhub-config.yaml
Normal file
@ -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"
|
||||
Loading…
x
Reference in New Issue
Block a user