@relay-federation/bridge 0.1.2 → 0.3.1
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 +753 -73
- package/dashboard/index.html +2212 -0
- package/lib/actions.js +373 -0
- package/lib/address-scanner.js +169 -0
- package/lib/address-watcher.js +161 -0
- package/lib/bsv-node-client.js +311 -0
- package/lib/bsv-peer.js +791 -0
- package/lib/config.js +32 -4
- package/lib/data-validator.js +6 -0
- package/lib/endpoint-probe.js +45 -0
- package/lib/gossip.js +266 -0
- package/lib/header-relay.js +6 -1
- package/lib/ip-diversity.js +88 -0
- package/lib/output-parser.js +494 -0
- package/lib/peer-manager.js +81 -12
- package/lib/persistent-store.js +708 -0
- package/lib/status-server.js +965 -14
- package/package.json +4 -2
package/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { PeerManager } from './lib/peer-manager.js'
|
|
|
6
6
|
import { HeaderRelay } from './lib/header-relay.js'
|
|
7
7
|
import { TxRelay } from './lib/tx-relay.js'
|
|
8
8
|
import { StatusServer } from './lib/status-server.js'
|
|
9
|
-
import
|
|
9
|
+
// network.js import removed — register/deregister now use local UTXOs + P2P broadcast
|
|
10
10
|
|
|
11
11
|
const command = process.argv[2]
|
|
12
12
|
|
|
@@ -23,9 +23,18 @@ switch (command) {
|
|
|
23
23
|
case 'status':
|
|
24
24
|
await cmdStatus()
|
|
25
25
|
break
|
|
26
|
+
case 'fund':
|
|
27
|
+
await cmdFund()
|
|
28
|
+
break
|
|
26
29
|
case 'deregister':
|
|
27
30
|
await cmdDeregister()
|
|
28
31
|
break
|
|
32
|
+
case 'secret':
|
|
33
|
+
await cmdSecret()
|
|
34
|
+
break
|
|
35
|
+
case 'backfill':
|
|
36
|
+
await cmdBackfill()
|
|
37
|
+
break
|
|
29
38
|
default:
|
|
30
39
|
console.log('relay-bridge — Federated SPV relay mesh bridge\n')
|
|
31
40
|
console.log('Commands:')
|
|
@@ -33,7 +42,10 @@ switch (command) {
|
|
|
33
42
|
console.log(' register Register this bridge on-chain')
|
|
34
43
|
console.log(' start Start the bridge server')
|
|
35
44
|
console.log(' status Show running bridge status')
|
|
45
|
+
console.log(' fund Import a funding transaction (raw hex)')
|
|
36
46
|
console.log(' deregister Deregister this bridge from the mesh')
|
|
47
|
+
console.log(' secret Show your operator secret for dashboard login')
|
|
48
|
+
console.log(' backfill Scan historical blocks for inscriptions/tokens')
|
|
37
49
|
console.log('')
|
|
38
50
|
console.log('Usage: relay-bridge <command> [options]')
|
|
39
51
|
process.exit(command ? 1 : 0)
|
|
@@ -51,15 +63,167 @@ async function cmdInit () {
|
|
|
51
63
|
const config = await initConfig(dir)
|
|
52
64
|
|
|
53
65
|
console.log('Bridge initialized!\n')
|
|
54
|
-
console.log(`
|
|
55
|
-
console.log(`
|
|
66
|
+
console.log(` Name: ${config.name}`)
|
|
67
|
+
console.log(` Config: ${dir}/config.json`)
|
|
68
|
+
console.log(` Endpoint: ${config.endpoint}`)
|
|
69
|
+
console.log(` Pubkey: ${config.pubkeyHex}`)
|
|
70
|
+
console.log(` Address: ${config.address}`)
|
|
71
|
+
console.log(` Secret: ${config.statusSecret}`)
|
|
72
|
+
console.log('')
|
|
73
|
+
console.log(' Save your operator secret! You need it to log into the dashboard.')
|
|
56
74
|
console.log('')
|
|
57
75
|
console.log('Next steps:')
|
|
58
|
-
console.log(
|
|
59
|
-
console.log(' 2.
|
|
76
|
+
console.log(` 1. Fund your bridge: send BSV to ${config.address}`)
|
|
77
|
+
console.log(' 2. Import the funding tx: relay-bridge fund <rawTxHex>')
|
|
60
78
|
console.log(' 3. Run: relay-bridge register')
|
|
61
79
|
}
|
|
62
80
|
|
|
81
|
+
async function cmdSecret () {
|
|
82
|
+
const dir = defaultConfigDir()
|
|
83
|
+
|
|
84
|
+
if (!(await configExists(dir))) {
|
|
85
|
+
console.log('No config found. Run: relay-bridge init')
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const config = await loadConfig(dir)
|
|
90
|
+
|
|
91
|
+
if (!config.statusSecret) {
|
|
92
|
+
console.log('No operator secret found in config.')
|
|
93
|
+
console.log('Add "statusSecret" to your config.json or re-initialize.')
|
|
94
|
+
process.exit(1)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`Operator secret: ${config.statusSecret}`)
|
|
98
|
+
console.log('')
|
|
99
|
+
console.log('Use this to log into the dashboard operator panel.')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function cmdBackfill () {
|
|
103
|
+
const dir = defaultConfigDir()
|
|
104
|
+
|
|
105
|
+
if (!(await configExists(dir))) {
|
|
106
|
+
console.log('No config found. Run: relay-bridge init')
|
|
107
|
+
process.exit(1)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const config = await loadConfig(dir)
|
|
111
|
+
const { PersistentStore } = await import('./lib/persistent-store.js')
|
|
112
|
+
const { parseTx } = await import('./lib/output-parser.js')
|
|
113
|
+
|
|
114
|
+
const dataDir = config.dataDir || join(dir, 'data')
|
|
115
|
+
const store = new PersistentStore(dataDir)
|
|
116
|
+
await store.open()
|
|
117
|
+
|
|
118
|
+
// Parse CLI args: --from=HEIGHT --to=HEIGHT
|
|
119
|
+
const args = {}
|
|
120
|
+
for (const arg of process.argv.slice(3)) {
|
|
121
|
+
const [k, v] = arg.replace(/^--/, '').split('=')
|
|
122
|
+
if (k && v) args[k] = v
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const fromHeight = parseInt(args.from || '800000', 10)
|
|
126
|
+
const toHeight = args.to === 'latest' || !args.to ? null : parseInt(args.to, 10)
|
|
127
|
+
const resumeHeight = await store.getMeta('backfill_height', null)
|
|
128
|
+
const startHeight = resumeHeight ? resumeHeight + 1 : fromHeight
|
|
129
|
+
|
|
130
|
+
console.log(`Backfill: scanning from block ${startHeight}${toHeight ? ' to ' + toHeight : ' to tip'}`)
|
|
131
|
+
if (resumeHeight) console.log(` Resuming from height ${resumeHeight + 1}`)
|
|
132
|
+
|
|
133
|
+
let indexed = 0
|
|
134
|
+
let blocksScanned = 0
|
|
135
|
+
let height = startHeight
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
while (true) {
|
|
139
|
+
// Get block hash for this height
|
|
140
|
+
const hashResp = await fetch(`https://api.whatsonchain.com/v1/bsv/main/block/height/${height}`)
|
|
141
|
+
if (!hashResp.ok) {
|
|
142
|
+
if (hashResp.status === 404) {
|
|
143
|
+
console.log(` Height ${height} not found — reached chain tip`)
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
console.log(` WoC error at height ${height}: ${hashResp.status}, retrying in 5s...`)
|
|
147
|
+
await new Promise(r => setTimeout(r, 5000))
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
const blockInfo = await hashResp.json()
|
|
151
|
+
const blockHash = blockInfo.hash || blockInfo
|
|
152
|
+
const blockTime = blockInfo.time || 0
|
|
153
|
+
|
|
154
|
+
// Get block txid list
|
|
155
|
+
await new Promise(r => setTimeout(r, 350)) // rate limit
|
|
156
|
+
const txListResp = await fetch(`https://api.whatsonchain.com/v1/bsv/main/block/${blockHash}/page/1`)
|
|
157
|
+
if (!txListResp.ok) {
|
|
158
|
+
console.log(` Failed to get tx list for block ${height}, skipping`)
|
|
159
|
+
height++
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
const txids = await txListResp.json()
|
|
163
|
+
|
|
164
|
+
// For each txid, check if already applied, then fetch + parse
|
|
165
|
+
for (const txid of txids) {
|
|
166
|
+
// Idempotency: skip if already processed
|
|
167
|
+
const applied = await store.getMeta(`applied!${txid}`, null)
|
|
168
|
+
if (applied) continue
|
|
169
|
+
|
|
170
|
+
// Check if we already have this tx
|
|
171
|
+
let rawHex = await store.getTx(txid)
|
|
172
|
+
if (!rawHex) {
|
|
173
|
+
await new Promise(r => setTimeout(r, 350)) // rate limit
|
|
174
|
+
const txResp = await fetch(`https://api.whatsonchain.com/v1/bsv/main/tx/${txid}/hex`)
|
|
175
|
+
if (!txResp.ok) continue
|
|
176
|
+
rawHex = await txResp.text()
|
|
177
|
+
await store.putTx(txid, rawHex)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Parse and check for inscriptions/BSV-20
|
|
181
|
+
const parsed = parseTx(rawHex)
|
|
182
|
+
let hasInterest = false
|
|
183
|
+
|
|
184
|
+
for (const output of parsed.outputs) {
|
|
185
|
+
if (output.type === 'ordinal' && output.parsed) {
|
|
186
|
+
await store.putInscription({
|
|
187
|
+
txid,
|
|
188
|
+
vout: output.vout,
|
|
189
|
+
contentType: output.parsed.contentType || null,
|
|
190
|
+
contentSize: output.parsed.content ? output.parsed.content.length / 2 : 0,
|
|
191
|
+
content: output.parsed.content || null,
|
|
192
|
+
isBsv20: output.parsed.isBsv20 || false,
|
|
193
|
+
bsv20: output.parsed.bsv20 || null,
|
|
194
|
+
timestamp: (blockTime || 0) * 1000,
|
|
195
|
+
address: output.hash160 || null
|
|
196
|
+
})
|
|
197
|
+
indexed++
|
|
198
|
+
hasInterest = true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Mark tx as confirmed (trusting WoC block placement)
|
|
203
|
+
await store.updateTxStatus(txid, 'confirmed', { blockHash, height, source: 'backfill' })
|
|
204
|
+
await store.putMeta(`applied!${txid}`, { height, blockHash })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await store.putMeta('backfill_height', height)
|
|
208
|
+
blocksScanned++
|
|
209
|
+
|
|
210
|
+
if (blocksScanned % 100 === 0) {
|
|
211
|
+
console.log(` Block ${height} — ${blocksScanned} scanned, ${indexed} inscriptions indexed`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (toHeight && height >= toHeight) break
|
|
215
|
+
height++
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.log(` Backfill error at height ${height}: ${err.message}`)
|
|
219
|
+
console.log(` Progress saved — resume with: relay-bridge backfill`)
|
|
220
|
+
} finally {
|
|
221
|
+
await store.close()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(`Backfill complete: ${blocksScanned} blocks scanned, ${indexed} inscriptions indexed`)
|
|
225
|
+
}
|
|
226
|
+
|
|
63
227
|
async function cmdRegister () {
|
|
64
228
|
const dir = defaultConfigDir()
|
|
65
229
|
|
|
@@ -75,18 +239,30 @@ async function cmdRegister () {
|
|
|
75
239
|
process.exit(1)
|
|
76
240
|
}
|
|
77
241
|
|
|
242
|
+
const { PersistentStore } = await import('./lib/persistent-store.js')
|
|
243
|
+
const { runRegister } = await import('./lib/actions.js')
|
|
244
|
+
|
|
245
|
+
const dataDir = config.dataDir || join(dir, 'data')
|
|
246
|
+
const store = new PersistentStore(dataDir)
|
|
247
|
+
await store.open()
|
|
248
|
+
|
|
78
249
|
console.log('Registration details:\n')
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const result = await runRegister({
|
|
253
|
+
config,
|
|
254
|
+
store,
|
|
255
|
+
log: (type, msg) => console.log(type === 'done' ? msg : ` ${msg}`)
|
|
256
|
+
})
|
|
257
|
+
console.log('')
|
|
258
|
+
console.log('Your bridge will appear in peer lists on next scan cycle.')
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.log(`Registration failed: ${err.message}`)
|
|
261
|
+
await store.close()
|
|
262
|
+
process.exit(1)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await store.close()
|
|
90
266
|
}
|
|
91
267
|
|
|
92
268
|
async function cmdStart () {
|
|
@@ -98,18 +274,421 @@ async function cmdStart () {
|
|
|
98
274
|
}
|
|
99
275
|
|
|
100
276
|
const config = await loadConfig(dir)
|
|
101
|
-
const
|
|
277
|
+
const rawPeerArg = process.argv[3] // optional: ws://host:port
|
|
278
|
+
const peerArg = (rawPeerArg && !rawPeerArg.startsWith('-')) ? rawPeerArg : null
|
|
279
|
+
|
|
280
|
+
// ── 1. Open persistent store ──────────────────────────────
|
|
281
|
+
const { PersistentStore } = await import('./lib/persistent-store.js')
|
|
282
|
+
const { PrivateKey } = await import('@bsv/sdk')
|
|
283
|
+
|
|
284
|
+
const dataDir = config.dataDir || join(dir, 'data')
|
|
285
|
+
const store = new PersistentStore(dataDir)
|
|
286
|
+
await store.open()
|
|
287
|
+
console.log(`Database opened: ${dataDir}`)
|
|
288
|
+
|
|
289
|
+
// Load persisted balance
|
|
290
|
+
const balance = await store.getBalance()
|
|
291
|
+
if (balance > 0) {
|
|
292
|
+
console.log(` Wallet balance: ${balance} satoshis`)
|
|
293
|
+
}
|
|
102
294
|
|
|
295
|
+
// ── 2. Core components ────────────────────────────────────
|
|
103
296
|
const peerManager = new PeerManager({ maxPeers: config.maxPeers })
|
|
104
297
|
const headerRelay = new HeaderRelay(peerManager)
|
|
105
298
|
const txRelay = new TxRelay(peerManager)
|
|
106
299
|
|
|
107
|
-
//
|
|
108
|
-
|
|
300
|
+
// ── 2b. Phase 2: Security layer ────────────────────────────
|
|
301
|
+
const { PeerScorer } = await import('./lib/peer-scorer.js')
|
|
302
|
+
const { ScoreActions } = await import('./lib/score-actions.js')
|
|
303
|
+
const { DataValidator } = await import('./lib/data-validator.js')
|
|
304
|
+
const { PeerHealth } = await import('./lib/peer-health.js')
|
|
305
|
+
const { AnchorManager } = await import('./lib/anchor-manager.js')
|
|
306
|
+
const { createHandshake } = await import('./lib/handshake.js')
|
|
307
|
+
|
|
308
|
+
const scorer = new PeerScorer()
|
|
309
|
+
const scoreActions = new ScoreActions(scorer, peerManager)
|
|
310
|
+
const dataValidator = new DataValidator(peerManager, scorer)
|
|
311
|
+
const peerHealth = new PeerHealth()
|
|
312
|
+
const anchorManager = new AnchorManager(peerManager, {
|
|
313
|
+
anchors: config.anchorBridges || []
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const handshake = createHandshake({
|
|
317
|
+
wif: config.wif,
|
|
318
|
+
pubkeyHex: config.pubkeyHex,
|
|
319
|
+
endpoint: config.endpoint
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// Wire peer health tracking
|
|
323
|
+
peerManager.on('peer:connect', ({ pubkeyHex }) => {
|
|
324
|
+
peerHealth.recordSeen(pubkeyHex)
|
|
325
|
+
scorer.setStakeAge(pubkeyHex, 7)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
peerManager.on('peer:disconnect', ({ pubkeyHex }) => {
|
|
329
|
+
peerHealth.recordOffline(pubkeyHex)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
// Wire peer:message → health.recordSeen (any message = peer is alive)
|
|
333
|
+
peerManager.on('peer:message', ({ pubkeyHex }) => {
|
|
334
|
+
peerHealth.recordSeen(pubkeyHex)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// Ping infrastructure — 60s interval, measures latency for scoring
|
|
338
|
+
const PING_INTERVAL_MS = 60000
|
|
339
|
+
const pendingPings = new Map() // pubkeyHex → timestamp
|
|
340
|
+
|
|
341
|
+
peerManager.on('peer:message', ({ pubkeyHex, message }) => {
|
|
342
|
+
if (message.type === 'ping') {
|
|
343
|
+
// Respond with pong
|
|
344
|
+
const conn = peerManager.peers.get(pubkeyHex)
|
|
345
|
+
if (conn) conn.send({ type: 'pong', nonce: message.nonce })
|
|
346
|
+
} else if (message.type === 'pong') {
|
|
347
|
+
// Record latency
|
|
348
|
+
const sentAt = pendingPings.get(pubkeyHex)
|
|
349
|
+
if (sentAt) {
|
|
350
|
+
const latency = Date.now() - sentAt
|
|
351
|
+
pendingPings.delete(pubkeyHex)
|
|
352
|
+
if (!peerHealth.isInGracePeriod(pubkeyHex)) {
|
|
353
|
+
scorer.recordPing(pubkeyHex, latency)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const pingTimer = setInterval(() => {
|
|
360
|
+
const nonce = Date.now().toString(36)
|
|
361
|
+
for (const [pubkeyHex, conn] of peerManager.peers) {
|
|
362
|
+
if (conn.connected) {
|
|
363
|
+
// Check for timed-out previous pings
|
|
364
|
+
if (pendingPings.has(pubkeyHex)) {
|
|
365
|
+
if (!peerHealth.isInGracePeriod(pubkeyHex)) {
|
|
366
|
+
scorer.recordPingTimeout(pubkeyHex)
|
|
367
|
+
}
|
|
368
|
+
pendingPings.delete(pubkeyHex)
|
|
369
|
+
}
|
|
370
|
+
pendingPings.set(pubkeyHex, Date.now())
|
|
371
|
+
conn.send({ type: 'ping', nonce })
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}, PING_INTERVAL_MS)
|
|
375
|
+
if (pingTimer.unref) pingTimer.unref()
|
|
376
|
+
|
|
377
|
+
// Health check — every 10 minutes, detect inactive peers
|
|
378
|
+
const HEALTH_CHECK_INTERVAL_MS = 10 * 60 * 1000
|
|
379
|
+
const healthTimer = setInterval(() => {
|
|
380
|
+
const { grace, inactive } = peerHealth.checkAll()
|
|
381
|
+
for (const pk of inactive) {
|
|
382
|
+
console.log(`Peer inactive (7d+): ${pk.slice(0, 16)}...`)
|
|
383
|
+
}
|
|
384
|
+
}, HEALTH_CHECK_INTERVAL_MS)
|
|
385
|
+
if (healthTimer.unref) healthTimer.unref()
|
|
386
|
+
|
|
387
|
+
// Start anchor monitoring
|
|
388
|
+
anchorManager.startMonitoring()
|
|
389
|
+
|
|
390
|
+
console.log(` Security: scoring, validation, health, anchors active`)
|
|
391
|
+
|
|
392
|
+
// ── 3. Address watcher — watch our own address + beacon ──
|
|
393
|
+
const { AddressWatcher } = await import('./lib/address-watcher.js')
|
|
394
|
+
const { addressToHash160 } = await import('./lib/output-parser.js')
|
|
395
|
+
const { BEACON_ADDRESS } = await import('@relay-federation/common/protocol')
|
|
396
|
+
const watcher = new AddressWatcher(txRelay, store)
|
|
397
|
+
watcher.watchPubkey(config.pubkeyHex, 'self')
|
|
398
|
+
const beaconHash160 = addressToHash160(BEACON_ADDRESS)
|
|
399
|
+
watcher.watchHash160(beaconHash160, 'beacon')
|
|
400
|
+
console.log(` Watching own address + beacon (${BEACON_ADDRESS})`)
|
|
401
|
+
|
|
402
|
+
// ── 4. Gossip manager — P2P peer discovery ────────────────
|
|
403
|
+
const { GossipManager } = await import('./lib/gossip.js')
|
|
404
|
+
const privKey = PrivateKey.fromWif(config.wif)
|
|
405
|
+
const gossipManager = new GossipManager(peerManager, {
|
|
406
|
+
privKey,
|
|
407
|
+
pubkeyHex: config.pubkeyHex,
|
|
408
|
+
endpoint: config.endpoint,
|
|
409
|
+
meshId: config.meshId
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
// Add seed peers to gossip directory
|
|
413
|
+
const seedPeers = config.seedPeers || []
|
|
414
|
+
for (const seed of seedPeers) {
|
|
415
|
+
gossipManager.addSeed(seed)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── 4a. Registry — track registered pubkeys for handshake gating ──
|
|
419
|
+
const registeredPubkeys = new Set()
|
|
420
|
+
registeredPubkeys.add(config.pubkeyHex) // always trust self
|
|
421
|
+
const seedEndpoints = new Set()
|
|
422
|
+
for (const seed of seedPeers) {
|
|
423
|
+
if (seed.pubkeyHex) registeredPubkeys.add(seed.pubkeyHex)
|
|
424
|
+
const ep = typeof seed === 'string' ? seed : seed.endpoint
|
|
425
|
+
if (ep) seedEndpoints.add(ep)
|
|
426
|
+
}
|
|
427
|
+
console.log(` Registry: ${registeredPubkeys.size} trusted pubkeys (self + seeds)`)
|
|
428
|
+
|
|
429
|
+
// ── 4b. Beacon address watcher — detect on-chain registrations ──
|
|
430
|
+
const { extractOpReturnData, decodePayload, PROTOCOL_PREFIX } = await import('../registry/lib/cbor.js')
|
|
431
|
+
const { Transaction: BsvTx } = await import('@bsv/sdk')
|
|
432
|
+
|
|
433
|
+
watcher.on('utxo:received', async ({ txid, hash160 }) => {
|
|
434
|
+
if (hash160 !== beaconHash160) return
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
const rawHex = await store.getTx(txid)
|
|
438
|
+
if (!rawHex) return
|
|
439
|
+
|
|
440
|
+
const tx = BsvTx.fromHex(rawHex)
|
|
441
|
+
const opReturnOutput = tx.outputs.find(out =>
|
|
442
|
+
out.satoshis === 0 && out.lockingScript.toHex().startsWith('006a')
|
|
443
|
+
)
|
|
444
|
+
if (!opReturnOutput) return
|
|
445
|
+
|
|
446
|
+
const { prefix, cborBytes } = extractOpReturnData(opReturnOutput.lockingScript)
|
|
447
|
+
if (prefix !== PROTOCOL_PREFIX) return
|
|
448
|
+
|
|
449
|
+
const entry = decodePayload(cborBytes)
|
|
450
|
+
|
|
451
|
+
if (entry.action === 'register') {
|
|
452
|
+
const pubHex = Buffer.from(entry.pubkey).toString('hex')
|
|
453
|
+
if (pubHex === config.pubkeyHex) return // skip self
|
|
454
|
+
|
|
455
|
+
registeredPubkeys.add(pubHex)
|
|
456
|
+
gossipManager.addSeed({
|
|
457
|
+
pubkeyHex: pubHex,
|
|
458
|
+
endpoint: entry.endpoint,
|
|
459
|
+
meshId: entry.mesh_id
|
|
460
|
+
})
|
|
461
|
+
console.log(`Beacon: new registration detected — ${pubHex.slice(0, 16)}... @ ${entry.endpoint}`)
|
|
462
|
+
const stakeAgeDays = Math.max(0, (Date.now() / 1000 - entry.timestamp) / 86400)
|
|
463
|
+
scorer.setStakeAge(pubHex, stakeAgeDays)
|
|
464
|
+
} else if (entry.action === 'deregister') {
|
|
465
|
+
const pubHex = Buffer.from(entry.pubkey).toString('hex')
|
|
466
|
+
registeredPubkeys.delete(pubHex)
|
|
467
|
+
console.log(`Beacon: deregistration detected — ${pubHex.slice(0, 16)}...`)
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
// Skip unparseable beacon txs
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
// ── Outbound handshake helper ──────────────────────────────
|
|
476
|
+
function performOutboundHandshake (conn) {
|
|
477
|
+
const { message: helloMsg, nonce } = handshake.createHello()
|
|
478
|
+
conn.send(helloMsg)
|
|
479
|
+
|
|
480
|
+
const onMessage = (msg) => {
|
|
481
|
+
if (msg.type === 'challenge_response') {
|
|
482
|
+
conn.removeListener('message', onMessage)
|
|
483
|
+
clearTimeout(timeout)
|
|
484
|
+
const result = handshake.handleChallengeResponse(msg, nonce, conn.isSeed ? null : registeredPubkeys)
|
|
485
|
+
if (result.error) {
|
|
486
|
+
console.log(`Handshake failed with ${conn.pubkeyHex.slice(0, 16)}...: ${result.error}`)
|
|
487
|
+
conn.destroy()
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
// Re-key if we didn't know their real pubkey
|
|
491
|
+
if (result.peerPubkey !== conn.pubkeyHex) {
|
|
492
|
+
peerManager.peers.delete(conn.pubkeyHex)
|
|
493
|
+
conn.pubkeyHex = result.peerPubkey
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Tie-break duplicate connections (inbound may have been accepted during handshake)
|
|
497
|
+
const existing = peerManager.peers.get(result.peerPubkey)
|
|
498
|
+
if (existing && existing !== conn) {
|
|
499
|
+
if (config.pubkeyHex > result.peerPubkey) {
|
|
500
|
+
// Higher pubkey drops outbound — keep existing inbound
|
|
501
|
+
console.log(` Duplicate: keeping inbound from ${result.peerPubkey.slice(0, 16)}...`)
|
|
502
|
+
conn._shouldReconnect = false
|
|
503
|
+
conn.destroy()
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
// Lower pubkey keeps outbound — drop existing inbound
|
|
507
|
+
console.log(` Duplicate: keeping outbound to ${result.peerPubkey.slice(0, 16)}...`)
|
|
508
|
+
existing._shouldReconnect = false
|
|
509
|
+
existing.destroy()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
peerManager.peers.set(result.peerPubkey, conn)
|
|
513
|
+
// Learn seed pubkeys so future inbound connections from them pass registry check
|
|
514
|
+
if (conn.isSeed && !registeredPubkeys.has(result.peerPubkey)) {
|
|
515
|
+
registeredPubkeys.add(result.peerPubkey)
|
|
516
|
+
console.log(` Seed pubkey learned: ${result.peerPubkey.slice(0, 16)}...`)
|
|
517
|
+
}
|
|
518
|
+
console.log(` Peer identified: ${result.peerPubkey.slice(0, 16)}... (v${result.selectedVersion})`)
|
|
519
|
+
|
|
520
|
+
// Send verify to complete handshake
|
|
521
|
+
conn.send(result.message)
|
|
522
|
+
// Handshake complete — now safe to announce peer:connect
|
|
523
|
+
peerManager.emit('peer:connect', { pubkeyHex: conn.pubkeyHex, endpoint: conn.endpoint })
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
conn.on('message', onMessage)
|
|
527
|
+
|
|
528
|
+
// Timeout: if no challenge_response within 10s, drop
|
|
529
|
+
const timeout = setTimeout(() => {
|
|
530
|
+
conn.removeListener('message', onMessage)
|
|
531
|
+
if (!conn.connected) return
|
|
532
|
+
console.log(`Handshake timeout: ${conn.pubkeyHex.slice(0, 16)}...`)
|
|
533
|
+
conn.destroy()
|
|
534
|
+
}, 10000)
|
|
535
|
+
if (timeout.unref) timeout.unref()
|
|
536
|
+
|
|
537
|
+
// Clean up on close — prevent stale handler on reconnect
|
|
538
|
+
conn.once('close', () => {
|
|
539
|
+
clearTimeout(timeout)
|
|
540
|
+
conn.removeListener('message', onMessage)
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ── 5. Start server ───────────────────────────────────────
|
|
545
|
+
await peerManager.startServer({ port: config.port, host: '0.0.0.0', pubkeyHex: config.pubkeyHex, endpoint: config.endpoint, handshake, registeredPubkeys, seedEndpoints })
|
|
109
546
|
console.log(`Bridge listening on port ${config.port}`)
|
|
110
547
|
console.log(` Pubkey: ${config.pubkeyHex}`)
|
|
111
548
|
console.log(` Mesh: ${config.meshId}`)
|
|
112
549
|
|
|
550
|
+
// ── 6. Persistence layer — save headers/txs to LevelDB ───
|
|
551
|
+
headerRelay.on('header:new', async (header) => {
|
|
552
|
+
try { await store.putHeader(header) } catch {}
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
headerRelay.on('header:sync', async ({ headers }) => {
|
|
556
|
+
if (headers && headers.length) {
|
|
557
|
+
try { await store.putHeaders(headers) } catch {}
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
txRelay.on('tx:new', async ({ txid, rawHex }) => {
|
|
562
|
+
try { await store.putTx(txid, rawHex) } catch {}
|
|
563
|
+
try { await store.updateTxStatus(txid, 'mempool', { source: 'p2p' }) } catch {}
|
|
564
|
+
// Index inscriptions
|
|
565
|
+
try {
|
|
566
|
+
const { parseTx } = await import('./lib/output-parser.js')
|
|
567
|
+
const parsed = parseTx(rawHex)
|
|
568
|
+
for (const output of parsed.outputs) {
|
|
569
|
+
if (output.type === 'ordinal' && output.parsed) {
|
|
570
|
+
await store.putInscription({
|
|
571
|
+
txid,
|
|
572
|
+
vout: output.vout,
|
|
573
|
+
contentType: output.parsed.contentType || null,
|
|
574
|
+
contentSize: output.parsed.content ? output.parsed.content.length / 2 : 0,
|
|
575
|
+
isBsv20: output.parsed.isBsv20 || false,
|
|
576
|
+
bsv20: output.parsed.bsv20 || null,
|
|
577
|
+
timestamp: Date.now(),
|
|
578
|
+
address: output.hash160 || null
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} catch {}
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
// ── 6b. BSV P2P header sync — connect to BSV nodes ──────
|
|
586
|
+
const { BSVNodeClient } = await import('./lib/bsv-node-client.js')
|
|
587
|
+
const bsvNode = new BSVNodeClient()
|
|
588
|
+
|
|
589
|
+
bsvNode.on('headers', async ({ headers, count }) => {
|
|
590
|
+
// Feed into HeaderRelay for peer propagation
|
|
591
|
+
const added = headerRelay.addHeaders(headers)
|
|
592
|
+
if (added > 0) {
|
|
593
|
+
console.log(`BSV P2P: synced ${added} headers (height: ${headerRelay.bestHeight})`)
|
|
594
|
+
// Persist to LevelDB
|
|
595
|
+
try { await store.putHeaders(headers) } catch {}
|
|
596
|
+
// Announce to federation peers
|
|
597
|
+
headerRelay.announceToAll()
|
|
598
|
+
}
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
bsvNode.on('connected', ({ host }) => {
|
|
602
|
+
console.log(`BSV P2P: connected to ${host}:8333`)
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
bsvNode.on('handshake', ({ userAgent, startHeight }) => {
|
|
606
|
+
console.log(`BSV P2P: handshake complete (${userAgent}, height: ${startHeight})`)
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
bsvNode.on('disconnected', () => {
|
|
610
|
+
console.log('BSV P2P: disconnected, will reconnect...')
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
bsvNode.on('error', (err) => {
|
|
614
|
+
// Don't crash — just log
|
|
615
|
+
if (err.code !== 'ECONNREFUSED' && err.code !== 'ETIMEDOUT') {
|
|
616
|
+
console.log(`BSV P2P: ${err.message}`)
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
// Auto-detect incoming payments: request txs announced via INV
|
|
621
|
+
bsvNode.on('tx:inv', ({ txids }) => {
|
|
622
|
+
for (const txid of txids) {
|
|
623
|
+
if (txRelay.seen.has(txid)) continue
|
|
624
|
+
bsvNode.getTx(txid, 10000).then(({ txid: id, rawHex }) => {
|
|
625
|
+
txRelay.broadcastTx(id, rawHex)
|
|
626
|
+
}).catch(() => {}) // ignore fetch failures
|
|
627
|
+
}
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
// Feed raw txs from BSV P2P into the mesh relay + address watcher
|
|
631
|
+
bsvNode.on('tx', ({ txid, rawHex }) => {
|
|
632
|
+
if (!txRelay.seen.has(txid)) {
|
|
633
|
+
txRelay.broadcastTx(txid, rawHex)
|
|
634
|
+
}
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
// Start the BSV P2P connection
|
|
638
|
+
bsvNode.connect()
|
|
639
|
+
|
|
640
|
+
// ── 7. Connect to peers ───────────────────────────────────
|
|
641
|
+
let gossipStarted = false
|
|
642
|
+
|
|
643
|
+
// Start gossip after first peer connection completes.
|
|
644
|
+
// With crypto handshake, peer:connect fires AFTER handshake verification,
|
|
645
|
+
// so gossip won't race the handshake anymore.
|
|
646
|
+
peerManager.on('peer:connect', () => {
|
|
647
|
+
if (!gossipStarted) {
|
|
648
|
+
gossipStarted = true
|
|
649
|
+
gossipManager.start()
|
|
650
|
+
gossipManager.requestPeersFromAll()
|
|
651
|
+
console.log('Gossip started')
|
|
652
|
+
|
|
653
|
+
// Periodic peer refresh — re-request peer lists every 10 minutes
|
|
654
|
+
// Catches registrations missed during downtime or initial gossip
|
|
655
|
+
const PEER_REFRESH_MS = 10 * 60 * 1000
|
|
656
|
+
const refreshTimer = setInterval(() => {
|
|
657
|
+
gossipManager.requestPeersFromAll()
|
|
658
|
+
}, PEER_REFRESH_MS)
|
|
659
|
+
if (refreshTimer.unref) refreshTimer.unref()
|
|
660
|
+
}
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
// Auto-connect to newly discovered peers (with reachability probe + IP diversity)
|
|
664
|
+
const { probeEndpoint } = await import('./lib/endpoint-probe.js')
|
|
665
|
+
const { checkIpDiversity } = await import('./lib/ip-diversity.js')
|
|
666
|
+
|
|
667
|
+
gossipManager.on('peer:discovered', async ({ pubkeyHex, endpoint }) => {
|
|
668
|
+
if (peerManager.peers.has(pubkeyHex) || pubkeyHex === config.pubkeyHex) return
|
|
669
|
+
|
|
670
|
+
// IP diversity check — prevent all peers clustering in one datacenter
|
|
671
|
+
const connectedEndpoints = [...peerManager.peers.values()]
|
|
672
|
+
.filter(c => c.endpoint).map(c => c.endpoint)
|
|
673
|
+
const diversity = checkIpDiversity(connectedEndpoints, endpoint)
|
|
674
|
+
if (!diversity.allowed) {
|
|
675
|
+
console.log(`IP diversity blocked: ${pubkeyHex.slice(0, 16)}... — ${diversity.reason}`)
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const reachable = await probeEndpoint(endpoint)
|
|
680
|
+
if (!reachable) {
|
|
681
|
+
console.log(`Probe failed: ${pubkeyHex.slice(0, 16)}... ${endpoint} — skipping`)
|
|
682
|
+
return
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
console.log(`Discovered peer via gossip: ${pubkeyHex.slice(0, 16)}... ${endpoint}`)
|
|
686
|
+
const conn = peerManager.connectToPeer({ pubkeyHex, endpoint })
|
|
687
|
+
if (conn) {
|
|
688
|
+
conn.on('open', () => performOutboundHandshake(conn))
|
|
689
|
+
}
|
|
690
|
+
})
|
|
691
|
+
|
|
113
692
|
if (peerArg) {
|
|
114
693
|
// Manual peer connection
|
|
115
694
|
console.log(`Connecting to peer: ${peerArg}`)
|
|
@@ -118,17 +697,24 @@ async function cmdStart () {
|
|
|
118
697
|
endpoint: peerArg
|
|
119
698
|
})
|
|
120
699
|
if (conn) {
|
|
121
|
-
conn.on('open', () =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
700
|
+
conn.on('open', () => performOutboundHandshake(conn))
|
|
701
|
+
}
|
|
702
|
+
} else if (seedPeers.length > 0) {
|
|
703
|
+
// Connect to seed peers (accept both string URLs and {pubkeyHex, endpoint} objects)
|
|
704
|
+
console.log(`Connecting to ${seedPeers.length} seed peer(s)...`)
|
|
705
|
+
for (let i = 0; i < seedPeers.length; i++) {
|
|
706
|
+
const seed = seedPeers[i]
|
|
707
|
+
const endpoint = typeof seed === 'string' ? seed : seed.endpoint
|
|
708
|
+
const pubkey = typeof seed === 'string' ? `seed_${i}` : seed.pubkeyHex
|
|
709
|
+
const conn = peerManager.connectToPeer({ pubkeyHex: pubkey, endpoint })
|
|
710
|
+
if (conn) {
|
|
711
|
+
conn.isSeed = true
|
|
712
|
+
conn.on('open', () => performOutboundHandshake(conn))
|
|
713
|
+
}
|
|
128
714
|
}
|
|
129
715
|
} else if (config.apiKey) {
|
|
130
|
-
//
|
|
131
|
-
console.log('Scanning chain for peers...')
|
|
716
|
+
// Fallback: scan chain for peers (legacy mode)
|
|
717
|
+
console.log('No seed peers configured. Scanning chain for peers...')
|
|
132
718
|
try {
|
|
133
719
|
const { scanRegistry } = await import('../registry/lib/scanner.js')
|
|
134
720
|
const { buildPeerList, excludeSelf } = await import('../registry/lib/discovery.js')
|
|
@@ -151,13 +737,7 @@ async function cmdStart () {
|
|
|
151
737
|
for (const peer of peers) {
|
|
152
738
|
const conn = peerManager.connectToPeer(peer)
|
|
153
739
|
if (conn) {
|
|
154
|
-
conn.on('open', () =>
|
|
155
|
-
conn.send({
|
|
156
|
-
type: 'hello',
|
|
157
|
-
pubkey: config.pubkeyHex,
|
|
158
|
-
endpoint: config.endpoint
|
|
159
|
-
})
|
|
160
|
-
})
|
|
740
|
+
conn.on('open', () => performOutboundHandshake(conn))
|
|
161
741
|
}
|
|
162
742
|
}
|
|
163
743
|
} catch (err) {
|
|
@@ -165,44 +745,105 @@ async function cmdStart () {
|
|
|
165
745
|
console.log('Start with manual peer: relay-bridge start ws://peer:port')
|
|
166
746
|
}
|
|
167
747
|
} else {
|
|
168
|
-
console.log('No peer
|
|
748
|
+
console.log('No seed peers, no manual peer, and no API key configured.')
|
|
169
749
|
console.log('Usage: relay-bridge start ws://peer:port')
|
|
750
|
+
console.log(' or: Add seedPeers to config.json')
|
|
170
751
|
}
|
|
171
752
|
|
|
172
|
-
//
|
|
753
|
+
// ── 8. Status server ──────────────────────────────────────
|
|
173
754
|
const statusPort = config.statusPort || 9333
|
|
174
755
|
const statusServer = new StatusServer({
|
|
175
756
|
port: statusPort,
|
|
176
757
|
peerManager,
|
|
177
758
|
headerRelay,
|
|
178
759
|
txRelay,
|
|
179
|
-
config
|
|
760
|
+
config,
|
|
761
|
+
scorer,
|
|
762
|
+
peerHealth,
|
|
763
|
+
bsvNodeClient: bsvNode,
|
|
764
|
+
store,
|
|
765
|
+
performOutboundHandshake,
|
|
766
|
+
registeredPubkeys,
|
|
767
|
+
gossipManager
|
|
180
768
|
})
|
|
181
769
|
await statusServer.start()
|
|
770
|
+
statusServer.startAppMonitoring()
|
|
182
771
|
console.log(` Status: http://127.0.0.1:${statusPort}/status`)
|
|
183
772
|
|
|
184
|
-
// Log events
|
|
773
|
+
// ── 9. Log events (dual: console + status server ring buffer) ──
|
|
185
774
|
peerManager.on('peer:connect', ({ pubkeyHex }) => {
|
|
186
|
-
|
|
775
|
+
const msg = `Peer connected: ${pubkeyHex.slice(0, 16)}...`
|
|
776
|
+
console.log(msg)
|
|
777
|
+
statusServer.addLog(msg)
|
|
187
778
|
})
|
|
188
779
|
|
|
189
780
|
peerManager.on('peer:disconnect', ({ pubkeyHex }) => {
|
|
190
|
-
|
|
781
|
+
const msg = `Peer disconnected: ${pubkeyHex ? pubkeyHex.slice(0, 16) + '...' : 'unknown'}`
|
|
782
|
+
console.log(msg)
|
|
783
|
+
statusServer.addLog(msg)
|
|
191
784
|
})
|
|
192
785
|
|
|
193
786
|
headerRelay.on('header:sync', ({ pubkeyHex, added, bestHeight }) => {
|
|
194
|
-
|
|
787
|
+
const msg = `Synced ${added} headers from ${pubkeyHex.slice(0, 16)}... (height: ${bestHeight})`
|
|
788
|
+
console.log(msg)
|
|
789
|
+
statusServer.addLog(msg)
|
|
195
790
|
})
|
|
196
791
|
|
|
197
792
|
txRelay.on('tx:new', ({ txid }) => {
|
|
198
|
-
|
|
793
|
+
const msg = `New tx: ${txid}`
|
|
794
|
+
console.log(msg)
|
|
795
|
+
statusServer.addLog(msg)
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
watcher.on('utxo:received', ({ txid, vout, satoshis }) => {
|
|
799
|
+
const msg = `UTXO received: ${txid}:${vout} (${satoshis} sat)`
|
|
800
|
+
console.log(msg)
|
|
801
|
+
statusServer.addLog(msg)
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
watcher.on('utxo:spent', ({ txid, vout, spentByTxid }) => {
|
|
805
|
+
const msg = `UTXO spent: ${txid.slice(0, 16)}...:${vout} by ${spentByTxid.slice(0, 16)}...`
|
|
806
|
+
console.log(msg)
|
|
807
|
+
statusServer.addLog(msg)
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
// Phase 2 events
|
|
811
|
+
scoreActions.on('peer:disconnected', ({ pubkeyHex, score }) => {
|
|
812
|
+
console.log(`Score disconnect: ${pubkeyHex.slice(0, 16)}... (score: ${score.toFixed(2)})`)
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
scoreActions.on('peer:blacklisted', ({ pubkeyHex, score }) => {
|
|
816
|
+
console.log(`Blacklisted: ${pubkeyHex.slice(0, 16)}... (score: ${score.toFixed(2)}, 24h)`)
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
dataValidator.on('validation:fail', ({ pubkeyHex, type, reason }) => {
|
|
820
|
+
console.log(`Bad data from ${pubkeyHex.slice(0, 16)}...: ${type} — ${reason}`)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
anchorManager.on('anchor:disconnect', ({ pubkeyHex }) => {
|
|
824
|
+
console.log(`Anchor disconnected: ${pubkeyHex.slice(0, 16)}...`)
|
|
199
825
|
})
|
|
200
826
|
|
|
201
|
-
|
|
827
|
+
anchorManager.on('anchor:low_score', ({ pubkeyHex, score }) => {
|
|
828
|
+
console.log(`Anchor low score: ${pubkeyHex.slice(0, 16)}... (${score.toFixed(2)})`)
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
peerHealth.on('peer:recovered', ({ pubkeyHex }) => {
|
|
832
|
+
console.log(`Peer recovered: ${pubkeyHex.slice(0, 16)}...`)
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
// ── 10. Graceful shutdown ─────────────────────────────────
|
|
202
836
|
const shutdown = async () => {
|
|
203
837
|
console.log('\nShutting down...')
|
|
838
|
+
clearInterval(pingTimer)
|
|
839
|
+
clearInterval(healthTimer)
|
|
840
|
+
anchorManager.stopMonitoring()
|
|
841
|
+
bsvNode.disconnect()
|
|
842
|
+
gossipManager.stop()
|
|
204
843
|
await statusServer.stop()
|
|
205
844
|
await peerManager.shutdown()
|
|
845
|
+
await store.close()
|
|
846
|
+
console.log('Database closed.')
|
|
206
847
|
process.exit(0)
|
|
207
848
|
}
|
|
208
849
|
|
|
@@ -266,9 +907,34 @@ async function cmdStatus () {
|
|
|
266
907
|
console.log(' Transactions')
|
|
267
908
|
console.log(` Mempool: ${status.txs.mempool}`)
|
|
268
909
|
console.log(` Seen: ${status.txs.seen}`)
|
|
910
|
+
|
|
911
|
+
// BSV Node
|
|
912
|
+
if (status.bsvNode) {
|
|
913
|
+
console.log('')
|
|
914
|
+
console.log(' BSV Node')
|
|
915
|
+
console.log(` Status: ${status.bsvNode.connected ? 'Connected' : 'Disconnected'}`)
|
|
916
|
+
console.log(` Host: ${status.bsvNode.host || '-'}`)
|
|
917
|
+
console.log(` Height: ${status.bsvNode.height || '-'}`)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Wallet
|
|
921
|
+
if (status.wallet) {
|
|
922
|
+
console.log('')
|
|
923
|
+
console.log(' Wallet')
|
|
924
|
+
console.log(` Balance: ${status.wallet.balanceSats !== null ? status.wallet.balanceSats + ' sats' : '-'}`)
|
|
925
|
+
}
|
|
269
926
|
}
|
|
270
927
|
|
|
271
|
-
async function
|
|
928
|
+
async function cmdFund () {
|
|
929
|
+
const rawHex = process.argv[3]
|
|
930
|
+
if (!rawHex) {
|
|
931
|
+
console.log('Usage: relay-bridge fund <rawTxHex>')
|
|
932
|
+
console.log('')
|
|
933
|
+
console.log(' Provide the raw hex of a transaction that pays to this bridge.')
|
|
934
|
+
console.log(' Get the raw hex from your wallet or a block explorer after sending BSV.')
|
|
935
|
+
process.exit(1)
|
|
936
|
+
}
|
|
937
|
+
|
|
272
938
|
const dir = defaultConfigDir()
|
|
273
939
|
|
|
274
940
|
if (!(await configExists(dir))) {
|
|
@@ -277,50 +943,64 @@ async function cmdDeregister () {
|
|
|
277
943
|
}
|
|
278
944
|
|
|
279
945
|
const config = await loadConfig(dir)
|
|
946
|
+
const { PersistentStore } = await import('./lib/persistent-store.js')
|
|
947
|
+
const { runFund } = await import('./lib/actions.js')
|
|
280
948
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
949
|
+
const dataDir = config.dataDir || join(dir, 'data')
|
|
950
|
+
const store = new PersistentStore(dataDir)
|
|
951
|
+
await store.open()
|
|
952
|
+
|
|
953
|
+
try {
|
|
954
|
+
await runFund({
|
|
955
|
+
config,
|
|
956
|
+
store,
|
|
957
|
+
rawHex,
|
|
958
|
+
log: (type, msg) => console.log(` ${msg}`)
|
|
959
|
+
})
|
|
960
|
+
} catch (err) {
|
|
961
|
+
console.log(`Fund failed: ${err.message}`)
|
|
284
962
|
process.exit(1)
|
|
963
|
+
} finally {
|
|
964
|
+
await store.close()
|
|
285
965
|
}
|
|
966
|
+
}
|
|
286
967
|
|
|
287
|
-
|
|
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('')
|
|
968
|
+
async function cmdDeregister () {
|
|
969
|
+
const dir = defaultConfigDir()
|
|
294
970
|
|
|
295
|
-
|
|
296
|
-
|
|
971
|
+
if (!(await configExists(dir))) {
|
|
972
|
+
console.log('No config found. Run: relay-bridge init')
|
|
973
|
+
process.exit(1)
|
|
974
|
+
}
|
|
297
975
|
|
|
298
|
-
|
|
299
|
-
|
|
976
|
+
const config = await loadConfig(dir)
|
|
977
|
+
const reason = process.argv[3] || 'shutdown'
|
|
300
978
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
process.exit(1)
|
|
304
|
-
}
|
|
979
|
+
const { PersistentStore } = await import('./lib/persistent-store.js')
|
|
980
|
+
const { runDeregister } = await import('./lib/actions.js')
|
|
305
981
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
utxos,
|
|
310
|
-
reason
|
|
311
|
-
})
|
|
982
|
+
const dataDir = config.dataDir || join(dir, 'data')
|
|
983
|
+
const store = new PersistentStore(dataDir)
|
|
984
|
+
await store.open()
|
|
312
985
|
|
|
313
|
-
|
|
314
|
-
await broadcastTx(config.spvEndpoint, config.apiKey, txHex)
|
|
986
|
+
console.log('Deregistration details:\n')
|
|
315
987
|
|
|
316
|
-
|
|
317
|
-
|
|
988
|
+
try {
|
|
989
|
+
await runDeregister({
|
|
990
|
+
config,
|
|
991
|
+
store,
|
|
992
|
+
reason,
|
|
993
|
+
log: (type, msg) => console.log(type === 'done' ? msg : ` ${msg}`)
|
|
994
|
+
})
|
|
318
995
|
console.log('')
|
|
319
996
|
console.log('Your bridge will be removed from peer lists on next scan cycle.')
|
|
320
997
|
} catch (err) {
|
|
321
998
|
console.log(`Deregistration failed: ${err.message}`)
|
|
999
|
+
await store.close()
|
|
322
1000
|
process.exit(1)
|
|
323
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
await store.close()
|
|
324
1004
|
}
|
|
325
1005
|
|
|
326
1006
|
function formatUptime (seconds) {
|