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