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