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

handshake-org / hsd / 17097146937

20 Aug 2025 11:33AM UTC coverage: 71.655% (+0.02%) from 71.638%
17097146937

Pull #943

github

web-flow
Merge a30427f89 into 8f42b5ddd
Pull Request #943: wallet-http: TransactionOptions minor refactor

8249 of 13364 branches covered (61.73%)

Branch coverage included in aggregate %.

43 of 51 new or added lines in 1 file covered. (84.31%)

3 existing lines in 3 files now uncovered.

26181 of 34686 relevant lines covered (75.48%)

34101.28 hits per line

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

65.98
/lib/wallet/http.js
1
/*!
2
 * http.js - wallet http server for hsd
3
 * Copyright (c) 2017-2019, Christopher Jeffrey (MIT License).
4
 * Copyright (c) 2019, Mark Tyneway (MIT License).
5
 * https://github.com/handshake-org/hsd
6
 */
7

8
'use strict';
9

10
const assert = require('bsert');
1✔
11
const path = require('path');
1✔
12
const {Server} = require('bweb');
1✔
13
const Validator = require('bval');
1✔
14
const base58 = require('bcrypto/lib/encoding/base58');
1✔
15
const MTX = require('../primitives/mtx');
1✔
16
const Outpoint = require('../primitives/outpoint');
1✔
17
const sha256 = require('bcrypto/lib/sha256');
1✔
18
const rules = require('../covenants/rules');
1✔
19
const random = require('bcrypto/lib/random');
1✔
20
const Covenant = require('../primitives/covenant');
1✔
21
const {safeEqual} = require('bcrypto/lib/safe');
1✔
22
const Network = require('../protocol/network');
1✔
23
const Address = require('../primitives/address');
1✔
24
const KeyRing = require('../primitives/keyring');
1✔
25
const Mnemonic = require('../hd/mnemonic');
1✔
26
const HDPrivateKey = require('../hd/private');
1✔
27
const HDPublicKey = require('../hd/public');
1✔
28
const {Resource} = require('../dns/resource');
1✔
29
const common = require('./common');
1✔
30

31
/** @typedef {import('../types').NetworkType} NetworkType */
32
/** @typedef {ReturnType<Validator['fromRequest']>} RequestValidator */
33
/** @typedef {import('../covenants/namestate')} NameState */
34
/** @typedef {import('./walletdb')} WalletDB */
35

36
/**
37
 * HTTP
38
 * @alias module:wallet.HTTP
39
 */
40

41
class HTTP extends Server {
42
  /**
43
   * Create an http server.
44
   * @constructor
45
   * @param {Object} options
46
   */
47

48
  constructor(options) {
49
    super(new HTTPOptions(options));
104✔
50

51
    this.network = this.options.network;
104✔
52
    this.logger = this.options.logger.context('wallet-http');
104✔
53
    /** @type {WalletDB} */
54
    this.wdb = this.options.node.wdb;
104✔
55
    this.rpc = this.options.node.rpc;
104✔
56
    this.maxTXs = this.wdb.options.maxHistoryTXs;
104✔
57

58
    this.init();
104✔
59
  }
60

61
  /**
62
   * Initialize http server.
63
   * @private
64
   */
65

66
  init() {
67
    this.on('request', (req, res) => {
104✔
68
      if (req.method === 'POST' && req.pathname === '/')
3,749✔
69
        return;
479✔
70

71
      this.logger.debug('Request for method=%s path=%s (%s).',
3,270✔
72
        req.method, req.pathname, req.socket.remoteAddress);
73
    });
74

75
    this.on('listening', (address) => {
104✔
76
      this.logger.info('Wallet HTTP server listening on %s (port=%d).',
105✔
77
        address.address, address.port);
78
    });
79

80
    this.initRouter();
104✔
81
    this.initSockets();
104✔
82
  }
83

84
  /**
85
   * Initialize routes.
86
   * @private
87
   */
88

89
  initRouter() {
90
    if (this.options.cors)
104!
91
      this.use(this.cors());
×
92

93
    if (!this.options.noAuth) {
104✔
94
      this.use(this.basicAuth({
26✔
95
        hash: sha256.digest,
96
        password: this.options.apiKey,
97
        realm: 'wallet'
98
      }));
99
    }
100

101
    this.use(this.bodyParser({
104✔
102
      type: 'json'
103
    }));
104

105
    this.use(async (req, res) => {
104✔
106
      if (!this.options.walletAuth) {
3,749!
107
        req.admin = true;
3,749✔
108
        return;
3,749✔
109
      }
110

111
      const valid = Validator.fromRequest(req);
×
112
      const token = valid.buf('token');
×
113

114
      if (token && safeEqual(token, this.options.adminToken)) {
×
115
        req.admin = true;
×
116
        return;
×
117
      }
118

119
      if (req.method === 'POST' && req.path.length === 0) {
×
120
        res.json(403);
×
121
        return;
×
122
      }
123
    });
124

125
    this.use(this.jsonRPC());
104✔
126
    this.use(this.router());
104✔
127

128
    this.error((err, req, res) => {
104✔
129
      const code = err.statusCode || 500;
18✔
130
      res.json(code, {
18✔
131
        error: {
132
          type: err.type,
133
          code: err.code,
134
          message: err.message
135
        }
136
      });
137
    });
138

139
    this.hook(async (req, res) => {
104✔
140
      if (req.path.length < 2)
3,270!
141
        return;
×
142

143
      if (req.path[0] !== 'wallet')
3,270!
144
        return;
×
145

146
      if (req.method === 'PUT' && req.path.length === 2)
3,270✔
147
        return;
38✔
148

149
      const valid = Validator.fromRequest(req);
3,232✔
150
      const id = valid.str('id');
3,232✔
151
      const token = valid.buf('token');
3,232✔
152

153
      if (!id) {
3,232!
154
        res.json(403);
×
155
        return;
×
156
      }
157

158
      if (req.admin || !this.options.walletAuth) {
3,232!
159
        const wallet = await this.wdb.get(id);
3,232✔
160

161
        if (!wallet) {
3,232!
162
          res.json(404);
×
163
          return;
×
164
        }
165

166
        req.wallet = wallet;
3,232✔
167

168
        return;
3,232✔
169
      }
170

171
      if (!token) {
×
172
        res.json(403);
×
173
        return;
×
174
      }
175

176
      let wallet;
177
      try {
×
178
        wallet = await this.wdb.auth(id, token);
×
179
      } catch (err) {
180
        this.logger.info('Auth failure for %s: %s.', id, err.message);
×
181
        res.json(403);
×
182
        return;
×
183
      }
184

185
      if (!wallet) {
×
186
        res.json(404);
×
187
        return;
×
188
      }
189

190
      req.wallet = wallet;
×
191

192
      this.logger.info('Successful auth for %s.', id);
×
193
    });
194

195
    // Rescan
196
    this.post('/rescan', async (req, res) => {
104✔
197
      if (!req.admin) {
×
198
        res.json(403);
×
199
        return;
×
200
      }
201

202
      const valid = Validator.fromRequest(req);
×
203
      const height = valid.u32('height');
×
204

205
      res.json(200, { success: true });
×
206

207
      await this.wdb.rescan(height);
×
208
    });
209

210
    // Recalculate balances
211
    this.post('/recalculate-balances', async (req, res) => {
104✔
212
      if (!req.admin) {
×
213
        res.json(403);
×
214
        return;
×
215
      }
216

217
      await this.wdb.recalculateBalances();
×
218

219
      res.json(200, { success: true });
×
220
    });
221

222
    // Deep Clean
223
    this.post('/deepclean', async (req, res) => {
104✔
224
      if (!req.admin) {
×
225
        res.json(403);
×
226
        return;
×
227
      }
228

229
      const valid = Validator.fromRequest(req);
×
230
      const disclaimer = valid.bool('I_HAVE_BACKED_UP_MY_WALLET', false);
×
231

232
      enforce(
×
233
        disclaimer,
234
        'Deep Clean requires I_HAVE_BACKED_UP_MY_WALLET=true'
235
      );
236

237
      await this.wdb.deepClean();
×
238

239
      res.json(200, { success: true });
×
240
    });
241

242
    // Resend
243
    this.post('/resend', async (req, res) => {
104✔
244
      if (!req.admin) {
×
245
        res.json(403);
×
246
        return;
×
247
      }
248

249
      await this.wdb.resend();
×
250

251
      res.json(200, { success: true });
×
252
    });
253

254
    // Backup WalletDB
255
    this.post('/backup', async (req, res) => {
104✔
256
      if (!req.admin) {
×
257
        res.json(403);
×
258
        return;
×
259
      }
260

261
      const valid = Validator.fromRequest(req);
×
262
      const path = valid.str('path');
×
263

264
      enforce(path, 'Path is required.');
×
265

266
      await this.wdb.backup(path);
×
267

268
      res.json(200, { success: true });
×
269
    });
270

271
    // List wallets
272
    this.get('/wallet', async (req, res) => {
104✔
273
      if (!req.admin) {
×
274
        res.json(403);
×
275
        return;
×
276
      }
277

278
      const wallets = await this.wdb.getWallets();
×
279
      res.json(200, wallets);
×
280
    });
281

282
    // Get wallet
283
    this.get('/wallet/:id', async (req, res) => {
104✔
284
      const balance = await req.wallet.getBalance();
2✔
285
      res.json(200, req.wallet.getJSON(false, balance));
2✔
286
    });
287

288
    // Get wallet master key
289
    this.get('/wallet/:id/master', (req, res) => {
104✔
290
      if (!req.admin) {
3!
291
        res.json(403);
×
292
        return;
×
293
      }
294

295
      res.json(200, req.wallet.master.getJSON(this.network, true));
3✔
296
    });
297

298
    // Create wallet
299
    this.put('/wallet/:id', async (req, res) => {
104✔
300
      const valid = Validator.fromRequest(req);
38✔
301

302
      let master = valid.str('master');
38✔
303
      let mnemonic = valid.str('mnemonic');
38✔
304
      let accountKey = valid.str('accountKey');
38✔
305
      const language = valid.str('language');
38✔
306

307
      if (master)
38!
308
        master = HDPrivateKey.fromBase58(master, this.network);
×
309

310
      if (mnemonic)
38✔
311
        mnemonic = Mnemonic.fromPhrase(mnemonic);
6✔
312

313
      if (accountKey)
38✔
314
        accountKey = HDPublicKey.fromBase58(accountKey, this.network);
2✔
315

316
      const wallet = await this.wdb.create({
38✔
317
        id: valid.str('id'),
318
        type: valid.str('type'),
319
        m: valid.u32('m'),
320
        n: valid.u32('n'),
321
        passphrase: valid.str('passphrase'),
322
        bip39Passphrase: valid.str('bip39Passphrase'),
323
        master: master,
324
        mnemonic: mnemonic,
325
        accountKey: accountKey,
326
        watchOnly: valid.bool('watchOnly'),
327
        lookahead: valid.u32('lookahead'),
328
        language: language
329
      });
330

331
      const balance = await wallet.getBalance();
38✔
332

333
      res.json(200, wallet.getJSON(false, balance));
38✔
334
    });
335

336
    // List accounts
337
    this.get('/wallet/:id/account', async (req, res) => {
104✔
338
      const accounts = await req.wallet.getAccounts();
×
339
      res.json(200, accounts);
×
340
    });
341

342
    // Get account
343
    this.get('/wallet/:id/account/:account', async (req, res) => {
104✔
344
      const valid = Validator.fromRequest(req);
116✔
345
      const acct = valid.str('account');
116✔
346
      const account = await req.wallet.getAccount(acct);
116✔
347

348
      if (!account) {
116!
349
        res.json(404);
×
350
        return;
×
351
      }
352

353
      const balance = await req.wallet.getBalance(account.accountIndex);
116✔
354

355
      res.json(200, account.getJSON(balance));
116✔
356
    });
357

358
    // Create account
359
    this.put('/wallet/:id/account/:account', async (req, res) => {
104✔
360
      const valid = Validator.fromRequest(req);
8✔
361
      const passphrase = valid.str('passphrase');
8✔
362

363
      let accountKey = valid.get('accountKey');
8✔
364

365
      if (accountKey)
8!
366
        accountKey = HDPublicKey.fromBase58(accountKey, this.network);
×
367

368
      const options = {
8✔
369
        name: valid.str('account'),
370
        watchOnly: valid.bool('watchOnly'),
371
        type: valid.str('type'),
372
        m: valid.u32('m'),
373
        n: valid.u32('n'),
374
        accountKey: accountKey,
375
        lookahead: valid.u32('lookahead')
376
      };
377

378
      const account = await req.wallet.createAccount(options, passphrase);
8✔
379
      const balance = await req.wallet.getBalance(account.accountIndex);
8✔
380

381
      res.json(200, account.getJSON(balance));
8✔
382
    });
383

384
    // Modify account
385
    this.patch('/wallet/:id/account/:account', async (req, res) => {
104✔
386
      const valid = Validator.fromRequest(req);
1✔
387
      const passphrase = valid.str('passphrase');
1✔
388
      const acct = valid.str('account');
1✔
389

390
      const options = {
1✔
391
        lookahead: valid.u32('lookahead')
392
      };
393

394
      const account = await req.wallet.modifyAccount(acct, options, passphrase);
1✔
395
      const balance = await req.wallet.getBalance(account.accountIndex);
1✔
396

397
      res.json(200, account.getJSON(balance));
1✔
398
    });
399

400
    // Change passphrase
401
    this.post('/wallet/:id/passphrase', async (req, res) => {
104✔
402
      const valid = Validator.fromRequest(req);
×
403
      const passphrase = valid.str('passphrase');
×
404
      const old = valid.str('old');
×
405

406
      enforce(passphrase, 'Passphrase is required.');
×
407

408
      await req.wallet.setPassphrase(passphrase, old);
×
409

410
      res.json(200, { success: true });
×
411
    });
412

413
    // Unlock wallet
414
    this.post('/wallet/:id/unlock', async (req, res) => {
104✔
415
      const valid = Validator.fromRequest(req);
8✔
416
      const passphrase = valid.str('passphrase');
8✔
417
      const timeout = valid.u32('timeout', 60);
8✔
418

419
      enforce(passphrase, 'Passphrase is required.');
5✔
420
      // setTimeout maximum is 24.85 days, we round to 24 days.
421
      enforce(timeout <= 2073600, 'Timeout must be less than 24 days.');
5✔
422

423
      await req.wallet.unlock(passphrase, timeout);
4✔
424

425
      res.json(200, { success: true });
3✔
426
    });
427

428
    // Lock wallet
429
    this.post('/wallet/:id/lock', async (req, res) => {
104✔
430
      await req.wallet.lock();
27✔
431
      res.json(200, { success: true });
27✔
432
    });
433

434
    // Import key
435
    this.post('/wallet/:id/import', async (req, res) => {
104✔
436
      const valid = Validator.fromRequest(req);
×
437
      const acct = valid.str('account');
×
438
      const passphrase = valid.str('passphrase');
×
439
      const pub = valid.buf('publicKey');
×
440
      const priv = valid.str('privateKey');
×
441
      const b58 = valid.str('address');
×
442

443
      if (pub) {
×
444
        const key = KeyRing.fromPublic(pub);
×
445
        await req.wallet.importKey(acct, key);
×
446
        res.json(200, { success: true });
×
447
        return;
×
448
      }
449

450
      if (priv) {
×
451
        const key = KeyRing.fromSecret(priv, this.network);
×
452
        await req.wallet.importKey(acct, key, passphrase);
×
453
        res.json(200, { success: true });
×
454
        return;
×
455
      }
456

457
      if (b58) {
×
458
        const addr = Address.fromString(b58, this.network);
×
459
        await req.wallet.importAddress(acct, addr);
×
460
        res.json(200, { success: true });
×
461
        return;
×
462
      }
463

464
      enforce(false, 'Key or address is required.');
×
465
    });
466

467
    // Generate new token
468
    this.post('/wallet/:id/retoken', async (req, res) => {
104✔
469
      const valid = Validator.fromRequest(req);
×
470
      const passphrase = valid.str('passphrase');
×
471
      const token = await req.wallet.retoken(passphrase);
×
472

473
      res.json(200, {
×
474
        token: token.toString('hex')
475
      });
476
    });
477

478
    // Send TX
479
    this.post('/wallet/:id/send', async (req, res) => {
104✔
480
      const valid = Validator.fromRequest(req);
948✔
481

482
      const options = TransactionOptions.fromValidator(valid, this.network);
948✔
483
      const tx = await req.wallet.send(options);
948✔
484

485
      const details = await req.wallet.getDetails(tx.hash());
948✔
486

487
      res.json(200, details.getJSON(this.network, this.wdb.height));
948✔
488
    });
489

490
    // Create TX
491
    this.post('/wallet/:id/create', async (req, res) => {
104✔
492
      const valid = Validator.fromRequest(req);
7✔
493
      const sign = valid.bool('sign', true);
7✔
494

495
      // TODO: Add create TX with locks for used Coins and/or
496
      // adds to the pending list.
497
      const options = TransactionOptions.fromValidator(valid, this.network);
7✔
498
      const tx = await req.wallet.createTX(options);
7✔
499

500
      if (sign)
7!
501
        await req.wallet.sign(tx, options.passphrase);
7✔
502

503
      const json = tx.getJSON(this.network);
7✔
504

505
      if (options.paths)
7✔
506
        await this.addOutputPaths(json, tx, req.wallet);
2✔
507

508
      res.json(200, json);
7✔
509
    });
510

511
    // Sign TX
512
    this.post('/wallet/:id/sign', async (req, res) => {
104✔
513
      const valid = Validator.fromRequest(req);
6✔
514
      const passphrase = valid.str('passphrase');
6✔
515
      const raw = valid.buf('tx');
6✔
516

517
      enforce(raw, 'TX is required.');
6✔
518

519
      const mtx = MTX.decode(raw);
6✔
520
      mtx.view = await req.wallet.getCoinView(mtx);
6✔
521

522
      await req.wallet.sign(mtx, passphrase);
6✔
523

524
      res.json(200, mtx.getJSON(this.network));
6✔
525
    });
526

527
    // Zap Wallet TXs
528
    this.post('/wallet/:id/zap', async (req, res) => {
104✔
529
      const valid = Validator.fromRequest(req);
5✔
530
      const acct = valid.str('account');
5✔
531
      const age = valid.u32('age');
5✔
532

533
      enforce(age != null, 'Age is required.');
5✔
534

535
      const total = await req.wallet.zap(acct, age);
5✔
536

537
      res.json(200, {
5✔
538
        success: true,
539
        zapped: total
540
      });
541
    });
542

543
    // Abandon Wallet TX
544
    this.del('/wallet/:id/tx/:hash', async (req, res) => {
104✔
545
      const valid = Validator.fromRequest(req);
2✔
546
      const hash = valid.bhash('hash');
2✔
547

548
      enforce(hash, 'Hash is required.');
2✔
549

550
      await req.wallet.abandon(hash);
2✔
551

552
      res.json(200, { success: true });
2✔
553
    });
554

555
    // List blocks
556
    this.get('/wallet/:id/block', async (req, res) => {
104✔
557
      const heights = await req.wallet.getBlocks();
×
558
      res.json(200, heights);
×
559
    });
560

561
    // Get Block Record
562
    this.get('/wallet/:id/block/:height', async (req, res) => {
104✔
563
      const valid = Validator.fromRequest(req);
×
564
      const height = valid.u32('height');
×
565

566
      enforce(height != null, 'Height is required.');
×
567

568
      const block = await req.wallet.getBlock(height);
×
569

570
      if (!block) {
×
571
        res.json(404);
×
572
        return;
×
573
      }
574

575
      res.json(200, block.toJSON());
×
576
    });
577

578
    // Add key
579
    this.put('/wallet/:id/shared-key', async (req, res) => {
104✔
580
      const valid = Validator.fromRequest(req);
3✔
581
      const acct = valid.str('account');
3✔
582
      const b58 = valid.str('accountKey');
3✔
583

584
      enforce(b58, 'Key is required.');
3✔
585

586
      const key = HDPublicKey.fromBase58(b58, this.network);
3✔
587
      const added = await req.wallet.addSharedKey(acct, key);
3✔
588

589
      res.json(200, {
3✔
590
        success: true,
591
        addedKey: added
592
      });
593
    });
594

595
    // Remove key
596
    this.del('/wallet/:id/shared-key', async (req, res) => {
104✔
597
      const valid = Validator.fromRequest(req);
×
598
      const acct = valid.str('account');
×
599
      const b58 = valid.str('accountKey');
×
600

601
      enforce(b58, 'Key is required.');
×
602

603
      const key = HDPublicKey.fromBase58(b58, this.network);
×
604
      const removed = await req.wallet.removeSharedKey(acct, key);
×
605

606
      res.json(200, {
×
607
        success: true,
608
        removedKey: removed
609
      });
610
    });
611

612
    // Get key by address
613
    this.get('/wallet/:id/key/:address', async (req, res) => {
104✔
614
      const valid = Validator.fromRequest(req);
19✔
615
      const b58 = valid.str('address');
19✔
616

617
      enforce(b58, 'Address is required.');
19✔
618

619
      const addr = Address.fromString(b58, this.network);
19✔
620
      const key = await req.wallet.getKey(addr);
19✔
621

622
      if (!key) {
19!
623
        res.json(404);
×
624
        return;
×
625
      }
626

627
      res.json(200, key.getJSON(this.network));
19✔
628
    });
629

630
    // Get private key
631
    this.get('/wallet/:id/wif/:address', async (req, res) => {
104✔
632
      const valid = Validator.fromRequest(req);
16✔
633
      const b58 = valid.str('address');
16✔
634
      const passphrase = valid.str('passphrase');
16✔
635

636
      enforce(b58, 'Address is required.');
16✔
637

638
      const addr = Address.fromString(b58, this.network);
16✔
639
      const key = await req.wallet.getPrivateKey(addr, passphrase);
16✔
640

641
      if (!key) {
16!
642
        res.json(404);
×
643
        return;
×
644
      }
645

646
      res.json(200, { privateKey: key.toSecret(this.network) });
16✔
647
    });
648

649
    // Create address
650
    this.post('/wallet/:id/address', async (req, res) => {
104✔
651
      const valid = Validator.fromRequest(req);
1,790✔
652
      const acct = valid.str('account');
1,790✔
653
      const addr = await req.wallet.createReceive(acct);
1,790✔
654

655
      res.json(200, addr.getJSON(this.network));
1,790✔
656
    });
657

658
    // Create change address
659
    this.post('/wallet/:id/change', async (req, res) => {
104✔
660
      const valid = Validator.fromRequest(req);
2✔
661
      const acct = valid.str('account');
2✔
662
      const addr = await req.wallet.createChange(acct);
2✔
663

664
      res.json(200, addr.getJSON(this.network));
2✔
665
    });
666

667
    // Wallet Balance
668
    this.get('/wallet/:id/balance', async (req, res) => {
104✔
669
      const valid = Validator.fromRequest(req);
57✔
670
      const acct = valid.str('account');
57✔
671
      const balance = await req.wallet.getBalance(acct);
57✔
672

673
      if (!balance) {
57!
674
        res.json(404);
×
675
        return;
×
676
      }
677

678
      res.json(200, balance.toJSON());
57✔
679
    });
680

681
    // Wallet UTXOs
682
    this.get('/wallet/:id/coin', async (req, res) => {
104✔
683
      const valid = Validator.fromRequest(req);
1✔
684
      const acct = valid.str('account');
1✔
685
      const coins = await req.wallet.getCoins(acct);
1✔
686
      const result = [];
1✔
687

688
      common.sortCoins(coins);
1✔
689

690
      for (const coin of coins)
1✔
691
        result.push(coin.getJSON(this.network));
20✔
692

693
      res.json(200, result);
1✔
694
    });
695

696
    // Locked coins
697
    this.get('/wallet/:id/locked', async (req, res) => {
104✔
698
      const locked = req.wallet.getLocked();
×
699
      const result = [];
×
700

701
      for (const outpoint of locked)
×
702
        result.push(outpoint.toJSON());
×
703

704
      res.json(200, result);
×
705
    });
706

707
    // Lock coin
708
    this.put('/wallet/:id/locked/:hash/:index', async (req, res) => {
104✔
709
      const valid = Validator.fromRequest(req);
×
710
      const hash = valid.bhash('hash');
×
711
      const index = valid.u32('index');
×
712

713
      enforce(hash, 'Hash is required.');
×
714
      enforce(index != null, 'Index is required.');
×
715

716
      const outpoint = new Outpoint(hash, index);
×
717

718
      req.wallet.lockCoin(outpoint);
×
719

720
      res.json(200, { success: true });
×
721
    });
722

723
    // Unlock coin
724
    this.del('/wallet/:id/locked/:hash/:index', async (req, res) => {
104✔
725
      const valid = Validator.fromRequest(req);
×
726
      const hash = valid.bhash('hash');
×
727
      const index = valid.u32('index');
×
728

729
      enforce(hash, 'Hash is required.');
×
730
      enforce(index != null, 'Index is required.');
×
731

732
      const outpoint = new Outpoint(hash, index);
×
733

734
      req.wallet.unlockCoin(outpoint);
×
735

736
      res.json(200, { success: true });
×
737
    });
738

739
    // Wallet Coin
740
    this.get('/wallet/:id/coin/:hash/:index', async (req, res) => {
104✔
741
      const valid = Validator.fromRequest(req);
2✔
742
      const hash = valid.bhash('hash');
2✔
743
      const index = valid.u32('index');
2✔
744

745
      enforce(hash, 'Hash is required.');
2✔
746
      enforce(index != null, 'Index is required.');
2✔
747

748
      const coin = await req.wallet.getCoin(hash, index);
2✔
749

750
      if (!coin) {
2!
751
        res.json(404);
×
752
        return;
×
753
      }
754

755
      res.json(200, coin.getJSON(this.network));
2✔
756
    });
757

758
    // Wallet TXs
759
    this.get('/wallet/:id/tx/history', async (req, res) => {
104✔
760
      const valid = Validator.fromRequest(req);
11✔
761
      const acct = valid.str('account');
11✔
762
      const reverse = valid.bool('reverse', false);
11✔
763
      const limit = valid.u32('limit', this.maxTXs);
11✔
764
      const after = valid.bhash('after');
11✔
765
      const time = valid.u32('time');
11✔
766

767
      enforce(limit <= this.maxTXs,
11✔
768
              `Limit above max of ${this.maxTXs}.`);
769

770
      let txs = [];
11✔
771
      const opts = {limit, reverse};
11✔
772

773
      if (after) {
11✔
774
        opts.hash = after;
3✔
775
        txs = await req.wallet.listHistoryAfter(acct, opts);
3✔
776
      } else if (time) {
8✔
777
        opts.time = time;
2✔
778
        txs = await req.wallet.listHistoryByTime(acct, opts);
2✔
779
      } else {
780
        txs = await req.wallet.listHistory(acct, opts);
6✔
781
      }
782

783
      const details = await req.wallet.toDetails(txs);
11✔
784

785
      const result = [];
11✔
786

787
      for (const item of details)
11✔
788
        result.push(item.getJSON(this.network, this.wdb.height));
572✔
789

790
      res.json(200, result);
11✔
791
    });
792

793
    // Wallet Pending TXs
794
    this.get('/wallet/:id/tx/unconfirmed', async (req, res) => {
104✔
795
      const valid = Validator.fromRequest(req);
16✔
796
      const acct = valid.str('account');
16✔
797
      const reverse = valid.bool('reverse', false);
16✔
798
      const limit = valid.u32('limit', this.maxTXs);
16✔
799
      const after = valid.bhash('after');
16✔
800
      const time = valid.u32('time');
16✔
801

802
      enforce(limit <= this.maxTXs,
16✔
803
              `Limit above max of ${this.maxTXs}.`);
804

805
      let txs = [];
16✔
806
      const opts = {limit, reverse};
16✔
807

808
      if (after) {
16✔
809
        opts.hash = after;
4✔
810
        txs = await req.wallet.listUnconfirmedAfter(acct, opts);
4✔
811
      } else if (time) {
12✔
812
        opts.time = time;
4✔
813
        txs = await req.wallet.listUnconfirmedByTime(acct, opts);
4✔
814
      } else {
815
        txs = await req.wallet.listUnconfirmed(acct, opts);
8✔
816
      }
817

818
      const details = await req.wallet.toDetails(txs);
16✔
819
      const result = [];
16✔
820

821
      for (const item of details)
16✔
822
        result.push(item.getJSON(this.network, this.wdb.height));
240✔
823

824
      res.json(200, result);
16✔
825
    });
826

827
    // Wallet TX
828
    this.get('/wallet/:id/tx/:hash', async (req, res) => {
104✔
829
      const valid = Validator.fromRequest(req);
2✔
830
      const hash = valid.bhash('hash');
2✔
831

832
      enforce(hash, 'Hash is required.');
2✔
833

834
      const tx = await req.wallet.getTX(hash);
2✔
835

836
      if (!tx) {
2✔
837
        res.json(404);
1✔
838
        return;
1✔
839
      }
840

841
      const details = await req.wallet.toDetails(tx);
1✔
842

843
      res.json(200, details.getJSON(this.network, this.wdb.height));
1✔
844
    });
845

846
    // Resend
847
    this.post('/wallet/:id/resend', async (req, res) => {
104✔
848
      await req.wallet.resend();
×
849
      res.json(200, { success: true });
×
850
    });
851

852
    // Wallet Name States
853
    this.get('/wallet/:id/name', async (req, res) => {
104✔
854
      const valid = Validator.fromRequest(req);
6✔
855
      const own = valid.bool('own', false);
6✔
856

857
      const height = this.wdb.height;
6✔
858
      const network = this.network;
6✔
859

860
      const names = await req.wallet.getNames();
6✔
861
      const items = [];
6✔
862

863
      for (const ns of names) {
6✔
864
        if (own) {
68✔
865
          const {hash, index} = ns.owner;
36✔
866
          const coin = await req.wallet.getCoin(hash, index);
36✔
867

868
          if (coin)
36✔
869
            items.push(ns.getJSON(height, network));
16✔
870
        } else {
871
          items.push(ns.getJSON(height, network));
32✔
872
        }
873
      }
874

875
      res.json(200, items);
6✔
876
    });
877

878
    // Wallet Name State
879
    this.get('/wallet/:id/name/:name', async (req, res) => {
104✔
880
      const valid = Validator.fromRequest(req);
20✔
881
      const name = valid.str('name');
20✔
882
      const own = valid.bool('own', false);
20✔
883

884
      enforce(name, 'Must pass name.');
20✔
885
      enforce(rules.verifyName(name), 'Must pass valid name.');
20✔
886

887
      const height = this.wdb.height;
20✔
888
      const network = this.network;
20✔
889
      /** @type {NameState?} */
890
      const ns = await req.wallet.getNameStateByName(name);
20✔
891

892
      if (!ns)
20!
893
        return res.json(404);
×
894

895
      if (own) {
20✔
896
        const {hash, index} = ns.owner;
18✔
897
        const coin = await req.wallet.getCoin(hash, index);
18✔
898

899
        if (!coin)
18✔
900
          return res.json(404);
10✔
901
      }
902

903
      return res.json(200, ns.getJSON(height, network));
10✔
904
    });
905

906
    // Wallet Auctions
907
    this.get('/wallet/:id/auction', async (req, res) => {
104✔
908
      const height = this.wdb.height;
×
909
      const network = this.network;
×
910

911
      const names = await req.wallet.getNames();
×
912
      const items = [];
×
913

914
      for (const ns of names) {
×
915
        const bids = await req.wallet.getBidsByName(ns.name);
×
916
        const reveals = await req.wallet.getRevealsByName(ns.name);
×
917
        const info = ns.getJSON(height, network);
×
918

919
        info.bids = [];
×
920
        info.reveals = [];
×
921

922
        for (const bid of bids)
×
923
          info.bids.push(bid.toJSON());
×
924

925
        for (const reveal of reveals)
×
926
          info.reveals.push(reveal.toJSON());
×
927

928
        items.push(info);
×
929
      }
930

931
      return res.json(200, items);
×
932
    });
933

934
    // Wallet Auction
935
    this.get('/wallet/:id/auction/:name', async (req, res) => {
104✔
936
      const valid = Validator.fromRequest(req);
3✔
937
      const name = valid.str('name');
3✔
938

939
      enforce(name, 'Must pass name.');
3✔
940
      enforce(rules.verifyName(name), 'Must pass valid name.');
3✔
941

942
      const height = this.wdb.height;
3✔
943
      const network = this.network;
3✔
944

945
      const ns = await req.wallet.getNameStateByName(name);
3✔
946

947
      if (!ns)
3!
948
        return res.json(404);
×
949

950
      const bids = await req.wallet.getBidsByName(name);
3✔
951
      const reveals = await req.wallet.getRevealsByName(name);
3✔
952

953
      const info = ns.getJSON(height, network);
3✔
954
      info.bids = [];
3✔
955
      info.reveals = [];
3✔
956

957
      for (const bid of bids)
3✔
958
        info.bids.push(bid.toJSON());
4✔
959

960
      for (const reveal of reveals)
3✔
961
        info.reveals.push(reveal.toJSON());
3✔
962

963
      return res.json(200, info);
3✔
964
    });
965

966
    // All Wallet Bids
967
    this.get('/wallet/:id/bid', async (req, res) => {
104✔
968
      const valid = Validator.fromRequest(req);
1✔
969
      const own = valid.bool('own', false);
1✔
970

971
      const bids = await req.wallet.getBidsByName();
1✔
972
      const items = [];
1✔
973

974
      for (const bid of bids) {
1✔
975
        if (!own || bid.own)
3!
976
          items.push(bid.toJSON());
3✔
977
      }
978

979
      res.json(200, items);
1✔
980
    });
981

982
    // Wallet Bids by Name
983
    this.get('/wallet/:id/bid/:name', async (req, res) => {
104✔
984
      const valid = Validator.fromRequest(req);
2✔
985
      const name = valid.str('name');
2✔
986
      let own = valid.bool('own', false);
2✔
987

988
      if (name)
2!
989
        enforce(rules.verifyName(name), 'Must pass valid name.');
2✔
990

991
      if (!name)
2!
992
        own = true;
×
993

994
      const bids = await req.wallet.getBidsByName(name);
2✔
995
      const items = [];
2✔
996

997
      for (const bid of bids) {
2✔
998
        if (!own || bid.own)
4✔
999
          items.push(bid.toJSON());
3✔
1000
      }
1001

1002
      res.json(200, items);
2✔
1003
    });
1004

1005
    // All Wallet Reveals
1006
    this.get('/wallet/:id/reveal', async (req, res) => {
104✔
1007
      const valid = Validator.fromRequest(req);
2✔
1008
      const own = valid.bool('own', false);
2✔
1009

1010
      const reveals = await req.wallet.getRevealsByName();
2✔
1011
      const items = [];
2✔
1012

1013
      for (const brv of reveals) {
2✔
1014
        if (!own || brv.own)
8!
1015
          items.push(brv.toJSON());
8✔
1016
      }
1017

1018
      res.json(200, items);
2✔
1019
    });
1020

1021
    // Wallet Reveals by Name
1022
    this.get('/wallet/:id/reveal/:name', async (req, res) => {
104✔
1023
      const valid = Validator.fromRequest(req);
4✔
1024
      const name = valid.str('name');
4✔
1025
      let own = valid.bool('own', false);
4✔
1026

1027
      if (name)
4!
1028
        enforce(rules.verifyName(name), 'Must pass valid name.');
4✔
1029

1030
      if (!name)
4!
1031
        own = true;
×
1032

1033
      const reveals = await req.wallet.getRevealsByName(name);
4✔
1034
      const items = [];
4✔
1035

1036
      for (const brv of reveals) {
4✔
1037
        if (!own || brv.own)
8✔
1038
          items.push(brv.toJSON());
7✔
1039
      }
1040

1041
      res.json(200, items);
4✔
1042
    });
1043

1044
    // Name Resource
1045
    this.get('/wallet/:id/resource/:name', async (req, res) => {
104✔
1046
      const valid = Validator.fromRequest(req);
2✔
1047
      const name = valid.str('name');
2✔
1048

1049
      enforce(name, 'Must pass name.');
2✔
1050
      enforce(rules.verifyName(name), 'Must pass valid name.');
2✔
1051

1052
      const ns = await req.wallet.getNameStateByName(name);
2✔
1053

1054
      if (!ns || ns.data.length === 0)
2✔
1055
        return res.json(404);
1✔
1056

1057
      try {
1✔
1058
        const resource = Resource.decode(ns.data);
1✔
1059
        return res.json(200, resource.toJSON());
1✔
1060
      } catch (e) {
1061
        return res.json(400);
×
1062
      }
1063
    });
1064

1065
    // Regenerate Nonce
1066
    this.get('/wallet/:id/nonce/:name', async (req, res) => {
104✔
1067
      const valid = Validator.fromRequest(req);
2✔
1068
      const name = valid.str('name');
2✔
1069
      const addr = valid.str('address');
2✔
1070
      const bid = valid.ufixed('bid');
2✔
1071

1072
      enforce(name, 'Name is required.');
2✔
1073
      enforce(rules.verifyName(name), 'Valid name is required.');
2✔
1074
      enforce(addr, 'Address is required.');
2✔
1075
      enforce(bid != null, 'Bid is required.');
2✔
1076

1077
      let address;
1078
      try {
2✔
1079
        address = Address.fromString(addr, this.network);
2✔
1080
      } catch (e) {
1081
        return req.json(400);
×
1082
      }
1083

1084
      const nameHash = rules.hashName(name);
2✔
1085
      const nonces = await req.wallet.generateNonces(nameHash, address, bid);
2✔
1086
      const blinds = nonces.map(nonce => rules.blind(bid, nonce));
2✔
1087

1088
      return res.json(200, {
2✔
1089
        address: address.toString(this.network),
1090
        blinds: blinds.map(blind => blind.toString('hex')),
2✔
1091
        nonces: nonces.map(nonce => nonce.toString('hex')),
2✔
1092
        bid: bid,
1093
        name: name,
1094
        nameHash: nameHash.toString('hex')
1095
      });
1096
    });
1097

1098
    // Create Open
1099
    this.post('/wallet/:id/open', async (req, res) => {
104✔
1100
      const valid = Validator.fromRequest(req);
31✔
1101
      const name = valid.str('name');
31✔
1102
      const broadcast = valid.bool('broadcast', true);
31✔
1103
      const sign = valid.bool('sign', true);
31✔
1104

1105
      enforce(name, 'Name is required.');
31✔
1106
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
31✔
1107

1108
      const options = TransactionOptions.fromValidator(valid, this.network);
30✔
1109

1110
      if (broadcast) {
30✔
1111
        // TODO: Add abort signal to close when request closes.
1112
        const tx = await req.wallet.sendOpen(name, options);
28✔
1113
        return res.json(200, tx.getJSON(this.network));
24✔
1114
      }
1115

1116
      // TODO: Add create TX with locks for used Coins and/or
1117
      // adds to the pending list.
1118
      const mtx = await req.wallet.createOpen(name, options);
2✔
1119

1120
      if (sign)
2✔
1121
        await req.wallet.sign(mtx, options.passphrase);
1✔
1122

1123
      const json = mtx.getJSON(this.network);
2✔
1124

1125
      if (options.paths)
2!
1126
        await this.addOutputPaths(json, mtx, req.wallet);
×
1127

1128
      return res.json(200, json);
2✔
1129
    });
1130

1131
    // Create Bid
1132
    this.post('/wallet/:id/bid', async (req, res) => {
104✔
1133
      const valid = Validator.fromRequest(req);
39✔
1134
      const name = valid.str('name');
39✔
1135
      const bid = valid.u64('bid');
39✔
1136
      const lockup = valid.u64('lockup');
39✔
1137
      const broadcast = valid.bool('broadcast', true);
39✔
1138
      const sign = valid.bool('sign', true);
39✔
1139

1140
      enforce(name, 'Name is required.');
39✔
1141
      enforce(bid != null, 'Bid is required.');
39✔
1142
      enforce(lockup != null, 'Lockup is required.');
38✔
1143
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
37!
1144

1145
      const options = TransactionOptions.fromValidator(valid, this.network);
37✔
1146

1147
      if (broadcast) {
37!
1148
        // TODO: Add abort signal to close when request closes.
1149
        const tx = await req.wallet.sendBid(name, bid, lockup, options);
37✔
1150
        return res.json(200, tx.getJSON(this.network));
35✔
1151
      }
1152

1153
      // TODO: Add create TX with locks for used Coins and/or
1154
      // adds to the pending list.
1155
      const mtx = await req.wallet.createBid(name, bid, lockup, options);
×
1156

1157
      if (sign)
×
1158
        await req.wallet.sign(mtx, options.passphrase);
×
1159

1160
      const json = mtx.getJSON(this.network);
×
1161

1162
      if (options.paths)
×
1163
        await this.addOutputPaths(json, mtx, req.wallet);
×
1164

1165
      return res.json(200, json);
×
1166
    });
1167

1168
    // Create auction-related transactions in advance (bid and reveal for now)
1169
    this.post('/wallet/:id/auction', async (req, res) => {
104✔
1170
      const valid = Validator.fromRequest(req);
1✔
1171
      const name = valid.str('name');
1✔
1172
      const bid = valid.u64('bid');
1✔
1173
      const lockup = valid.u64('lockup');
1✔
1174
      const passphrase = valid.str('passphrase');
1✔
1175
      const sign = valid.bool('sign', true);
1✔
1176
      const broadcastBid = valid.bool('broadcastBid');
1✔
1177

1178
      enforce(name, 'Name is required.');
1✔
1179
      enforce(bid != null, 'Bid is required.');
1✔
1180
      enforce(lockup != null, 'Lockup is required.');
1✔
1181
      enforce(broadcastBid != null, 'broadcastBid is required.');
1✔
1182
      enforce(broadcastBid ? sign : true, 'Must sign when broadcasting.');
1!
1183

1184
      const options = TransactionOptions.fromValidator(valid, this.network);
1✔
1185
      const auctionTXs = await req.wallet.createAuctionTXs(
1✔
1186
        name,
1187
        bid,
1188
        lockup,
1189
        options
1190
      );
1191

1192
      if (broadcastBid)
1!
1193
        auctionTXs.bid = await req.wallet.sendMTX(auctionTXs.bid, passphrase);
1✔
1194

1195
      if (sign) {
1!
1196
        if (!broadcastBid)
1!
1197
          await req.wallet.sign(auctionTXs.bid, passphrase);
×
1198

1199
        await req.wallet.sign(auctionTXs.reveal, passphrase);
1✔
1200
      }
1201

1202
      const jsonBid = auctionTXs.bid.getJSON(this.network);
1✔
1203
      const jsonReveal = auctionTXs.reveal.getJSON(this.network);
1✔
1204

1205
      if (options.paths) {
1!
1206
        await this.addOutputPaths(jsonBid, auctionTXs.bid, req.wallet);
×
1207
        await this.addOutputPaths(jsonReveal, auctionTXs.reveal, req.wallet);
×
1208
      }
1209

1210
      return res.json(200, {
1✔
1211
        bid: jsonBid,
1212
        reveal: jsonReveal
1213
      });
1214
    });
1215

1216
    // Create Reveal
1217
    this.post('/wallet/:id/reveal', async (req, res) => {
104✔
1218
      const valid = Validator.fromRequest(req);
20✔
1219
      const name = valid.str('name');
20✔
1220
      const broadcast = valid.bool('broadcast', true);
20✔
1221
      const sign = valid.bool('sign', true);
20✔
1222

1223
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
20!
1224

1225
      const options = TransactionOptions.fromValidator(valid, this.network);
20✔
1226

1227
      if (broadcast) {
20!
1228
        let tx;
1229

1230
        if (name) {
20✔
1231
          // TODO: Add abort signal to close when request closes.
1232
          tx = await req.wallet.sendReveal(name, options);
18✔
1233
        } else {
1234
          // TODO: Add abort signal to close when request closes.
1235
          tx = await req.wallet.sendRevealAll(options);
2✔
1236
        }
1237

1238
        return res.json(200, tx.getJSON(this.network));
19✔
1239
      }
1240

1241
      let mtx;
1242

1243
      // TODO: Add create TX with locks for used Coins and/or
1244
      // adds to the pending list.
1245
      if (name) {
×
1246
        mtx = await req.wallet.createReveal(name, options);
×
1247
      } else {
1248
        mtx = await req.wallet.createRevealAll(options);
×
1249
      }
1250

1251
      if (sign)
×
1252
        await req.wallet.sign(mtx, options.passphrase);
×
1253

1254
      const json = mtx.getJSON(this.network);
×
1255

1256
      if (options.paths)
×
1257
        await this.addOutputPaths(json, mtx, req.wallet);
×
1258

1259
      return res.json(200, json);
×
1260
    });
1261

1262
    // Create Redeem
1263
    this.post('/wallet/:id/redeem', async (req, res) => {
104✔
1264
      const valid = Validator.fromRequest(req);
7✔
1265
      const name = valid.str('name');
7✔
1266
      const broadcast = valid.bool('broadcast', true);
7✔
1267
      const sign = valid.bool('sign', true);
7✔
1268

1269
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
7!
1270

1271
      const options = TransactionOptions.fromValidator(valid, this.network);
7✔
1272

1273
      if (broadcast) {
7!
1274
        let tx;
1275

1276
        if (name) {
7✔
1277
          // TODO: Add abort signal to close when request closes.
1278
          tx = await req.wallet.sendRedeem(name, options);
4✔
1279
        } else {
1280
          // TODO: Add abort signal to close when request closes.
1281
          tx = await req.wallet.sendRedeemAll(options);
3✔
1282
        }
1283

1284
        return res.json(200, tx.getJSON(this.network));
5✔
1285
      }
1286

1287
      let mtx;
1288

1289
      // TODO: Add create TX with locks for used Coins and/or
1290
      // adds to the pending list.
1291
      if (!name) {
×
1292
        mtx = await req.wallet.createRedeemAll(options);
×
1293
      } else {
1294
        mtx = await req.wallet.createRedeem(name, options);
×
1295
      }
1296

1297
      if (sign)
×
1298
        await req.wallet.sign(mtx, options.passphrase);
×
1299

1300
      const json = mtx.getJSON(this.network);
×
1301

1302
      if (options.paths)
×
1303
        await this.addOutputPaths(json, mtx, req.wallet);
×
1304

1305
      return res.json(200, json);
×
1306
    });
1307

1308
    // Create Update
1309
    this.post('/wallet/:id/update', async (req, res) => {
104✔
1310
      const valid = Validator.fromRequest(req);
12✔
1311
      const name = valid.str('name');
12✔
1312
      const data = valid.obj('data');
12✔
1313
      const broadcast = valid.bool('broadcast', true);
12✔
1314
      const sign = valid.bool('sign', true);
12✔
1315

1316
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
12!
1317
      enforce(name, 'Must pass name.');
12✔
1318
      enforce(data, 'Must pass data.');
12✔
1319

1320
      let resource;
1321
      try {
12✔
1322
        resource = Resource.fromJSON(data);
12✔
1323
      } catch (e) {
1324
        return res.json(400);
×
1325
      }
1326

1327
      const options = TransactionOptions.fromValidator(valid, this.network);
12✔
1328

1329
      if (broadcast) {
12!
1330
        // TODO: Add abort signal to close when request closes.
1331
        const tx = await req.wallet.sendUpdate(name, resource, options);
12✔
1332
        return res.json(200, tx.getJSON(this.network));
11✔
1333
      }
1334

1335
      // TODO: Add create TX with locks for used Coins and/or
1336
      // adds to the pending list.
1337
      const mtx = await req.wallet.createUpdate(name, resource, options);
×
1338

1339
      if (sign)
×
1340
        await req.wallet.sign(mtx, options.passphrase);
×
1341

1342
      const json = mtx.getJSON(this.network);
×
1343

1344
      if (options.paths)
×
1345
        await this.addOutputPaths(json, mtx, req.wallet);
×
1346

1347
      return res.json(200, json);
×
1348
    });
1349

1350
    // Create Renewal
1351
    this.post('/wallet/:id/renewal', async (req, res) => {
104✔
1352
      const valid = Validator.fromRequest(req);
4✔
1353
      const name = valid.str('name');
4✔
1354
      const broadcast = valid.bool('broadcast', true);
4✔
1355
      const sign = valid.bool('sign', true);
4✔
1356

1357
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
4!
1358
      enforce(name, 'Must pass name.');
4✔
1359

1360
      const options = TransactionOptions.fromValidator(valid, this.network);
4✔
1361

1362
      if (broadcast) {
4!
1363
        // TODO: Add abort signal to close when request closes.
1364
        const tx = await req.wallet.sendRenewal(name, options);
4✔
1365
        return res.json(200, tx.getJSON(this.network));
4✔
1366
      }
1367

1368
      const mtx = await req.wallet.createRenewal(name, options);
×
1369

1370
      if (sign)
×
1371
        await req.wallet.sign(mtx, options.passphrase);
×
1372

1373
      const json = mtx.getJSON(this.network);
×
1374

1375
      if (options.paths)
×
1376
        await this.addOutputPaths(json, mtx, req.wallet);
×
1377

1378
      return res.json(200, json);
×
1379
    });
1380

1381
    // Create Transfer
1382
    this.post('/wallet/:id/transfer', async (req, res) => {
104✔
1383
      const valid = Validator.fromRequest(req);
12✔
1384
      const name = valid.str('name');
12✔
1385
      const address = valid.str('address');
12✔
1386
      const broadcast = valid.bool('broadcast', true);
12✔
1387
      const sign = valid.bool('sign', true);
12✔
1388

1389
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
12!
1390
      enforce(name, 'Must pass name.');
12✔
1391
      enforce(address, 'Must pass address.');
12✔
1392

1393
      const addr = Address.fromString(address, this.network);
12✔
1394
      const options = TransactionOptions.fromValidator(valid, this.network);
12✔
1395

1396
      if (broadcast) {
12!
1397
        // TODO: Add abort signal to close when request closes.
1398
        const tx = await req.wallet.sendTransfer(name, addr, options);
12✔
1399
        return res.json(200, tx.getJSON(this.network));
12✔
1400
      }
1401

1402
      // TODO: Add create TX with locks for used Coins and/or
1403
      // adds to the pending list.
1404
      const mtx = await req.wallet.createTransfer(name, addr, options);
×
1405

1406
      if (sign)
×
1407
        await req.wallet.sign(mtx, options.passphrase);
×
1408

1409
      const json = mtx.getJSON(this.network);
×
1410

1411
      if (options.paths)
×
1412
        await this.addOutputPaths(json, mtx, req.wallet);
×
1413

1414
      return res.json(200, json);
×
1415
    });
1416

1417
    // Create Cancel
1418
    this.post('/wallet/:id/cancel', async (req, res) => {
104✔
1419
      const valid = Validator.fromRequest(req);
4✔
1420
      const name = valid.str('name');
4✔
1421
      const broadcast = valid.bool('broadcast', true);
4✔
1422
      const sign = valid.bool('sign', true);
4✔
1423

1424
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
4!
1425
      enforce(name, 'Must pass name.');
4✔
1426

1427
      const options = TransactionOptions.fromValidator(valid, this.network);
4✔
1428

1429
      if (broadcast) {
4!
1430
        // TODO: Add abort signal to close when request closes.
1431
        const tx = await req.wallet.sendCancel(name, options);
4✔
1432
        return res.json(200, tx.getJSON(this.network));
4✔
1433
      }
1434

1435
      // TODO: Add create TX with locks for used Coins and/or
1436
      // adds to the pending list.
1437
      const mtx = await req.wallet.createCancel(name, options);
×
1438

1439
      if (sign)
×
1440
        await req.wallet.sign(mtx, options.passphrase);
×
1441

1442
      const json = mtx.getJSON(this.network);
×
1443

1444
      if (options.paths)
×
1445
        await this.addOutputPaths(json, mtx, req.wallet);
×
1446

1447
      return res.json(200, json);
×
1448
    });
1449

1450
    // Create Finalize
1451
    this.post('/wallet/:id/finalize', async (req, res) => {
104✔
1452
      const valid = Validator.fromRequest(req);
4✔
1453
      const name = valid.str('name');
4✔
1454
      const broadcast = valid.bool('broadcast', true);
4✔
1455
      const sign = valid.bool('sign', true);
4✔
1456

1457
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
4!
1458
      enforce(name, 'Must pass name.');
4✔
1459

1460
      const options = TransactionOptions.fromValidator(valid, this.network);
4✔
1461

1462
      if (broadcast) {
4!
1463
        // TODO: Add abort signal to close when request closes.
1464
        const tx = await req.wallet.sendFinalize(name, options);
4✔
1465
        return res.json(200, tx.getJSON(this.network));
4✔
1466
      }
1467

1468
      const mtx = await req.wallet.createFinalize(name, options);
×
1469

1470
      if (sign)
×
1471
        await req.wallet.sign(mtx, options.passphrase);
×
1472

1473
      const json = mtx.getJSON(this.network);
×
1474

1475
      if (options.paths)
×
1476
        await this.addOutputPaths(json, mtx, req.wallet);
×
1477

1478
      return res.json(200, json);
×
1479
    });
1480

1481
    // Create Revoke
1482
    this.post('/wallet/:id/revoke', async (req, res) => {
104✔
1483
      const valid = Validator.fromRequest(req);
4✔
1484
      const name = valid.str('name');
4✔
1485
      const broadcast = valid.bool('broadcast', true);
4✔
1486
      const sign = valid.bool('sign', true);
4✔
1487

1488
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
4!
1489
      enforce(name, 'Must pass name.');
4✔
1490

1491
      const options = TransactionOptions.fromValidator(valid, this.network);
4✔
1492

1493
      if (broadcast) {
4!
1494
        // TODO: Add abort signal to close when request closes.
1495
        const tx = await req.wallet.sendRevoke(name, options);
4✔
1496
        return res.json(200, tx.getJSON(this.network));
4✔
1497
      }
1498

1499
      // TODO: Add create TX with locks for used Coins and/or
1500
      // adds to the pending list.
1501
      const mtx = await req.wallet.createRevoke(name, options);
×
1502

1503
      if (sign)
×
1504
        await req.wallet.sign(mtx, options.passphrase);
×
1505

1506
      const json = mtx.getJSON(this.network);
×
1507

1508
      if (options.paths)
×
1509
        await this.addOutputPaths(json, mtx, req.wallet);
×
1510

1511
      return res.json(200, json);
×
1512
    });
1513
  }
1514

1515
  /**
1516
   * Add wallet path information to JSON outputs
1517
   * @private
1518
   */
1519

1520
   async addOutputPaths(json, tx, wallet) {
1521
    for (let i = 0; i < tx.outputs.length; i++) {
2✔
1522
      const {address} = tx.outputs[i];
4✔
1523
      const path = await wallet.getPath(address);
4✔
1524

1525
      if (!path)
4✔
1526
        continue;
1✔
1527

1528
      json.outputs[i].path = path.getJSON(this.network);
3✔
1529
    }
1530

1531
    return json;
2✔
1532
   }
1533

1534
  /**
1535
   * Initialize websockets.
1536
   * @private
1537
   */
1538

1539
  initSockets() {
1540
    const handleTX = (event, wallet, tx, details) => {
104✔
1541
      const name = `w:${wallet.id}`;
15,934✔
1542

1543
      if (!this.channel(name) && !this.channel('w:*'))
15,934✔
1544
        return;
15,586✔
1545

1546
      const json = details.getJSON(this.network, this.wdb.liveHeight());
348✔
1547

1548
      if (this.channel(name))
348!
1549
        this.to(name, event, wallet.id, json);
348✔
1550

1551
      if (this.channel('w:*'))
348!
1552
        this.to('w:*', event, wallet.id, json);
×
1553
    };
1554

1555
    this.wdb.on('tx', (wallet, tx, details) => {
104✔
1556
      handleTX('tx', wallet, tx, details);
11,127✔
1557
    });
1558

1559
    this.wdb.on('confirmed', (wallet, tx, details) => {
104✔
1560
      handleTX('confirmed', wallet, tx, details);
3,899✔
1561
    });
1562

1563
    this.wdb.on('unconfirmed', (wallet, tx, details) => {
104✔
1564
      handleTX('unconfirmed', wallet, tx, details);
907✔
1565
    });
1566

1567
    this.wdb.on('conflict', (wallet, tx, details) => {
104✔
1568
      handleTX('conflict', wallet, tx, details);
1✔
1569
    });
1570

1571
    this.wdb.on('balance', (wallet, balance) => {
104✔
1572
      const name = `w:${wallet.id}`;
17,046✔
1573

1574
      if (!this.channel(name) && !this.channel('w:*'))
17,046✔
1575
        return;
16,698✔
1576

1577
      const json = balance.toJSON();
348✔
1578

1579
      if (this.channel(name))
348!
1580
        this.to(name, 'balance', wallet.id, json);
348✔
1581

1582
      if (this.channel('w:*'))
348!
1583
        this.to('w:*', 'balance', wallet.id, json);
×
1584
    });
1585

1586
    this.wdb.on('address', (wallet, receive) => {
104✔
1587
      const name = `w:${wallet.id}`;
2,350✔
1588

1589
      if (!this.channel(name) && !this.channel('w:*'))
2,350✔
1590
        return;
2,297✔
1591

1592
      const json = [];
53✔
1593

1594
      for (const addr of receive)
53✔
1595
        json.push(addr.getJSON(this.network));
53✔
1596

1597
      if (this.channel(name))
53!
1598
        this.to(name, 'address', wallet.id, json);
53✔
1599

1600
      if (this.channel('w:*'))
53!
1601
        this.to('w:*', 'address', wallet.id, json);
×
1602
    });
1603
  }
1604

1605
  /**
1606
   * Handle new websocket.
1607
   * @param {WebSocket} socket
1608
   */
1609

1610
  handleSocket(socket) {
1611
    socket.hook('auth', (...args) => {
67✔
1612
      if (socket.channel('auth'))
67!
1613
        throw new Error('Already authed.');
×
1614

1615
      if (!this.options.noAuth) {
67✔
1616
        const valid = new Validator(args);
25✔
1617
        const key = valid.str(0, '');
25✔
1618

1619
        if (key.length > 255)
25!
1620
          throw new Error('Invalid API key.');
×
1621

1622
        const data = Buffer.from(key, 'utf8');
25✔
1623
        const hash = sha256.digest(data);
25✔
1624

1625
        if (!safeEqual(hash, this.options.apiHash))
25!
1626
          throw new Error('Invalid API key.');
×
1627
      }
1628

1629
      socket.join('auth');
67✔
1630

1631
      this.logger.info('Successful auth from %s.', socket.host);
67✔
1632

1633
      this.handleAuth(socket);
67✔
1634

1635
      return null;
67✔
1636
    });
1637
  }
1638

1639
  /**
1640
   * Handle new auth'd websocket.
1641
   * @private
1642
   * @param {WebSocket} socket
1643
   */
1644

1645
  handleAuth(socket) {
1646
    socket.hook('join', async (...args) => {
67✔
1647
      const valid = new Validator(args);
10✔
1648
      const id = valid.str(0, '');
10✔
1649
      const token = valid.buf(1);
10✔
1650

1651
      if (!id)
10!
1652
        throw new Error('Invalid parameter.');
×
1653

1654
      if (!this.options.walletAuth) {
10!
1655
        socket.join('admin');
10✔
1656
      } else if (token) {
×
1657
        if (safeEqual(token, this.options.adminToken))
×
1658
          socket.join('admin');
×
1659
      }
1660

1661
      if (socket.channel('admin') || !this.options.walletAuth) {
10!
1662
        socket.join(`w:${id}`);
10✔
1663
        return null;
10✔
1664
      }
1665

1666
      if (id === '*')
×
1667
        throw new Error('Bad token.');
×
1668

1669
      if (!token)
×
1670
        throw new Error('Invalid parameter.');
×
1671

1672
      let wallet;
1673
      try {
×
1674
        wallet = await this.wdb.auth(id, token);
×
1675
      } catch (e) {
1676
        this.logger.info('Wallet auth failure for %s: %s.', id, e.message);
×
1677
        throw new Error('Bad token.');
×
1678
      }
1679

1680
      if (!wallet)
×
1681
        throw new Error('Wallet does not exist.');
×
1682

1683
      this.logger.info('Successful wallet auth for %s.', id);
×
1684

1685
      socket.join(`w:${id}`);
×
1686

1687
      return null;
×
1688
    });
1689

1690
    socket.hook('leave', (...args) => {
67✔
1691
      const valid = new Validator(args);
×
1692
      const id = valid.str(0, '');
×
1693

1694
      if (!id)
×
1695
        throw new Error('Invalid parameter.');
×
1696

1697
      socket.leave(`w:${id}`);
×
1698

1699
      return null;
×
1700
    });
1701
  }
1702
}
1703

1704
class HTTPOptions {
1705
  /**
1706
   * HTTPOptions
1707
   * @alias module:http.HTTPOptions
1708
   * @constructor
1709
   * @param {Object} options
1710
   */
1711

1712
  constructor(options) {
1713
    this.network = Network.primary;
104✔
1714
    this.logger = null;
104✔
1715
    this.node = null;
104✔
1716
    this.apiKey = base58.encode(random.randomBytes(20));
104✔
1717
    this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
104✔
1718
    this.adminToken = random.randomBytes(32);
104✔
1719
    this.serviceHash = this.apiHash;
104✔
1720
    this.noAuth = false;
104✔
1721
    this.cors = false;
104✔
1722
    this.walletAuth = false;
104✔
1723

1724
    this.prefix = null;
104✔
1725
    this.host = '127.0.0.1';
104✔
1726
    this.port = 8080;
104✔
1727
    this.ssl = false;
104✔
1728
    this.keyFile = null;
104✔
1729
    this.certFile = null;
104✔
1730

1731
    this.fromOptions(options);
104✔
1732
  }
1733

1734
  /**
1735
   * Inject properties from object.
1736
   * @private
1737
   * @param {Object} options
1738
   * @returns {HTTPOptions}
1739
   */
1740

1741
  fromOptions(options) {
1742
    assert(options);
104✔
1743
    assert(options.node && typeof options.node === 'object',
104✔
1744
      'HTTP Server requires a WalletDB.');
1745

1746
    this.node = options.node;
104✔
1747
    this.network = options.node.network;
104✔
1748
    this.logger = options.node.logger;
104✔
1749
    this.port = this.network.walletPort;
104✔
1750

1751
    if (options.logger != null) {
104!
1752
      assert(typeof options.logger === 'object');
104✔
1753
      this.logger = options.logger;
104✔
1754
    }
1755

1756
    if (options.apiKey != null) {
104✔
1757
      assert(typeof options.apiKey === 'string',
26✔
1758
        'API key must be a string.');
1759
      assert(options.apiKey.length <= 255,
26✔
1760
        'API key must be under 255 bytes.');
1761
      this.apiKey = options.apiKey;
26✔
1762
      this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
26✔
1763
    }
1764

1765
    if (options.adminToken != null) {
104!
1766
      if (typeof options.adminToken === 'string') {
×
1767
        assert(options.adminToken.length === 64,
×
1768
          'Admin token must be a 32 byte hex string.');
1769
        const token = Buffer.from(options.adminToken, 'hex');
×
1770
        assert(token.length === 32,
×
1771
          'Admin token must be a 32 byte hex string.');
1772
        this.adminToken = token;
×
1773
      } else {
1774
        assert(Buffer.isBuffer(options.adminToken),
×
1775
          'Admin token must be a hex string or buffer.');
1776
        assert(options.adminToken.length === 32,
×
1777
          'Admin token must be 32 bytes.');
1778
        this.adminToken = options.adminToken;
×
1779
      }
1780
    }
1781

1782
    if (options.noAuth != null) {
104!
1783
      assert(typeof options.noAuth === 'boolean');
×
1784
      this.noAuth = options.noAuth;
×
1785
    }
1786

1787
    if (options.cors != null) {
104!
1788
      assert(typeof options.cors === 'boolean');
×
1789
      this.cors = options.cors;
×
1790
    }
1791

1792
    if (options.walletAuth != null) {
104!
1793
      assert(typeof options.walletAuth === 'boolean');
×
1794
      this.walletAuth = options.walletAuth;
×
1795
    }
1796

1797
    if (options.prefix != null) {
104✔
1798
      assert(typeof options.prefix === 'string');
13✔
1799
      this.prefix = options.prefix;
13✔
1800
      this.keyFile = path.join(this.prefix, 'key.pem');
13✔
1801
      this.certFile = path.join(this.prefix, 'cert.pem');
13✔
1802
    }
1803

1804
    if (options.host != null) {
104!
1805
      assert(typeof options.host === 'string');
×
1806
      this.host = options.host;
×
1807
    }
1808

1809
    if (options.port != null) {
104✔
1810
      assert((options.port & 0xffff) === options.port,
39✔
1811
        'Port must be a number.');
1812
      this.port = options.port;
39✔
1813
    }
1814

1815
    if (options.ssl != null) {
104!
1816
      assert(typeof options.ssl === 'boolean');
×
1817
      this.ssl = options.ssl;
×
1818
    }
1819

1820
    if (options.keyFile != null) {
104!
1821
      assert(typeof options.keyFile === 'string');
×
1822
      this.keyFile = options.keyFile;
×
1823
    }
1824

1825
    if (options.certFile != null) {
104!
1826
      assert(typeof options.certFile === 'string');
×
1827
      this.certFile = options.certFile;
×
1828
    }
1829

1830
    // Allow no-auth implicitly
1831
    // if we're listening locally.
1832
    if (!options.apiKey) {
104✔
1833
      if (   this.host === '127.0.0.1'
78!
1834
          || this.host === '::1'
1835
          || this.host === 'localhost')
1836
        this.noAuth = true;
78✔
1837
    }
1838

1839
    return this;
104✔
1840
  }
1841

1842
  /**
1843
   * Instantiate http options from object.
1844
   * @param {Object} options
1845
   * @returns {HTTPOptions}
1846
   */
1847

1848
  static fromOptions(options) {
1849
    return new HTTPOptions().fromOptions(options);
×
1850
  }
1851
}
1852

1853
class TransactionOptions {
1854
  /**
1855
   * TransactionOptions
1856
   * @alias module:http.TransactionOptions
1857
   * @constructor
1858
   * @param {RequestValidator} [valid]
1859
   * @param {(NetworkType|Network)?} [network]
1860
   */
1861

1862
  constructor(valid, network) {
1863
    this.rate = null;
1,090✔
1864
    this.maxFee = null;
1,090✔
1865
    this.selection = null;
1,090✔
1866
    this.sweepdustMinValue = null;
1,090✔
1867
    this.smart = null;
1,090✔
1868
    this.account = null;
1,090✔
1869
    this.locktime = null;
1,090✔
1870
    this.sort = null;
1,090✔
1871
    this.subtractFee = null;
1,090✔
1872
    this.subtractIndex = null;
1,090✔
1873
    this.depth = null;
1,090✔
1874
    this.paths = null;
1,090✔
1875
    this.passphrase = null;
1,090✔
1876
    this.hardFee = null;
1,090✔
1877
    this.blocks = null;
1,090✔
1878
    this.network = network || Network.primary;
1,090!
1879
    this.outputs = [];
1,090✔
1880

1881
    if (valid)
1,090!
1882
      this.fromValidator(valid, network);
1,090✔
1883
  }
1884

1885
  /**
1886
   * Inject properties from Validator.
1887
   * @private
1888
   * @param {RequestValidator} valid
1889
   * @param {(NetworkType|Network)?} [network]
1890
   * @returns {TransactionOptions}
1891
   */
1892

1893
  fromValidator(valid, network) {
1894
    assert(valid);
1,090✔
1895

1896
    if (network)
1,090!
1897
      this.network = network;
1,090✔
1898

1899
    if (valid.has('rate'))
1,090!
NEW
1900
      this.rate = valid.u64('rate');
×
1901

1902
    if (valid.has('maxFee'))
1,090!
NEW
1903
      this.maxFee = valid.u64('maxFee');
×
1904

1905
    if (valid.has('selection'))
1,090!
NEW
1906
      this.selection = valid.str('selection');
×
1907

1908
    if (valid.has('sweepdustMinValue'))
1,090!
NEW
1909
      this.sweepdustMinValue = valid.u64('sweepdustMinValue');
×
1910

1911
    if (valid.has('smart'))
1,090!
NEW
1912
      this.smart = valid.bool('smart');
×
1913

1914
    if (valid.has('account'))
1,090✔
1915
      this.account = valid.str('account');
2✔
1916

1917
    if (valid.has('locktime'))
1,090✔
1918
      this.locktime = valid.u64('locktime');
1✔
1919

1920
    if (valid.has('sort'))
1,090✔
1921
      this.sort = valid.bool('sort');
1✔
1922

1923
    if (valid.has('subtractFee'))
1,090✔
1924
      this.subtractFee = valid.bool('subtractFee');
4✔
1925

1926
    if (valid.has('subtractIndex'))
1,090!
NEW
1927
      this.subtractIndex = valid.i32('subtractIndex');
×
1928

1929
    if (valid.has('confirmations') || valid.has('depth'))
1,090!
NEW
1930
      this.depth = valid.u32(['confirmations', 'depth']);
×
1931

1932
    if (valid.has('paths'))
1,090✔
1933
      this.paths = valid.bool('paths');
2✔
1934

1935
    if (valid.has('passphrase'))
1,090✔
1936
      this.passphrase = valid.str('passphrase');
50✔
1937

1938
    if (valid.has('hardFee'))
1,090✔
1939
      this.hardFee = valid.u64('hardFee');
92✔
1940

1941
    if (valid.has('blocks'))
1,090!
NEW
1942
      this.blocks = valid.u32('blocks');
×
1943

1944
    if (valid.has('outputs')) {
1,090✔
1945
      const outputs = valid.array('outputs');
955✔
1946

1947
      for (const output of outputs) {
955✔
1948
        const valid = new Validator(output);
979✔
1949

1950
        const addrstr = valid.str('address');
979✔
1951
        let addr;
1952

1953
        if (addrstr)
979!
1954
          addr = Address.fromString(addrstr, network);
979✔
1955

1956
        let covenant = valid.obj('covenant');
979✔
1957

1958
        if (covenant)
979✔
1959
          covenant = Covenant.fromJSON(covenant);
2✔
1960

1961
        this.outputs.push({
979✔
1962
          value: valid.u64('value'),
1963
          address: addr,
1964
          covenant: covenant
1965
        });
1966
      }
1967
    }
1968

1969
    return this;
1,090✔
1970
  }
1971

1972
  /**
1973
   * Instantiate transaction options
1974
   * from Validator.
1975
   * @param {RequestValidator} [valid]
1976
   * @param {(NetworkType|Network)?} [network]
1977
   * @returns {TransactionOptions}
1978
   */
1979

1980
  static fromValidator(valid, network) {
1981
    return new this(valid, network);
1,090✔
1982
  }
1983
}
1984

1985
/*
1986
 * Helpers
1987
 */
1988

1989
function enforce(value, msg) {
1990
  if (!value) {
509✔
1991
    const err = new Error(msg);
4✔
1992
    err.statusCode = 400;
4✔
1993
    throw err;
4✔
1994
  }
1995
}
1996

1997
/*
1998
 * Expose
1999
 */
2000

2001
HTTP.HTTP = HTTP;
1✔
2002
HTTP.TransactionOptions = TransactionOptions;
1✔
2003

2004
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