@relay-federation/bridge 0.3.2 → 0.3.4
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 +1 -1
- package/dashboard/index.html +533 -34
- package/lib/actions.js +3 -3
- package/lib/anchor-manager.js +0 -1
- package/lib/config.js +0 -1
- package/lib/peer-manager.js +2 -13
- package/lib/status-server.js +0 -1
- package/package.json +3 -2
package/cli.js
CHANGED
|
@@ -293,7 +293,7 @@ async function cmdStart () {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
// ── 2. Core components ────────────────────────────────────
|
|
296
|
-
const peerManager = new PeerManager(
|
|
296
|
+
const peerManager = new PeerManager()
|
|
297
297
|
const headerRelay = new HeaderRelay(peerManager)
|
|
298
298
|
const txRelay = new TxRelay(peerManager)
|
|
299
299
|
|
package/dashboard/index.html
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Federation Mesh Explorer</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/three@0.137.0/build/three.min.js"></script>
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/controls/OrbitControls.js"></script>
|
|
7
9
|
<style>
|
|
8
10
|
:root {
|
|
9
11
|
--bg-base: #121518;
|
|
@@ -185,7 +187,7 @@
|
|
|
185
187
|
.tab-content {
|
|
186
188
|
flex: 1;
|
|
187
189
|
overflow-y: auto;
|
|
188
|
-
padding: 20px
|
|
190
|
+
padding: 14px 20px;
|
|
189
191
|
}
|
|
190
192
|
.tab-content::-webkit-scrollbar { width: 6px; }
|
|
191
193
|
.tab-content::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -196,14 +198,14 @@
|
|
|
196
198
|
display: grid;
|
|
197
199
|
grid-template-columns: repeat(5, 1fr);
|
|
198
200
|
gap: 12px;
|
|
199
|
-
margin-bottom:
|
|
201
|
+
margin-bottom: 12px;
|
|
200
202
|
}
|
|
201
203
|
.stat-card {
|
|
202
|
-
padding:
|
|
204
|
+
padding: 12px 16px;
|
|
203
205
|
text-align: center;
|
|
204
206
|
}
|
|
205
207
|
.stat-card .stat-value {
|
|
206
|
-
font-size:
|
|
208
|
+
font-size: 22px;
|
|
207
209
|
font-weight: 700;
|
|
208
210
|
color: var(--text-primary);
|
|
209
211
|
font-variant-numeric: tabular-nums;
|
|
@@ -221,7 +223,7 @@
|
|
|
221
223
|
/* ── Overview cards grid ─────────────────────────── */
|
|
222
224
|
.cards-grid {
|
|
223
225
|
display: grid;
|
|
224
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
226
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
225
227
|
gap: 12px;
|
|
226
228
|
margin-bottom: 20px;
|
|
227
229
|
}
|
|
@@ -583,13 +585,15 @@
|
|
|
583
585
|
|
|
584
586
|
/* ── Peers ───────────────────────────────────────── */
|
|
585
587
|
.peer-entry {
|
|
586
|
-
padding:
|
|
588
|
+
padding: 6px 0;
|
|
587
589
|
border-bottom: 1px solid var(--border);
|
|
588
590
|
font-size: 12px;
|
|
589
591
|
}
|
|
590
592
|
.peer-entry:last-child { border-bottom: none; }
|
|
591
593
|
.peer-entry .peer-name { color: var(--accent-blue); font-family: var(--mono); }
|
|
592
|
-
.peer-entry .peer-meta { color: var(--text-dim); margin-top:
|
|
594
|
+
.peer-entry .peer-meta { color: var(--text-dim); margin-top: 2px; font-size: 11px; }
|
|
595
|
+
.peer-entry .score-bars { display: none; }
|
|
596
|
+
.peer-entry.expanded .score-bars { display: block; }
|
|
593
597
|
.peer-dot {
|
|
594
598
|
display: inline-block;
|
|
595
599
|
width: 6px;
|
|
@@ -647,6 +651,88 @@
|
|
|
647
651
|
.mini-map .mm-label { fill: var(--text-muted); font-size: 9px; font-family: inherit; text-anchor: middle; }
|
|
648
652
|
.mini-map .mm-label.name { fill: var(--accent-blue); font-size: 10px; font-weight: 600; }
|
|
649
653
|
|
|
654
|
+
/* ── 3D Mesh Map ──────────────────────────────────── */
|
|
655
|
+
.mesh3d-container {
|
|
656
|
+
position: relative;
|
|
657
|
+
width: 100%;
|
|
658
|
+
height: 180px;
|
|
659
|
+
border-radius: var(--radius-sm);
|
|
660
|
+
overflow: hidden;
|
|
661
|
+
background: radial-gradient(ellipse at center, rgba(20,30,50,0.9) 0%, rgba(8,10,14,0.95) 100%);
|
|
662
|
+
cursor: pointer;
|
|
663
|
+
}
|
|
664
|
+
.mesh3d-overlay {
|
|
665
|
+
position: fixed;
|
|
666
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
667
|
+
z-index: 9999;
|
|
668
|
+
background: rgba(0,0,0,0.85);
|
|
669
|
+
display: flex;
|
|
670
|
+
align-items: center;
|
|
671
|
+
justify-content: center;
|
|
672
|
+
}
|
|
673
|
+
.mesh3d-overlay .mesh3d-expanded {
|
|
674
|
+
position: relative;
|
|
675
|
+
width: 90vw;
|
|
676
|
+
height: 85vh;
|
|
677
|
+
border-radius: var(--radius);
|
|
678
|
+
overflow: hidden;
|
|
679
|
+
background: radial-gradient(ellipse at center, rgba(20,30,50,0.95) 0%, rgba(8,10,14,0.98) 100%);
|
|
680
|
+
border: 1px solid var(--border-light);
|
|
681
|
+
}
|
|
682
|
+
.mesh3d-overlay .mesh3d-expanded canvas {
|
|
683
|
+
display: block;
|
|
684
|
+
width: 100% !important;
|
|
685
|
+
height: 100% !important;
|
|
686
|
+
}
|
|
687
|
+
.mesh3d-overlay .mesh3d-close {
|
|
688
|
+
position: absolute;
|
|
689
|
+
top: 12px; right: 16px;
|
|
690
|
+
z-index: 10;
|
|
691
|
+
background: rgba(0,0,0,0.5);
|
|
692
|
+
border: 1px solid var(--border);
|
|
693
|
+
color: var(--text-muted);
|
|
694
|
+
font-size: 18px;
|
|
695
|
+
width: 32px; height: 32px;
|
|
696
|
+
border-radius: 50%;
|
|
697
|
+
cursor: pointer;
|
|
698
|
+
display: flex;
|
|
699
|
+
align-items: center;
|
|
700
|
+
justify-content: center;
|
|
701
|
+
}
|
|
702
|
+
.mesh3d-overlay .mesh3d-close:hover { color: var(--text-primary); background: rgba(255,255,255,0.1); }
|
|
703
|
+
.mesh3d-container canvas {
|
|
704
|
+
display: block;
|
|
705
|
+
width: 100% !important;
|
|
706
|
+
height: 100% !important;
|
|
707
|
+
border-radius: var(--radius-sm);
|
|
708
|
+
}
|
|
709
|
+
.mesh3d-fallback {
|
|
710
|
+
display: flex;
|
|
711
|
+
flex-direction: column;
|
|
712
|
+
align-items: center;
|
|
713
|
+
justify-content: center;
|
|
714
|
+
height: 100%;
|
|
715
|
+
color: var(--text-dim);
|
|
716
|
+
font-size: 12px;
|
|
717
|
+
gap: 8px;
|
|
718
|
+
}
|
|
719
|
+
.mesh3d-label {
|
|
720
|
+
position: absolute;
|
|
721
|
+
font-size: 10px;
|
|
722
|
+
font-family: var(--mono);
|
|
723
|
+
pointer-events: none;
|
|
724
|
+
white-space: nowrap;
|
|
725
|
+
transform: translate(-50%, -50%);
|
|
726
|
+
background: rgba(0,0,0,0.7);
|
|
727
|
+
padding: 2px 7px;
|
|
728
|
+
border-radius: 4px;
|
|
729
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
730
|
+
letter-spacing: 0.5px;
|
|
731
|
+
}
|
|
732
|
+
.mesh3d-label.center { color: var(--accent-blue); font-weight: 700; font-size: 12px; border-color: rgba(88,166,255,0.3); background: rgba(88,166,255,0.12); text-shadow: 0 0 8px rgba(88,166,255,0.5); }
|
|
733
|
+
.mesh3d-label.online { color: #7ee8a0; border-color: rgba(63,185,80,0.25); text-shadow: 0 0 6px rgba(63,185,80,0.4); }
|
|
734
|
+
.mesh3d-label.offline { color: var(--accent-red); opacity: 0.7; border-color: rgba(248,81,73,0.2); }
|
|
735
|
+
|
|
650
736
|
/* ── Modal ───────────────────────────────────────── */
|
|
651
737
|
.modal-overlay {
|
|
652
738
|
display: none;
|
|
@@ -917,11 +1003,9 @@ function generateQR(text) {
|
|
|
917
1003
|
}
|
|
918
1004
|
|
|
919
1005
|
// ── Configuration ──────────────────────────────────
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
];
|
|
924
|
-
let BRIDGES = [...SEED_BRIDGES];
|
|
1006
|
+
// Bootstrap from self — discover the rest of the mesh via /discover
|
|
1007
|
+
const SEED_URLS = [window.location.origin];
|
|
1008
|
+
let BRIDGES = SEED_URLS.map(url => ({ name: null, url }));
|
|
925
1009
|
const POLL_INTERVAL = 5000;
|
|
926
1010
|
let discoveryDone = false;
|
|
927
1011
|
|
|
@@ -978,10 +1062,17 @@ async function fetchBridge(bridge) {
|
|
|
978
1062
|
const r = await fetch(bridge.url + '/status' + getAuthParam(bridge.url), { signal: AbortSignal.timeout(5000) });
|
|
979
1063
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
980
1064
|
const data = await r.json();
|
|
981
|
-
|
|
1065
|
+
// Bridge self-reports its name — use that as canonical, fall back to URL-derived name
|
|
1066
|
+
const selfName = (data.bridge && data.bridge.name) ? data.bridge.name : null;
|
|
1067
|
+
const fallbackName = bridge.name || ('bridge-' + new URL(bridge.url).hostname.split('.').pop());
|
|
1068
|
+
data._name = selfName || fallbackName;
|
|
1069
|
+
// Update BRIDGES entry so the name sticks for future polls
|
|
1070
|
+
if (selfName && !bridge.name) bridge.name = selfName;
|
|
1071
|
+
data._url = bridge.url; data._error = null; data._lastSeen = Date.now();
|
|
982
1072
|
return data;
|
|
983
1073
|
} catch (e) {
|
|
984
|
-
|
|
1074
|
+
const fallbackName = bridge.name || ('bridge-' + new URL(bridge.url).hostname.split('.').pop());
|
|
1075
|
+
return { _name: fallbackName, _url: bridge.url, _error: e.message,
|
|
985
1076
|
bridge: { pubkeyHex: null, endpoint: null, meshId: null, uptimeSeconds: 0 },
|
|
986
1077
|
peers: { connected: 0, max: 0, list: [] },
|
|
987
1078
|
headers: { bestHeight: -1, bestHash: null, count: 0 },
|
|
@@ -998,10 +1089,11 @@ async function fetchMempool(bridge) {
|
|
|
998
1089
|
}
|
|
999
1090
|
async function discoverBridges() {
|
|
1000
1091
|
const known = new Map();
|
|
1001
|
-
|
|
1002
|
-
for (const
|
|
1092
|
+
// Seed URLs start with no name — /status will fill them in
|
|
1093
|
+
for (const url of SEED_URLS) known.set(url, null);
|
|
1094
|
+
for (const url of SEED_URLS) {
|
|
1003
1095
|
try {
|
|
1004
|
-
const r = await fetch(
|
|
1096
|
+
const r = await fetch(url + '/discover', { signal: AbortSignal.timeout(5000) });
|
|
1005
1097
|
if (!r.ok) continue;
|
|
1006
1098
|
const data = await r.json();
|
|
1007
1099
|
if (!data.bridges) continue;
|
|
@@ -1009,8 +1101,8 @@ async function discoverBridges() {
|
|
|
1009
1101
|
if (!b.statusUrl) continue;
|
|
1010
1102
|
const base = b.statusUrl.replace(/\/status$/, '');
|
|
1011
1103
|
if (!known.has(base)) {
|
|
1012
|
-
|
|
1013
|
-
known.set(base, name);
|
|
1104
|
+
// Use name from /discover if available, otherwise placeholder until /status fills it in
|
|
1105
|
+
known.set(base, b.name || null);
|
|
1014
1106
|
}
|
|
1015
1107
|
}
|
|
1016
1108
|
} catch {}
|
|
@@ -1031,12 +1123,9 @@ async function pollAll() {
|
|
|
1031
1123
|
if (!selectedBridge && bridgeData.size > 0) {
|
|
1032
1124
|
selectedBridge = bridgeData.keys().next().value;
|
|
1033
1125
|
}
|
|
1034
|
-
// Fetch price from
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
fetch(firstOnline._url + '/price', { signal: AbortSignal.timeout(5000) })
|
|
1038
|
-
.then(r => r.ok ? r.json() : null).then(d => { if (d) latestPrice = d; }).catch(() => {});
|
|
1039
|
-
}
|
|
1126
|
+
// Fetch price from self (same-origin, always works)
|
|
1127
|
+
fetch('/price', { signal: AbortSignal.timeout(5000) })
|
|
1128
|
+
.then(r => r.ok ? r.json() : null).then(d => { if (d) latestPrice = d; }).catch(() => {});
|
|
1040
1129
|
renderHeader();
|
|
1041
1130
|
renderBridgeRail();
|
|
1042
1131
|
renderActiveTab(true);
|
|
@@ -1088,6 +1177,8 @@ function setTab(tab) {
|
|
|
1088
1177
|
if (inp) savedExplorerInput = inp.value;
|
|
1089
1178
|
if (res) savedExplorerResult = res.innerHTML;
|
|
1090
1179
|
}
|
|
1180
|
+
// Destroy 3D scene when leaving overview
|
|
1181
|
+
if (activeTab === 'overview' && tab !== 'overview') destroyMeshMap3D();
|
|
1091
1182
|
activeTab = tab;
|
|
1092
1183
|
const buttons = document.querySelectorAll('#headerTabs button');
|
|
1093
1184
|
const tabs = ['overview', 'mempool', 'explorer', 'inscriptions', 'tokens', 'apps'];
|
|
@@ -1101,6 +1192,13 @@ function renderActiveTab(fromPoll) {
|
|
|
1101
1192
|
const el = document.getElementById('tabContent');
|
|
1102
1193
|
// Skip apps/explorer re-render during poll (preserves state)
|
|
1103
1194
|
if (fromPoll && (activeTab === 'apps' || activeTab === 'explorer' || activeTab === 'inscriptions' || activeTab === 'tokens')) return;
|
|
1195
|
+
// On poll, if overview tab has active 3D scene, just update data — don't rebuild DOM
|
|
1196
|
+
if (fromPoll && activeTab === 'overview' && mesh3d) {
|
|
1197
|
+
updateMeshMap3D();
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
// Destroy 3D before rebuilding overview DOM
|
|
1201
|
+
if (activeTab === 'overview') destroyMeshMap3D();
|
|
1104
1202
|
let html;
|
|
1105
1203
|
switch (activeTab) {
|
|
1106
1204
|
case 'overview': html = renderOverviewTab(); break;
|
|
@@ -1115,6 +1213,20 @@ function renderActiveTab(fromPoll) {
|
|
|
1115
1213
|
if (el.innerHTML !== html) {
|
|
1116
1214
|
el.innerHTML = html;
|
|
1117
1215
|
if (activeTab === 'explorer') restoreExplorer();
|
|
1216
|
+
// Init 3D mesh after overview DOM is in place
|
|
1217
|
+
if (activeTab === 'overview') {
|
|
1218
|
+
const onlineBridges = [...bridgeData.values()].filter(b => !b._error);
|
|
1219
|
+
if (onlineBridges.length > 0) {
|
|
1220
|
+
setTimeout(() => {
|
|
1221
|
+
if (!initMeshMap3D('mesh3dCanvas')) {
|
|
1222
|
+
// Fallback to 2D SVG if WebGL unavailable
|
|
1223
|
+
const b = bridgeData.get(selectedBridge);
|
|
1224
|
+
const container = document.getElementById('mesh3dCanvas');
|
|
1225
|
+
if (container && b) container.innerHTML = '<div class="mesh3d-fallback">' + renderMiniMap(b) + '<span>WebGL not available</span></div>';
|
|
1226
|
+
}
|
|
1227
|
+
}, 50);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1118
1230
|
}
|
|
1119
1231
|
}
|
|
1120
1232
|
|
|
@@ -1159,6 +1271,14 @@ function renderOverviewTab() {
|
|
|
1159
1271
|
|
|
1160
1272
|
if (b._error) html += '<div class="error-banner">' + b._error + '</div>';
|
|
1161
1273
|
|
|
1274
|
+
// 3D Mesh Map — compact inline, click to expand
|
|
1275
|
+
const onlineBridgeCount = [...bridgeData.values()].filter(bb => !bb._error).length;
|
|
1276
|
+
if (onlineBridgeCount > 0) {
|
|
1277
|
+
html += '<div class="detail-card glass" style="margin-bottom:12px;padding:10px 14px"><h3 style="margin-bottom:6px">Mesh Topology (' + onlineBridgeCount + ' nodes, ' + bridgeData.size + ' known) <span style="font-size:9px;color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">— click to expand</span></h3>';
|
|
1278
|
+
html += '<div class="mesh3d-container" id="mesh3dCanvas" onclick="expandMeshMap()"></div>';
|
|
1279
|
+
html += '</div>';
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1162
1282
|
html += '<div class="cards-grid">';
|
|
1163
1283
|
|
|
1164
1284
|
// Bridge info card
|
|
@@ -1206,17 +1326,22 @@ function renderOverviewTab() {
|
|
|
1206
1326
|
}
|
|
1207
1327
|
|
|
1208
1328
|
// Peers card
|
|
1209
|
-
html += '<div class="detail-card glass"><h3>Peers (' + b.peers.connected + '
|
|
1329
|
+
html += '<div class="detail-card glass"><h3>Peers (' + b.peers.connected + ')</h3>';
|
|
1210
1330
|
if (b.peers.list.length === 0) {
|
|
1211
1331
|
html += '<div style="color:var(--text-dim);font-size:12px">No peers connected</div>';
|
|
1212
1332
|
} else {
|
|
1213
|
-
|
|
1333
|
+
const showAll = window._showAllPeers || false;
|
|
1334
|
+
const visiblePeers = showAll ? b.peers.list : b.peers.list.slice(0, 3);
|
|
1335
|
+
for (const p of visiblePeers) {
|
|
1214
1336
|
const dotC = p.connected ? 'green' : 'red';
|
|
1215
|
-
html += '<div class="peer-entry"><div><span class="peer-dot ' + dotC + '"></span><span class="peer-name">' + truncPubkey(p.pubkeyHex) + '</span> <span style="color:var(--text-dim)">score ' + (p.score !== undefined ? p.score.toFixed(2) : '?') + '</span></div>';
|
|
1337
|
+
html += '<div class="peer-entry" style="cursor:pointer" onclick="this.classList.toggle(\'expanded\')"><div><span class="peer-dot ' + dotC + '"></span><span class="peer-name">' + truncPubkey(p.pubkeyHex) + '</span> <span style="color:var(--text-dim)">score ' + (p.score !== undefined ? p.score.toFixed(2) : '?') + '</span></div>';
|
|
1216
1338
|
html += '<div class="peer-meta">' + (p.endpoint || '') + (p.health ? ' · ' + p.health : '') + '</div>';
|
|
1217
|
-
if (p.scoreBreakdown) html += renderScoreBars(p.scoreBreakdown);
|
|
1339
|
+
if (p.scoreBreakdown) html += '<div class="score-bars">' + renderScoreBars(p.scoreBreakdown) + '</div>';
|
|
1218
1340
|
html += '</div>';
|
|
1219
1341
|
}
|
|
1342
|
+
if (b.peers.list.length > 3) {
|
|
1343
|
+
html += '<div style="text-align:center;padding:4px 0"><button class="btn ghost" style="font-size:11px;padding:2px 10px" onclick="window._showAllPeers=!window._showAllPeers;renderActiveTab()">' + (showAll ? 'Show less' : 'Show all ' + b.peers.list.length + ' peers') + '</button></div>';
|
|
1344
|
+
}
|
|
1220
1345
|
}
|
|
1221
1346
|
html += '</div>';
|
|
1222
1347
|
|
|
@@ -1235,11 +1360,6 @@ function renderOverviewTab() {
|
|
|
1235
1360
|
html += '</div>';
|
|
1236
1361
|
}
|
|
1237
1362
|
|
|
1238
|
-
// Mini map card
|
|
1239
|
-
if (b.peers && b.peers.list && b.peers.list.length > 0) {
|
|
1240
|
-
html += '<div class="detail-card glass"><h3>Mesh Map</h3>' + renderMiniMap(b) + '</div>';
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
1363
|
html += '</div>'; // cards-grid
|
|
1244
1364
|
|
|
1245
1365
|
html += '<div style="color:var(--text-dim);font-size:10px;text-align:center;padding:12px 0">Updated ' + new Date().toLocaleTimeString() + '</div>';
|
|
@@ -1686,6 +1806,385 @@ function renderParsedData(type, protocol, parsed, txid, vout) {
|
|
|
1686
1806
|
}
|
|
1687
1807
|
|
|
1688
1808
|
|
|
1809
|
+
// ── 3D Mesh Map ───────────────────────────────────
|
|
1810
|
+
let mesh3d = null; // { renderer, scene, camera, controls, animId, nodes, lines, particles, labels, container, lastInteraction }
|
|
1811
|
+
|
|
1812
|
+
function hasWebGL() {
|
|
1813
|
+
try { const c = document.createElement('canvas'); return !!(window.WebGLRenderingContext && (c.getContext('webgl') || c.getContext('experimental-webgl'))); } catch { return false; }
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function initMeshMap3D(containerId) {
|
|
1817
|
+
if (mesh3d) destroyMeshMap3D();
|
|
1818
|
+
const container = document.getElementById(containerId);
|
|
1819
|
+
if (!container || !hasWebGL() || typeof THREE === 'undefined') return false;
|
|
1820
|
+
|
|
1821
|
+
const w = container.clientWidth, h = container.clientHeight;
|
|
1822
|
+
|
|
1823
|
+
// ── Build topology from ALL bridges ──────────────
|
|
1824
|
+
const allBridges = [...bridgeData.values()].filter(b => !b._error);
|
|
1825
|
+
const n = allBridges.length;
|
|
1826
|
+
if (n === 0) return false;
|
|
1827
|
+
|
|
1828
|
+
// Map pubkey → bridge name for cross-referencing
|
|
1829
|
+
const pubkeyToName = new Map();
|
|
1830
|
+
for (const b of allBridges) {
|
|
1831
|
+
if (b.bridge && b.bridge.pubkeyHex) pubkeyToName.set(b.bridge.pubkeyHex, b._name);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// Build edge list: pairs of bridge indices that are connected
|
|
1835
|
+
const edges = [];
|
|
1836
|
+
const edgeSet = new Set();
|
|
1837
|
+
for (let i = 0; i < n; i++) {
|
|
1838
|
+
const b = allBridges[i];
|
|
1839
|
+
if (!b.peers || !b.peers.list) continue;
|
|
1840
|
+
for (const p of b.peers.list) {
|
|
1841
|
+
if (!p.connected) continue;
|
|
1842
|
+
const peerName = pubkeyToName.get(p.pubkeyHex);
|
|
1843
|
+
if (!peerName) continue;
|
|
1844
|
+
const j = allBridges.findIndex(bb => bb._name === peerName);
|
|
1845
|
+
if (j < 0 || j === i) continue;
|
|
1846
|
+
const key = Math.min(i, j) + ':' + Math.max(i, j);
|
|
1847
|
+
if (edgeSet.has(key)) continue;
|
|
1848
|
+
edgeSet.add(key);
|
|
1849
|
+
// Get score data from the peer entry
|
|
1850
|
+
const score = p.score !== undefined ? p.score : 0.5;
|
|
1851
|
+
const breakdown = p.scoreBreakdown || null;
|
|
1852
|
+
edges.push({ a: i, b: j, score, breakdown });
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// ── Scene setup ──────────────────────────────────
|
|
1857
|
+
const scene = new THREE.Scene();
|
|
1858
|
+
const camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 200);
|
|
1859
|
+
// Scale camera distance based on node count
|
|
1860
|
+
const camDist = Math.max(14, n * 2.5);
|
|
1861
|
+
camera.position.set(0, camDist * 0.4, camDist);
|
|
1862
|
+
|
|
1863
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
1864
|
+
renderer.setSize(w, h);
|
|
1865
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1866
|
+
renderer.setClearColor(0x000000, 0);
|
|
1867
|
+
container.appendChild(renderer.domElement);
|
|
1868
|
+
|
|
1869
|
+
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
1870
|
+
controls.enableDamping = true;
|
|
1871
|
+
controls.dampingFactor = 0.08;
|
|
1872
|
+
controls.enablePan = false;
|
|
1873
|
+
controls.minDistance = 5;
|
|
1874
|
+
controls.maxDistance = camDist * 2;
|
|
1875
|
+
controls.autoRotate = true;
|
|
1876
|
+
controls.autoRotateSpeed = 0.5;
|
|
1877
|
+
|
|
1878
|
+
// ── Lights ───────────────────────────────────────
|
|
1879
|
+
scene.add(new THREE.AmbientLight(0x334466, 0.8));
|
|
1880
|
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.9);
|
|
1881
|
+
dirLight.position.set(5, 10, 7);
|
|
1882
|
+
scene.add(dirLight);
|
|
1883
|
+
const rimLight = new THREE.DirectionalLight(0x58a6ff, 0.3);
|
|
1884
|
+
rimLight.position.set(-3, -5, -3);
|
|
1885
|
+
scene.add(rimLight);
|
|
1886
|
+
|
|
1887
|
+
// ── Starfield ────────────────────────────────────
|
|
1888
|
+
const starGeo = new THREE.BufferGeometry();
|
|
1889
|
+
const starPositions = new Float32Array(400 * 3);
|
|
1890
|
+
for (let i = 0; i < 400; i++) {
|
|
1891
|
+
starPositions[i * 3] = (Math.random() - 0.5) * 120;
|
|
1892
|
+
starPositions[i * 3 + 1] = (Math.random() - 0.5) * 120;
|
|
1893
|
+
starPositions[i * 3 + 2] = (Math.random() - 0.5) * 120;
|
|
1894
|
+
}
|
|
1895
|
+
starGeo.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
|
|
1896
|
+
scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({ color: 0xffffff, size: 0.15, transparent: true, opacity: 0.35 })));
|
|
1897
|
+
|
|
1898
|
+
// ── Node positions — even distribution in 3D ─────
|
|
1899
|
+
const orbitRadius = Math.max(5, n * 1.2);
|
|
1900
|
+
const nodePositions = [];
|
|
1901
|
+
for (let i = 0; i < n; i++) {
|
|
1902
|
+
if (n === 1) {
|
|
1903
|
+
nodePositions.push(new THREE.Vector3(0, 0, 0));
|
|
1904
|
+
} else if (n === 2) {
|
|
1905
|
+
nodePositions.push(new THREE.Vector3(i === 0 ? -3 : 3, 0, 0));
|
|
1906
|
+
} else {
|
|
1907
|
+
// Distribute on a circle (flatten Y) for small counts, Fibonacci sphere for large
|
|
1908
|
+
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
|
1909
|
+
const theta = Math.PI * (1 + Math.sqrt(5)) * i;
|
|
1910
|
+
const x = orbitRadius * Math.sin(phi) * Math.cos(theta);
|
|
1911
|
+
const y = orbitRadius * Math.cos(phi) * 0.35;
|
|
1912
|
+
const z = orbitRadius * Math.sin(phi) * Math.sin(theta);
|
|
1913
|
+
nodePositions.push(new THREE.Vector3(x, y, z));
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// ── Create bridge nodes ──────────────────────────
|
|
1918
|
+
const nodes = [];
|
|
1919
|
+
const labelEls = [];
|
|
1920
|
+
const glowRings = [];
|
|
1921
|
+
|
|
1922
|
+
for (let i = 0; i < n; i++) {
|
|
1923
|
+
const b = allBridges[i];
|
|
1924
|
+
const isSelected = b._name === selectedBridge;
|
|
1925
|
+
const isOnline = !b._error;
|
|
1926
|
+
const peerCount = b.peers ? b.peers.connected : 0;
|
|
1927
|
+
|
|
1928
|
+
// All bridges same size — color based on health only
|
|
1929
|
+
const radius = 0.65;
|
|
1930
|
+
const nodeGeo = new THREE.SphereGeometry(radius, 32, 32);
|
|
1931
|
+
|
|
1932
|
+
let color, emissive, emissiveIntensity;
|
|
1933
|
+
if (isOnline && peerCount > 0) {
|
|
1934
|
+
color = 0x3fb950; emissive = 0x2a5a2a; emissiveIntensity = 0.7;
|
|
1935
|
+
} else if (isOnline) {
|
|
1936
|
+
color = 0xd29922; emissive = 0x4a3a10; emissiveIntensity = 0.5;
|
|
1937
|
+
} else {
|
|
1938
|
+
color = 0xf85149; emissive = 0x5a2020; emissiveIntensity = 0.5;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const nodeMat = new THREE.MeshStandardMaterial({ color, emissive, emissiveIntensity, roughness: 0.2, metalness: 0.6 });
|
|
1942
|
+
const nodeMesh = new THREE.Mesh(nodeGeo, nodeMat);
|
|
1943
|
+
nodeMesh.position.copy(nodePositions[i]);
|
|
1944
|
+
scene.add(nodeMesh);
|
|
1945
|
+
|
|
1946
|
+
// Point light per node — same for all
|
|
1947
|
+
const lightColor = isOnline ? 0x3fb950 : 0xf85149;
|
|
1948
|
+
const pointLight = new THREE.PointLight(lightColor, 0.8, 6);
|
|
1949
|
+
nodeMesh.add(pointLight);
|
|
1950
|
+
|
|
1951
|
+
// Subtle selection ring — thin white ring if this is the sidebar-selected bridge
|
|
1952
|
+
if (isSelected) {
|
|
1953
|
+
const selRing = new THREE.Mesh(
|
|
1954
|
+
new THREE.RingGeometry(0.85, 0.95, 48),
|
|
1955
|
+
new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.35, side: THREE.DoubleSide })
|
|
1956
|
+
);
|
|
1957
|
+
selRing.rotation.x = -Math.PI / 2;
|
|
1958
|
+
selRing.position.copy(nodePositions[i]);
|
|
1959
|
+
scene.add(selRing);
|
|
1960
|
+
glowRings.push(selRing);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
nodes.push({ mesh: nodeMesh, bridge: b, isSelected });
|
|
1964
|
+
|
|
1965
|
+
// HTML label — bridge name, all same style (online/offline only)
|
|
1966
|
+
const label = document.createElement('div');
|
|
1967
|
+
label.className = 'mesh3d-label ' + (isOnline ? 'online' : 'offline');
|
|
1968
|
+
label.textContent = b._name;
|
|
1969
|
+
container.appendChild(label);
|
|
1970
|
+
labelEls.push({ el: label, nodeMesh });
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// ── Create edges between connected bridges ───────
|
|
1974
|
+
const edgeObjs = [];
|
|
1975
|
+
const particles = [];
|
|
1976
|
+
|
|
1977
|
+
for (const edge of edges) {
|
|
1978
|
+
const posA = nodePositions[edge.a];
|
|
1979
|
+
const posB = nodePositions[edge.b];
|
|
1980
|
+
|
|
1981
|
+
// Bezier midpoint — arc upward for visual clarity
|
|
1982
|
+
const mid = new THREE.Vector3(
|
|
1983
|
+
(posA.x + posB.x) * 0.5,
|
|
1984
|
+
(posA.y + posB.y) * 0.5 + orbitRadius * 0.2,
|
|
1985
|
+
(posA.z + posB.z) * 0.5
|
|
1986
|
+
);
|
|
1987
|
+
const curve = new THREE.QuadraticBezierCurve3(posA.clone(), mid, posB.clone());
|
|
1988
|
+
const tubeGeo = new THREE.TubeGeometry(curve, 48, 0.03, 8, false);
|
|
1989
|
+
const tubeMat = new THREE.MeshBasicMaterial({ color: 0x58a6ff, transparent: true, opacity: 0.45 });
|
|
1990
|
+
const tubeMesh = new THREE.Mesh(tubeGeo, tubeMat);
|
|
1991
|
+
scene.add(tubeMesh);
|
|
1992
|
+
edgeObjs.push({ mesh: tubeMesh, curve, edge });
|
|
1993
|
+
|
|
1994
|
+
// Traffic particles — count driven by mempool activity, speed by latency
|
|
1995
|
+
// Get mempool sizes for the two bridges on this edge
|
|
1996
|
+
const bridgeA = allBridges[edge.a];
|
|
1997
|
+
const bridgeB = allBridges[edge.b];
|
|
1998
|
+
const mempoolA = bridgeA.txs ? bridgeA.txs.mempool : 0;
|
|
1999
|
+
const mempoolB = bridgeB.txs ? bridgeB.txs.mempool : 0;
|
|
2000
|
+
const traffic = Math.max(mempoolA, mempoolB);
|
|
2001
|
+
// 1 idle particle (keepalive pulse) + 1 per 5 mempool txs, max 8
|
|
2002
|
+
const pCount = Math.min(8, 1 + Math.floor(traffic / 5));
|
|
2003
|
+
// Speed: slow idle crawl when quiet, faster with more RTT score (inverted — high score = low latency = faster)
|
|
2004
|
+
const rtt = (edge.breakdown && edge.breakdown.responseTime) ? edge.breakdown.responseTime : 0.5;
|
|
2005
|
+
const baseSpeed = traffic > 0 ? 0.0015 : 0.0006; // idle crawl vs active
|
|
2006
|
+
const speed = baseSpeed + rtt * 0.002;
|
|
2007
|
+
for (let j = 0; j < pCount; j++) {
|
|
2008
|
+
const pGeo = new THREE.SphereGeometry(0.07, 8, 8);
|
|
2009
|
+
// Idle particles are dimmer, active ones brighter
|
|
2010
|
+
const brightness = traffic > 0 ? 0x7ec8ff : 0x4a7a9a;
|
|
2011
|
+
const pMat = new THREE.MeshBasicMaterial({ color: brightness, transparent: true, opacity: traffic > 0 ? 0.9 : 0.4 });
|
|
2012
|
+
const pMesh = new THREE.Mesh(pGeo, pMat);
|
|
2013
|
+
scene.add(pMesh);
|
|
2014
|
+
particles.push({ mesh: pMesh, curve, t: j / pCount, speed, direction: j % 2 === 0 ? 1 : -1, isIdle: traffic === 0 });
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// ── Auto-rotate pause ────────────────────────────
|
|
2019
|
+
let lastInteraction = 0;
|
|
2020
|
+
const onInteract = () => { lastInteraction = Date.now(); controls.autoRotate = false; };
|
|
2021
|
+
renderer.domElement.addEventListener('pointerdown', onInteract);
|
|
2022
|
+
renderer.domElement.addEventListener('wheel', onInteract);
|
|
2023
|
+
|
|
2024
|
+
// ── Animation loop ───────────────────────────────
|
|
2025
|
+
const clock = new THREE.Clock();
|
|
2026
|
+
let animId;
|
|
2027
|
+
|
|
2028
|
+
function animate() {
|
|
2029
|
+
animId = requestAnimationFrame(animate);
|
|
2030
|
+
clock.getDelta();
|
|
2031
|
+
const t = clock.getElapsedTime();
|
|
2032
|
+
|
|
2033
|
+
if (!controls.autoRotate && Date.now() - lastInteraction > 10000) controls.autoRotate = true;
|
|
2034
|
+
controls.update();
|
|
2035
|
+
|
|
2036
|
+
// Edge pulsing
|
|
2037
|
+
for (let i = 0; i < edgeObjs.length; i++) {
|
|
2038
|
+
edgeObjs[i].mesh.material.opacity = 0.3 + 0.2 * Math.sin(t * 2 + i * 1.3);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Glow ring animation on selected bridge
|
|
2042
|
+
for (let i = 0; i < glowRings.length; i++) {
|
|
2043
|
+
const gr = glowRings[i];
|
|
2044
|
+
gr.material.opacity = (i % 2 === 0 ? 0.15 : 0.05) + (i % 2 === 0 ? 0.12 : 0.05) * Math.sin(t * 1.5 + i);
|
|
2045
|
+
gr.rotation.z = t * (i % 2 === 0 ? 0.1 : -0.08);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// Particles along edges — idle ones crawl & fade, active ones zip
|
|
2049
|
+
for (const p of particles) {
|
|
2050
|
+
p.t += p.speed * p.direction;
|
|
2051
|
+
if (p.t > 1) p.t -= 1;
|
|
2052
|
+
if (p.t < 0) p.t += 1;
|
|
2053
|
+
const pos = p.curve.getPointAt(Math.max(0, Math.min(1, p.t)));
|
|
2054
|
+
p.mesh.position.copy(pos);
|
|
2055
|
+
if (p.isIdle) {
|
|
2056
|
+
// Slow breathing pulse — barely visible keepalive
|
|
2057
|
+
p.mesh.material.opacity = 0.15 + 0.2 * Math.sin(p.t * Math.PI);
|
|
2058
|
+
} else {
|
|
2059
|
+
p.mesh.material.opacity = 0.4 + 0.5 * Math.sin(p.t * Math.PI);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// Project labels 3D → 2D
|
|
2064
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
2065
|
+
for (const lb of labelEls) {
|
|
2066
|
+
const pos = lb.nodeMesh.position.clone();
|
|
2067
|
+
pos.y += (lb.el.classList.contains('center') ? 1.6 : 1.2);
|
|
2068
|
+
pos.project(camera);
|
|
2069
|
+
const sx = (pos.x * 0.5 + 0.5) * rect.width;
|
|
2070
|
+
const sy = (-pos.y * 0.5 + 0.5) * rect.height;
|
|
2071
|
+
lb.el.style.left = sx + 'px';
|
|
2072
|
+
lb.el.style.top = sy + 'px';
|
|
2073
|
+
lb.el.style.display = pos.z > 1 ? 'none' : '';
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
renderer.render(scene, camera);
|
|
2077
|
+
}
|
|
2078
|
+
animate();
|
|
2079
|
+
|
|
2080
|
+
// Resize — reads from mesh3d.container so it works after expand/collapse swap
|
|
2081
|
+
const onResize = () => {
|
|
2082
|
+
const c = mesh3d ? mesh3d.container : container;
|
|
2083
|
+
const nw = c.clientWidth, nh = c.clientHeight;
|
|
2084
|
+
if (nw === 0 || nh === 0) return;
|
|
2085
|
+
camera.aspect = nw / nh;
|
|
2086
|
+
camera.updateProjectionMatrix();
|
|
2087
|
+
renderer.setSize(nw, nh);
|
|
2088
|
+
};
|
|
2089
|
+
window.addEventListener('resize', onResize);
|
|
2090
|
+
|
|
2091
|
+
mesh3d = { renderer, scene, camera, controls, animId, nodes, edges: edgeObjs, particles, labels: labelEls, container, onResize, glowRings };
|
|
2092
|
+
return true;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function updateMeshMap3D() {
|
|
2096
|
+
if (!mesh3d) return;
|
|
2097
|
+
const allBridges = [...bridgeData.values()].filter(b => !b._error);
|
|
2098
|
+
// If bridge count changed, full rebuild needed
|
|
2099
|
+
if (allBridges.length !== mesh3d.nodes.length) return;
|
|
2100
|
+
// Update node visuals
|
|
2101
|
+
for (let i = 0; i < mesh3d.nodes.length && i < allBridges.length; i++) {
|
|
2102
|
+
const node = mesh3d.nodes[i];
|
|
2103
|
+
const b = allBridges[i];
|
|
2104
|
+
const isOnline = !b._error;
|
|
2105
|
+
const peerCount = b.peers ? b.peers.connected : 0;
|
|
2106
|
+
if (isOnline && peerCount > 0) {
|
|
2107
|
+
node.mesh.material.color.setHex(0x3fb950);
|
|
2108
|
+
node.mesh.material.emissive.setHex(0x2a5a2a);
|
|
2109
|
+
} else if (isOnline) {
|
|
2110
|
+
node.mesh.material.color.setHex(0xd29922);
|
|
2111
|
+
node.mesh.material.emissive.setHex(0x4a3a10);
|
|
2112
|
+
} else {
|
|
2113
|
+
node.mesh.material.color.setHex(0xf85149);
|
|
2114
|
+
node.mesh.material.emissive.setHex(0x5a2020);
|
|
2115
|
+
}
|
|
2116
|
+
node.bridge = b;
|
|
2117
|
+
// Update label
|
|
2118
|
+
if (mesh3d.labels[i]) {
|
|
2119
|
+
mesh3d.labels[i].el.className = 'mesh3d-label ' + (isOnline ? 'online' : 'offline');
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function destroyMeshMap3D() {
|
|
2125
|
+
if (!mesh3d) return;
|
|
2126
|
+
// Remove overlay if open
|
|
2127
|
+
const overlay = document.getElementById('mesh3dOverlay');
|
|
2128
|
+
if (overlay) overlay.remove();
|
|
2129
|
+
cancelAnimationFrame(mesh3d.animId);
|
|
2130
|
+
window.removeEventListener('resize', mesh3d.onResize);
|
|
2131
|
+
// Dispose geometries and materials
|
|
2132
|
+
mesh3d.scene.traverse(obj => {
|
|
2133
|
+
if (obj.geometry) obj.geometry.dispose();
|
|
2134
|
+
if (obj.material) {
|
|
2135
|
+
if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
|
|
2136
|
+
else obj.material.dispose();
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
mesh3d.renderer.dispose();
|
|
2140
|
+
// Remove canvas
|
|
2141
|
+
if (mesh3d.renderer.domElement && mesh3d.renderer.domElement.parentNode) {
|
|
2142
|
+
mesh3d.renderer.domElement.parentNode.removeChild(mesh3d.renderer.domElement);
|
|
2143
|
+
}
|
|
2144
|
+
// Remove labels
|
|
2145
|
+
for (const lb of mesh3d.labels) {
|
|
2146
|
+
if (lb.el && lb.el.parentNode) lb.el.parentNode.removeChild(lb.el);
|
|
2147
|
+
}
|
|
2148
|
+
mesh3d = null;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
function expandMeshMap() {
|
|
2152
|
+
if (!mesh3d) return;
|
|
2153
|
+
// Create overlay
|
|
2154
|
+
const overlay = document.createElement('div');
|
|
2155
|
+
overlay.className = 'mesh3d-overlay';
|
|
2156
|
+
overlay.id = 'mesh3dOverlay';
|
|
2157
|
+
overlay.innerHTML = '<div class="mesh3d-expanded"><button class="mesh3d-close" onclick="collapseMeshMap()">×</button></div>';
|
|
2158
|
+
document.body.appendChild(overlay);
|
|
2159
|
+
// Click backdrop to close
|
|
2160
|
+
overlay.addEventListener('click', function(e) { if (e.target === overlay) collapseMeshMap(); });
|
|
2161
|
+
// Move canvas + labels into expanded container
|
|
2162
|
+
const expanded = overlay.querySelector('.mesh3d-expanded');
|
|
2163
|
+
expanded.appendChild(mesh3d.renderer.domElement);
|
|
2164
|
+
for (const lb of mesh3d.labels) {
|
|
2165
|
+
if (lb.el) expanded.appendChild(lb.el);
|
|
2166
|
+
}
|
|
2167
|
+
mesh3d.container = expanded;
|
|
2168
|
+
// Resize renderer to new container
|
|
2169
|
+
setTimeout(() => { mesh3d.onResize(); }, 50);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function collapseMeshMap() {
|
|
2173
|
+
const overlay = document.getElementById('mesh3dOverlay');
|
|
2174
|
+
if (!overlay) return;
|
|
2175
|
+
// Move canvas + labels back to inline container
|
|
2176
|
+
const inline = document.getElementById('mesh3dCanvas');
|
|
2177
|
+
if (mesh3d && inline) {
|
|
2178
|
+
inline.appendChild(mesh3d.renderer.domElement);
|
|
2179
|
+
for (const lb of mesh3d.labels) {
|
|
2180
|
+
if (lb.el) inline.appendChild(lb.el);
|
|
2181
|
+
}
|
|
2182
|
+
mesh3d.container = inline;
|
|
2183
|
+
setTimeout(() => { mesh3d.onResize(); }, 50);
|
|
2184
|
+
}
|
|
2185
|
+
overlay.remove();
|
|
2186
|
+
}
|
|
2187
|
+
|
|
1689
2188
|
|
|
1690
2189
|
// ── Mini peer map ──────────────────────────────────
|
|
1691
2190
|
function renderMiniMap(b) {
|
package/lib/actions.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function runRegister ({ config, store, log }) {
|
|
|
24
24
|
log('step', `Mesh: ${config.meshId}`)
|
|
25
25
|
log('step', `Capabilities: ${config.capabilities.join(', ')}`)
|
|
26
26
|
|
|
27
|
-
const { buildRegistrationTx } = await import('
|
|
27
|
+
const { buildRegistrationTx } = await import('@relay-federation/registry/lib/registration.js')
|
|
28
28
|
const { BSVNodeClient } = await import('./bsv-node-client.js')
|
|
29
29
|
|
|
30
30
|
// Get UTXOs from local store
|
|
@@ -65,7 +65,7 @@ export async function runRegister ({ config, store, log }) {
|
|
|
65
65
|
|
|
66
66
|
try {
|
|
67
67
|
// Step 1: Build and broadcast stake bond tx
|
|
68
|
-
const { buildStakeBondTx } = await import('
|
|
68
|
+
const { buildStakeBondTx } = await import('@relay-federation/registry/lib/stake-bond.js')
|
|
69
69
|
const { MIN_STAKE_SATS } = await import('@relay-federation/common/protocol')
|
|
70
70
|
|
|
71
71
|
log('step', `Building stake bond (${MIN_STAKE_SATS} sats)...`)
|
|
@@ -136,7 +136,7 @@ export async function runDeregister ({ config, store, reason = 'shutdown', log }
|
|
|
136
136
|
log('step', `Pubkey: ${config.pubkeyHex}`)
|
|
137
137
|
log('step', `Reason: ${reason}`)
|
|
138
138
|
|
|
139
|
-
const { buildDeregistrationTx } = await import('
|
|
139
|
+
const { buildDeregistrationTx } = await import('@relay-federation/registry/lib/registration.js')
|
|
140
140
|
const { BSVNodeClient } = await import('./bsv-node-client.js')
|
|
141
141
|
|
|
142
142
|
// Get UTXOs from local store
|
package/lib/anchor-manager.js
CHANGED
|
@@ -83,7 +83,6 @@ export class AnchorManager extends EventEmitter {
|
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Connect to all configured anchors that we're not already connected to.
|
|
86
|
-
* Respects maxPeers — if at capacity, still tries anchors (they're priority).
|
|
87
86
|
*
|
|
88
87
|
* @returns {number} Number of new connections initiated
|
|
89
88
|
*/
|
package/lib/config.js
CHANGED
package/lib/peer-manager.js
CHANGED
|
@@ -20,11 +20,9 @@ import { PeerConnection } from './peer-connection.js'
|
|
|
20
20
|
export class PeerManager extends EventEmitter {
|
|
21
21
|
/**
|
|
22
22
|
* @param {object} [opts]
|
|
23
|
-
* @param {number} [opts.maxPeers=20] - Maximum number of peer connections
|
|
24
23
|
*/
|
|
25
|
-
constructor (
|
|
24
|
+
constructor () {
|
|
26
25
|
super()
|
|
27
|
-
this.maxPeers = opts.maxPeers || 20
|
|
28
26
|
/** @type {Map<string, PeerConnection>} pubkeyHex → PeerConnection */
|
|
29
27
|
this.peers = new Map()
|
|
30
28
|
this._server = null
|
|
@@ -36,17 +34,13 @@ export class PeerManager extends EventEmitter {
|
|
|
36
34
|
* @param {object} peer - Peer from buildPeerList()
|
|
37
35
|
* @param {string} peer.pubkeyHex
|
|
38
36
|
* @param {string} peer.endpoint
|
|
39
|
-
* @returns {PeerConnection|null} The connection, or null if
|
|
37
|
+
* @returns {PeerConnection|null} The connection, or null if already connected
|
|
40
38
|
*/
|
|
41
39
|
connectToPeer (peer) {
|
|
42
40
|
if (this.peers.has(peer.pubkeyHex)) {
|
|
43
41
|
return this.peers.get(peer.pubkeyHex)
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
if (this.peers.size >= this.maxPeers) {
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
44
|
const conn = new PeerConnection({
|
|
51
45
|
endpoint: peer.endpoint,
|
|
52
46
|
pubkeyHex: peer.pubkeyHex
|
|
@@ -74,11 +68,6 @@ export class PeerManager extends EventEmitter {
|
|
|
74
68
|
return null
|
|
75
69
|
}
|
|
76
70
|
|
|
77
|
-
if (this.peers.size >= this.maxPeers) {
|
|
78
|
-
socket.close()
|
|
79
|
-
return null
|
|
80
|
-
}
|
|
81
|
-
|
|
82
71
|
const conn = new PeerConnection({
|
|
83
72
|
endpoint,
|
|
84
73
|
pubkeyHex,
|
package/lib/status-server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relay-federation/bridge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Bridge server — WebSocket peering, header sync, tx relay, CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@bsv/sdk": "^1.10.1",
|
|
14
|
-
"@relay-federation/common": "^0.
|
|
14
|
+
"@relay-federation/common": "^0.2.0",
|
|
15
|
+
"@relay-federation/registry": "^0.2.0",
|
|
15
16
|
"level": "^10.0.0",
|
|
16
17
|
"ws": "^8.19.0"
|
|
17
18
|
},
|