@relay-federation/bridge 0.3.1 → 0.3.3
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 +55 -7
- package/dashboard/index.html +532 -27
- package/lib/actions.js +3 -3
- package/lib/anchor-manager.js +0 -1
- package/lib/config.js +0 -1
- package/lib/peer-manager.js +3 -14
- package/lib/status-server.js +0 -1
- package/package.json +2 -1
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
|
|
|
@@ -320,9 +320,10 @@ async function cmdStart () {
|
|
|
320
320
|
})
|
|
321
321
|
|
|
322
322
|
// Wire peer health tracking
|
|
323
|
-
peerManager.on('peer:connect', ({ pubkeyHex }) => {
|
|
323
|
+
peerManager.on('peer:connect', ({ pubkeyHex, endpoint }) => {
|
|
324
324
|
peerHealth.recordSeen(pubkeyHex)
|
|
325
325
|
scorer.setStakeAge(pubkeyHex, 7)
|
|
326
|
+
if (endpoint) gossipManager.addSeed({ pubkeyHex, endpoint, meshId: config.meshId })
|
|
326
327
|
})
|
|
327
328
|
|
|
328
329
|
peerManager.on('peer:disconnect', ({ pubkeyHex }) => {
|
|
@@ -430,6 +431,8 @@ async function cmdStart () {
|
|
|
430
431
|
const { extractOpReturnData, decodePayload, PROTOCOL_PREFIX } = await import('../registry/lib/cbor.js')
|
|
431
432
|
const { Transaction: BsvTx } = await import('@bsv/sdk')
|
|
432
433
|
|
|
434
|
+
// Registry bootstrapped via discoverNewPeers() after server start (no WoC dependency)
|
|
435
|
+
|
|
433
436
|
watcher.on('utxo:received', async ({ txid, hash160 }) => {
|
|
434
437
|
if (hash160 !== beaconHash160) return
|
|
435
438
|
|
|
@@ -641,14 +644,17 @@ async function cmdStart () {
|
|
|
641
644
|
let gossipStarted = false
|
|
642
645
|
|
|
643
646
|
// Start gossip after first peer connection completes.
|
|
644
|
-
//
|
|
645
|
-
//
|
|
647
|
+
// Delay 5s so all seed handshakes finish before gossip broadcasts
|
|
648
|
+
// (immediate broadcast would send announce/getpeers through connections
|
|
649
|
+
// whose inbound side is still waiting for verify — breaking the handshake).
|
|
646
650
|
peerManager.on('peer:connect', () => {
|
|
647
651
|
if (!gossipStarted) {
|
|
648
652
|
gossipStarted = true
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
653
|
+
setTimeout(() => {
|
|
654
|
+
gossipManager.start()
|
|
655
|
+
gossipManager.requestPeersFromAll()
|
|
656
|
+
console.log('Gossip started')
|
|
657
|
+
}, 5000)
|
|
652
658
|
|
|
653
659
|
// Periodic peer refresh — re-request peer lists every 10 minutes
|
|
654
660
|
// Catches registrations missed during downtime or initial gossip
|
|
@@ -703,6 +709,7 @@ async function cmdStart () {
|
|
|
703
709
|
// Connect to seed peers (accept both string URLs and {pubkeyHex, endpoint} objects)
|
|
704
710
|
console.log(`Connecting to ${seedPeers.length} seed peer(s)...`)
|
|
705
711
|
for (let i = 0; i < seedPeers.length; i++) {
|
|
712
|
+
if (i > 0) await new Promise(r => setTimeout(r, 2000)) // stagger to avoid handshake races
|
|
706
713
|
const seed = seedPeers[i]
|
|
707
714
|
const endpoint = typeof seed === 'string' ? seed : seed.endpoint
|
|
708
715
|
const pubkey = typeof seed === 'string' ? `seed_${i}` : seed.pubkeyHex
|
|
@@ -770,6 +777,47 @@ async function cmdStart () {
|
|
|
770
777
|
statusServer.startAppMonitoring()
|
|
771
778
|
console.log(` Status: http://127.0.0.1:${statusPort}/status`)
|
|
772
779
|
|
|
780
|
+
// ── Peer discovery — bootstrap registry from seed peers, then periodic refresh ──
|
|
781
|
+
const knownEndpoints = new Set()
|
|
782
|
+
for (const sp of (config.seedPeers || [])) knownEndpoints.add(sp.endpoint)
|
|
783
|
+
knownEndpoints.add(config.endpoint)
|
|
784
|
+
|
|
785
|
+
async function discoverNewPeers () {
|
|
786
|
+
const peersToQuery = [...(config.seedPeers || [])]
|
|
787
|
+
for (const [, conn] of peerManager.peers) {
|
|
788
|
+
if (conn.endpoint && conn.readyState === 1) peersToQuery.push({ endpoint: conn.endpoint })
|
|
789
|
+
}
|
|
790
|
+
for (const peer of peersToQuery) {
|
|
791
|
+
try {
|
|
792
|
+
const ep = peer.endpoint || ''
|
|
793
|
+
const u = new URL(ep)
|
|
794
|
+
const statusUrl = 'http://' + u.hostname + ':' + (parseInt(u.port, 10) + 1000) + '/discover'
|
|
795
|
+
const res = await fetch(statusUrl, { signal: AbortSignal.timeout(5000) })
|
|
796
|
+
if (!res.ok) continue
|
|
797
|
+
const data = await res.json()
|
|
798
|
+
if (!data.bridges) continue
|
|
799
|
+
for (const b of data.bridges) {
|
|
800
|
+
if (!b.endpoint) continue
|
|
801
|
+
if (b.pubkeyHex) registeredPubkeys.add(b.pubkeyHex)
|
|
802
|
+
seedEndpoints.add(b.endpoint)
|
|
803
|
+
if (knownEndpoints.has(b.endpoint)) continue
|
|
804
|
+
knownEndpoints.add(b.endpoint)
|
|
805
|
+
const conn = peerManager.connectToPeer({ endpoint: b.endpoint, pubkeyHex: b.pubkeyHex })
|
|
806
|
+
if (conn) {
|
|
807
|
+
conn.on('open', () => performOutboundHandshake(conn))
|
|
808
|
+
const msg = `Discovered new peer: ${b.name || b.pubkeyHex?.slice(0, 16) || b.endpoint}`
|
|
809
|
+
console.log(msg)
|
|
810
|
+
statusServer.addLog(msg)
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
} catch {}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
await discoverNewPeers()
|
|
817
|
+
console.log(` Registry: ${registeredPubkeys.size} trusted pubkeys after peer discovery`)
|
|
818
|
+
setTimeout(discoverNewPeers, 5000)
|
|
819
|
+
setInterval(discoverNewPeers, 300000)
|
|
820
|
+
|
|
773
821
|
// ── 9. Log events (dual: console + status server ring buffer) ──
|
|
774
822
|
peerManager.on('peer:connect', ({ pubkeyHex }) => {
|
|
775
823
|
const msg = `Peer connected: ${pubkeyHex.slice(0, 16)}...`
|
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,12 @@ function generateQR(text) {
|
|
|
917
1003
|
}
|
|
918
1004
|
|
|
919
1005
|
// ── Configuration ──────────────────────────────────
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1006
|
+
// Seed URLs only — names come from each bridge's /status response
|
|
1007
|
+
const SEED_URLS = [
|
|
1008
|
+
'http://144.202.48.217:9333',
|
|
1009
|
+
'http://45.63.77.31:9333'
|
|
923
1010
|
];
|
|
924
|
-
let BRIDGES =
|
|
1011
|
+
let BRIDGES = SEED_URLS.map(url => ({ name: null, url }));
|
|
925
1012
|
const POLL_INTERVAL = 5000;
|
|
926
1013
|
let discoveryDone = false;
|
|
927
1014
|
|
|
@@ -978,10 +1065,17 @@ async function fetchBridge(bridge) {
|
|
|
978
1065
|
const r = await fetch(bridge.url + '/status' + getAuthParam(bridge.url), { signal: AbortSignal.timeout(5000) });
|
|
979
1066
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
980
1067
|
const data = await r.json();
|
|
981
|
-
|
|
1068
|
+
// Bridge self-reports its name — use that as canonical, fall back to URL-derived name
|
|
1069
|
+
const selfName = (data.bridge && data.bridge.name) ? data.bridge.name : null;
|
|
1070
|
+
const fallbackName = bridge.name || ('bridge-' + new URL(bridge.url).hostname.split('.').pop());
|
|
1071
|
+
data._name = selfName || fallbackName;
|
|
1072
|
+
// Update BRIDGES entry so the name sticks for future polls
|
|
1073
|
+
if (selfName && !bridge.name) bridge.name = selfName;
|
|
1074
|
+
data._url = bridge.url; data._error = null; data._lastSeen = Date.now();
|
|
982
1075
|
return data;
|
|
983
1076
|
} catch (e) {
|
|
984
|
-
|
|
1077
|
+
const fallbackName = bridge.name || ('bridge-' + new URL(bridge.url).hostname.split('.').pop());
|
|
1078
|
+
return { _name: fallbackName, _url: bridge.url, _error: e.message,
|
|
985
1079
|
bridge: { pubkeyHex: null, endpoint: null, meshId: null, uptimeSeconds: 0 },
|
|
986
1080
|
peers: { connected: 0, max: 0, list: [] },
|
|
987
1081
|
headers: { bestHeight: -1, bestHash: null, count: 0 },
|
|
@@ -998,10 +1092,11 @@ async function fetchMempool(bridge) {
|
|
|
998
1092
|
}
|
|
999
1093
|
async function discoverBridges() {
|
|
1000
1094
|
const known = new Map();
|
|
1001
|
-
|
|
1002
|
-
for (const
|
|
1095
|
+
// Seed URLs start with no name — /status will fill them in
|
|
1096
|
+
for (const url of SEED_URLS) known.set(url, null);
|
|
1097
|
+
for (const url of SEED_URLS) {
|
|
1003
1098
|
try {
|
|
1004
|
-
const r = await fetch(
|
|
1099
|
+
const r = await fetch(url + '/discover', { signal: AbortSignal.timeout(5000) });
|
|
1005
1100
|
if (!r.ok) continue;
|
|
1006
1101
|
const data = await r.json();
|
|
1007
1102
|
if (!data.bridges) continue;
|
|
@@ -1009,8 +1104,8 @@ async function discoverBridges() {
|
|
|
1009
1104
|
if (!b.statusUrl) continue;
|
|
1010
1105
|
const base = b.statusUrl.replace(/\/status$/, '');
|
|
1011
1106
|
if (!known.has(base)) {
|
|
1012
|
-
|
|
1013
|
-
known.set(base, name);
|
|
1107
|
+
// Use name from /discover if available, otherwise placeholder until /status fills it in
|
|
1108
|
+
known.set(base, b.name || null);
|
|
1014
1109
|
}
|
|
1015
1110
|
}
|
|
1016
1111
|
} catch {}
|
|
@@ -1088,6 +1183,8 @@ function setTab(tab) {
|
|
|
1088
1183
|
if (inp) savedExplorerInput = inp.value;
|
|
1089
1184
|
if (res) savedExplorerResult = res.innerHTML;
|
|
1090
1185
|
}
|
|
1186
|
+
// Destroy 3D scene when leaving overview
|
|
1187
|
+
if (activeTab === 'overview' && tab !== 'overview') destroyMeshMap3D();
|
|
1091
1188
|
activeTab = tab;
|
|
1092
1189
|
const buttons = document.querySelectorAll('#headerTabs button');
|
|
1093
1190
|
const tabs = ['overview', 'mempool', 'explorer', 'inscriptions', 'tokens', 'apps'];
|
|
@@ -1101,6 +1198,13 @@ function renderActiveTab(fromPoll) {
|
|
|
1101
1198
|
const el = document.getElementById('tabContent');
|
|
1102
1199
|
// Skip apps/explorer re-render during poll (preserves state)
|
|
1103
1200
|
if (fromPoll && (activeTab === 'apps' || activeTab === 'explorer' || activeTab === 'inscriptions' || activeTab === 'tokens')) return;
|
|
1201
|
+
// On poll, if overview tab has active 3D scene, just update data — don't rebuild DOM
|
|
1202
|
+
if (fromPoll && activeTab === 'overview' && mesh3d) {
|
|
1203
|
+
updateMeshMap3D();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
// Destroy 3D before rebuilding overview DOM
|
|
1207
|
+
if (activeTab === 'overview') destroyMeshMap3D();
|
|
1104
1208
|
let html;
|
|
1105
1209
|
switch (activeTab) {
|
|
1106
1210
|
case 'overview': html = renderOverviewTab(); break;
|
|
@@ -1115,6 +1219,20 @@ function renderActiveTab(fromPoll) {
|
|
|
1115
1219
|
if (el.innerHTML !== html) {
|
|
1116
1220
|
el.innerHTML = html;
|
|
1117
1221
|
if (activeTab === 'explorer') restoreExplorer();
|
|
1222
|
+
// Init 3D mesh after overview DOM is in place
|
|
1223
|
+
if (activeTab === 'overview') {
|
|
1224
|
+
const onlineBridges = [...bridgeData.values()].filter(b => !b._error);
|
|
1225
|
+
if (onlineBridges.length > 0) {
|
|
1226
|
+
setTimeout(() => {
|
|
1227
|
+
if (!initMeshMap3D('mesh3dCanvas')) {
|
|
1228
|
+
// Fallback to 2D SVG if WebGL unavailable
|
|
1229
|
+
const b = bridgeData.get(selectedBridge);
|
|
1230
|
+
const container = document.getElementById('mesh3dCanvas');
|
|
1231
|
+
if (container && b) container.innerHTML = '<div class="mesh3d-fallback">' + renderMiniMap(b) + '<span>WebGL not available</span></div>';
|
|
1232
|
+
}
|
|
1233
|
+
}, 50);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1118
1236
|
}
|
|
1119
1237
|
}
|
|
1120
1238
|
|
|
@@ -1159,6 +1277,14 @@ function renderOverviewTab() {
|
|
|
1159
1277
|
|
|
1160
1278
|
if (b._error) html += '<div class="error-banner">' + b._error + '</div>';
|
|
1161
1279
|
|
|
1280
|
+
// 3D Mesh Map — compact inline, click to expand
|
|
1281
|
+
const onlineBridgeCount = [...bridgeData.values()].filter(bb => !bb._error).length;
|
|
1282
|
+
if (onlineBridgeCount > 0) {
|
|
1283
|
+
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>';
|
|
1284
|
+
html += '<div class="mesh3d-container" id="mesh3dCanvas" onclick="expandMeshMap()"></div>';
|
|
1285
|
+
html += '</div>';
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1162
1288
|
html += '<div class="cards-grid">';
|
|
1163
1289
|
|
|
1164
1290
|
// Bridge info card
|
|
@@ -1206,17 +1332,22 @@ function renderOverviewTab() {
|
|
|
1206
1332
|
}
|
|
1207
1333
|
|
|
1208
1334
|
// Peers card
|
|
1209
|
-
html += '<div class="detail-card glass"><h3>Peers (' + b.peers.connected + '
|
|
1335
|
+
html += '<div class="detail-card glass"><h3>Peers (' + b.peers.connected + ')</h3>';
|
|
1210
1336
|
if (b.peers.list.length === 0) {
|
|
1211
1337
|
html += '<div style="color:var(--text-dim);font-size:12px">No peers connected</div>';
|
|
1212
1338
|
} else {
|
|
1213
|
-
|
|
1339
|
+
const showAll = window._showAllPeers || false;
|
|
1340
|
+
const visiblePeers = showAll ? b.peers.list : b.peers.list.slice(0, 3);
|
|
1341
|
+
for (const p of visiblePeers) {
|
|
1214
1342
|
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>';
|
|
1343
|
+
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
1344
|
html += '<div class="peer-meta">' + (p.endpoint || '') + (p.health ? ' · ' + p.health : '') + '</div>';
|
|
1217
|
-
if (p.scoreBreakdown) html += renderScoreBars(p.scoreBreakdown);
|
|
1345
|
+
if (p.scoreBreakdown) html += '<div class="score-bars">' + renderScoreBars(p.scoreBreakdown) + '</div>';
|
|
1218
1346
|
html += '</div>';
|
|
1219
1347
|
}
|
|
1348
|
+
if (b.peers.list.length > 3) {
|
|
1349
|
+
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>';
|
|
1350
|
+
}
|
|
1220
1351
|
}
|
|
1221
1352
|
html += '</div>';
|
|
1222
1353
|
|
|
@@ -1235,11 +1366,6 @@ function renderOverviewTab() {
|
|
|
1235
1366
|
html += '</div>';
|
|
1236
1367
|
}
|
|
1237
1368
|
|
|
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
1369
|
html += '</div>'; // cards-grid
|
|
1244
1370
|
|
|
1245
1371
|
html += '<div style="color:var(--text-dim);font-size:10px;text-align:center;padding:12px 0">Updated ' + new Date().toLocaleTimeString() + '</div>';
|
|
@@ -1686,6 +1812,385 @@ function renderParsedData(type, protocol, parsed, txid, vout) {
|
|
|
1686
1812
|
}
|
|
1687
1813
|
|
|
1688
1814
|
|
|
1815
|
+
// ── 3D Mesh Map ───────────────────────────────────
|
|
1816
|
+
let mesh3d = null; // { renderer, scene, camera, controls, animId, nodes, lines, particles, labels, container, lastInteraction }
|
|
1817
|
+
|
|
1818
|
+
function hasWebGL() {
|
|
1819
|
+
try { const c = document.createElement('canvas'); return !!(window.WebGLRenderingContext && (c.getContext('webgl') || c.getContext('experimental-webgl'))); } catch { return false; }
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function initMeshMap3D(containerId) {
|
|
1823
|
+
if (mesh3d) destroyMeshMap3D();
|
|
1824
|
+
const container = document.getElementById(containerId);
|
|
1825
|
+
if (!container || !hasWebGL() || typeof THREE === 'undefined') return false;
|
|
1826
|
+
|
|
1827
|
+
const w = container.clientWidth, h = container.clientHeight;
|
|
1828
|
+
|
|
1829
|
+
// ── Build topology from ALL bridges ──────────────
|
|
1830
|
+
const allBridges = [...bridgeData.values()].filter(b => !b._error);
|
|
1831
|
+
const n = allBridges.length;
|
|
1832
|
+
if (n === 0) return false;
|
|
1833
|
+
|
|
1834
|
+
// Map pubkey → bridge name for cross-referencing
|
|
1835
|
+
const pubkeyToName = new Map();
|
|
1836
|
+
for (const b of allBridges) {
|
|
1837
|
+
if (b.bridge && b.bridge.pubkeyHex) pubkeyToName.set(b.bridge.pubkeyHex, b._name);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// Build edge list: pairs of bridge indices that are connected
|
|
1841
|
+
const edges = [];
|
|
1842
|
+
const edgeSet = new Set();
|
|
1843
|
+
for (let i = 0; i < n; i++) {
|
|
1844
|
+
const b = allBridges[i];
|
|
1845
|
+
if (!b.peers || !b.peers.list) continue;
|
|
1846
|
+
for (const p of b.peers.list) {
|
|
1847
|
+
if (!p.connected) continue;
|
|
1848
|
+
const peerName = pubkeyToName.get(p.pubkeyHex);
|
|
1849
|
+
if (!peerName) continue;
|
|
1850
|
+
const j = allBridges.findIndex(bb => bb._name === peerName);
|
|
1851
|
+
if (j < 0 || j === i) continue;
|
|
1852
|
+
const key = Math.min(i, j) + ':' + Math.max(i, j);
|
|
1853
|
+
if (edgeSet.has(key)) continue;
|
|
1854
|
+
edgeSet.add(key);
|
|
1855
|
+
// Get score data from the peer entry
|
|
1856
|
+
const score = p.score !== undefined ? p.score : 0.5;
|
|
1857
|
+
const breakdown = p.scoreBreakdown || null;
|
|
1858
|
+
edges.push({ a: i, b: j, score, breakdown });
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// ── Scene setup ──────────────────────────────────
|
|
1863
|
+
const scene = new THREE.Scene();
|
|
1864
|
+
const camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 200);
|
|
1865
|
+
// Scale camera distance based on node count
|
|
1866
|
+
const camDist = Math.max(14, n * 2.5);
|
|
1867
|
+
camera.position.set(0, camDist * 0.4, camDist);
|
|
1868
|
+
|
|
1869
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
1870
|
+
renderer.setSize(w, h);
|
|
1871
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1872
|
+
renderer.setClearColor(0x000000, 0);
|
|
1873
|
+
container.appendChild(renderer.domElement);
|
|
1874
|
+
|
|
1875
|
+
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
1876
|
+
controls.enableDamping = true;
|
|
1877
|
+
controls.dampingFactor = 0.08;
|
|
1878
|
+
controls.enablePan = false;
|
|
1879
|
+
controls.minDistance = 5;
|
|
1880
|
+
controls.maxDistance = camDist * 2;
|
|
1881
|
+
controls.autoRotate = true;
|
|
1882
|
+
controls.autoRotateSpeed = 0.5;
|
|
1883
|
+
|
|
1884
|
+
// ── Lights ───────────────────────────────────────
|
|
1885
|
+
scene.add(new THREE.AmbientLight(0x334466, 0.8));
|
|
1886
|
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.9);
|
|
1887
|
+
dirLight.position.set(5, 10, 7);
|
|
1888
|
+
scene.add(dirLight);
|
|
1889
|
+
const rimLight = new THREE.DirectionalLight(0x58a6ff, 0.3);
|
|
1890
|
+
rimLight.position.set(-3, -5, -3);
|
|
1891
|
+
scene.add(rimLight);
|
|
1892
|
+
|
|
1893
|
+
// ── Starfield ────────────────────────────────────
|
|
1894
|
+
const starGeo = new THREE.BufferGeometry();
|
|
1895
|
+
const starPositions = new Float32Array(400 * 3);
|
|
1896
|
+
for (let i = 0; i < 400; i++) {
|
|
1897
|
+
starPositions[i * 3] = (Math.random() - 0.5) * 120;
|
|
1898
|
+
starPositions[i * 3 + 1] = (Math.random() - 0.5) * 120;
|
|
1899
|
+
starPositions[i * 3 + 2] = (Math.random() - 0.5) * 120;
|
|
1900
|
+
}
|
|
1901
|
+
starGeo.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
|
|
1902
|
+
scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({ color: 0xffffff, size: 0.15, transparent: true, opacity: 0.35 })));
|
|
1903
|
+
|
|
1904
|
+
// ── Node positions — even distribution in 3D ─────
|
|
1905
|
+
const orbitRadius = Math.max(5, n * 1.2);
|
|
1906
|
+
const nodePositions = [];
|
|
1907
|
+
for (let i = 0; i < n; i++) {
|
|
1908
|
+
if (n === 1) {
|
|
1909
|
+
nodePositions.push(new THREE.Vector3(0, 0, 0));
|
|
1910
|
+
} else if (n === 2) {
|
|
1911
|
+
nodePositions.push(new THREE.Vector3(i === 0 ? -3 : 3, 0, 0));
|
|
1912
|
+
} else {
|
|
1913
|
+
// Distribute on a circle (flatten Y) for small counts, Fibonacci sphere for large
|
|
1914
|
+
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
|
1915
|
+
const theta = Math.PI * (1 + Math.sqrt(5)) * i;
|
|
1916
|
+
const x = orbitRadius * Math.sin(phi) * Math.cos(theta);
|
|
1917
|
+
const y = orbitRadius * Math.cos(phi) * 0.35;
|
|
1918
|
+
const z = orbitRadius * Math.sin(phi) * Math.sin(theta);
|
|
1919
|
+
nodePositions.push(new THREE.Vector3(x, y, z));
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// ── Create bridge nodes ──────────────────────────
|
|
1924
|
+
const nodes = [];
|
|
1925
|
+
const labelEls = [];
|
|
1926
|
+
const glowRings = [];
|
|
1927
|
+
|
|
1928
|
+
for (let i = 0; i < n; i++) {
|
|
1929
|
+
const b = allBridges[i];
|
|
1930
|
+
const isSelected = b._name === selectedBridge;
|
|
1931
|
+
const isOnline = !b._error;
|
|
1932
|
+
const peerCount = b.peers ? b.peers.connected : 0;
|
|
1933
|
+
|
|
1934
|
+
// All bridges same size — color based on health only
|
|
1935
|
+
const radius = 0.65;
|
|
1936
|
+
const nodeGeo = new THREE.SphereGeometry(radius, 32, 32);
|
|
1937
|
+
|
|
1938
|
+
let color, emissive, emissiveIntensity;
|
|
1939
|
+
if (isOnline && peerCount > 0) {
|
|
1940
|
+
color = 0x3fb950; emissive = 0x2a5a2a; emissiveIntensity = 0.7;
|
|
1941
|
+
} else if (isOnline) {
|
|
1942
|
+
color = 0xd29922; emissive = 0x4a3a10; emissiveIntensity = 0.5;
|
|
1943
|
+
} else {
|
|
1944
|
+
color = 0xf85149; emissive = 0x5a2020; emissiveIntensity = 0.5;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
const nodeMat = new THREE.MeshStandardMaterial({ color, emissive, emissiveIntensity, roughness: 0.2, metalness: 0.6 });
|
|
1948
|
+
const nodeMesh = new THREE.Mesh(nodeGeo, nodeMat);
|
|
1949
|
+
nodeMesh.position.copy(nodePositions[i]);
|
|
1950
|
+
scene.add(nodeMesh);
|
|
1951
|
+
|
|
1952
|
+
// Point light per node — same for all
|
|
1953
|
+
const lightColor = isOnline ? 0x3fb950 : 0xf85149;
|
|
1954
|
+
const pointLight = new THREE.PointLight(lightColor, 0.8, 6);
|
|
1955
|
+
nodeMesh.add(pointLight);
|
|
1956
|
+
|
|
1957
|
+
// Subtle selection ring — thin white ring if this is the sidebar-selected bridge
|
|
1958
|
+
if (isSelected) {
|
|
1959
|
+
const selRing = new THREE.Mesh(
|
|
1960
|
+
new THREE.RingGeometry(0.85, 0.95, 48),
|
|
1961
|
+
new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.35, side: THREE.DoubleSide })
|
|
1962
|
+
);
|
|
1963
|
+
selRing.rotation.x = -Math.PI / 2;
|
|
1964
|
+
selRing.position.copy(nodePositions[i]);
|
|
1965
|
+
scene.add(selRing);
|
|
1966
|
+
glowRings.push(selRing);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
nodes.push({ mesh: nodeMesh, bridge: b, isSelected });
|
|
1970
|
+
|
|
1971
|
+
// HTML label — bridge name, all same style (online/offline only)
|
|
1972
|
+
const label = document.createElement('div');
|
|
1973
|
+
label.className = 'mesh3d-label ' + (isOnline ? 'online' : 'offline');
|
|
1974
|
+
label.textContent = b._name;
|
|
1975
|
+
container.appendChild(label);
|
|
1976
|
+
labelEls.push({ el: label, nodeMesh });
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// ── Create edges between connected bridges ───────
|
|
1980
|
+
const edgeObjs = [];
|
|
1981
|
+
const particles = [];
|
|
1982
|
+
|
|
1983
|
+
for (const edge of edges) {
|
|
1984
|
+
const posA = nodePositions[edge.a];
|
|
1985
|
+
const posB = nodePositions[edge.b];
|
|
1986
|
+
|
|
1987
|
+
// Bezier midpoint — arc upward for visual clarity
|
|
1988
|
+
const mid = new THREE.Vector3(
|
|
1989
|
+
(posA.x + posB.x) * 0.5,
|
|
1990
|
+
(posA.y + posB.y) * 0.5 + orbitRadius * 0.2,
|
|
1991
|
+
(posA.z + posB.z) * 0.5
|
|
1992
|
+
);
|
|
1993
|
+
const curve = new THREE.QuadraticBezierCurve3(posA.clone(), mid, posB.clone());
|
|
1994
|
+
const tubeGeo = new THREE.TubeGeometry(curve, 48, 0.03, 8, false);
|
|
1995
|
+
const tubeMat = new THREE.MeshBasicMaterial({ color: 0x58a6ff, transparent: true, opacity: 0.45 });
|
|
1996
|
+
const tubeMesh = new THREE.Mesh(tubeGeo, tubeMat);
|
|
1997
|
+
scene.add(tubeMesh);
|
|
1998
|
+
edgeObjs.push({ mesh: tubeMesh, curve, edge });
|
|
1999
|
+
|
|
2000
|
+
// Traffic particles — count driven by mempool activity, speed by latency
|
|
2001
|
+
// Get mempool sizes for the two bridges on this edge
|
|
2002
|
+
const bridgeA = allBridges[edge.a];
|
|
2003
|
+
const bridgeB = allBridges[edge.b];
|
|
2004
|
+
const mempoolA = bridgeA.txs ? bridgeA.txs.mempool : 0;
|
|
2005
|
+
const mempoolB = bridgeB.txs ? bridgeB.txs.mempool : 0;
|
|
2006
|
+
const traffic = Math.max(mempoolA, mempoolB);
|
|
2007
|
+
// 1 idle particle (keepalive pulse) + 1 per 5 mempool txs, max 8
|
|
2008
|
+
const pCount = Math.min(8, 1 + Math.floor(traffic / 5));
|
|
2009
|
+
// Speed: slow idle crawl when quiet, faster with more RTT score (inverted — high score = low latency = faster)
|
|
2010
|
+
const rtt = (edge.breakdown && edge.breakdown.responseTime) ? edge.breakdown.responseTime : 0.5;
|
|
2011
|
+
const baseSpeed = traffic > 0 ? 0.0015 : 0.0006; // idle crawl vs active
|
|
2012
|
+
const speed = baseSpeed + rtt * 0.002;
|
|
2013
|
+
for (let j = 0; j < pCount; j++) {
|
|
2014
|
+
const pGeo = new THREE.SphereGeometry(0.07, 8, 8);
|
|
2015
|
+
// Idle particles are dimmer, active ones brighter
|
|
2016
|
+
const brightness = traffic > 0 ? 0x7ec8ff : 0x4a7a9a;
|
|
2017
|
+
const pMat = new THREE.MeshBasicMaterial({ color: brightness, transparent: true, opacity: traffic > 0 ? 0.9 : 0.4 });
|
|
2018
|
+
const pMesh = new THREE.Mesh(pGeo, pMat);
|
|
2019
|
+
scene.add(pMesh);
|
|
2020
|
+
particles.push({ mesh: pMesh, curve, t: j / pCount, speed, direction: j % 2 === 0 ? 1 : -1, isIdle: traffic === 0 });
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// ── Auto-rotate pause ────────────────────────────
|
|
2025
|
+
let lastInteraction = 0;
|
|
2026
|
+
const onInteract = () => { lastInteraction = Date.now(); controls.autoRotate = false; };
|
|
2027
|
+
renderer.domElement.addEventListener('pointerdown', onInteract);
|
|
2028
|
+
renderer.domElement.addEventListener('wheel', onInteract);
|
|
2029
|
+
|
|
2030
|
+
// ── Animation loop ───────────────────────────────
|
|
2031
|
+
const clock = new THREE.Clock();
|
|
2032
|
+
let animId;
|
|
2033
|
+
|
|
2034
|
+
function animate() {
|
|
2035
|
+
animId = requestAnimationFrame(animate);
|
|
2036
|
+
clock.getDelta();
|
|
2037
|
+
const t = clock.getElapsedTime();
|
|
2038
|
+
|
|
2039
|
+
if (!controls.autoRotate && Date.now() - lastInteraction > 10000) controls.autoRotate = true;
|
|
2040
|
+
controls.update();
|
|
2041
|
+
|
|
2042
|
+
// Edge pulsing
|
|
2043
|
+
for (let i = 0; i < edgeObjs.length; i++) {
|
|
2044
|
+
edgeObjs[i].mesh.material.opacity = 0.3 + 0.2 * Math.sin(t * 2 + i * 1.3);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// Glow ring animation on selected bridge
|
|
2048
|
+
for (let i = 0; i < glowRings.length; i++) {
|
|
2049
|
+
const gr = glowRings[i];
|
|
2050
|
+
gr.material.opacity = (i % 2 === 0 ? 0.15 : 0.05) + (i % 2 === 0 ? 0.12 : 0.05) * Math.sin(t * 1.5 + i);
|
|
2051
|
+
gr.rotation.z = t * (i % 2 === 0 ? 0.1 : -0.08);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// Particles along edges — idle ones crawl & fade, active ones zip
|
|
2055
|
+
for (const p of particles) {
|
|
2056
|
+
p.t += p.speed * p.direction;
|
|
2057
|
+
if (p.t > 1) p.t -= 1;
|
|
2058
|
+
if (p.t < 0) p.t += 1;
|
|
2059
|
+
const pos = p.curve.getPointAt(Math.max(0, Math.min(1, p.t)));
|
|
2060
|
+
p.mesh.position.copy(pos);
|
|
2061
|
+
if (p.isIdle) {
|
|
2062
|
+
// Slow breathing pulse — barely visible keepalive
|
|
2063
|
+
p.mesh.material.opacity = 0.15 + 0.2 * Math.sin(p.t * Math.PI);
|
|
2064
|
+
} else {
|
|
2065
|
+
p.mesh.material.opacity = 0.4 + 0.5 * Math.sin(p.t * Math.PI);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// Project labels 3D → 2D
|
|
2070
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
2071
|
+
for (const lb of labelEls) {
|
|
2072
|
+
const pos = lb.nodeMesh.position.clone();
|
|
2073
|
+
pos.y += (lb.el.classList.contains('center') ? 1.6 : 1.2);
|
|
2074
|
+
pos.project(camera);
|
|
2075
|
+
const sx = (pos.x * 0.5 + 0.5) * rect.width;
|
|
2076
|
+
const sy = (-pos.y * 0.5 + 0.5) * rect.height;
|
|
2077
|
+
lb.el.style.left = sx + 'px';
|
|
2078
|
+
lb.el.style.top = sy + 'px';
|
|
2079
|
+
lb.el.style.display = pos.z > 1 ? 'none' : '';
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
renderer.render(scene, camera);
|
|
2083
|
+
}
|
|
2084
|
+
animate();
|
|
2085
|
+
|
|
2086
|
+
// Resize — reads from mesh3d.container so it works after expand/collapse swap
|
|
2087
|
+
const onResize = () => {
|
|
2088
|
+
const c = mesh3d ? mesh3d.container : container;
|
|
2089
|
+
const nw = c.clientWidth, nh = c.clientHeight;
|
|
2090
|
+
if (nw === 0 || nh === 0) return;
|
|
2091
|
+
camera.aspect = nw / nh;
|
|
2092
|
+
camera.updateProjectionMatrix();
|
|
2093
|
+
renderer.setSize(nw, nh);
|
|
2094
|
+
};
|
|
2095
|
+
window.addEventListener('resize', onResize);
|
|
2096
|
+
|
|
2097
|
+
mesh3d = { renderer, scene, camera, controls, animId, nodes, edges: edgeObjs, particles, labels: labelEls, container, onResize, glowRings };
|
|
2098
|
+
return true;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function updateMeshMap3D() {
|
|
2102
|
+
if (!mesh3d) return;
|
|
2103
|
+
const allBridges = [...bridgeData.values()].filter(b => !b._error);
|
|
2104
|
+
// If bridge count changed, full rebuild needed
|
|
2105
|
+
if (allBridges.length !== mesh3d.nodes.length) return;
|
|
2106
|
+
// Update node visuals
|
|
2107
|
+
for (let i = 0; i < mesh3d.nodes.length && i < allBridges.length; i++) {
|
|
2108
|
+
const node = mesh3d.nodes[i];
|
|
2109
|
+
const b = allBridges[i];
|
|
2110
|
+
const isOnline = !b._error;
|
|
2111
|
+
const peerCount = b.peers ? b.peers.connected : 0;
|
|
2112
|
+
if (isOnline && peerCount > 0) {
|
|
2113
|
+
node.mesh.material.color.setHex(0x3fb950);
|
|
2114
|
+
node.mesh.material.emissive.setHex(0x2a5a2a);
|
|
2115
|
+
} else if (isOnline) {
|
|
2116
|
+
node.mesh.material.color.setHex(0xd29922);
|
|
2117
|
+
node.mesh.material.emissive.setHex(0x4a3a10);
|
|
2118
|
+
} else {
|
|
2119
|
+
node.mesh.material.color.setHex(0xf85149);
|
|
2120
|
+
node.mesh.material.emissive.setHex(0x5a2020);
|
|
2121
|
+
}
|
|
2122
|
+
node.bridge = b;
|
|
2123
|
+
// Update label
|
|
2124
|
+
if (mesh3d.labels[i]) {
|
|
2125
|
+
mesh3d.labels[i].el.className = 'mesh3d-label ' + (isOnline ? 'online' : 'offline');
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function destroyMeshMap3D() {
|
|
2131
|
+
if (!mesh3d) return;
|
|
2132
|
+
// Remove overlay if open
|
|
2133
|
+
const overlay = document.getElementById('mesh3dOverlay');
|
|
2134
|
+
if (overlay) overlay.remove();
|
|
2135
|
+
cancelAnimationFrame(mesh3d.animId);
|
|
2136
|
+
window.removeEventListener('resize', mesh3d.onResize);
|
|
2137
|
+
// Dispose geometries and materials
|
|
2138
|
+
mesh3d.scene.traverse(obj => {
|
|
2139
|
+
if (obj.geometry) obj.geometry.dispose();
|
|
2140
|
+
if (obj.material) {
|
|
2141
|
+
if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
|
|
2142
|
+
else obj.material.dispose();
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
mesh3d.renderer.dispose();
|
|
2146
|
+
// Remove canvas
|
|
2147
|
+
if (mesh3d.renderer.domElement && mesh3d.renderer.domElement.parentNode) {
|
|
2148
|
+
mesh3d.renderer.domElement.parentNode.removeChild(mesh3d.renderer.domElement);
|
|
2149
|
+
}
|
|
2150
|
+
// Remove labels
|
|
2151
|
+
for (const lb of mesh3d.labels) {
|
|
2152
|
+
if (lb.el && lb.el.parentNode) lb.el.parentNode.removeChild(lb.el);
|
|
2153
|
+
}
|
|
2154
|
+
mesh3d = null;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
function expandMeshMap() {
|
|
2158
|
+
if (!mesh3d) return;
|
|
2159
|
+
// Create overlay
|
|
2160
|
+
const overlay = document.createElement('div');
|
|
2161
|
+
overlay.className = 'mesh3d-overlay';
|
|
2162
|
+
overlay.id = 'mesh3dOverlay';
|
|
2163
|
+
overlay.innerHTML = '<div class="mesh3d-expanded"><button class="mesh3d-close" onclick="collapseMeshMap()">×</button></div>';
|
|
2164
|
+
document.body.appendChild(overlay);
|
|
2165
|
+
// Click backdrop to close
|
|
2166
|
+
overlay.addEventListener('click', function(e) { if (e.target === overlay) collapseMeshMap(); });
|
|
2167
|
+
// Move canvas + labels into expanded container
|
|
2168
|
+
const expanded = overlay.querySelector('.mesh3d-expanded');
|
|
2169
|
+
expanded.appendChild(mesh3d.renderer.domElement);
|
|
2170
|
+
for (const lb of mesh3d.labels) {
|
|
2171
|
+
if (lb.el) expanded.appendChild(lb.el);
|
|
2172
|
+
}
|
|
2173
|
+
mesh3d.container = expanded;
|
|
2174
|
+
// Resize renderer to new container
|
|
2175
|
+
setTimeout(() => { mesh3d.onResize(); }, 50);
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
function collapseMeshMap() {
|
|
2179
|
+
const overlay = document.getElementById('mesh3dOverlay');
|
|
2180
|
+
if (!overlay) return;
|
|
2181
|
+
// Move canvas + labels back to inline container
|
|
2182
|
+
const inline = document.getElementById('mesh3dCanvas');
|
|
2183
|
+
if (mesh3d && inline) {
|
|
2184
|
+
inline.appendChild(mesh3d.renderer.domElement);
|
|
2185
|
+
for (const lb of mesh3d.labels) {
|
|
2186
|
+
if (lb.el) inline.appendChild(lb.el);
|
|
2187
|
+
}
|
|
2188
|
+
mesh3d.container = inline;
|
|
2189
|
+
setTimeout(() => { mesh3d.onResize(); }, 50);
|
|
2190
|
+
}
|
|
2191
|
+
overlay.remove();
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1689
2194
|
|
|
1690
2195
|
// ── Mini peer map ──────────────────────────────────
|
|
1691
2196
|
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,
|
|
@@ -169,7 +158,7 @@ export class PeerManager extends EventEmitter {
|
|
|
169
158
|
// If cryptographic handshake is available, use it
|
|
170
159
|
if (opts.handshake && msg.nonce && Array.isArray(msg.versions)) {
|
|
171
160
|
const isSeed = opts.seedEndpoints && opts.seedEndpoints.has(msg.endpoint)
|
|
172
|
-
const result = opts.handshake.handleHello(msg,
|
|
161
|
+
const result = opts.handshake.handleHello(msg, null) // cryptographic handshake is the security layer — no whitelist gate
|
|
173
162
|
if (result.error) {
|
|
174
163
|
ws.send(JSON.stringify({ type: 'error', error: result.error }))
|
|
175
164
|
ws.close()
|
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.3",
|
|
4
4
|
"description": "Bridge server — WebSocket peering, header sync, tx relay, CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@bsv/sdk": "^1.10.1",
|
|
14
14
|
"@relay-federation/common": "^0.1.0",
|
|
15
|
+
"@relay-federation/registry": "^0.1.0",
|
|
15
16
|
"level": "^10.0.0",
|
|
16
17
|
"ws": "^8.19.0"
|
|
17
18
|
},
|