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

handshake-org / hsd / 15042451478

15 May 2025 10:16AM UTC coverage: 19.792% (-51.5%) from 71.269%
15042451478

Pull #928

github

web-flow
Merge fa1d30da4 into 5f11d622b
Pull Request #928: Wallet coinselection

1570 of 13236 branches covered (11.86%)

Branch coverage included in aggregate %.

217 of 388 new or added lines in 6 files covered. (55.93%)

17866 existing lines in 127 files now uncovered.

7855 of 34384 relevant lines covered (22.84%)

129.11 hits per line

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

20.06
/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));
28✔
40

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

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

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

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

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

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

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

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

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

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

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

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

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

102
    this.error((err, req, res) => {
28✔
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) => {
28✔
UNCOV
114
      const totalTX = this.mempool ? this.mempool.map.size : 0;
×
UNCOV
115
      const size = this.mempool ? this.mempool.getSize() : 0;
×
UNCOV
116
      const claims = this.mempool ? this.mempool.claims.size : 0;
×
UNCOV
117
      const airdrops = this.mempool ? this.mempool.airdrops.size : 0;
×
UNCOV
118
      const orphans = this.mempool ? this.mempool.orphans.size : 0;
×
UNCOV
119
      const brontide = this.pool.hosts.brontide;
×
120

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

UNCOV
128
      const addr = this.pool.hosts.getLocal();
×
129

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

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

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

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

UNCOV
156
        treeCompaction.compactOnInit = chainOptions.compactTreeOnInit;
×
157

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

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

UNCOV
169
      res.json(200, {
×
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) => {
28✔
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) => {
28✔
UNCOV
242
      const valid = Validator.fromRequest(req);
×
UNCOV
243
      const hash = valid.bhash('hash');
×
UNCOV
244
      const index = valid.u32('index');
×
245

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

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

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

UNCOV
257
      res.json(200, coin.getJSON(this.network));
×
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) => {
28✔
UNCOV
265
      const valid = Validator.fromRequest(req);
×
UNCOV
266
      const addresses = valid.array('addresses');
×
267

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

UNCOV
271
      this.logger.warning('%s %s %s',
×
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

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

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

UNCOV
284
      for (const coin of coins)
×
UNCOV
285
        result.push(coin.getJSON(this.network));
×
286

UNCOV
287
      res.json(200, result);
×
288
    });
289

290
    // TX by hash
291
    this.get('/tx/:hash', async (req, res) => {
28✔
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) => {
28✔
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) => {
28✔
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) => {
28✔
UNCOV
364
      const valid = Validator.fromRequest(req);
×
UNCOV
365
      const hash = valid.uintbhash('block');
×
366

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

UNCOV
370
      const block = await this.chain.getBlock(hash);
×
371

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

UNCOV
377
      const view = await this.chain.getBlockView(block);
×
378

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

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

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

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

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

UNCOV
397
      const entry = await this.chain.getEntry(hash);
×
398

UNCOV
399
      if (!entry) {
×
UNCOV
400
        res.json(404);
×
UNCOV
401
        return;
×
402
      }
403

UNCOV
404
      res.json(200, entry.toJSON());
×
405
    });
406

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

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

UNCOV
414
      for (const hash of hashes)
×
UNCOV
415
        result.push(hash.toString('hex'));
×
416

UNCOV
417
      res.json(200, result);
×
418
    });
419

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

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

UNCOV
427
      const rejects = this.mempool.rejects;
×
UNCOV
428
      res.json(200, {
×
429
        items: rejects.items,
430
        filter: verbose ? rejects.filter.toString('hex') : undefined,
×
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) => {
28✔
UNCOV
441
      enforce(this.mempool, 'No mempool available.');
×
442

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

UNCOV
446
      enforce(hash, 'Must pass hash.');
×
447

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

UNCOV
450
      res.json(200, { invalid });
×
451
    });
452

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

UNCOV
458
      enforce(raw, 'TX is required.');
×
459

UNCOV
460
      const tx = TX.decode(raw);
×
461

UNCOV
462
      await this.node.sendTX(tx);
×
463

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

467
    // Broadcast Claim
468
    this.post('/claim', async (req, res) => {
28✔
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) => {
28✔
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) => {
28✔
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) {
UNCOV
518
    socket.hook('auth', (...args) => {
×
UNCOV
519
      if (socket.channel('auth'))
×
520
        throw new Error('Already authed.');
×
521

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

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

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

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

UNCOV
536
      socket.join('auth');
×
537

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

UNCOV
541
      return null;
×
542
    });
543

UNCOV
544
    socket.fire('version', {
×
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) {
UNCOV
557
    socket.hook('watch chain', () => {
×
UNCOV
558
      socket.join('chain');
×
UNCOV
559
      return null;
×
560
    });
561

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

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

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

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

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

UNCOV
584
      socket.filter = BloomFilter.decode(data);
×
585

UNCOV
586
      return null;
×
587
    });
588

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

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

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

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

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

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

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

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

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

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

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

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

625
      return mtp;
×
626
    });
627

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

UNCOV
633
      return this.chain.getHashes(start, end);
×
634
    });
635

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

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

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

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

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

UNCOV
655
      const items = new Validator(chunks);
×
656

UNCOV
657
      for (let i = 0; i < chunks.length; i++) {
×
UNCOV
658
        const data = items.buf(i);
×
659

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

UNCOV
663
        socket.filter.add(data);
×
664

UNCOV
665
        if (this.node.spv)
×
666
          this.pool.watch(data);
×
667
      }
668

UNCOV
669
      return null;
×
670
    });
671

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

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

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

UNCOV
684
      return this.fees.estimateFee(blocks);
×
685
    });
686

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

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

UNCOV
694
      const tx = TX.decode(data);
×
695

UNCOV
696
      this.node.relay(tx);
×
697

UNCOV
698
      return null;
×
699
    });
700

UNCOV
701
    socket.hook('send claim', (...args) => {
×
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

UNCOV
715
    socket.hook('get name', async (...args) => {
×
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

UNCOV
727
    socket.hook('rescan', (...args) => {
×
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

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

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

UNCOV
747
      if (rawFilter)
×
UNCOV
748
        filter = BloomFilter.fromRaw(rawFilter);
×
749

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

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

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

762
    this.chain.on('connect', (entry, block, view) => {
28✔
UNCOV
763
      const sockets = this.channel('chain');
×
764

UNCOV
765
      if (!sockets)
×
UNCOV
766
        return;
×
767

UNCOV
768
      const raw = entry.encode();
×
769

UNCOV
770
      this.to('chain', 'chain connect', raw);
×
771

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

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

UNCOV
781
      if (!sockets)
×
UNCOV
782
        return;
×
783

UNCOV
784
      const raw = entry.encode();
×
785

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

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

UNCOV
793
      if (!sockets)
×
UNCOV
794
        return;
×
795

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

799
    pool.on('tx', (tx) => {
28✔
UNCOV
800
      const sockets = this.channel('mempool');
×
801

UNCOV
802
      if (!sockets)
×
UNCOV
803
        return;
×
804

UNCOV
805
      const raw = tx.encode();
×
806

UNCOV
807
      for (const socket of sockets) {
×
UNCOV
808
        if (!this.filterTX(socket, tx))
×
UNCOV
809
          continue;
×
810

UNCOV
811
        socket.fire('tx', raw);
×
812
      }
813
    });
814

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

UNCOV
818
      if (!sockets)
×
UNCOV
819
        return;
×
820

UNCOV
821
      this.to('chain', 'tree commit', root, entry, block);
×
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) {
UNCOV
834
    if (!socket.filter)
×
UNCOV
835
      return [];
×
836

UNCOV
837
    const txs = [];
×
838

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

UNCOV
844
    return txs;
×
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) {
UNCOV
856
    if (!socket.filter)
×
UNCOV
857
      return false;
×
858

UNCOV
859
    return tx.testAndMaybeUpdate(socket.filter);
×
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) {
×
UNCOV
894
    const iter = async (entry, txs) => {
×
UNCOV
895
      const block = entry.encode();
×
UNCOV
896
      const raw = [];
×
897

UNCOV
898
      for (const tx of txs)
×
UNCOV
899
        raw.push(tx.encode());
×
900

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

UNCOV
905
      switch (actionType) {
×
906
        case scanActions.NEXT:
907
        case scanActions.ABORT:
908
        case scanActions.REPEAT: {
UNCOV
909
          return {
×
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.
UNCOV
920
          const rawFilter = valid.buf('filter');
×
UNCOV
921
          let filter = null;
×
922

UNCOV
923
          if (rawFilter != null)
×
UNCOV
924
            filter = BloomFilter.fromRaw(rawFilter);
×
925

UNCOV
926
          return {
×
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.
UNCOV
935
          const chunks = valid.array('chunks');
×
936

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

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

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

UNCOV
951
    try {
×
UNCOV
952
      await this.node.scanInteractive(start, filter, iter, fullLock);
×
953
    } catch (err) {
UNCOV
954
      await socket.call('block rescan interactive abort', err.message);
×
UNCOV
955
      throw err;
×
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;
28✔
970
    this.logger = null;
28✔
971
    this.node = null;
28✔
972
    this.apiKey = base58.encode(random.randomBytes(20));
28✔
973
    this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
28✔
974
    this.noAuth = false;
28✔
975
    this.cors = false;
28✔
976

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

984
    this.fromOptions(options);
28✔
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);
28✔
996
    assert(options.node && typeof options.node === 'object',
28✔
997
      'HTTP Server requires a Node.');
998

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

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

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

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

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

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

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

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

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

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

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

1057
    if (options.certFile != null) {
28!
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) {
28✔
1065
      if (   this.host === '127.0.0.1'
19!
1066
          || this.host === '::1'
1067
          || this.host === 'localhost')
1068
        this.noAuth = true;
19✔
1069
    }
1070

1071
    return this;
28✔
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) {
UNCOV
1090
  if (!value) {
×
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

© 2026 Coveralls, Inc