@relay-federation/bridge 0.1.0
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 +336 -0
- package/lib/anchor-manager.js +158 -0
- package/lib/config.js +69 -0
- package/lib/data-validator.js +205 -0
- package/lib/handshake.js +165 -0
- package/lib/header-relay.js +184 -0
- package/lib/peer-connection.js +114 -0
- package/lib/peer-health.js +172 -0
- package/lib/peer-manager.js +214 -0
- package/lib/peer-scorer.js +285 -0
- package/lib/score-actions.js +135 -0
- package/lib/status-server.js +115 -0
- package/lib/tx-relay.js +117 -0
- package/package.json +28 -0
package/cli.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { initConfig, loadConfig, configExists, defaultConfigDir } from './lib/config.js'
|
|
5
|
+
import { PeerManager } from './lib/peer-manager.js'
|
|
6
|
+
import { HeaderRelay } from './lib/header-relay.js'
|
|
7
|
+
import { TxRelay } from './lib/tx-relay.js'
|
|
8
|
+
import { StatusServer } from './lib/status-server.js'
|
|
9
|
+
import { fetchUtxos, broadcastTx } from '@relay-federation/common/network'
|
|
10
|
+
|
|
11
|
+
const command = process.argv[2]
|
|
12
|
+
|
|
13
|
+
switch (command) {
|
|
14
|
+
case 'init':
|
|
15
|
+
await cmdInit()
|
|
16
|
+
break
|
|
17
|
+
case 'register':
|
|
18
|
+
await cmdRegister()
|
|
19
|
+
break
|
|
20
|
+
case 'start':
|
|
21
|
+
await cmdStart()
|
|
22
|
+
break
|
|
23
|
+
case 'status':
|
|
24
|
+
await cmdStatus()
|
|
25
|
+
break
|
|
26
|
+
case 'deregister':
|
|
27
|
+
await cmdDeregister()
|
|
28
|
+
break
|
|
29
|
+
default:
|
|
30
|
+
console.log('relay-bridge — Federated SPV relay mesh bridge\n')
|
|
31
|
+
console.log('Commands:')
|
|
32
|
+
console.log(' init Generate bridge identity and config')
|
|
33
|
+
console.log(' register Register this bridge on-chain')
|
|
34
|
+
console.log(' start Start the bridge server')
|
|
35
|
+
console.log(' status Show running bridge status')
|
|
36
|
+
console.log(' deregister Deregister this bridge from the mesh')
|
|
37
|
+
console.log('')
|
|
38
|
+
console.log('Usage: relay-bridge <command> [options]')
|
|
39
|
+
process.exit(command ? 1 : 0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function cmdInit () {
|
|
43
|
+
const dir = defaultConfigDir()
|
|
44
|
+
|
|
45
|
+
if (await configExists(dir)) {
|
|
46
|
+
console.log(`Config already exists at ${dir}/config.json`)
|
|
47
|
+
console.log('To re-initialize, delete the existing config first.')
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const config = await initConfig(dir)
|
|
52
|
+
|
|
53
|
+
console.log('Bridge initialized!\n')
|
|
54
|
+
console.log(` Config: ${dir}/config.json`)
|
|
55
|
+
console.log(` Pubkey: ${config.pubkeyHex}`)
|
|
56
|
+
console.log('')
|
|
57
|
+
console.log('Next steps:')
|
|
58
|
+
console.log(' 1. Edit config.json — set your WSS endpoint and API key')
|
|
59
|
+
console.log(' 2. Fund your bridge address with BSV')
|
|
60
|
+
console.log(' 3. Run: relay-bridge register')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function cmdRegister () {
|
|
64
|
+
const dir = defaultConfigDir()
|
|
65
|
+
|
|
66
|
+
if (!(await configExists(dir))) {
|
|
67
|
+
console.log('No config found. Run: relay-bridge init')
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const config = await loadConfig(dir)
|
|
72
|
+
|
|
73
|
+
if (config.endpoint === 'wss://your-bridge.example.com:8333') {
|
|
74
|
+
console.log('Error: Update your endpoint in config.json before registering.')
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log('Registration details:\n')
|
|
79
|
+
console.log(` Pubkey: ${config.pubkeyHex}`)
|
|
80
|
+
console.log(` Endpoint: ${config.endpoint}`)
|
|
81
|
+
console.log(` Mesh: ${config.meshId}`)
|
|
82
|
+
console.log(` Capabilities: ${config.capabilities.join(', ')}`)
|
|
83
|
+
console.log(` SPV Endpoint: ${config.spvEndpoint}`)
|
|
84
|
+
console.log('')
|
|
85
|
+
console.log('On-chain registration requires:')
|
|
86
|
+
console.log(' - Funded wallet (stake bond + tx fees)')
|
|
87
|
+
console.log(' - Valid API key for SPV bridge access')
|
|
88
|
+
console.log('')
|
|
89
|
+
console.log('Broadcast support coming in Phase 2.')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function cmdStart () {
|
|
93
|
+
const dir = defaultConfigDir()
|
|
94
|
+
|
|
95
|
+
if (!(await configExists(dir))) {
|
|
96
|
+
console.log('No config found. Run: relay-bridge init')
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const config = await loadConfig(dir)
|
|
101
|
+
const peerArg = process.argv[3] // optional: ws://host:port
|
|
102
|
+
|
|
103
|
+
const peerManager = new PeerManager({ maxPeers: config.maxPeers })
|
|
104
|
+
const headerRelay = new HeaderRelay(peerManager)
|
|
105
|
+
const txRelay = new TxRelay(peerManager)
|
|
106
|
+
|
|
107
|
+
// Start server
|
|
108
|
+
await peerManager.startServer({ port: config.port, host: '0.0.0.0' })
|
|
109
|
+
console.log(`Bridge listening on port ${config.port}`)
|
|
110
|
+
console.log(` Pubkey: ${config.pubkeyHex}`)
|
|
111
|
+
console.log(` Mesh: ${config.meshId}`)
|
|
112
|
+
|
|
113
|
+
if (peerArg) {
|
|
114
|
+
// Manual peer connection
|
|
115
|
+
console.log(`Connecting to peer: ${peerArg}`)
|
|
116
|
+
const conn = peerManager.connectToPeer({
|
|
117
|
+
pubkeyHex: 'manual_peer',
|
|
118
|
+
endpoint: peerArg
|
|
119
|
+
})
|
|
120
|
+
if (conn) {
|
|
121
|
+
conn.on('open', () => {
|
|
122
|
+
conn.send({
|
|
123
|
+
type: 'hello',
|
|
124
|
+
pubkey: config.pubkeyHex,
|
|
125
|
+
endpoint: config.endpoint
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
} else if (config.apiKey) {
|
|
130
|
+
// Scan chain for peers
|
|
131
|
+
console.log('Scanning chain for peers...')
|
|
132
|
+
try {
|
|
133
|
+
const { scanRegistry } = await import('../registry/lib/scanner.js')
|
|
134
|
+
const { buildPeerList, excludeSelf } = await import('../registry/lib/discovery.js')
|
|
135
|
+
const { savePeerCache, loadPeerCache } = await import('../registry/lib/peer-cache.js')
|
|
136
|
+
|
|
137
|
+
const cachePath = join(dir, 'cache', 'peers.json')
|
|
138
|
+
let peers = await loadPeerCache(cachePath)
|
|
139
|
+
|
|
140
|
+
if (!peers) {
|
|
141
|
+
const entries = await scanRegistry({
|
|
142
|
+
spvEndpoint: config.spvEndpoint,
|
|
143
|
+
apiKey: config.apiKey
|
|
144
|
+
})
|
|
145
|
+
peers = buildPeerList(entries)
|
|
146
|
+
peers = excludeSelf(peers, config.pubkeyHex)
|
|
147
|
+
await savePeerCache(peers, cachePath)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`Found ${peers.length} peers`)
|
|
151
|
+
for (const peer of peers) {
|
|
152
|
+
const conn = peerManager.connectToPeer(peer)
|
|
153
|
+
if (conn) {
|
|
154
|
+
conn.on('open', () => {
|
|
155
|
+
conn.send({
|
|
156
|
+
type: 'hello',
|
|
157
|
+
pubkey: config.pubkeyHex,
|
|
158
|
+
endpoint: config.endpoint
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.log(`Peer scan failed: ${err.message}`)
|
|
165
|
+
console.log('Start with manual peer: relay-bridge start ws://peer:port')
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
console.log('No peer specified and no API key configured.')
|
|
169
|
+
console.log('Usage: relay-bridge start ws://peer:port')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Start status server (localhost only)
|
|
173
|
+
const statusPort = config.statusPort || 9333
|
|
174
|
+
const statusServer = new StatusServer({
|
|
175
|
+
port: statusPort,
|
|
176
|
+
peerManager,
|
|
177
|
+
headerRelay,
|
|
178
|
+
txRelay,
|
|
179
|
+
config
|
|
180
|
+
})
|
|
181
|
+
await statusServer.start()
|
|
182
|
+
console.log(` Status: http://127.0.0.1:${statusPort}/status`)
|
|
183
|
+
|
|
184
|
+
// Log events
|
|
185
|
+
peerManager.on('peer:connect', ({ pubkeyHex }) => {
|
|
186
|
+
console.log(`Peer connected: ${pubkeyHex}`)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
peerManager.on('peer:disconnect', ({ pubkeyHex }) => {
|
|
190
|
+
console.log(`Peer disconnected: ${pubkeyHex}`)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
headerRelay.on('header:sync', ({ pubkeyHex, added, bestHeight }) => {
|
|
194
|
+
console.log(`Synced ${added} headers from ${pubkeyHex} (height: ${bestHeight})`)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
txRelay.on('tx:new', ({ txid }) => {
|
|
198
|
+
console.log(`New tx: ${txid}`)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// Graceful shutdown
|
|
202
|
+
const shutdown = async () => {
|
|
203
|
+
console.log('\nShutting down...')
|
|
204
|
+
await statusServer.stop()
|
|
205
|
+
await peerManager.shutdown()
|
|
206
|
+
process.exit(0)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
process.on('SIGINT', shutdown)
|
|
210
|
+
process.on('SIGTERM', shutdown)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function cmdStatus () {
|
|
214
|
+
const dir = defaultConfigDir()
|
|
215
|
+
|
|
216
|
+
if (!(await configExists(dir))) {
|
|
217
|
+
console.log('No config found. Run: relay-bridge init')
|
|
218
|
+
process.exit(1)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const config = await loadConfig(dir)
|
|
222
|
+
const statusPort = config.statusPort || 9333
|
|
223
|
+
|
|
224
|
+
let status
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(`http://127.0.0.1:${statusPort}/status`)
|
|
227
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
228
|
+
status = await res.json()
|
|
229
|
+
} catch {
|
|
230
|
+
console.log('Bridge is not running.')
|
|
231
|
+
console.log(` (expected status server on port ${statusPort})`)
|
|
232
|
+
console.log(' Start it with: relay-bridge start')
|
|
233
|
+
process.exit(1)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log('Bridge Status\n')
|
|
237
|
+
|
|
238
|
+
// Bridge identity
|
|
239
|
+
console.log(' Bridge')
|
|
240
|
+
console.log(` Pubkey: ${status.bridge.pubkeyHex}`)
|
|
241
|
+
console.log(` Endpoint: ${status.bridge.endpoint}`)
|
|
242
|
+
console.log(` Mesh: ${status.bridge.meshId}`)
|
|
243
|
+
console.log(` Uptime: ${formatUptime(status.bridge.uptimeSeconds)}`)
|
|
244
|
+
|
|
245
|
+
// Peers
|
|
246
|
+
console.log('')
|
|
247
|
+
console.log(` Peers (${status.peers.connected}/${status.peers.max})`)
|
|
248
|
+
if (status.peers.list.length === 0) {
|
|
249
|
+
console.log(' (no peers)')
|
|
250
|
+
} else {
|
|
251
|
+
for (const peer of status.peers.list) {
|
|
252
|
+
const tag = peer.connected ? 'connected' : 'disconnected'
|
|
253
|
+
console.log(` ${peer.pubkeyHex.slice(0, 16)}... ${peer.endpoint} (${tag})`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Headers
|
|
258
|
+
console.log('')
|
|
259
|
+
console.log(' Headers')
|
|
260
|
+
console.log(` Best Height: ${status.headers.bestHeight}`)
|
|
261
|
+
console.log(` Best Hash: ${status.headers.bestHash || '(none)'}`)
|
|
262
|
+
console.log(` Stored: ${status.headers.count}`)
|
|
263
|
+
|
|
264
|
+
// Transactions
|
|
265
|
+
console.log('')
|
|
266
|
+
console.log(' Transactions')
|
|
267
|
+
console.log(` Mempool: ${status.txs.mempool}`)
|
|
268
|
+
console.log(` Seen: ${status.txs.seen}`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function cmdDeregister () {
|
|
272
|
+
const dir = defaultConfigDir()
|
|
273
|
+
|
|
274
|
+
if (!(await configExists(dir))) {
|
|
275
|
+
console.log('No config found. Run: relay-bridge init')
|
|
276
|
+
process.exit(1)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const config = await loadConfig(dir)
|
|
280
|
+
|
|
281
|
+
if (!config.apiKey) {
|
|
282
|
+
console.log('Error: API key required for deregistration (broadcasts via SPV bridge).')
|
|
283
|
+
console.log('Set "apiKey" in config.json.')
|
|
284
|
+
process.exit(1)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const reason = process.argv[3] || 'shutdown'
|
|
288
|
+
|
|
289
|
+
console.log('Deregistration details:\n')
|
|
290
|
+
console.log(` Pubkey: ${config.pubkeyHex}`)
|
|
291
|
+
console.log(` Reason: ${reason}`)
|
|
292
|
+
console.log(` SPV: ${config.spvEndpoint}`)
|
|
293
|
+
console.log('')
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const { buildDeregistrationTx } = await import('../registry/lib/registration.js')
|
|
297
|
+
|
|
298
|
+
// Fetch UTXOs from SPV bridge
|
|
299
|
+
const utxos = await fetchUtxos(config.spvEndpoint, config.apiKey, config.pubkeyHex)
|
|
300
|
+
|
|
301
|
+
if (!utxos.length) {
|
|
302
|
+
console.log('Error: No UTXOs found. Wallet needs funding for tx fees.')
|
|
303
|
+
process.exit(1)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Build deregistration tx
|
|
307
|
+
const { txHex, txid } = await buildDeregistrationTx({
|
|
308
|
+
wif: config.wif,
|
|
309
|
+
utxos,
|
|
310
|
+
reason
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// Broadcast via SPV bridge
|
|
314
|
+
await broadcastTx(config.spvEndpoint, config.apiKey, txHex)
|
|
315
|
+
|
|
316
|
+
console.log('Deregistration broadcast successful!')
|
|
317
|
+
console.log(` txid: ${txid}`)
|
|
318
|
+
console.log('')
|
|
319
|
+
console.log('Your bridge will be removed from peer lists on next scan cycle.')
|
|
320
|
+
} catch (err) {
|
|
321
|
+
console.log(`Deregistration failed: ${err.message}`)
|
|
322
|
+
process.exit(1)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function formatUptime (seconds) {
|
|
327
|
+
const days = Math.floor(seconds / 86400)
|
|
328
|
+
const hours = Math.floor((seconds % 86400) / 3600)
|
|
329
|
+
const minutes = Math.floor((seconds % 3600) / 60)
|
|
330
|
+
const secs = seconds % 60
|
|
331
|
+
|
|
332
|
+
if (days > 0) return `${days}d ${hours}h ${minutes}m`
|
|
333
|
+
if (hours > 0) return `${hours}h ${minutes}m ${secs}s`
|
|
334
|
+
if (minutes > 0) return `${minutes}m ${secs}s`
|
|
335
|
+
return `${secs}s`
|
|
336
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MIN_ANCHORS = 2
|
|
4
|
+
const DEFAULT_RECONNECT_INTERVAL_MS = 30000 // 30 seconds
|
|
5
|
+
const DEFAULT_LOW_SCORE_THRESHOLD = 0.3
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AnchorManager — ensures minimum connections to well-known anchor bridges.
|
|
9
|
+
*
|
|
10
|
+
* Anchors are hardcoded in config. This manager:
|
|
11
|
+
* - Ensures at least N anchor connections are maintained
|
|
12
|
+
* - Auto-reconnects to anchors on disconnect
|
|
13
|
+
* - Allows dropping low-scoring anchors (with warning)
|
|
14
|
+
* - Does NOT make anchors immune to scoring
|
|
15
|
+
*
|
|
16
|
+
* Emits:
|
|
17
|
+
* 'anchor:connect' — { pubkeyHex, endpoint }
|
|
18
|
+
* 'anchor:disconnect' — { pubkeyHex, endpoint }
|
|
19
|
+
* 'anchor:low_score' — { pubkeyHex, score } — warning, anchor scoring below threshold
|
|
20
|
+
*/
|
|
21
|
+
export class AnchorManager extends EventEmitter {
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('./peer-manager.js').PeerManager} peerManager
|
|
24
|
+
* @param {object} [opts]
|
|
25
|
+
* @param {Array<{ pubkeyHex: string, endpoint: string }>} [opts.anchors=[]] — anchor bridge list
|
|
26
|
+
* @param {number} [opts.minAnchors=2] — minimum anchor connections to maintain
|
|
27
|
+
* @param {number} [opts.reconnectIntervalMs=30000] — how often to check anchor connections
|
|
28
|
+
* @param {number} [opts.lowScoreThreshold=0.3] — score below which to warn about anchor
|
|
29
|
+
*/
|
|
30
|
+
constructor (peerManager, opts = {}) {
|
|
31
|
+
super()
|
|
32
|
+
this.peerManager = peerManager
|
|
33
|
+
this._anchors = opts.anchors || []
|
|
34
|
+
this._minAnchors = opts.minAnchors ?? DEFAULT_MIN_ANCHORS
|
|
35
|
+
this._reconnectIntervalMs = opts.reconnectIntervalMs ?? DEFAULT_RECONNECT_INTERVAL_MS
|
|
36
|
+
this._lowScoreThreshold = opts.lowScoreThreshold ?? DEFAULT_LOW_SCORE_THRESHOLD
|
|
37
|
+
this._reconnectTimer = null
|
|
38
|
+
this._anchorSet = new Set(this._anchors.map(a => a.pubkeyHex))
|
|
39
|
+
|
|
40
|
+
// Listen for disconnects to track anchor status
|
|
41
|
+
this.peerManager.on('peer:disconnect', ({ pubkeyHex, endpoint }) => {
|
|
42
|
+
if (this._anchorSet.has(pubkeyHex)) {
|
|
43
|
+
this.emit('anchor:disconnect', { pubkeyHex, endpoint })
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
this.peerManager.on('peer:connect', ({ pubkeyHex, endpoint }) => {
|
|
48
|
+
if (this._anchorSet.has(pubkeyHex)) {
|
|
49
|
+
this.emit('anchor:connect', { pubkeyHex, endpoint })
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a pubkey is an anchor bridge.
|
|
56
|
+
* @param {string} pubkeyHex
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
isAnchor (pubkeyHex) {
|
|
60
|
+
return this._anchorSet.has(pubkeyHex)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the list of configured anchor bridges.
|
|
65
|
+
* @returns {Array<{ pubkeyHex: string, endpoint: string }>}
|
|
66
|
+
*/
|
|
67
|
+
getAnchors () {
|
|
68
|
+
return [...this._anchors]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get count of currently connected anchor bridges.
|
|
73
|
+
* @returns {number}
|
|
74
|
+
*/
|
|
75
|
+
connectedAnchorCount () {
|
|
76
|
+
let count = 0
|
|
77
|
+
for (const anchor of this._anchors) {
|
|
78
|
+
const conn = this.peerManager.peers.get(anchor.pubkeyHex)
|
|
79
|
+
if (conn && conn.connected) count++
|
|
80
|
+
}
|
|
81
|
+
return count
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
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
|
+
*
|
|
88
|
+
* @returns {number} Number of new connections initiated
|
|
89
|
+
*/
|
|
90
|
+
ensureConnections () {
|
|
91
|
+
let initiated = 0
|
|
92
|
+
|
|
93
|
+
for (const anchor of this._anchors) {
|
|
94
|
+
const existing = this.peerManager.peers.get(anchor.pubkeyHex)
|
|
95
|
+
if (existing && existing.connected) continue
|
|
96
|
+
|
|
97
|
+
// If we have a disconnected connection object, remove it first
|
|
98
|
+
if (existing && !existing.connected) {
|
|
99
|
+
this.peerManager.peers.delete(anchor.pubkeyHex)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const conn = this.peerManager.connectToPeer(anchor)
|
|
103
|
+
if (conn) initiated++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return initiated
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Start periodic anchor connection monitoring.
|
|
111
|
+
* Checks every reconnectIntervalMs and reconnects to missing anchors.
|
|
112
|
+
*/
|
|
113
|
+
startMonitoring () {
|
|
114
|
+
if (this._reconnectTimer) return
|
|
115
|
+
|
|
116
|
+
this._reconnectTimer = setInterval(() => {
|
|
117
|
+
const connected = this.connectedAnchorCount()
|
|
118
|
+
if (connected < this._minAnchors) {
|
|
119
|
+
this.ensureConnections()
|
|
120
|
+
}
|
|
121
|
+
}, this._reconnectIntervalMs)
|
|
122
|
+
|
|
123
|
+
// Don't prevent process exit
|
|
124
|
+
if (this._reconnectTimer.unref) {
|
|
125
|
+
this._reconnectTimer.unref()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Stop periodic monitoring.
|
|
131
|
+
*/
|
|
132
|
+
stopMonitoring () {
|
|
133
|
+
if (this._reconnectTimer) {
|
|
134
|
+
clearInterval(this._reconnectTimer)
|
|
135
|
+
this._reconnectTimer = null
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check anchor scores and emit warnings for low-scoring anchors.
|
|
141
|
+
*
|
|
142
|
+
* @param {import('./peer-scorer.js').PeerScorer} scorer
|
|
143
|
+
* @returns {Array<{ pubkeyHex: string, score: number }>} Low-scoring anchors
|
|
144
|
+
*/
|
|
145
|
+
checkAnchorScores (scorer) {
|
|
146
|
+
const lowScoring = []
|
|
147
|
+
|
|
148
|
+
for (const anchor of this._anchors) {
|
|
149
|
+
const score = scorer.getScore(anchor.pubkeyHex)
|
|
150
|
+
if (score < this._lowScoreThreshold) {
|
|
151
|
+
lowScoring.push({ pubkeyHex: anchor.pubkeyHex, score })
|
|
152
|
+
this.emit('anchor:low_score', { pubkeyHex: anchor.pubkeyHex, score })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return lowScoring
|
|
157
|
+
}
|
|
158
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, access } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { PrivateKey } from '@bsv/sdk'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_DIR = join(homedir(), '.relay-bridge')
|
|
7
|
+
const CONFIG_FILE = 'config.json'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the default config directory path.
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function defaultConfigDir () {
|
|
14
|
+
return DEFAULT_DIR
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize a new bridge config with a fresh key pair.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} [dir] — Config directory (default: ~/.relay-bridge)
|
|
21
|
+
* @returns {Promise<object>} The generated config
|
|
22
|
+
*/
|
|
23
|
+
export async function initConfig (dir = DEFAULT_DIR) {
|
|
24
|
+
const privKey = PrivateKey.fromRandom()
|
|
25
|
+
|
|
26
|
+
const config = {
|
|
27
|
+
wif: privKey.toWif(),
|
|
28
|
+
pubkeyHex: privKey.toPublicKey().toString(),
|
|
29
|
+
endpoint: 'wss://your-bridge.example.com:8333',
|
|
30
|
+
meshId: 'indelible',
|
|
31
|
+
capabilities: ['tx_relay', 'header_sync', 'broadcast', 'address_history'],
|
|
32
|
+
spvEndpoint: 'https://relay.indelible.one',
|
|
33
|
+
apiKey: '',
|
|
34
|
+
port: 8333,
|
|
35
|
+
statusPort: 9333,
|
|
36
|
+
maxPeers: 20
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await mkdir(dir, { recursive: true })
|
|
40
|
+
await writeFile(join(dir, CONFIG_FILE), JSON.stringify(config, null, 2))
|
|
41
|
+
|
|
42
|
+
return config
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load an existing bridge config.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} [dir] — Config directory (default: ~/.relay-bridge)
|
|
49
|
+
* @returns {Promise<object>}
|
|
50
|
+
*/
|
|
51
|
+
export async function loadConfig (dir = DEFAULT_DIR) {
|
|
52
|
+
const raw = await readFile(join(dir, CONFIG_FILE), 'utf8')
|
|
53
|
+
return JSON.parse(raw)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a config file exists.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} [dir] — Config directory (default: ~/.relay-bridge)
|
|
60
|
+
* @returns {Promise<boolean>}
|
|
61
|
+
*/
|
|
62
|
+
export async function configExists (dir = DEFAULT_DIR) {
|
|
63
|
+
try {
|
|
64
|
+
await access(join(dir, CONFIG_FILE))
|
|
65
|
+
return true
|
|
66
|
+
} catch {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
}
|