@relay-federation/bridge 0.3.14 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +3 -5
- package/dashboard/index.html +241 -5
- package/lib/address-watcher.js +172 -161
- package/lib/bsv-node-client.js +15 -0
- package/lib/bsv-peer.js +2 -2
- package/lib/persistent-store.js +896 -811
- package/lib/status-server.js +367 -42
- package/lib/tx-relay.js +29 -0
- package/lib/x402-endpoints.js +46 -0
- package/lib/x402-middleware.js +348 -0
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -744,13 +744,10 @@ async function cmdStart () {
|
|
|
744
744
|
}
|
|
745
745
|
})
|
|
746
746
|
|
|
747
|
-
//
|
|
747
|
+
// Track txids announced via BSV P2P inv (lightweight, no full tx fetch)
|
|
748
748
|
bsvNode.on('tx:inv', ({ txids }) => {
|
|
749
749
|
for (const txid of txids) {
|
|
750
|
-
|
|
751
|
-
bsvNode.getTx(txid, 10000).then(({ txid: id, rawHex }) => {
|
|
752
|
-
txRelay.broadcastTx(id, rawHex)
|
|
753
|
-
}).catch(() => {}) // ignore fetch failures
|
|
750
|
+
txRelay.trackTxid(txid)
|
|
754
751
|
}
|
|
755
752
|
})
|
|
756
753
|
|
|
@@ -894,6 +891,7 @@ async function cmdStart () {
|
|
|
894
891
|
peerHealth,
|
|
895
892
|
bsvNodeClient: bsvNode,
|
|
896
893
|
store,
|
|
894
|
+
addressWatcher: watcher,
|
|
897
895
|
performOutboundHandshake,
|
|
898
896
|
registeredPubkeys,
|
|
899
897
|
gossipManager
|
package/dashboard/index.html
CHANGED
|
@@ -951,6 +951,7 @@
|
|
|
951
951
|
<button onclick="setTab('inscriptions')">Inscriptions</button>
|
|
952
952
|
<button onclick="setTab('tokens')">Tokens</button>
|
|
953
953
|
<button onclick="setTab('apps')">Apps</button>
|
|
954
|
+
<button onclick="setTab('x402')">x402</button>
|
|
954
955
|
</div>
|
|
955
956
|
<div class="header-right">
|
|
956
957
|
<div class="header-stats">
|
|
@@ -1100,9 +1101,11 @@ let activeTab = 'overview';
|
|
|
1100
1101
|
let savedExplorerInput = '';
|
|
1101
1102
|
let savedExplorerResult = '';
|
|
1102
1103
|
let appsData = null;
|
|
1104
|
+
let x402Data = null;
|
|
1103
1105
|
let latestPrice = null;
|
|
1104
1106
|
let mempoolHistory = [];
|
|
1105
1107
|
let peerCountHistory = [];
|
|
1108
|
+
let rssHistory = [];
|
|
1106
1109
|
|
|
1107
1110
|
function openCardOverlay(type) {
|
|
1108
1111
|
const b = bridgeData.get(selectedBridge);
|
|
@@ -1196,7 +1199,8 @@ function renderCardExpanded(type, b, op) {
|
|
|
1196
1199
|
html += '<div class="panel-row"><span class="label">Mesh Bridges</span><span class="value">' + b.peers.connected + ' connected</span></div>';
|
|
1197
1200
|
html += '<div style="font-size:11px;color:var(--text-dim);margin:16px 0 12px;text-transform:uppercase;letter-spacing:1px">Transactions</div>';
|
|
1198
1201
|
html += '<div class="panel-row"><span class="label">Mempool</span><span class="value">' + b.txs.mempool + ' txs</span></div>';
|
|
1199
|
-
html += '<div class="panel-row"><span class="label">Seen</span><span class="value">' + b.txs.
|
|
1202
|
+
html += '<div class="panel-row"><span class="label">Network Seen</span><span class="value">' + (b.txs.known || 0).toLocaleString() + ' txids</span></div>';
|
|
1203
|
+
html += '<div class="panel-row"><span class="label">Relayed</span><span class="value">' + b.txs.seen + ' txs</span></div>';
|
|
1200
1204
|
// Mempool sparkline (last 20 polls)
|
|
1201
1205
|
if (mempoolHistory.length > 1) {
|
|
1202
1206
|
html += '<div style="margin-top:12px"><div style="font-size:11px;color:var(--text-dim);margin-bottom:6px">Mempool History (last ' + mempoolHistory.length + ' polls)</div>';
|
|
@@ -1280,6 +1284,40 @@ function renderCardExpanded(type, b, op) {
|
|
|
1280
1284
|
html += '<p style="margin-bottom:8px">More BSV bonded + longer held unspent = higher BND score. Minimum: 0.01 BSV.</p>';
|
|
1281
1285
|
html += '</div></div>';
|
|
1282
1286
|
break;
|
|
1287
|
+
case 'system':
|
|
1288
|
+
if (!b.system) return '';
|
|
1289
|
+
var sysMem = b.system;
|
|
1290
|
+
var sysMemPct = Math.round(sysMem.usedMemMB / sysMem.totalMemMB * 100);
|
|
1291
|
+
var sysMemClr = sysMemPct > 85 ? 'var(--accent-red)' : sysMemPct > 70 ? 'var(--accent-yellow)' : 'var(--accent-green)';
|
|
1292
|
+
html += '<h2>System</h2>';
|
|
1293
|
+
// RAM hero
|
|
1294
|
+
html += '<div style="text-align:center;padding:20px 0 16px">';
|
|
1295
|
+
html += '<div style="font-size:32px;font-weight:700;color:' + sysMemClr + ';letter-spacing:-1px">' + sysMemPct + '%</div>';
|
|
1296
|
+
html += '<div style="font-size:14px;color:var(--text-muted);margin-top:4px">RAM Usage</div>';
|
|
1297
|
+
html += '</div>';
|
|
1298
|
+
// RAM bar
|
|
1299
|
+
html += '<div style="margin:0 0 20px"><div style="height:8px;background:var(--border);border-radius:4px;overflow:hidden"><div style="height:100%;width:' + sysMemPct + '%;background:' + sysMemClr + ';border-radius:4px;transition:width 0.5s"></div></div>';
|
|
1300
|
+
html += '<div style="display:flex;justify-content:space-between;font-size:11px;margin-top:4px;color:var(--text-dim)"><span>Used: ' + sysMem.usedMemMB + ' MB</span><span>Free: ' + sysMem.freeMemMB + ' MB</span><span>Total: ' + sysMem.totalMemMB + ' MB</span></div></div>';
|
|
1301
|
+
// Process
|
|
1302
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px">Process</div>';
|
|
1303
|
+
html += '<div class="panel-row"><span class="label">RSS</span><span class="value">' + sysMem.processRssMB + ' MB</span></div>';
|
|
1304
|
+
html += '<div class="panel-row"><span class="label">Node.js</span><span class="value">' + sysMem.nodeVersion + '</span></div>';
|
|
1305
|
+
// CPU
|
|
1306
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin:16px 0 12px;text-transform:uppercase;letter-spacing:1px">CPU</div>';
|
|
1307
|
+
html += '<div class="panel-row"><span class="label">Cores</span><span class="value">' + sysMem.cpuCount + '</span></div>';
|
|
1308
|
+
html += '<div class="panel-row"><span class="label">Load (1m)</span><span class="value">' + sysMem.loadAvg[0] + '</span></div>';
|
|
1309
|
+
html += '<div class="panel-row"><span class="label">Load (5m)</span><span class="value">' + sysMem.loadAvg[1] + '</span></div>';
|
|
1310
|
+
html += '<div class="panel-row"><span class="label">Load (15m)</span><span class="value">' + sysMem.loadAvg[2] + '</span></div>';
|
|
1311
|
+
// OS
|
|
1312
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin:16px 0 12px;text-transform:uppercase;letter-spacing:1px">Host</div>';
|
|
1313
|
+
html += '<div class="panel-row"><span class="label">Platform</span><span class="value">' + sysMem.platform + ' / ' + sysMem.arch + '</span></div>';
|
|
1314
|
+
html += '<div class="panel-row"><span class="label">OS Uptime</span><span class="value">' + fmtUptime(sysMem.osUptime) + '</span></div>';
|
|
1315
|
+
// RSS sparkline
|
|
1316
|
+
if (rssHistory && rssHistory.length > 1) {
|
|
1317
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin:16px 0 12px;text-transform:uppercase;letter-spacing:1px">Process RSS (last ' + rssHistory.length + ' polls)</div>';
|
|
1318
|
+
html += renderSparkline(rssHistory, 'rgba(210,153,34,0.7)');
|
|
1319
|
+
}
|
|
1320
|
+
break;
|
|
1283
1321
|
default: return '';
|
|
1284
1322
|
}
|
|
1285
1323
|
return html;
|
|
@@ -1311,6 +1349,7 @@ function renderSparkline(data, color) {
|
|
|
1311
1349
|
return svg + '</svg>';
|
|
1312
1350
|
}
|
|
1313
1351
|
function scoreColor(v) { return v >= 0.7 ? 'green' : v >= 0.4 ? 'yellow' : 'red'; }
|
|
1352
|
+
function fmtAge(ts) { var s = Math.floor((Date.now() - ts) / 1000); if (s < 60) return s + 's ago'; if (s < 3600) return Math.floor(s / 60) + 'm ago'; if (s < 86400) return Math.floor(s / 3600) + 'h ago'; return Math.floor(s / 86400) + 'd ago'; }
|
|
1314
1353
|
function fmtSats(n) { return n === null || n === undefined ? '-' : n.toLocaleString(); }
|
|
1315
1354
|
function fmtHeight(n) { return n > 0 ? n.toLocaleString() : '-'; }
|
|
1316
1355
|
function isOperator(url) { return !!operatorTokens[url]; }
|
|
@@ -1390,7 +1429,11 @@ async function discoverBridges() {
|
|
|
1390
1429
|
}
|
|
1391
1430
|
|
|
1392
1431
|
// ── Poll ───────────────────────────────────────────
|
|
1432
|
+
let polling = false;
|
|
1393
1433
|
async function pollAll() {
|
|
1434
|
+
if (polling) return;
|
|
1435
|
+
polling = true;
|
|
1436
|
+
try {
|
|
1394
1437
|
if (!discoveryDone) await discoverBridges();
|
|
1395
1438
|
const results = await Promise.all(BRIDGES.map(fetchBridge));
|
|
1396
1439
|
const mempools = await Promise.all(BRIDGES.map(fetchMempool));
|
|
@@ -1405,12 +1448,14 @@ async function pollAll() {
|
|
|
1405
1448
|
const sel = bridgeData.get(selectedBridge);
|
|
1406
1449
|
if (sel && sel.txs) { mempoolHistory.push(sel.txs.mempool); if (mempoolHistory.length > 20) mempoolHistory.shift(); }
|
|
1407
1450
|
if (sel && sel.peers) { peerCountHistory.push(sel.peers.connected); if (peerCountHistory.length > 20) peerCountHistory.shift(); }
|
|
1451
|
+
if (sel && sel.system) { rssHistory.push(sel.system.processRssMB); if (rssHistory.length > 20) rssHistory.shift(); }
|
|
1408
1452
|
// Fetch price from self (same-origin, always works)
|
|
1409
1453
|
fetch('/price', { signal: AbortSignal.timeout(5000) })
|
|
1410
1454
|
.then(r => r.ok ? r.json() : null).then(d => { if (d) { latestPrice = d; const el = document.querySelector('.stats-hero .stat-card:last-child .stat-value'); if (el) el.textContent = '$' + d.usd.toFixed(2); } }).catch(() => {});
|
|
1411
1455
|
renderHeader();
|
|
1412
1456
|
renderBridgeRail();
|
|
1413
1457
|
renderActiveTab(true);
|
|
1458
|
+
} finally { polling = false; }
|
|
1414
1459
|
}
|
|
1415
1460
|
|
|
1416
1461
|
// ── Header stats ───────────────────────────────────
|
|
@@ -1464,17 +1509,18 @@ function setTab(tab) {
|
|
|
1464
1509
|
if (activeTab === 'overview' && tab !== 'overview') destroyMeshMap3D();
|
|
1465
1510
|
activeTab = tab;
|
|
1466
1511
|
const buttons = document.querySelectorAll('#headerTabs button');
|
|
1467
|
-
const tabs = ['overview', 'mempool', 'explorer', 'inscriptions', 'tokens', 'apps'];
|
|
1512
|
+
const tabs = ['overview', 'mempool', 'explorer', 'inscriptions', 'tokens', 'apps', 'x402'];
|
|
1468
1513
|
buttons.forEach((btn, i) => btn.classList.toggle('active', tabs[i] === tab));
|
|
1469
1514
|
renderActiveTab();
|
|
1470
1515
|
if (tab === 'apps') setTimeout(fetchAppsData, 50);
|
|
1471
1516
|
if (tab === 'tokens') setTimeout(fetchTokens, 50);
|
|
1517
|
+
if (tab === 'x402') setTimeout(fetchX402Data, 50);
|
|
1472
1518
|
}
|
|
1473
1519
|
|
|
1474
1520
|
function renderActiveTab(fromPoll) {
|
|
1475
1521
|
const el = document.getElementById('tabContent');
|
|
1476
1522
|
// Skip apps/explorer re-render during poll (preserves state)
|
|
1477
|
-
if (fromPoll && (activeTab === 'apps' || activeTab === 'explorer' || activeTab === 'inscriptions' || activeTab === 'tokens')) return;
|
|
1523
|
+
if (fromPoll && (activeTab === 'apps' || activeTab === 'explorer' || activeTab === 'inscriptions' || activeTab === 'tokens' || activeTab === 'x402')) return;
|
|
1478
1524
|
// On poll, if overview tab has active 3D scene, just update data — don't rebuild DOM
|
|
1479
1525
|
if (fromPoll && activeTab === 'overview' && mesh3d) {
|
|
1480
1526
|
updateMeshMap3D();
|
|
@@ -1490,6 +1536,7 @@ function renderActiveTab(fromPoll) {
|
|
|
1490
1536
|
case 'inscriptions': html = renderInscriptionsTab(); break;
|
|
1491
1537
|
case 'tokens': html = renderTokensTab(); break;
|
|
1492
1538
|
case 'apps': html = renderAppsTab(); break;
|
|
1539
|
+
case 'x402': html = renderX402Tab(); break;
|
|
1493
1540
|
default: html = renderOverviewTab();
|
|
1494
1541
|
}
|
|
1495
1542
|
// Only update DOM if content actually changed (prevents flash)
|
|
@@ -1637,6 +1684,18 @@ function renderOverviewTab() {
|
|
|
1637
1684
|
}
|
|
1638
1685
|
html += '</div>';
|
|
1639
1686
|
|
|
1687
|
+
// System card
|
|
1688
|
+
if (b.system) {
|
|
1689
|
+
var memPct = Math.round(b.system.usedMemMB / b.system.totalMemMB * 100);
|
|
1690
|
+
var memColor = memPct > 85 ? 'red' : memPct > 70 ? 'yellow' : 'green';
|
|
1691
|
+
html += '<div class="detail-card glass" onclick="openCardOverlay(\'system\')"><h3>System <span style="font-size:9px;color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">— click to expand</span></h3>';
|
|
1692
|
+
html += '<div class="panel-row"><span class="label">RAM</span><span class="value ' + memColor + '">' + b.system.usedMemMB + ' / ' + b.system.totalMemMB + ' MB (' + memPct + '%)</span></div>';
|
|
1693
|
+
html += '<div class="panel-row"><span class="label">Process RSS</span><span class="value">' + b.system.processRssMB + ' MB</span></div>';
|
|
1694
|
+
html += '<div class="panel-row"><span class="label">CPU Load</span><span class="value">' + b.system.loadAvg[0] + ' / ' + b.system.loadAvg[1] + ' / ' + b.system.loadAvg[2] + '</span></div>';
|
|
1695
|
+
html += '<div class="panel-row"><span class="label">OS Uptime</span><span class="value">' + fmtUptime(b.system.osUptime) + '</span></div>';
|
|
1696
|
+
html += '</div>';
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1640
1699
|
html += '</div>'; // cards-grid
|
|
1641
1700
|
|
|
1642
1701
|
return html;
|
|
@@ -1649,6 +1708,14 @@ function renderMempoolTab() {
|
|
|
1649
1708
|
|
|
1650
1709
|
let html = '<div class="tab-title">Mempool <span class="count">' + (b._mempool ? b._mempool.count : 0) + ' transactions</span></div>';
|
|
1651
1710
|
|
|
1711
|
+
const known = b.txs ? b.txs.known || 0 : 0;
|
|
1712
|
+
if (known > 0) {
|
|
1713
|
+
html += '<div class="glass" style="padding:16px;margin-bottom:12px;display:flex;gap:24px;align-items:center">';
|
|
1714
|
+
html += '<div><span class="label">Network Txids Seen</span><div style="font-size:1.5em;font-weight:700;color:var(--accent)">' + known.toLocaleString() + '</div></div>';
|
|
1715
|
+
html += '<div style="color:var(--text-dim);font-size:0.85em">Transactions observed via P2P (10 min window)</div>';
|
|
1716
|
+
html += '</div>';
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1652
1719
|
if (!b._mempool || b._mempool.txs.length === 0) {
|
|
1653
1720
|
html += '<div class="glass" style="padding:40px;text-align:center"><div style="color:var(--text-dim)">No transactions in mempool</div></div>';
|
|
1654
1721
|
return html;
|
|
@@ -2519,8 +2586,8 @@ function showAuthModal(bridgeUrl) {
|
|
|
2519
2586
|
openModal('<h2>Operator Login</h2><div style="margin-bottom:12px;font-size:13px;color:var(--text-secondary)">' + bName + ' — ' + bIp + '</div><div class="form-group"><label>statusSecret (from bridge config.json)</label><input type="password" id="authInput" value="' + existing + '" placeholder="64-char hex token"></div><div class="modal-actions"><button class="btn" onclick="closeModal()">Cancel</button><button class="btn primary" onclick="saveAuth(\'' + bridgeUrl + '\')">Login</button></div>');
|
|
2520
2587
|
setTimeout(() => document.getElementById('authInput').focus(), 100);
|
|
2521
2588
|
}
|
|
2522
|
-
function saveAuth(bridgeUrl) { const t = document.getElementById('authInput').value.trim(); if (!t) delete operatorTokens[bridgeUrl]; else operatorTokens[bridgeUrl] = t; localStorage.setItem('relay_operator_tokens', JSON.stringify(operatorTokens)); closeModal();
|
|
2523
|
-
function logoutOperator(bridgeUrl) { delete operatorTokens[bridgeUrl]; localStorage.setItem('relay_operator_tokens', JSON.stringify(operatorTokens));
|
|
2589
|
+
async function saveAuth(bridgeUrl) { const t = document.getElementById('authInput').value.trim(); if (!t) delete operatorTokens[bridgeUrl]; else operatorTokens[bridgeUrl] = t; localStorage.setItem('relay_operator_tokens', JSON.stringify(operatorTokens)); closeModal(); destroyMeshMap3D(); const bridge = BRIDGES.find(b => b.url === bridgeUrl); if (bridge) { const data = await fetchBridge(bridge); bridgeData.set(data._name, data); } renderActiveTab(false); }
|
|
2590
|
+
async function logoutOperator(bridgeUrl) { delete operatorTokens[bridgeUrl]; localStorage.setItem('relay_operator_tokens', JSON.stringify(operatorTokens)); destroyMeshMap3D(); const bridge = BRIDGES.find(b => b.url === bridgeUrl); if (bridge) { const data = await fetchBridge(bridge); bridgeData.set(data._name, data); } renderActiveTab(false); }
|
|
2524
2591
|
|
|
2525
2592
|
function showRegisterModal(bridgeUrl) {
|
|
2526
2593
|
openModal('<h2>Register Bridge</h2><div class="warning">This will broadcast a stake bond + registration transaction to the BSV network. Ensure your bridge wallet is funded.</div><div class="modal-actions"><button class="btn" onclick="closeModal()">Cancel</button><button class="btn primary" id="regBtn" onclick="doRegister(\'' + bridgeUrl + '\')">Register</button></div><div id="jobLog" class="job-log" style="display:none"></div>');
|
|
@@ -2968,6 +3035,175 @@ async function fetchAppsData() {
|
|
|
2968
3035
|
}
|
|
2969
3036
|
}
|
|
2970
3037
|
|
|
3038
|
+
// ── x402 tab ──────────────────────────────────────
|
|
3039
|
+
function renderX402Tab() {
|
|
3040
|
+
let html = '<div style="margin-bottom:10px"><span style="font-size:15px;font-weight:600;color:var(--text-primary)">x402 Payment Gate</span>';
|
|
3041
|
+
html += '<span style="font-size:11px;color:var(--text-muted);margin-left:10px">Free reads, paid writes</span></div>';
|
|
3042
|
+
|
|
3043
|
+
if (!x402Data) {
|
|
3044
|
+
html += '<div class="glass" style="padding:20px;color:var(--text-muted)">Loading...</div>';
|
|
3045
|
+
return html;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
// Status row
|
|
3049
|
+
var statusClr = x402Data.enabled ? 'var(--accent-green)' : 'var(--text-dim)';
|
|
3050
|
+
var statusTxt = x402Data.enabled ? 'ENABLED' : 'DISABLED';
|
|
3051
|
+
html += '<div class="glass" style="padding:10px 16px;margin-bottom:8px;display:flex;align-items:center;gap:10px">';
|
|
3052
|
+
html += '<div style="width:8px;height:8px;border-radius:50%;background:' + statusClr + '"></div>';
|
|
3053
|
+
html += '<span style="font-size:13px;font-weight:600;color:' + statusClr + '">' + statusTxt + '</span>';
|
|
3054
|
+
if (x402Data.payTo) {
|
|
3055
|
+
html += '<span style="margin-left:auto;font-family:monospace;font-size:11px;color:var(--text-muted)">' + escapeHtml(x402Data.payTo) + '</span>';
|
|
3056
|
+
}
|
|
3057
|
+
html += '</div>';
|
|
3058
|
+
|
|
3059
|
+
// Revenue card
|
|
3060
|
+
if (x402Data.revenue) {
|
|
3061
|
+
var rev = x402Data.revenue;
|
|
3062
|
+
html += '<div class="glass" style="padding:12px 16px;margin-bottom:8px">';
|
|
3063
|
+
html += '<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Revenue</div>';
|
|
3064
|
+
html += '<div style="display:flex;align-items:baseline;gap:16px;margin-bottom:8px">';
|
|
3065
|
+
html += '<div><span style="font-size:22px;font-weight:700;color:var(--accent-green)">' + Number(rev.totalSatsEarned).toLocaleString() + '</span><span style="font-size:11px;color:var(--text-muted);margin-left:6px">total sats</span></div>';
|
|
3066
|
+
html += '<div style="font-size:12px;color:var(--text-dim)">' + Number(rev.todaySats).toLocaleString() + ' today</div>';
|
|
3067
|
+
html += '<div style="font-size:12px;color:var(--text-dim)">' + Number(rev.weekSats).toLocaleString() + ' this week</div>';
|
|
3068
|
+
html += '</div>';
|
|
3069
|
+
html += '<div class="panel-row"><span class="label">Receipts</span><span class="value">' + rev.totalReceipts + '</span></div>';
|
|
3070
|
+
html += '<div class="panel-row"><span class="label">Pending</span><span class="value">' + rev.pendingClaims + '</span></div>';
|
|
3071
|
+
html += '</div>';
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// Pricing table
|
|
3075
|
+
if (x402Data.endpoints && x402Data.endpoints.length > 0) {
|
|
3076
|
+
html += '<div class="glass" style="padding:16px;margin-bottom:12px">';
|
|
3077
|
+
html += '<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px">Pricing</div>';
|
|
3078
|
+
for (var i = 0; i < x402Data.endpoints.length; i++) {
|
|
3079
|
+
var ep = x402Data.endpoints[i];
|
|
3080
|
+
html += '<div class="panel-row"><span class="label" style="font-family:monospace;font-size:11px">' + escapeHtml(ep.method) + ' ' + escapeHtml(ep.path) + '</span><span class="value" style="color:var(--accent-green)">' + ep.satoshis.toLocaleString() + ' sats</span></div>';
|
|
3081
|
+
}
|
|
3082
|
+
html += '</div>';
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
// Recent receipts (operator only)
|
|
3086
|
+
if (x402Data.recentReceipts && x402Data.recentReceipts.length > 0) {
|
|
3087
|
+
html += '<div class="glass" style="padding:16px;margin-bottom:12px">';
|
|
3088
|
+
html += '<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px">Recent Receipts</div>';
|
|
3089
|
+
for (var j = 0; j < x402Data.recentReceipts.length; j++) {
|
|
3090
|
+
var rx = x402Data.recentReceipts[j];
|
|
3091
|
+
var age = rx.createdAt ? fmtAge(rx.createdAt) : '?';
|
|
3092
|
+
html += '<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);font-size:11px">';
|
|
3093
|
+
html += '<span style="font-family:monospace;color:var(--text-muted)">' + escapeHtml(rx.txid.slice(0, 12)) + '...</span>';
|
|
3094
|
+
html += '<span style="color:var(--text-dim)">' + escapeHtml(rx.endpoint) + '</span>';
|
|
3095
|
+
html += '<span style="color:var(--accent-green)">' + Number(rx.satoshisPaid).toLocaleString() + ' sats</span>';
|
|
3096
|
+
html += '<span style="color:var(--text-dim)">' + age + '</span>';
|
|
3097
|
+
html += '</div>';
|
|
3098
|
+
}
|
|
3099
|
+
html += '</div>';
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// Settings panel (operator only)
|
|
3103
|
+
var b = bridgeData.get(selectedBridge);
|
|
3104
|
+
if (b && isOperator(b._url)) {
|
|
3105
|
+
html += '<div class="glass" style="padding:12px 16px;margin-bottom:8px">';
|
|
3106
|
+
html += '<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Settings</div>';
|
|
3107
|
+
|
|
3108
|
+
// Enable/disable checkbox
|
|
3109
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">';
|
|
3110
|
+
html += '<input type="checkbox" id="x402Enabled"' + (x402Data.enabled ? ' checked' : '') + ' style="cursor:pointer">';
|
|
3111
|
+
html += '<label for="x402Enabled" class="label" style="cursor:pointer">Enable Payment Gate</label>';
|
|
3112
|
+
html += '</div>';
|
|
3113
|
+
|
|
3114
|
+
// PayTo address
|
|
3115
|
+
html += '<div style="margin-bottom:8px">';
|
|
3116
|
+
html += '<div class="label" style="margin-bottom:4px">Pay To Address</div>';
|
|
3117
|
+
html += '<input type="text" id="x402PayTo" value="' + escapeHtml(x402Data.payTo || '') + '" placeholder="1YourBSVAddress..." style="width:100%;padding:6px 8px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-family:monospace;font-size:11px;box-sizing:border-box">';
|
|
3118
|
+
html += '</div>';
|
|
3119
|
+
|
|
3120
|
+
// Endpoint pricing
|
|
3121
|
+
html += '<div class="label" style="margin-bottom:8px">Endpoint Pricing (satoshis)</div>';
|
|
3122
|
+
var epList = x402Data.endpoints || [];
|
|
3123
|
+
html += '<div id="x402Endpoints">';
|
|
3124
|
+
for (var k = 0; k < epList.length; k++) {
|
|
3125
|
+
html += '<div style="display:flex;gap:8px;margin-bottom:6px;align-items:center">';
|
|
3126
|
+
html += '<input type="text" value="' + escapeHtml(epList[k].method + ':' + epList[k].path) + '" class="x402-ep-key" style="flex:2;padding:4px 6px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-family:monospace;font-size:11px">';
|
|
3127
|
+
html += '<input type="number" value="' + epList[k].satoshis + '" class="x402-ep-val" style="flex:1;padding:4px 6px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-family:monospace;font-size:11px">';
|
|
3128
|
+
html += '<button onclick="removeX402Endpoint(this)" style="background:none;border:none;color:var(--accent-red);cursor:pointer;font-size:14px">×</button>';
|
|
3129
|
+
html += '</div>';
|
|
3130
|
+
}
|
|
3131
|
+
html += '</div>';
|
|
3132
|
+
html += '<button class="btn" style="font-size:11px;padding:4px 12px;margin-bottom:12px" onclick="addX402Endpoint()">+ Add Endpoint</button>';
|
|
3133
|
+
|
|
3134
|
+
// Save button
|
|
3135
|
+
html += '<div style="margin-top:8px;text-align:right">';
|
|
3136
|
+
html += '<button class="btn primary" style="font-size:12px;padding:6px 20px" onclick="saveX402Settings()">Save Settings</button>';
|
|
3137
|
+
html += '</div>';
|
|
3138
|
+
html += '</div>';
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// Discovery endpoint hint
|
|
3142
|
+
html += '<div class="glass" style="padding:12px;font-size:11px;color:var(--text-dim)">';
|
|
3143
|
+
html += 'Discovery: <span style="font-family:monospace;color:var(--text-muted)">GET /.well-known/x402</span>';
|
|
3144
|
+
html += '</div>';
|
|
3145
|
+
|
|
3146
|
+
return html;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
async function fetchX402Data() {
|
|
3150
|
+
var b = bridgeData.get(selectedBridge);
|
|
3151
|
+
if (!b) return;
|
|
3152
|
+
try {
|
|
3153
|
+
var r = await fetch(b._url + '/x402' + getAuthParam(b._url), { signal: AbortSignal.timeout(10000) });
|
|
3154
|
+
if (!r.ok) { x402Data = { enabled: false, endpoints: [] }; if (activeTab === 'x402') { var el = document.getElementById('tabContent'); if (el) el.innerHTML = renderX402Tab(); } return; }
|
|
3155
|
+
x402Data = await r.json();
|
|
3156
|
+
if (activeTab === 'x402') {
|
|
3157
|
+
var el = document.getElementById('tabContent');
|
|
3158
|
+
if (el) el.innerHTML = renderX402Tab();
|
|
3159
|
+
}
|
|
3160
|
+
} catch (e) {
|
|
3161
|
+
x402Data = { enabled: false, endpoints: [] };
|
|
3162
|
+
if (activeTab === 'x402') {
|
|
3163
|
+
var el = document.getElementById('tabContent');
|
|
3164
|
+
if (el) el.innerHTML = '<div class="glass" style="padding:20px;color:var(--accent-red)">' + escapeHtml(e.message) + '</div>';
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
|
|
3170
|
+
function addX402Endpoint() {
|
|
3171
|
+
var container = document.getElementById('x402Endpoints');
|
|
3172
|
+
if (!container) return;
|
|
3173
|
+
var div = document.createElement('div');
|
|
3174
|
+
div.style.cssText = 'display:flex;gap:8px;margin-bottom:6px;align-items:center';
|
|
3175
|
+
div.innerHTML = '<input type="text" value="POST:/api/broadcast" class="x402-ep-key" style="flex:2;padding:4px 6px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-family:monospace;font-size:11px">' +
|
|
3176
|
+
'<input type="number" value="1000" class="x402-ep-val" style="flex:1;padding:4px 6px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-family:monospace;font-size:11px">' +
|
|
3177
|
+
'<button onclick="removeX402Endpoint(this)" style="background:none;border:none;color:var(--accent-red);cursor:pointer;font-size:14px">\u00d7</button>';
|
|
3178
|
+
container.appendChild(div);
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
function removeX402Endpoint(btn) { btn.parentElement.remove(); }
|
|
3182
|
+
|
|
3183
|
+
async function saveX402Settings() {
|
|
3184
|
+
var b = bridgeData.get(selectedBridge);
|
|
3185
|
+
if (!b) return;
|
|
3186
|
+
var enabled = document.getElementById('x402Enabled')?.checked || false;
|
|
3187
|
+
var payTo = document.getElementById('x402PayTo').value.trim();
|
|
3188
|
+
var keys = document.querySelectorAll('.x402-ep-key');
|
|
3189
|
+
var vals = document.querySelectorAll('.x402-ep-val');
|
|
3190
|
+
var endpoints = {};
|
|
3191
|
+
for (var i = 0; i < keys.length; i++) {
|
|
3192
|
+
var k = keys[i].value.trim();
|
|
3193
|
+
var v = parseInt(vals[i].value, 10);
|
|
3194
|
+
if (k && !isNaN(v)) endpoints[k] = v;
|
|
3195
|
+
}
|
|
3196
|
+
try {
|
|
3197
|
+
var r = await fetch(b._url + '/x402' + getAuthParam(b._url), {
|
|
3198
|
+
method: 'PATCH',
|
|
3199
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3200
|
+
body: JSON.stringify({ enabled: enabled, payTo: payTo, endpoints: endpoints })
|
|
3201
|
+
});
|
|
3202
|
+
if (r.ok) { fetchX402Data(); }
|
|
3203
|
+
else { var err = await r.json().catch(function() { return {}; }); alert(err.error || 'Save failed'); }
|
|
3204
|
+
} catch (e) { alert(e.message); }
|
|
3205
|
+
}
|
|
3206
|
+
|
|
2971
3207
|
// ── Log viewer ─────────────────────────────────────
|
|
2972
3208
|
function openLogViewer(bridgeUrl) {
|
|
2973
3209
|
document.getElementById('logViewerBody').innerHTML = '';
|