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

handshake-org / hsd / 11933012791

20 Nov 2024 12:04PM UTC coverage: 71.254% (+1.2%) from 70.08%
11933012791

push

github

nodech
Merge PR #888 from 'nodech/wallet-pagination'

8055 of 13154 branches covered (61.24%)

Branch coverage included in aggregate %.

820 of 883 new or added lines in 16 files covered. (92.87%)

7 existing lines in 6 files now uncovered.

25716 of 34241 relevant lines covered (75.1%)

34509.23 hits per line

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

69.91
/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));
186✔
40

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

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

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

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

60
  init() {
61
    this.on('request', (req, res) => {
186✔
62
      if (req.method === 'POST' && req.pathname === '/')
599✔
63
        return;
541✔
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) => {
186✔
70
      this.logger.info('Node HTTP server listening on %s (port=%d).',
185✔
71
        address.address, address.port);
72
    });
73

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

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

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

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

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

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

102
    this.error((err, req, res) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
186✔
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) => {
141✔
519
      if (socket.channel('auth'))
126!
520
        throw new Error('Already authed.');
×
521

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

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

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

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

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

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

541
      return null;
126✔
542
    });
543

544
    socket.fire('version', {
141✔
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', () => {
126✔
558
      socket.join('chain');
122✔
559
      return null;
122✔
560
    });
561

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

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

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

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

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

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

586
      return null;
25✔
587
    });
588

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

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

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

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

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

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

608
      return entry.encode();
46✔
609
    });
610

611
    socket.hook('get median time', async (...args) => {
126✔
NEW
612
      const valid = new Validator(args);
×
NEW
613
      const block = valid.uintbhash(0);
×
614

NEW
615
      if (block == null)
×
NEW
616
        throw new Error('Invalid parameter.');
×
617

NEW
618
      const entry = await this.chain.getEntry(block);
×
619

NEW
620
      if (!entry)
×
NEW
621
        return null;
×
622

NEW
623
      const mtp = await this.chain.getMedianTime(entry);
×
624

NEW
625
      return mtp;
×
626
    });
627

628
    socket.hook('get hashes', async (...args) => {
126✔
629
      const valid = new Validator(args);
1✔
630
      const start = valid.i32(0, -1);
1✔
631
      const end = valid.i32(1, -1);
1✔
632

633
      return this.chain.getHashes(start, end);
1✔
634
    });
635

636
    socket.hook('get entries', async (...args) => {
126✔
637
      const valid = new Validator(args);
7✔
638
      const start = valid.i32(0, -1);
7✔
639
      const end = valid.i32(1, -1);
7✔
640

641
      const entries = await this.chain.getEntries(start, end);
7✔
642
      return entries.map(entry => entry.encode());
22✔
643
    });
644

645
    socket.hook('add filter', (...args) => {
126✔
646
      const valid = new Validator(args);
3,002✔
647
      const chunks = valid.array(0);
3,002✔
648

649
      if (!chunks)
3,002!
650
        throw new Error('Invalid parameter.');
×
651

652
      if (!socket.filter)
3,002!
653
        throw new Error('No filter set.');
×
654

655
      const items = new Validator(chunks);
3,002✔
656

657
      for (let i = 0; i < chunks.length; i++) {
3,002✔
658
        const data = items.buf(i);
3,002✔
659

660
        if (!data)
3,002!
661
          throw new Error('Bad data chunk.');
×
662

663
        socket.filter.add(data);
3,002✔
664

665
        if (this.node.spv)
3,002!
666
          this.pool.watch(data);
×
667
      }
668

669
      return null;
3,002✔
670
    });
671

672
    socket.hook('reset filter', () => {
126✔
673
      socket.filter = null;
×
674
      return null;
×
675
    });
676

677
    socket.hook('estimate fee', (...args) => {
126✔
678
      const valid = new Validator(args);
22✔
679
      const blocks = valid.u32(0);
22✔
680

681
      if (!this.fees)
22!
682
        return this.network.feeRate;
×
683

684
      return this.fees.estimateFee(blocks);
22✔
685
    });
686

687
    socket.hook('send', (...args) => {
126✔
688
      const valid = new Validator(args);
25✔
689
      const data = valid.buf(0);
25✔
690

691
      if (!data)
25!
692
        throw new Error('Invalid parameter.');
×
693

694
      const tx = TX.decode(data);
25✔
695

696
      this.node.relay(tx);
25✔
697

698
      return null;
25✔
699
    });
700

701
    socket.hook('send claim', (...args) => {
126✔
702
      const valid = new Validator(args);
×
703
      const data = valid.buf(0);
×
704

705
      if (!data)
×
706
        throw new Error('Invalid parameter.');
×
707

708
      const claim = Claim.decode(data);
×
709

710
      this.node.relayClaim(claim);
×
711

712
      return null;
×
713
    });
714

715
    socket.hook('get name', async (...args) => {
126✔
716
      const valid = new Validator(args);
×
717
      const nameHash = valid.bhash(0);
×
718

719
      if (!nameHash)
×
720
        throw new Error('Invalid parameter.');
×
721

722
      const ns = await this.node.getNameStatus(nameHash);
×
723

724
      return ns.getJSON(this.chain.height + 1, this.network);
×
725
    });
726

727
    socket.hook('rescan', (...args) => {
126✔
728
      const valid = new Validator(args);
×
729
      const start = valid.uintbhash(0);
×
730

731
      if (start == null)
×
732
        throw new Error('Invalid parameter.');
×
733

734
      return this.scan(socket, start);
×
735
    });
736

737
    socket.hook('rescan interactive', (...args) => {
126✔
738
      const valid = new Validator(args);
59✔
739
      const start = valid.uintbhash(0);
59✔
740
      const rawFilter = valid.buf(1);
59✔
741
      const fullLock = valid.bool(2, false);
59✔
742
      let filter = socket.filter;
59✔
743

744
      if (start == null)
59!
745
        throw new Error('Invalid parameter.');
×
746

747
      if (rawFilter)
59✔
748
        filter = BloomFilter.fromRaw(rawFilter);
13✔
749

750
      return this.scanInteractive(socket, start, filter, fullLock);
59✔
751
    });
752
  }
753

754
  /**
755
   * Bind to chain events.
756
   * @private
757
   */
758

759
  initSockets() {
760
    const pool = this.mempool || this.pool;
186✔
761

762
    this.chain.on('connect', (entry, block, view) => {
186✔
763
      const sockets = this.channel('chain');
9,179✔
764

765
      if (!sockets)
9,179✔
766
        return;
6,610✔
767

768
      const raw = entry.encode();
2,569✔
769

770
      this.to('chain', 'chain connect', raw);
2,569✔
771

772
      for (const socket of sockets) {
2,569✔
773
        const txs = this.filterBlock(socket, block);
2,798✔
774
        socket.fire('block connect', raw, txs);
2,798✔
775
      }
776
    });
777

778
    this.chain.on('disconnect', (entry, block, view) => {
186✔
779
      const sockets = this.channel('chain');
202✔
780

781
      if (!sockets)
202✔
782
        return;
73✔
783

784
      const raw = entry.encode();
129✔
785

786
      this.to('chain', 'chain disconnect', raw);
129✔
787
      this.to('chain', 'block disconnect', raw);
129✔
788
    });
789

790
    this.chain.on('reset', (tip) => {
186✔
791
      const sockets = this.channel('chain');
11✔
792

793
      if (!sockets)
11✔
794
        return;
7✔
795

796
      this.to('chain', 'chain reset', tip.encode());
4✔
797
    });
798

799
    pool.on('tx', (tx) => {
186✔
800
      const sockets = this.channel('mempool');
1,968✔
801

802
      if (!sockets)
1,968✔
803
        return;
804✔
804

805
      const raw = tx.encode();
1,164✔
806

807
      for (const socket of sockets) {
1,164✔
808
        if (!this.filterTX(socket, tx))
1,189✔
809
          continue;
1,164✔
810

811
        socket.fire('tx', raw);
25✔
812
      }
813
    });
814

815
    this.chain.on('tree commit', (root, entry, block) => {
186✔
816
      const sockets = this.channel('chain');
1,804✔
817

818
      if (!sockets)
1,804✔
819
        return;
1,304✔
820

821
      this.to('chain', 'tree commit', root, entry, block);
500✔
822
    });
823
  }
824

825
  /**
826
   * Filter block by socket.
827
   * @private
828
   * @param {WebSocket} socket
829
   * @param {Block} block
830
   * @returns {TX[]}
831
   */
832

833
  filterBlock(socket, block) {
834
    if (!socket.filter)
2,798✔
835
      return [];
2,571✔
836

837
    const txs = [];
227✔
838

839
    for (const tx of block.txs) {
227✔
840
      if (this.filterTX(socket, tx))
272✔
841
        txs.push(tx.encode());
213✔
842
    }
843

844
    return txs;
227✔
845
  }
846

847
  /**
848
   * Filter transaction by socket.
849
   * @private
850
   * @param {WebSocket} socket
851
   * @param {TX} tx
852
   * @returns {Boolean}
853
   */
854

855
  filterTX(socket, tx) {
856
    if (!socket.filter)
1,461✔
857
      return false;
1,164✔
858

859
    return tx.testAndMaybeUpdate(socket.filter);
297✔
860
  }
861

862
  /**
863
   * Scan using a socket's filter.
864
   * @private
865
   * @param {WebSocket} socket
866
   * @param {Hash} start
867
   * @returns {Promise}
868
   */
869

870
  async scan(socket, start) {
871
    await this.node.scan(start, socket.filter, (entry, txs) => {
×
872
      const block = entry.encode();
×
873
      const raw = [];
×
874

875
      for (const tx of txs)
×
876
        raw.push(tx.encode());
×
877

878
      return socket.call('block rescan', block, raw);
×
879
    });
880

881
    return null;
×
882
  }
883

884
  /**
885
   * Scan using a socket's filter (interactive).
886
   * @param {WebSocket} socket
887
   * @param {Hash} start
888
   * @param {BloomFilter} filter
889
   * @param {Boolean} [fullLock=false]
890
   * @returns {Promise}
891
   */
892

893
  async scanInteractive(socket, start, filter, fullLock = false) {
×
894
    const iter = async (entry, txs) => {
59✔
895
      const block = entry.encode();
557✔
896
      const raw = [];
557✔
897

898
      for (const tx of txs)
557✔
899
        raw.push(tx.encode());
820✔
900

901
      const action = await socket.call('block rescan interactive', block, raw);
557✔
902
      const valid = new Validator(action);
555✔
903
      const actionType = valid.i32('type');
555✔
904

905
      switch (actionType) {
555!
906
        case scanActions.NEXT:
907
        case scanActions.ABORT:
908
        case scanActions.REPEAT: {
909
          return {
514✔
910
            type: actionType
911
          };
912
        }
913
        case scanActions.REPEAT_SET: {
914
          // NOTE: This is operation is on the heavier side,
915
          // because it sends the whole Filter that can be quite
916
          // big depending on the situation.
917
          // NOTE: In HTTP Context REPEAT_SET wont modify socket.filter
918
          // but instead setup new one for the rescan. Further REPEAT_ADDs will
919
          // modify this filter instead of the socket.filter.
920
          const rawFilter = valid.buf('filter');
35✔
921
          let filter = null;
35✔
922

923
          if (rawFilter != null)
35✔
924
            filter = BloomFilter.fromRaw(rawFilter);
27✔
925

926
          return {
35✔
927
            type: scanActions.REPEAT_SET,
928
            filter: filter
929
          };
930
        }
931
        case scanActions.REPEAT_ADD: {
932
          // NOTE: This operation depending on the filter
933
          // that was provided can be either modifying the
934
          // socket.filter or the filter provided by REPEAT_SET.
935
          const chunks = valid.array('chunks');
6✔
936

937
          if (!chunks)
6!
938
            throw new Error('Invalid parameter.');
×
939

940
          return {
6✔
941
            type: scanActions.REPEAT_ADD,
942
            chunks: chunks
943
          };
944
        }
945

946
        default:
947
          throw new Error('Unknown action.');
×
948
      }
949
    };
950

951
    try {
59✔
952
      await this.node.scanInteractive(start, filter, iter, fullLock);
59✔
953
    } catch (err) {
954
      await socket.call('block rescan interactive abort', err.message);
30✔
955
      throw err;
28✔
956
    }
957
  }
958
}
959

960
class HTTPOptions {
961
  /**
962
   * HTTPOptions
963
   * @alias module:http.HTTPOptions
964
   * @constructor
965
   * @param {Object} options
966
   */
967

968
  constructor(options) {
969
    this.network = Network.primary;
186✔
970
    this.logger = null;
186✔
971
    this.node = null;
186✔
972
    this.apiKey = base58.encode(random.randomBytes(20));
186✔
973
    this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
186✔
974
    this.noAuth = false;
186✔
975
    this.cors = false;
186✔
976

977
    this.prefix = null;
186✔
978
    this.host = '127.0.0.1';
186✔
979
    this.port = 8080;
186✔
980
    this.ssl = false;
186✔
981
    this.keyFile = null;
186✔
982
    this.certFile = null;
186✔
983

984
    this.fromOptions(options);
186✔
985
  }
986

987
  /**
988
   * Inject properties from object.
989
   * @private
990
   * @param {Object} options
991
   * @returns {HTTPOptions}
992
   */
993

994
  fromOptions(options) {
995
    assert(options);
186✔
996
    assert(options.node && typeof options.node === 'object',
186✔
997
      'HTTP Server requires a Node.');
998

999
    this.node = options.node;
186✔
1000
    this.network = options.node.network;
186✔
1001
    this.logger = options.node.logger;
186✔
1002

1003
    this.port = this.network.rpcPort;
186✔
1004

1005
    if (options.logger != null) {
186!
1006
      assert(typeof options.logger === 'object');
186✔
1007
      this.logger = options.logger;
186✔
1008
    }
1009

1010
    if (options.apiKey != null) {
186✔
1011
      assert(typeof options.apiKey === 'string',
37✔
1012
        'API key must be a string.');
1013
      assert(options.apiKey.length <= 255,
37✔
1014
        'API key must be under 256 bytes.');
1015
      this.apiKey = options.apiKey;
37✔
1016
      this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
37✔
1017
    }
1018

1019
    if (options.noAuth != null) {
186!
1020
      assert(typeof options.noAuth === 'boolean');
×
1021
      this.noAuth = options.noAuth;
×
1022
    }
1023

1024
    if (options.cors != null) {
186!
1025
      assert(typeof options.cors === 'boolean');
×
1026
      this.cors = options.cors;
×
1027
    }
1028

1029
    if (options.prefix != null) {
186!
1030
      assert(typeof options.prefix === 'string');
186✔
1031
      this.prefix = options.prefix;
186✔
1032
      this.keyFile = path.join(this.prefix, 'key.pem');
186✔
1033
      this.certFile = path.join(this.prefix, 'cert.pem');
186✔
1034
    }
1035

1036
    if (options.host != null) {
186!
1037
      assert(typeof options.host === 'string');
×
1038
      this.host = options.host;
×
1039
    }
1040

1041
    if (options.port != null) {
186✔
1042
      assert((options.port & 0xffff) === options.port,
70✔
1043
        'Port must be a number.');
1044
      this.port = options.port;
70✔
1045
    }
1046

1047
    if (options.ssl != null) {
186!
1048
      assert(typeof options.ssl === 'boolean');
×
1049
      this.ssl = options.ssl;
×
1050
    }
1051

1052
    if (options.keyFile != null) {
186!
1053
      assert(typeof options.keyFile === 'string');
×
1054
      this.keyFile = options.keyFile;
×
1055
    }
1056

1057
    if (options.certFile != null) {
186!
1058
      assert(typeof options.certFile === 'string');
×
1059
      this.certFile = options.certFile;
×
1060
    }
1061

1062
    // Allow no-auth implicitly
1063
    // if we're listening locally.
1064
    if (!options.apiKey) {
186✔
1065
      if (   this.host === '127.0.0.1'
149!
1066
          || this.host === '::1'
1067
          || this.host === 'localhost')
1068
        this.noAuth = true;
149✔
1069
    }
1070

1071
    return this;
186✔
1072
  }
1073

1074
  /**
1075
   * Instantiate http options from object.
1076
   * @param {Object} options
1077
   * @returns {HTTPOptions}
1078
   */
1079

1080
  static fromOptions(options) {
1081
    return new HTTPOptions().fromOptions(options);
×
1082
  }
1083
}
1084

1085
/*
1086
 * Helpers
1087
 */
1088

1089
function enforce(value, msg) {
1090
  if (!value) {
30!
1091
    const err = new Error(msg);
×
1092
    err.statusCode = 400;
×
1093
    throw err;
×
1094
  }
1095
}
1096

1097
/*
1098
 * Expose
1099
 */
1100

1101
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