• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

handshake-org / hsd / 7221071989

15 Dec 2023 10:45AM UTC coverage: 68.695% (+0.04%) from 68.654%
7221071989

push

github

nodech
Merge PR #856 from 'nodech/interactive-scan'

7544 of 12828 branches covered (0.0%)

Branch coverage included in aggregate %.

80 of 106 new or added lines in 10 files covered. (75.47%)

3 existing lines in 3 files now uncovered.

24071 of 33194 relevant lines covered (72.52%)

34072.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.82
/lib/node/http.js
1
/*!
2
 * server.js - http server for hsd
3
 * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
4
 * https://github.com/handshake-org/hsd
5
 */
6

7
'use strict';
8

9
const assert = require('bsert');
1✔
10
const path = require('path');
1✔
11
const {Server} = require('bweb');
1✔
12
const Validator = require('bval');
1✔
13
const base58 = require('bcrypto/lib/encoding/base58');
1✔
14
const {BloomFilter} = require('@handshake-org/bfilter');
1✔
15
const sha256 = require('bcrypto/lib/sha256');
1✔
16
const random = require('bcrypto/lib/random');
1✔
17
const {safeEqual} = require('bcrypto/lib/safe');
1✔
18
const util = require('../utils/util');
1✔
19
const TX = require('../primitives/tx');
1✔
20
const Claim = require('../primitives/claim');
1✔
21
const Address = require('../primitives/address');
1✔
22
const Network = require('../protocol/network');
1✔
23
const scanActions = require('../blockchain/common').scanActions;
1✔
24
const pkg = require('../pkg');
1✔
25

26
/**
27
 * HTTP
28
 * @alias module:http.Server
29
 */
30

31
class HTTP extends Server {
32
  /**
33
   * Create an http server.
34
   * @constructor
35
   * @param {Object} options
36
   */
37

38
  constructor(options) {
39
    super(new HTTPOptions(options));
118✔
40

41
    this.network = this.options.network;
118✔
42
    this.logger = this.options.logger.context('node-http');
118✔
43
    this.node = this.options.node;
118✔
44

45
    this.chain = this.node.chain;
118✔
46
    this.mempool = this.node.mempool;
118✔
47
    this.pool = this.node.pool;
118✔
48
    this.fees = this.node.fees;
118✔
49
    this.miner = this.node.miner;
118✔
50
    this.rpc = this.node.rpc;
118✔
51

52
    this.init();
118✔
53
  }
54

55
  /**
56
   * Initialize routes.
57
   * @private
58
   */
59

60
  init() {
61
    this.on('request', (req, res) => {
118✔
62
      if (req.method === 'POST' && req.pathname === '/')
952✔
63
        return;
894✔
64

65
      this.logger.debug('Request for method=%s path=%s (%s).',
58✔
66
        req.method, req.pathname, req.socket.remoteAddress);
67
    });
68

69
    this.on('listening', (address) => {
118✔
70
      this.logger.info('Node HTTP server listening on %s (port=%d).',
117✔
71
        address.address, address.port);
72
    });
73

74
    this.initRouter();
118✔
75
    this.initSockets();
118✔
76
  }
77

78
  /**
79
   * Initialize routes.
80
   * @private
81
   */
82

83
  initRouter() {
84
    if (this.options.cors)
118!
85
      this.use(this.cors());
×
86

87
    if (!this.options.noAuth) {
118✔
88
      this.use(this.basicAuth({
15✔
89
        hash: sha256.digest,
90
        password: this.options.apiKey,
91
        realm: 'node'
92
      }));
93
    }
94

95
    this.use(this.bodyParser({
118✔
96
      type: 'json'
97
    }));
98

99
    this.use(this.jsonRPC());
118✔
100
    this.use(this.router());
118✔
101

102
    this.error((err, req, res) => {
118✔
103
      const code = err.statusCode || 500;
×
104
      res.json(code, {
×
105
        error: {
106
          type: err.type,
107
          code: err.code,
108
          message: err.message
109
        }
110
      });
111
    });
112

113
    this.get('/', async (req, res) => {
118✔
114
      const totalTX = this.mempool ? this.mempool.map.size : 0;
33✔
115
      const size = this.mempool ? this.mempool.getSize() : 0;
33✔
116
      const claims = this.mempool ? this.mempool.claims.size : 0;
33✔
117
      const airdrops = this.mempool ? this.mempool.airdrops.size : 0;
33✔
118
      const orphans = this.mempool ? this.mempool.orphans.size : 0;
33✔
119
      const brontide = this.pool.hosts.brontide;
33✔
120

121
      const pub = {
33✔
122
        listen: this.pool.options.listen,
123
        host: null,
124
        port: null,
125
        brontidePort: null
126
      };
127

128
      const addr = this.pool.hosts.getLocal();
33✔
129

130
      if (addr && pub.listen) {
33✔
131
        pub.host = addr.host;
1✔
132
        pub.port = addr.port;
1✔
133
        pub.brontidePort = brontide.port;
1✔
134
      }
135

136
      const treeInterval = this.network.names.treeInterval;
33✔
137
      const prevHeight = this.chain.height - 1;
33✔
138
      const treeRootHeight = this.chain.height === 0 ? 0 :
33✔
139
        prevHeight - (prevHeight % treeInterval) + 1;
140

141
      const treeCompaction = {
33✔
142
        compacted: false,
143
        compactOnInit: false,
144
        compactInterval: null,
145
        lastCompaction: null,
146
        nextCompaction: null
147
      };
148

149
      if (!this.chain.options.spv) {
33✔
150
        const chainOptions = this.chain.options;
32✔
151
        const {
152
          compactionHeight,
153
          compactFrom
154
        } = await this.chain.getCompactionHeights();
32✔
155

156
        treeCompaction.compactOnInit = chainOptions.compactTreeOnInit;
32✔
157

158
        if (chainOptions.compactTreeOnInit) {
32✔
159
          treeCompaction.compactInterval = chainOptions.compactTreeInitInterval;
1✔
160
          treeCompaction.nextCompaction = compactFrom;
1✔
161
        }
162

163
        if (compactionHeight > 0) {
32!
164
          treeCompaction.compacted = true;
×
165
          treeCompaction.lastCompaction = compactionHeight;
×
166
        }
167
      }
168

169
      res.json(200, {
33✔
170
        version: pkg.version,
171
        network: this.network.type,
172
        chain: {
173
          height: this.chain.height,
174
          tip: this.chain.tip.hash.toString('hex'),
175
          treeRoot: this.chain.tip.treeRoot.toString('hex'),
176
          treeRootHeight: treeRootHeight,
177
          progress: this.chain.getProgress(),
178
          indexers: {
179
            indexTX: this.chain.options.indexTX,
180
            indexAddress: this.chain.options.indexAddress
181
          },
182
          options: {
183
            spv: this.chain.options.spv,
184
            prune: this.chain.options.prune
185
          },
186
          treeCompaction: treeCompaction,
187
          state: {
188
            tx: this.chain.db.state.tx,
189
            coin: this.chain.db.state.coin,
190
            value: this.chain.db.state.value,
191
            burned: this.chain.db.state.burned
192
          }
193
        },
194
        pool: {
195
          host: this.pool.options.host,
196
          port: this.pool.options.port,
197
          brontidePort: this.pool.options.brontidePort,
198
          identitykey: brontide.getKey('base32'),
199
          agent: this.pool.options.agent,
200
          services: this.pool.options.services.toString(2),
201
          outbound: this.pool.peers.outbound,
202
          inbound: this.pool.peers.inbound,
203
          public: pub
204
        },
205
        mempool: {
206
          tx: totalTX,
207
          size: size,
208
          claims: claims,
209
          airdrops: airdrops,
210
          orphans: orphans
211
        },
212
        time: {
213
          uptime: this.node.uptime(),
214
          system: util.now(),
215
          adjusted: this.network.now(),
216
          offset: this.network.time.offset
217
        },
218
        memory: this.logger.memoryUsage()
219
      });
220
    });
221

222
    // UTXO by address
223
    this.get('/coin/address/:address', async (req, res) => {
118✔
224
      const valid = Validator.fromRequest(req);
×
225
      const address = valid.str('address');
×
226

227
      enforce(address, 'Address is required.');
×
228
      enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.');
×
229

230
      const addr = Address.fromString(address, this.network);
×
231
      const coins = await this.node.getCoinsByAddress(addr);
×
232
      const result = [];
×
233

234
      for (const coin of coins)
×
235
        result.push(coin.getJSON(this.network));
×
236

237
      res.json(200, result);
×
238
    });
239

240
    // UTXO by id
241
    this.get('/coin/:hash/:index', async (req, res) => {
118✔
242
      const valid = Validator.fromRequest(req);
1✔
243
      const hash = valid.bhash('hash');
1✔
244
      const index = valid.u32('index');
1✔
245

246
      enforce(hash, 'Hash is required.');
1✔
247
      enforce(index != null, 'Index is required.');
1✔
248
      enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.');
1✔
249

250
      const coin = await this.node.getCoin(hash, index);
1✔
251

252
      if (!coin) {
1!
253
        res.json(404);
×
254
        return;
×
255
      }
256

257
      res.json(200, coin.getJSON(this.network));
1✔
258
    });
259

260
    // Bulk read UTXOs
261
    // TODO(boymanjor): Deprecate this endpoint
262
    // once the equivalent functionality is included
263
    // in the wallet API.
264
    this.post('/coin/address', async (req, res) => {
118✔
265
      const valid = Validator.fromRequest(req);
1✔
266
      const addresses = valid.array('addresses');
1✔
267

268
      enforce(addresses, 'Addresses is required.');
1✔
269
      enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.');
1✔
270

271
      this.logger.warning('%s %s %s',
1✔
272
        'Warning: endpoint being considered for deprecation.',
273
        'Known to cause CPU exhaustion if too many addresses',
274
        'are queried or too many results are found.');
275

276
      const addrs = [];
1✔
277
      for (const address of addresses) {
1✔
278
        addrs.push(Address.fromString(address, this.network));
1✔
279
      }
280

281
      const coins = await this.node.getCoinsByAddress(addrs);
1✔
282
      const result = [];
1✔
283

284
      for (const coin of coins)
1✔
285
        result.push(coin.getJSON(this.network));
5✔
286

287
      res.json(200, result);
1✔
288
    });
289

290
    // TX by hash
291
    this.get('/tx/:hash', async (req, res) => {
118✔
292
      const valid = Validator.fromRequest(req);
×
293
      const hash = valid.bhash('hash');
×
294

295
      enforce(hash, 'Hash is required.');
×
296
      enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.');
×
297

298
      const meta = await this.node.getMeta(hash);
×
299

300
      if (!meta) {
×
301
        res.json(404);
×
302
        return;
×
303
      }
304

305
      const view = await this.node.getMetaView(meta);
×
306

307
      res.json(200, meta.getJSON(this.network, view, this.chain.height));
×
308
    });
309

310
    // TX by address
311
    this.get('/tx/address/:address', async (req, res) => {
118✔
312
      const valid = Validator.fromRequest(req);
×
313
      const address = valid.str('address');
×
314

315
      enforce(address, 'Address is required.');
×
316
      enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.');
×
317

318
      const addr = Address.fromString(address, this.network);
×
319
      const metas = await this.node.getMetaByAddress(addr);
×
320
      const result = [];
×
321

322
      for (const meta of metas) {
×
323
        const view = await this.node.getMetaView(meta);
×
324
        result.push(meta.getJSON(this.network, view, this.chain.height));
×
325
      }
326

327
      res.json(200, result);
×
328
    });
329

330
    // Bulk read TXs
331
    // TODO(boymanjor): Deprecate this endpoint
332
    // once the equivalent functionality is included
333
    // in the wallet API.
334
    this.post('/tx/address', async (req, res) => {
118✔
335
      const valid = Validator.fromRequest(req);
×
336
      const addresses = valid.array('addresses');
×
337

338
      enforce(addresses, 'Addresses is required.');
×
339
      enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.');
×
340

341
      this.logger.warning('%s %s %s',
×
342
        'Warning: endpoint being considered for deprecation.',
343
        'Known to cause CPU exhaustion if too many addresses',
344
        'are queried or too many results are found.');
345

346
      const addrs = [];
×
347
      for (const address of addresses) {
×
348
        addrs.push(Address.fromString(address, this.network));
×
349
      }
350

351
      const metas = await this.node.getMetaByAddress(addrs);
×
352
      const result = [];
×
353

354
      for (const meta of metas) {
×
355
        const view = await this.node.getMetaView(meta);
×
356
        result.push(meta.getJSON(this.network, view, this.chain.height));
×
357
      }
358

359
      res.json(200, result);
×
360
    });
361

362
    // Block by hash/height
363
    this.get('/block/:block', async (req, res) => {
118✔
364
      const valid = Validator.fromRequest(req);
1✔
365
      const hash = valid.uintbhash('block');
1✔
366

367
      enforce(hash != null, 'Hash or height required.');
1✔
368
      enforce(!this.chain.options.spv, 'Cannot get block in SPV mode.');
1✔
369

370
      const block = await this.chain.getBlock(hash);
1✔
371

372
      if (!block) {
1!
373
        res.json(404);
×
374
        return;
×
375
      }
376

377
      const view = await this.chain.getBlockView(block);
1✔
378

379
      if (!view) {
1!
380
        res.json(404);
×
381
        return;
×
382
      }
383

384
      const height = await this.chain.getHeight(hash);
1✔
385
      const depth = this.chain.height - height + 1;
1✔
386

387
      res.json(200, block.getJSON(this.network, view, height, depth));
1✔
388
    });
389

390
    // Block Header by hash/height
391
    this.get('/header/:block', async (req, res) => {
118✔
392
      const valid = Validator.fromRequest(req);
14✔
393
      const hash = valid.uintbhash('block');
14✔
394

395
      enforce(hash != null, 'Hash or height required.');
14✔
396

397
      const entry = await this.chain.getEntry(hash);
14✔
398

399
      if (!entry) {
14✔
400
        res.json(404);
1✔
401
        return;
1✔
402
      }
403

404
      res.json(200, entry.toJSON());
13✔
405
    });
406

407
    // Mempool snapshot
408
    this.get('/mempool', async (req, res) => {
118✔
409
      enforce(this.mempool, 'No mempool available.');
4✔
410

411
      const hashes = this.mempool.getSnapshot();
4✔
412
      const result = [];
4✔
413

414
      for (const hash of hashes)
4✔
415
        result.push(hash.toString('hex'));
2✔
416

417
      res.json(200, result);
4✔
418
    });
419

420
    // Mempool Rejection Filter
421
    this.get('/mempool/invalid', async (req, res) => {
118✔
422
      enforce(this.mempool, 'No mempool available.');
2✔
423

424
      const valid = Validator.fromRequest(req);
2✔
425
      const verbose = valid.bool('verbose', false);
2✔
426

427
      const rejects = this.mempool.rejects;
2✔
428
      res.json(200, {
2✔
429
        items: rejects.items,
430
        filter: verbose ? rejects.filter.toString('hex') : undefined,
2✔
431
        size: rejects.size,
432
        entries: rejects.entries,
433
        n: rejects.n,
434
        limit: rejects.limit,
435
        tweak: rejects.tweak
436
      });
437
    });
438

439
    // Mempool Rejection Test
440
    this.get('/mempool/invalid/:hash', async (req, res) => {
118✔
441
      enforce(this.mempool, 'No mempool available.');
1✔
442

443
      const valid = Validator.fromRequest(req);
1✔
444
      const hash = valid.bhash('hash');
1✔
445

446
      enforce(hash, 'Must pass hash.');
1✔
447

448
      const invalid = this.mempool.rejects.test(hash, 'hex');
1✔
449

450
      res.json(200, { invalid });
1✔
451
    });
452

453
    // Broadcast TX
454
    this.post('/broadcast', async (req, res) => {
118✔
455
      const valid = Validator.fromRequest(req);
1✔
456
      const raw = valid.buf('tx');
1✔
457

458
      enforce(raw, 'TX is required.');
1✔
459

460
      const tx = TX.decode(raw);
1✔
461

462
      await this.node.sendTX(tx);
1✔
463

464
      res.json(200, { success: true });
1✔
465
    });
466

467
    // Broadcast Claim
468
    this.post('/claim', async (req, res) => {
118✔
469
      const valid = Validator.fromRequest(req);
×
470
      const raw = valid.buf('claim');
×
471

472
      enforce(raw, 'Claim is required.');
×
473

474
      const claim = Claim.decode(raw);
×
475

476
      await this.node.sendClaim(claim);
×
477

478
      res.json(200, { success: true });
×
479
    });
480

481
    // Estimate fee
482
    this.get('/fee', async (req, res) => {
118✔
483
      const valid = Validator.fromRequest(req);
×
484
      const blocks = valid.u32('blocks');
×
485

486
      if (!this.fees) {
×
487
        res.json(200, { rate: this.network.feeRate });
×
488
        return;
×
489
      }
490

491
      const fee = this.fees.estimateFee(blocks);
×
492

493
      res.json(200, { rate: fee });
×
494
    });
495

496
    // Reset chain
497
    this.post('/reset', async (req, res) => {
118✔
498
      const valid = Validator.fromRequest(req);
×
499
      const height = valid.u32('height');
×
500

501
      enforce(height != null, 'Height is required.');
×
502
      enforce(height <= this.chain.height,
×
503
        'Height cannot be greater than chain tip.');
504

505
      await this.chain.reset(height);
×
506

507
      res.json(200, { success: true });
×
508
    });
509
  }
510

511
  /**
512
   * Handle new websocket.
513
   * @private
514
   * @param {WebSocket} socket
515
   */
516

517
  handleSocket(socket) {
518
    socket.hook('auth', (...args) => {
47✔
519
      if (socket.channel('auth'))
37!
520
        throw new Error('Already authed.');
×
521

522
      if (!this.options.noAuth) {
37✔
523
        const valid = new Validator(args);
9✔
524
        const key = valid.str(0, '');
9✔
525

526
        if (key.length > 255)
9!
527
          throw new Error('Invalid API key.');
×
528

529
        const data = Buffer.from(key, 'ascii');
9✔
530
        const hash = sha256.digest(data);
9✔
531

532
        if (!safeEqual(hash, this.options.apiHash))
9!
533
          throw new Error('Invalid API key.');
×
534
      }
535

536
      socket.join('auth');
37✔
537

538
      this.logger.info('Successful auth from %s.', socket.host);
37✔
539
      this.handleAuth(socket);
37✔
540

541
      return null;
37✔
542
    });
543

544
    socket.fire('version', {
47✔
545
      version: pkg.version,
546
      network: this.network.type
547
    });
548
  }
549

550
  /**
551
   * Handle new auth'd websocket.
552
   * @private
553
   * @param {WebSocket} socket
554
   */
555

556
  handleAuth(socket) {
557
    socket.hook('watch chain', () => {
37✔
558
      socket.join('chain');
36✔
559
      return null;
36✔
560
    });
561

562
    socket.hook('unwatch chain', () => {
37✔
563
      socket.leave('chain');
×
564
      return null;
×
565
    });
566

567
    socket.hook('watch mempool', () => {
37✔
568
      socket.join('mempool');
35✔
569
      return null;
35✔
570
    });
571

572
    socket.hook('unwatch mempool', () => {
37✔
573
      socket.leave('mempool');
×
574
      return null;
×
575
    });
576

577
    socket.hook('set filter', (...args) => {
37✔
578
      const valid = new Validator(args);
13✔
579
      const data = valid.buf(0);
13✔
580

581
      if (!data)
13!
582
        throw new Error('Invalid parameter.');
×
583

584
      socket.filter = BloomFilter.decode(data);
13✔
585

586
      return null;
13✔
587
    });
588

589
    socket.hook('get tip', () => {
37✔
590
      return this.chain.tip.encode();
×
591
    });
592

593
    socket.hook('get entry', async (...args) => {
37✔
594
      const valid = new Validator(args);
×
595
      const block = valid.uintbhash(0);
×
596

597
      if (block == null)
×
598
        throw new Error('Invalid parameter.');
×
599

600
      const entry = await this.chain.getEntry(block);
×
601

602
      if (!entry)
×
603
        return null;
×
604

605
      if (!await this.chain.isMainChain(entry))
×
606
        return null;
×
607

608
      return entry.encode();
×
609
    });
610

611
    socket.hook('get hashes', async (...args) => {
37✔
612
      const valid = new Validator(args);
×
613
      const start = valid.i32(0, -1);
×
614
      const end = valid.i32(1, -1);
×
615

616
      return this.chain.getHashes(start, end);
×
617
    });
618

619
    socket.hook('add filter', (...args) => {
37✔
620
      const valid = new Validator(args);
×
621
      const chunks = valid.array(0);
×
622

623
      if (!chunks)
×
624
        throw new Error('Invalid parameter.');
×
625

626
      if (!socket.filter)
×
627
        throw new Error('No filter set.');
×
628

629
      const items = new Validator(chunks);
×
630

631
      for (let i = 0; i < chunks.length; i++) {
×
632
        const data = items.buf(i);
×
633

634
        if (!data)
×
635
          throw new Error('Bad data chunk.');
×
636

637
        socket.filter.add(data);
×
638

639
        if (this.node.spv)
×
640
          this.pool.watch(data);
×
641
      }
642

643
      return null;
×
644
    });
645

646
    socket.hook('reset filter', () => {
37✔
647
      socket.filter = null;
×
648
      return null;
×
649
    });
650

651
    socket.hook('estimate fee', (...args) => {
37✔
652
      const valid = new Validator(args);
×
653
      const blocks = valid.u32(0);
×
654

655
      if (!this.fees)
×
656
        return this.network.feeRate;
×
657

658
      return this.fees.estimateFee(blocks);
×
659
    });
660

661
    socket.hook('send', (...args) => {
37✔
662
      const valid = new Validator(args);
×
663
      const data = valid.buf(0);
×
664

665
      if (!data)
×
666
        throw new Error('Invalid parameter.');
×
667

668
      const tx = TX.decode(data);
×
669

670
      this.node.relay(tx);
×
671

672
      return null;
×
673
    });
674

675
    socket.hook('send claim', (...args) => {
37✔
676
      const valid = new Validator(args);
×
677
      const data = valid.buf(0);
×
678

679
      if (!data)
×
680
        throw new Error('Invalid parameter.');
×
681

682
      const claim = Claim.decode(data);
×
683

684
      this.node.relayClaim(claim);
×
685

686
      return null;
×
687
    });
688

689
    socket.hook('get name', async (...args) => {
37✔
690
      const valid = new Validator(args);
×
691
      const nameHash = valid.bhash(0);
×
692

693
      if (!nameHash)
×
694
        throw new Error('Invalid parameter.');
×
695

696
      const ns = await this.node.getNameStatus(nameHash);
×
697

698
      return ns.getJSON(this.chain.height + 1, this.network);
×
699
    });
700

701
    socket.hook('rescan', (...args) => {
37✔
702
      const valid = new Validator(args);
×
703
      const start = valid.uintbhash(0);
×
704

705
      if (start == null)
×
706
        throw new Error('Invalid parameter.');
×
707

708
      return this.scan(socket, start);
×
709
    });
710

711
    socket.hook('rescan interactive', (...args) => {
37✔
712
      const valid = new Validator(args);
38✔
713
      const start = valid.uintbhash(0);
38✔
714
      const rawFilter = valid.buf(1);
38✔
715
      let filter = socket.filter;
38✔
716

717
      if (start == null)
38!
NEW
718
        throw new Error('Invalid parameter.');
×
719

720
      if (rawFilter)
38✔
721
        filter = BloomFilter.fromRaw(rawFilter);
13✔
722

723
      return this.scanInteractive(socket, start, filter);
38✔
724
    });
725
  }
726

727
  /**
728
   * Bind to chain events.
729
   * @private
730
   */
731

732
  initSockets() {
733
    const pool = this.mempool || this.pool;
118✔
734

735
    this.chain.on('connect', (entry, block, view) => {
118✔
736
      const sockets = this.channel('chain');
8,025✔
737

738
      if (!sockets)
8,025✔
739
        return;
6,610✔
740

741
      const raw = entry.encode();
1,415✔
742

743
      this.to('chain', 'chain connect', raw);
1,415✔
744

745
      for (const socket of sockets) {
1,415✔
746
        const txs = this.filterBlock(socket, block);
1,416✔
747
        socket.fire('block connect', raw, txs);
1,416✔
748
      }
749
    });
750

751
    this.chain.on('disconnect', (entry, block, view) => {
118✔
752
      const sockets = this.channel('chain');
36✔
753

754
      if (!sockets)
36✔
755
        return;
33✔
756

757
      const raw = entry.encode();
3✔
758

759
      this.to('chain', 'chain disconnect', raw);
3✔
760
      this.to('chain', 'block disconnect', raw);
3✔
761
    });
762

763
    this.chain.on('reset', (tip) => {
118✔
764
      const sockets = this.channel('chain');
6✔
765

766
      if (!sockets)
6!
767
        return;
6✔
768

769
      this.to('chain', 'chain reset', tip.encode());
×
770
    });
771

772
    pool.on('tx', (tx) => {
118✔
773
      const sockets = this.channel('mempool');
1,047✔
774

775
      if (!sockets)
1,047✔
776
        return;
803✔
777

778
      const raw = tx.encode();
244✔
779

780
      for (const socket of sockets) {
244✔
781
        if (!this.filterTX(socket, tx))
244!
782
          continue;
244✔
783

784
        socket.fire('tx', raw);
×
785
      }
786
    });
787

788
    this.chain.on('tree commit', (root, entry, block) => {
118✔
789
      const sockets = this.channel('chain');
1,582✔
790

791
      if (!sockets)
1,582✔
792
        return;
1,302✔
793

794
      this.to('chain', 'tree commit', root, entry, block);
280✔
795
    });
796
  }
797

798
  /**
799
   * Filter block by socket.
800
   * @private
801
   * @param {WebSocket} socket
802
   * @param {Block} block
803
   * @returns {TX[]}
804
   */
805

806
  filterBlock(socket, block) {
807
    if (!socket.filter)
1,416!
808
      return [];
1,416✔
809

810
    const txs = [];
×
811

812
    for (const tx of block.txs) {
×
813
      if (this.filterTX(socket, tx))
×
814
        txs.push(tx.encode());
×
815
    }
816

817
    return txs;
×
818
  }
819

820
  /**
821
   * Filter transaction by socket.
822
   * @private
823
   * @param {WebSocket} socket
824
   * @param {TX} tx
825
   * @returns {Boolean}
826
   */
827

828
  filterTX(socket, tx) {
829
    if (!socket.filter)
244!
830
      return false;
244✔
831

NEW
832
    return tx.testAndMaybeUpdate(socket.filter);
×
833
  }
834

835
  /**
836
   * Scan using a socket's filter.
837
   * @private
838
   * @param {WebSocket} socket
839
   * @param {Hash} start
840
   * @returns {Promise}
841
   */
842

843
  async scan(socket, start) {
844
    await this.node.scan(start, socket.filter, (entry, txs) => {
×
845
      const block = entry.encode();
×
846
      const raw = [];
×
847

848
      for (const tx of txs)
×
849
        raw.push(tx.encode());
×
850

851
      return socket.call('block rescan', block, raw);
×
852
    });
853

NEW
854
    return null;
×
855
  }
856

857
  /**
858
   * Scan using a socket's filter (interactive).
859
   * @param {WebSocket} socket
860
   * @param {Hash} start
861
   * @param {BloomFilter} filter
862
   * @returns {Promise}
863
   */
864

865
  async scanInteractive(socket, start, filter) {
866
    const iter = async (entry, txs) => {
38✔
867
      const block = entry.encode();
233✔
868
      const raw = [];
233✔
869

870
      for (const tx of txs)
233✔
871
        raw.push(tx.encode());
454✔
872

873
      const action = await socket.call('block rescan interactive', block, raw);
233✔
874
      const valid = new Validator(action);
232✔
875
      const actionType = valid.i32('type');
232✔
876

877
      switch (actionType) {
232!
878
        case scanActions.NEXT:
879
        case scanActions.ABORT:
880
        case scanActions.REPEAT: {
881
          return {
191✔
882
            type: actionType
883
          };
884
        }
885
        case scanActions.REPEAT_SET: {
886
          // NOTE: This is operation is on the heavier side,
887
          // because it sends the whole Filter that can be quite
888
          // big depending on the situation.
889
          // NOTE: In HTTP Context REPEAT_SET wont modify socket.filter
890
          // but instead setup new one for the rescan. Further REPEAT_ADDs will
891
          // modify this filter instead of the socket.filter.
892
          const rawFilter = valid.buf('filter');
35✔
893
          let filter = null;
35✔
894

895
          if (rawFilter != null)
35✔
896
            filter = BloomFilter.fromRaw(rawFilter);
27✔
897

898
          return {
35✔
899
            type: scanActions.REPEAT_SET,
900
            filter: filter
901
          };
902
        }
903
        case scanActions.REPEAT_ADD: {
904
          // NOTE: This operation depending on the filter
905
          // that was provided can be either modifying the
906
          // socket.filter or the filter provided by REPEAT_SET.
907
          const chunks = valid.array('chunks');
6✔
908

909
          if (!chunks)
6!
NEW
910
            throw new Error('Invalid parameter.');
×
911

912
          return {
6✔
913
            type: scanActions.REPEAT_ADD,
914
            chunks: chunks
915
          };
916
        }
917

918
        default:
NEW
919
          throw new Error('Unknown action.');
×
920
      }
921
    };
922

923
    try {
38✔
924
      await this.node.scanInteractive(start, filter, iter);
38✔
925
    } catch (err) {
926
      return socket.call('block rescan interactive abort', err.message);
28✔
927
    }
928

929
    return null;
10✔
930
  }
931
}
932

933
class HTTPOptions {
934
  /**
935
   * HTTPOptions
936
   * @alias module:http.HTTPOptions
937
   * @constructor
938
   * @param {Object} options
939
   */
940

941
  constructor(options) {
942
    this.network = Network.primary;
118✔
943
    this.logger = null;
118✔
944
    this.node = null;
118✔
945
    this.apiKey = base58.encode(random.randomBytes(20));
118✔
946
    this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
118✔
947
    this.noAuth = false;
118✔
948
    this.cors = false;
118✔
949

950
    this.prefix = null;
118✔
951
    this.host = '127.0.0.1';
118✔
952
    this.port = 8080;
118✔
953
    this.ssl = false;
118✔
954
    this.keyFile = null;
118✔
955
    this.certFile = null;
118✔
956

957
    this.fromOptions(options);
118✔
958
  }
959

960
  /**
961
   * Inject properties from object.
962
   * @private
963
   * @param {Object} options
964
   * @returns {HTTPOptions}
965
   */
966

967
  fromOptions(options) {
968
    assert(options);
118✔
969
    assert(options.node && typeof options.node === 'object',
118✔
970
      'HTTP Server requires a Node.');
971

972
    this.node = options.node;
118✔
973
    this.network = options.node.network;
118✔
974
    this.logger = options.node.logger;
118✔
975

976
    this.port = this.network.rpcPort;
118✔
977

978
    if (options.logger != null) {
118!
979
      assert(typeof options.logger === 'object');
118✔
980
      this.logger = options.logger;
118✔
981
    }
982

983
    if (options.apiKey != null) {
118✔
984
      assert(typeof options.apiKey === 'string',
15✔
985
        'API key must be a string.');
986
      assert(options.apiKey.length <= 255,
15✔
987
        'API key must be under 256 bytes.');
988
      this.apiKey = options.apiKey;
15✔
989
      this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
15✔
990
    }
991

992
    if (options.noAuth != null) {
118!
993
      assert(typeof options.noAuth === 'boolean');
×
994
      this.noAuth = options.noAuth;
×
995
    }
996

997
    if (options.cors != null) {
118!
998
      assert(typeof options.cors === 'boolean');
×
999
      this.cors = options.cors;
×
1000
    }
1001

1002
    if (options.prefix != null) {
118!
1003
      assert(typeof options.prefix === 'string');
118✔
1004
      this.prefix = options.prefix;
118✔
1005
      this.keyFile = path.join(this.prefix, 'key.pem');
118✔
1006
      this.certFile = path.join(this.prefix, 'cert.pem');
118✔
1007
    }
1008

1009
    if (options.host != null) {
118!
1010
      assert(typeof options.host === 'string');
×
1011
      this.host = options.host;
×
1012
    }
1013

1014
    if (options.port != null) {
118✔
1015
      assert((options.port & 0xffff) === options.port,
24✔
1016
        'Port must be a number.');
1017
      this.port = options.port;
24✔
1018
    }
1019

1020
    if (options.ssl != null) {
118!
1021
      assert(typeof options.ssl === 'boolean');
×
1022
      this.ssl = options.ssl;
×
1023
    }
1024

1025
    if (options.keyFile != null) {
118!
1026
      assert(typeof options.keyFile === 'string');
×
1027
      this.keyFile = options.keyFile;
×
1028
    }
1029

1030
    if (options.certFile != null) {
118!
1031
      assert(typeof options.certFile === 'string');
×
1032
      this.certFile = options.certFile;
×
1033
    }
1034

1035
    // Allow no-auth implicitly
1036
    // if we're listening locally.
1037
    if (!options.apiKey) {
118✔
1038
      if (   this.host === '127.0.0.1'
103!
1039
          || this.host === '::1'
1040
          || this.host === 'localhost')
1041
        this.noAuth = true;
103✔
1042
    }
1043

1044
    return this;
118✔
1045
  }
1046

1047
  /**
1048
   * Instantiate http options from object.
1049
   * @param {Object} options
1050
   * @returns {HTTPOptions}
1051
   */
1052

1053
  static fromOptions(options) {
1054
    return new HTTPOptions().fromOptions(options);
×
1055
  }
1056
}
1057

1058
/*
1059
 * Helpers
1060
 */
1061

1062
function enforce(value, msg) {
1063
  if (!value) {
30!
1064
    const err = new Error(msg);
×
1065
    err.statusCode = 400;
×
1066
    throw err;
×
1067
  }
1068
}
1069

1070
/*
1071
 * Expose
1072
 */
1073

1074
module.exports = HTTP;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc