frontend: polish app launch and release experience
This commit is contained in:
@@ -103,6 +103,47 @@ const walletState = {
|
||||
}
|
||||
function randomHex(bytes) { return Array.from({length: bytes}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join('') }
|
||||
|
||||
const bitcoinRelayMockState = {
|
||||
settings: {
|
||||
enabled_for_peers: true,
|
||||
allow_peer_requests: true,
|
||||
allow_http: false,
|
||||
allow_https: true,
|
||||
allow_tor: true,
|
||||
selected_peer_pubkey: '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04',
|
||||
http_endpoint: '',
|
||||
https_endpoint: 'https://shard.tx1138.com/',
|
||||
tor_endpoint: 'http://btc-relay-demoabcdefghijklmnop.onion/',
|
||||
},
|
||||
trusted_nodes: [
|
||||
{
|
||||
pubkey: '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04',
|
||||
onion: 'trustedalphaabcdefghijklmnop.onion',
|
||||
name: 'Trusted Alpha',
|
||||
relay_approved: true,
|
||||
},
|
||||
{
|
||||
pubkey: '02f6ab6c88037cd527a92f3a016a7bd18bb2ebd91d5a3efb1161481b5cf7d9ea2a',
|
||||
onion: 'trustedbetabcdefghijklmnopq.onion',
|
||||
name: 'Trusted Beta',
|
||||
relay_approved: false,
|
||||
},
|
||||
],
|
||||
requests: [
|
||||
{
|
||||
id: 'relay-demo-incoming',
|
||||
direction: 'incoming',
|
||||
status: 'pending',
|
||||
peer_pubkey: '02f6ab6c88037cd527a92f3a016a7bd18bb2ebd91d5a3efb1161481b5cf7d9ea2a',
|
||||
peer_onion: 'trustedbetabcdefghijklmnopq.onion',
|
||||
peer_name: 'Trusted Beta',
|
||||
message: 'Can I use your node for a Wasabi broadcast?',
|
||||
created_at: new Date(Date.now() - 12 * 60 * 1000).toISOString(),
|
||||
updated_at: new Date(Date.now() - 12 * 60 * 1000).toISOString(),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// User state (simulated file-based storage)
|
||||
let userState = {
|
||||
setupComplete: false,
|
||||
@@ -719,6 +760,16 @@ function staticApp({ id, title, version, shortDesc, longDesc, license, state, la
|
||||
|
||||
// Static dev apps (always shown in My Apps when using mock backend)
|
||||
const staticDevApps = {
|
||||
'bitcoin-knots': staticApp({
|
||||
id: 'bitcoin-knots',
|
||||
title: 'Bitcoin Knots',
|
||||
version: '27.1',
|
||||
shortDesc: 'Full Bitcoin node',
|
||||
longDesc: 'Validate and relay Bitcoin blocks and transactions with the Archipelago custom node UI.',
|
||||
state: 'running',
|
||||
lanPort: 8334,
|
||||
icon: '/assets/img/app-icons/bitcoin-knots.webp',
|
||||
}),
|
||||
bitcoin: staticApp({
|
||||
id: 'bitcoin',
|
||||
title: 'Bitcoin Core',
|
||||
@@ -765,6 +816,16 @@ const staticDevApps = {
|
||||
state: 'running',
|
||||
lanPort: null,
|
||||
}),
|
||||
meshtastic: staticApp({
|
||||
id: 'meshtastic',
|
||||
title: 'Meshtastic',
|
||||
version: '2-daily-alpine',
|
||||
shortDesc: 'LoRa mesh networking',
|
||||
longDesc: 'Open-source mesh networking for LoRa radios. Create decentralized communication networks.',
|
||||
state: 'running',
|
||||
lanPort: 4403,
|
||||
icon: '/assets/img/app-icons/meshcore.svg',
|
||||
}),
|
||||
filebrowser: staticApp({
|
||||
id: 'filebrowser',
|
||||
title: 'File Browser',
|
||||
@@ -826,6 +887,200 @@ app.options('/rpc/v1', (req, res) => {
|
||||
res.status(200).end()
|
||||
})
|
||||
|
||||
app.get('/', (_req, res) => {
|
||||
const uiPort = process.env.VITE_DEV_SERVER_PORT || '8102'
|
||||
res
|
||||
.status(200)
|
||||
.type('html')
|
||||
.send(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="refresh" content="0; url=http://localhost:${uiPort}/">
|
||||
<title>Archipelago dev backend</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>This is the mock JSON-RPC backend. Open the dashboard at
|
||||
<a href="http://localhost:${uiPort}/">http://localhost:${uiPort}/</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>`)
|
||||
})
|
||||
|
||||
app.get('/rpc/v1', (_req, res) => {
|
||||
const uiPort = process.env.VITE_DEV_SERVER_PORT || '8102'
|
||||
res
|
||||
.status(405)
|
||||
.type('text/plain')
|
||||
.send(`JSON-RPC is available at /rpc/v1 for POST requests only.\nOpen the dashboard at http://localhost:${uiPort}/.\n`)
|
||||
})
|
||||
|
||||
function mockBitcoinBlockchainInfo() {
|
||||
return {
|
||||
chain: 'main',
|
||||
blocks: 902418,
|
||||
headers: 902418,
|
||||
bestblockhash: randomHex(32),
|
||||
difficulty: 126_984_812_384_099.3,
|
||||
verificationprogress: 0.999998,
|
||||
initialblockdownload: false,
|
||||
size_on_disk: 678_000_000_000,
|
||||
pruned: false,
|
||||
chainwork: '00000000000000000000000000000000000000008d4b42d8b9b0000000000000',
|
||||
}
|
||||
}
|
||||
|
||||
function mockBitcoinNetworkInfo() {
|
||||
return {
|
||||
version: 270100,
|
||||
subversion: '/Satoshi:27.1/Knots:20240513/',
|
||||
protocolversion: 70016,
|
||||
localservices: '0000000000000409',
|
||||
localrelay: true,
|
||||
timeoffset: 0,
|
||||
networkactive: true,
|
||||
connections: 18,
|
||||
networks: [
|
||||
{ name: 'ipv4', limited: false, reachable: true, proxy: '', proxy_randomize_credentials: false },
|
||||
{ name: 'onion', limited: false, reachable: true, proxy: '127.0.0.1:9050', proxy_randomize_credentials: true },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function bitcoinRelayStatusPayload() {
|
||||
return {
|
||||
settings: bitcoinRelayMockState.settings,
|
||||
trusted_nodes: bitcoinRelayMockState.trusted_nodes,
|
||||
requests: bitcoinRelayMockState.requests,
|
||||
local_node: {
|
||||
synced: true,
|
||||
blocks: 902418,
|
||||
headers: 902418,
|
||||
chain: 'main',
|
||||
status_ok: true,
|
||||
status_stale: false,
|
||||
error: null,
|
||||
},
|
||||
credentials: {
|
||||
username: 'txrelay',
|
||||
available: true,
|
||||
password_available: true,
|
||||
rpcauth_available: true,
|
||||
client_env_available: true,
|
||||
client_env_path: '/var/lib/archipelago/secrets/bitcoin-rpc-txrelay-client.env',
|
||||
restart_hint: 'If this was just generated, restart Bitcoin Core/Knots so bitcoind loads the txrelay rpcauth whitelist.',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/app/bitcoin-ui/', async (_req, res) => {
|
||||
try {
|
||||
const html = await fs.readFile(path.join(__dirname, '..', 'docker', 'bitcoin-ui', 'index.html'), 'utf8')
|
||||
res.type('html').send(html)
|
||||
} catch (error) {
|
||||
res.status(500).type('text/plain').send(`Unable to load Bitcoin UI mock: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/app/bitcoin-ui/bitcoin-status', (_req, res) => {
|
||||
res.json({
|
||||
ok: true,
|
||||
stale: false,
|
||||
updated_at_ms: Date.now(),
|
||||
error: null,
|
||||
blockchain_info: mockBitcoinBlockchainInfo(),
|
||||
network_info: mockBitcoinNetworkInfo(),
|
||||
index_info: {
|
||||
txindex: { synced: true, best_block_height: 902418 },
|
||||
blockfilterindex: { synced: true, best_block_height: 902418 },
|
||||
},
|
||||
zmq_notifications: [
|
||||
{ type: 'pubhashblock', address: 'tcp://127.0.0.1:28332', hwm: 1000 },
|
||||
{ type: 'pubrawtx', address: 'tcp://127.0.0.1:28333', hwm: 1000 },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/app/bitcoin-ui/bitcoin-rpc/', (req, res) => {
|
||||
const { id = 'bitcoin-ui', method } = req.body || {}
|
||||
const results = {
|
||||
getblockchaininfo: mockBitcoinBlockchainInfo(),
|
||||
getnetworkinfo: mockBitcoinNetworkInfo(),
|
||||
getindexinfo: {
|
||||
txindex: { synced: true, best_block_height: 902418 },
|
||||
blockfilterindex: { synced: true, best_block_height: 902418 },
|
||||
},
|
||||
getzmqnotifications: [
|
||||
{ type: 'pubhashblock', address: 'tcp://127.0.0.1:28332', hwm: 1000 },
|
||||
{ type: 'pubrawtx', address: 'tcp://127.0.0.1:28333', hwm: 1000 },
|
||||
],
|
||||
getmempoolinfo: { loaded: true, size: 18452, bytes: 62400000, usage: 143000000, maxmempool: 300000000 },
|
||||
getpeerinfo: Array.from({ length: 18 }, (_, index) => ({ id: index, addr: `198.51.100.${index + 10}:8333`, inbound: index % 3 === 0 })),
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(results, method)) {
|
||||
return res.json({ id, result: null, error: { code: -32601, message: `Method not found: ${method}` } })
|
||||
}
|
||||
res.json({ id, result: results[method], error: null })
|
||||
})
|
||||
|
||||
app.post('/app/bitcoin-ui/rpc/v1', (req, res) => {
|
||||
const { method, params = {} } = req.body || {}
|
||||
const now = new Date().toISOString()
|
||||
switch (method) {
|
||||
case 'bitcoin.relay-status':
|
||||
return res.json({ result: bitcoinRelayStatusPayload(), error: null })
|
||||
case 'bitcoin.relay-update-settings':
|
||||
bitcoinRelayMockState.settings = {
|
||||
...bitcoinRelayMockState.settings,
|
||||
...params,
|
||||
}
|
||||
return res.json({ result: bitcoinRelayStatusPayload(), error: null })
|
||||
case 'bitcoin.relay-request-peer': {
|
||||
const peer = bitcoinRelayMockState.trusted_nodes.find(p => p.pubkey === params.peer_pubkey)
|
||||
if (!peer) return res.status(400).json({ error: { message: 'Peer is not in trusted nodes' } })
|
||||
const request = {
|
||||
id: `relay-demo-${Date.now()}`,
|
||||
direction: 'outbound',
|
||||
status: 'pending',
|
||||
peer_pubkey: peer.pubkey,
|
||||
peer_onion: peer.onion,
|
||||
peer_name: peer.name,
|
||||
message: params.message || '',
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
bitcoinRelayMockState.requests.push(request)
|
||||
return res.json({ result: { ok: true, request_id: request.id }, error: null })
|
||||
}
|
||||
case 'bitcoin.relay-approve-request':
|
||||
case 'bitcoin.relay-reject-request': {
|
||||
const requestId = params.id || params.request_id
|
||||
const request = bitcoinRelayMockState.requests.find(r => r.id === requestId)
|
||||
if (!request) return res.status(404).json({ error: { message: `Request not found: ${requestId}` } })
|
||||
request.status = method.endsWith('approve-request') ? 'approved' : 'rejected'
|
||||
request.updated_at = now
|
||||
if (request.status === 'approved') {
|
||||
request.approved_endpoint = bitcoinRelayMockState.settings.https_endpoint || bitcoinRelayMockState.settings.tor_endpoint || bitcoinRelayMockState.settings.http_endpoint
|
||||
request.credential_secret_path = '/var/lib/archipelago/secrets/bitcoin-relay-peer-demo.env'
|
||||
}
|
||||
return res.json({ result: { ok: true, request_id: request.id }, error: null })
|
||||
}
|
||||
case 'bitcoin.relay-create-tor-service':
|
||||
bitcoinRelayMockState.settings.allow_tor = true
|
||||
bitcoinRelayMockState.settings.tor_endpoint = 'http://btc-relay-demoabcdefghijklmnop.onion/'
|
||||
return res.json({
|
||||
result: {
|
||||
created: true,
|
||||
name: 'bitcoin-rpc',
|
||||
onion_address: 'btc-relay-demoabcdefghijklmnop.onion',
|
||||
},
|
||||
error: null,
|
||||
})
|
||||
default:
|
||||
return res.status(404).json({ error: { message: `Unknown mock Bitcoin UI RPC method: ${method}` } })
|
||||
}
|
||||
})
|
||||
|
||||
// RPC endpoint
|
||||
app.post('/rpc/v1', (req, res) => {
|
||||
const { method, params } = req.body
|
||||
|
||||
Reference in New Issue
Block a user