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

handshake-org / hsd / 10579987464

27 Aug 2024 02:17PM UTC coverage: 69.91% (-0.003%) from 69.913%
10579987464

push

github

nodech
Merge PR #899 from 'nodech/fix-methods'

7735 of 12889 branches covered (60.01%)

Branch coverage included in aggregate %.

11 of 15 new or added lines in 4 files covered. (73.33%)

2 existing lines in 2 files now uncovered.

24630 of 33406 relevant lines covered (73.73%)

34126.63 hits per line

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

61.49
/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
/**
32
 * HTTP
33
 * @alias module:wallet.HTTP
34
 */
35

36
class HTTP extends Server {
37
  /**
38
   * Create an http server.
39
   * @constructor
40
   * @param {Object} options
41
   */
42

43
  constructor(options) {
44
    super(new HTTPOptions(options));
90✔
45

46
    this.network = this.options.network;
90✔
47
    this.logger = this.options.logger.context('wallet-http');
90✔
48
    this.wdb = this.options.node.wdb;
90✔
49
    this.rpc = this.options.node.rpc;
90✔
50

51
    this.init();
90✔
52
  }
53

54
  /**
55
   * Initialize http server.
56
   * @private
57
   */
58

59
  init() {
60
    this.on('request', (req, res) => {
90✔
61
      if (req.method === 'POST' && req.pathname === '/')
1,885✔
62
        return;
412✔
63

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

68
    this.on('listening', (address) => {
90✔
69
      this.logger.info('Wallet HTTP server listening on %s (port=%d).',
91✔
70
        address.address, address.port);
71
    });
72

73
    this.initRouter();
90✔
74
    this.initSockets();
90✔
75
  }
76

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

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

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

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

98
    this.use(async (req, res) => {
90✔
99
      if (!this.options.walletAuth) {
1,885!
100
        req.admin = true;
1,885✔
101
        return;
1,885✔
102
      }
103

104
      const valid = Validator.fromRequest(req);
×
105
      const token = valid.buf('token');
×
106

107
      if (token && safeEqual(token, this.options.adminToken)) {
×
108
        req.admin = true;
×
109
        return;
×
110
      }
111

112
      if (req.method === 'POST' && req.path.length === 0) {
×
113
        res.json(403);
×
114
        return;
×
115
      }
116
    });
117

118
    this.use(this.jsonRPC());
90✔
119
    this.use(this.router());
90✔
120

121
    this.error((err, req, res) => {
90✔
122
      const code = err.statusCode || 500;
13✔
123
      res.json(code, {
13✔
124
        error: {
125
          type: err.type,
126
          code: err.code,
127
          message: err.message
128
        }
129
      });
130
    });
131

132
    this.hook(async (req, res) => {
90✔
133
      if (req.path.length < 2)
1,473!
134
        return;
×
135

136
      if (req.path[0] !== 'wallet')
1,473!
137
        return;
×
138

139
      if (req.method === 'PUT' && req.path.length === 2)
1,473✔
140
        return;
34✔
141

142
      const valid = Validator.fromRequest(req);
1,439✔
143
      const id = valid.str('id');
1,439✔
144
      const token = valid.buf('token');
1,439✔
145

146
      if (!id) {
1,439!
147
        res.json(403);
×
148
        return;
×
149
      }
150

151
      if (req.admin || !this.options.walletAuth) {
1,439!
152
        const wallet = await this.wdb.get(id);
1,439✔
153

154
        if (!wallet) {
1,439!
155
          res.json(404);
×
156
          return;
×
157
        }
158

159
        req.wallet = wallet;
1,439✔
160

161
        return;
1,439✔
162
      }
163

164
      if (!token) {
×
165
        res.json(403);
×
166
        return;
×
167
      }
168

169
      let wallet;
170
      try {
×
171
        wallet = await this.wdb.auth(id, token);
×
172
      } catch (err) {
173
        this.logger.info('Auth failure for %s: %s.', id, err.message);
×
174
        res.json(403);
×
175
        return;
×
176
      }
177

178
      if (!wallet) {
×
179
        res.json(404);
×
180
        return;
×
181
      }
182

183
      req.wallet = wallet;
×
184

185
      this.logger.info('Successful auth for %s.', id);
×
186
    });
187

188
    // Rescan
189
    this.post('/rescan', async (req, res) => {
90✔
190
      if (!req.admin) {
×
191
        res.json(403);
×
192
        return;
×
193
      }
194

195
      const valid = Validator.fromRequest(req);
×
196
      const height = valid.u32('height');
×
197

198
      res.json(200, { success: true });
×
199

200
      await this.wdb.rescan(height);
×
201
    });
202

203
    // Deep Clean
204
    this.post('/deepclean', async (req, res) => {
90✔
205
      if (!req.admin) {
×
206
        res.json(403);
×
207
        return;
×
208
      }
209

210
      const valid = Validator.fromRequest(req);
×
211
      const disclaimer = valid.bool('I_HAVE_BACKED_UP_MY_WALLET', false);
×
212

213
      enforce(
×
214
        disclaimer,
215
        'Deep Clean requires I_HAVE_BACKED_UP_MY_WALLET=true'
216
      );
217

218
      await this.wdb.deepClean();
×
219

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

223
    // Resend
224
    this.post('/resend', async (req, res) => {
90✔
225
      if (!req.admin) {
×
226
        res.json(403);
×
227
        return;
×
228
      }
229

230
      await this.wdb.resend();
×
231

232
      res.json(200, { success: true });
×
233
    });
234

235
    // Backup WalletDB
236
    this.post('/backup', async (req, res) => {
90✔
237
      if (!req.admin) {
×
238
        res.json(403);
×
239
        return;
×
240
      }
241

242
      const valid = Validator.fromRequest(req);
×
243
      const path = valid.str('path');
×
244

245
      enforce(path, 'Path is required.');
×
246

247
      await this.wdb.backup(path);
×
248

249
      res.json(200, { success: true });
×
250
    });
251

252
    // List wallets
253
    this.get('/wallet', async (req, res) => {
90✔
254
      if (!req.admin) {
×
255
        res.json(403);
×
256
        return;
×
257
      }
258

259
      const wallets = await this.wdb.getWallets();
×
260
      res.json(200, wallets);
×
261
    });
262

263
    // Get wallet
264
    this.get('/wallet/:id', async (req, res) => {
90✔
265
      const balance = await req.wallet.getBalance();
2✔
266
      res.json(200, req.wallet.getJSON(false, balance));
2✔
267
    });
268

269
    // Get wallet master key
270
    this.get('/wallet/:id/master', (req, res) => {
90✔
271
      if (!req.admin) {
3!
272
        res.json(403);
×
273
        return;
×
274
      }
275

276
      res.json(200, req.wallet.master.getJSON(this.network, true));
3✔
277
    });
278

279
    // Create wallet
280
    this.put('/wallet/:id', async (req, res) => {
90✔
281
      const valid = Validator.fromRequest(req);
34✔
282

283
      let master = valid.str('master');
34✔
284
      let mnemonic = valid.str('mnemonic');
34✔
285
      let accountKey = valid.str('accountKey');
34✔
286
      const language = valid.str('language');
34✔
287

288
      if (master)
34!
289
        master = HDPrivateKey.fromBase58(master, this.network);
×
290

291
      if (mnemonic)
34✔
292
        mnemonic = Mnemonic.fromPhrase(mnemonic);
6✔
293

294
      if (accountKey)
34✔
295
        accountKey = HDPublicKey.fromBase58(accountKey, this.network);
2✔
296

297
      const wallet = await this.wdb.create({
34✔
298
        id: valid.str('id'),
299
        type: valid.str('type'),
300
        m: valid.u32('m'),
301
        n: valid.u32('n'),
302
        passphrase: valid.str('passphrase'),
303
        bip39Passphrase: valid.str('bip39Passphrase'),
304
        master: master,
305
        mnemonic: mnemonic,
306
        accountKey: accountKey,
307
        watchOnly: valid.bool('watchOnly'),
308
        lookahead: valid.u32('lookahead'),
309
        language: language
310
      });
311

312
      const balance = await wallet.getBalance();
34✔
313

314
      res.json(200, wallet.getJSON(false, balance));
34✔
315
    });
316

317
    // List accounts
318
    this.get('/wallet/:id/account', async (req, res) => {
90✔
319
      const accounts = await req.wallet.getAccounts();
×
320
      res.json(200, accounts);
×
321
    });
322

323
    // Get account
324
    this.get('/wallet/:id/account/:account', async (req, res) => {
90✔
325
      const valid = Validator.fromRequest(req);
116✔
326
      const acct = valid.str('account');
116✔
327
      const account = await req.wallet.getAccount(acct);
116✔
328

329
      if (!account) {
116!
330
        res.json(404);
×
331
        return;
×
332
      }
333

334
      const balance = await req.wallet.getBalance(account.accountIndex);
116✔
335

336
      res.json(200, account.getJSON(balance));
116✔
337
    });
338

339
    // Create account
340
    this.put('/wallet/:id/account/:account', async (req, res) => {
90✔
341
      const valid = Validator.fromRequest(req);
5✔
342
      const passphrase = valid.str('passphrase');
5✔
343

344
      let accountKey = valid.get('accountKey');
5✔
345

346
      if (accountKey)
5!
347
        accountKey = HDPublicKey.fromBase58(accountKey, this.network);
×
348

349
      const options = {
5✔
350
        name: valid.str('account'),
351
        watchOnly: valid.bool('watchOnly'),
352
        type: valid.str('type'),
353
        m: valid.u32('m'),
354
        n: valid.u32('n'),
355
        accountKey: accountKey,
356
        lookahead: valid.u32('lookahead')
357
      };
358

359
      const account = await req.wallet.createAccount(options, passphrase);
5✔
360
      const balance = await req.wallet.getBalance(account.accountIndex);
5✔
361

362
      res.json(200, account.getJSON(balance));
5✔
363
    });
364

365
    // Modify account
366
    this.patch('/wallet/:id/account/:account', async (req, res) => {
90✔
367
      const valid = Validator.fromRequest(req);
1✔
368
      const passphrase = valid.str('passphrase');
1✔
369
      const acct = valid.str('account');
1✔
370

371
      const options = {
1✔
372
        lookahead: valid.u32('lookahead')
373
      };
374

375
      const account = await req.wallet.modifyAccount(acct, options, passphrase);
1✔
376
      const balance = await req.wallet.getBalance(account.accountIndex);
1✔
377

378
      res.json(200, account.getJSON(balance));
1✔
379
    });
380

381
    // Change passphrase
382
    this.post('/wallet/:id/passphrase', async (req, res) => {
90✔
383
      const valid = Validator.fromRequest(req);
×
384
      const passphrase = valid.str('passphrase');
×
385
      const old = valid.str('old');
×
386

387
      enforce(passphrase, 'Passphrase is required.');
×
388

389
      await req.wallet.setPassphrase(passphrase, old);
×
390

391
      res.json(200, { success: true });
×
392
    });
393

394
    // Unlock wallet
395
    this.post('/wallet/:id/unlock', async (req, res) => {
90✔
396
      const valid = Validator.fromRequest(req);
×
397
      const passphrase = valid.str('passphrase');
×
398
      const timeout = valid.u32('timeout');
×
399

400
      enforce(passphrase, 'Passphrase is required.');
×
401

402
      await req.wallet.unlock(passphrase, timeout);
×
403

404
      res.json(200, { success: true });
×
405
    });
406

407
    // Lock wallet
408
    this.post('/wallet/:id/lock', async (req, res) => {
90✔
409
      await req.wallet.lock();
22✔
410
      res.json(200, { success: true });
22✔
411
    });
412

413
    // Import key
414
    this.post('/wallet/:id/import', async (req, res) => {
90✔
415
      const valid = Validator.fromRequest(req);
×
416
      const acct = valid.str('account');
×
417
      const passphrase = valid.str('passphrase');
×
418
      const pub = valid.buf('publicKey');
×
419
      const priv = valid.str('privateKey');
×
420
      const b58 = valid.str('address');
×
421

422
      if (pub) {
×
423
        const key = KeyRing.fromPublic(pub);
×
424
        await req.wallet.importKey(acct, key);
×
425
        res.json(200, { success: true });
×
426
        return;
×
427
      }
428

429
      if (priv) {
×
430
        const key = KeyRing.fromSecret(priv, this.network);
×
431
        await req.wallet.importKey(acct, key, passphrase);
×
432
        res.json(200, { success: true });
×
433
        return;
×
434
      }
435

436
      if (b58) {
×
437
        const addr = Address.fromString(b58, this.network);
×
438
        await req.wallet.importAddress(acct, addr);
×
439
        res.json(200, { success: true });
×
440
        return;
×
441
      }
442

443
      enforce(false, 'Key or address is required.');
×
444
    });
445

446
    // Generate new token
447
    this.post('/wallet/:id/retoken', async (req, res) => {
90✔
448
      const valid = Validator.fromRequest(req);
×
449
      const passphrase = valid.str('passphrase');
×
450
      const token = await req.wallet.retoken(passphrase);
×
451

452
      res.json(200, {
×
453
        token: token.toString('hex')
454
      });
455
    });
456

457
    // Send TX
458
    this.post('/wallet/:id/send', async (req, res) => {
90✔
459
      const valid = Validator.fromRequest(req);
96✔
460

461
      const options = TransactionOptions.fromValidator(valid);
96✔
462
      const tx = await req.wallet.send(options);
96✔
463

464
      const details = await req.wallet.getDetails(tx.hash());
96✔
465

466
      res.json(200, details.getJSON(this.network, this.wdb.height));
96✔
467
    });
468

469
    // Create TX
470
    this.post('/wallet/:id/create', async (req, res) => {
90✔
471
      const valid = Validator.fromRequest(req);
7✔
472
      const sign = valid.bool('sign', true);
7✔
473

474
      // TODO: Add create TX with locks for used Coins and/or
475
      // adds to the pending list.
476
      const options = TransactionOptions.fromValidator(valid);
7✔
477
      const tx = await req.wallet.createTX(options);
7✔
478

479
      if (sign)
7!
480
        await req.wallet.sign(tx, options.passphrase);
7✔
481

482
      const json = tx.getJSON(this.network);
7✔
483

484
      if (options.paths)
7✔
485
        await this.addOutputPaths(json, tx, req.wallet);
2✔
486

487
      res.json(200, json);
7✔
488
    });
489

490
    // Sign TX
491
    this.post('/wallet/:id/sign', async (req, res) => {
90✔
492
      const valid = Validator.fromRequest(req);
6✔
493
      const passphrase = valid.str('passphrase');
6✔
494
      const raw = valid.buf('tx');
6✔
495

496
      enforce(raw, 'TX is required.');
6✔
497

498
      const tx = MTX.decode(raw);
6✔
499
      tx.view = await req.wallet.getCoinView(tx);
6✔
500

501
      await req.wallet.sign(tx, passphrase);
6✔
502

503
      res.json(200, tx.getJSON(this.network));
6✔
504
    });
505

506
    // Zap Wallet TXs
507
    this.post('/wallet/:id/zap', async (req, res) => {
90✔
508
      const valid = Validator.fromRequest(req);
×
509
      const acct = valid.str('account');
×
510
      const age = valid.u32('age');
×
511

512
      enforce(age, 'Age is required.');
×
513

514
      await req.wallet.zap(acct, age);
×
515

516
      res.json(200, { success: true });
×
517
    });
518

519
    // Abandon Wallet TX
520
    this.del('/wallet/:id/tx/:hash', async (req, res) => {
90✔
521
      const valid = Validator.fromRequest(req);
2✔
522
      const hash = valid.bhash('hash');
2✔
523

524
      enforce(hash, 'Hash is required.');
2✔
525

526
      await req.wallet.abandon(hash);
2✔
527

528
      res.json(200, { success: true });
2✔
529
    });
530

531
    // List blocks
532
    this.get('/wallet/:id/block', async (req, res) => {
90✔
533
      const heights = await req.wallet.getBlocks();
×
534
      res.json(200, heights);
×
535
    });
536

537
    // Get Block Record
538
    this.get('/wallet/:id/block/:height', async (req, res) => {
90✔
539
      const valid = Validator.fromRequest(req);
×
540
      const height = valid.u32('height');
×
541

542
      enforce(height != null, 'Height is required.');
×
543

544
      const block = await req.wallet.getBlock(height);
×
545

546
      if (!block) {
×
547
        res.json(404);
×
548
        return;
×
549
      }
550

551
      res.json(200, block.toJSON());
×
552
    });
553

554
    // Add key
555
    this.put('/wallet/:id/shared-key', async (req, res) => {
90✔
556
      const valid = Validator.fromRequest(req);
3✔
557
      const acct = valid.str('account');
3✔
558
      const b58 = valid.str('accountKey');
3✔
559

560
      enforce(b58, 'Key is required.');
3✔
561

562
      const key = HDPublicKey.fromBase58(b58, this.network);
3✔
563
      const added = await req.wallet.addSharedKey(acct, key);
3✔
564

565
      res.json(200, {
3✔
566
        success: true,
567
        addedKey: added
568
      });
569
    });
570

571
    // Remove key
572
    this.del('/wallet/:id/shared-key', async (req, res) => {
90✔
573
      const valid = Validator.fromRequest(req);
×
574
      const acct = valid.str('account');
×
575
      const b58 = valid.str('accountKey');
×
576

577
      enforce(b58, 'Key is required.');
×
578

579
      const key = HDPublicKey.fromBase58(b58, this.network);
×
580
      const removed = await req.wallet.removeSharedKey(acct, key);
×
581

582
      res.json(200, {
×
583
        success: true,
584
        removedKey: removed
585
      });
586
    });
587

588
    // Get key by address
589
    this.get('/wallet/:id/key/:address', async (req, res) => {
90✔
590
      const valid = Validator.fromRequest(req);
19✔
591
      const b58 = valid.str('address');
19✔
592

593
      enforce(b58, 'Address is required.');
19✔
594

595
      const addr = Address.fromString(b58, this.network);
19✔
596
      const key = await req.wallet.getKey(addr);
19✔
597

598
      if (!key) {
19!
599
        res.json(404);
×
600
        return;
×
601
      }
602

603
      res.json(200, key.getJSON(this.network));
19✔
604
    });
605

606
    // Get private key
607
    this.get('/wallet/:id/wif/:address', async (req, res) => {
90✔
608
      const valid = Validator.fromRequest(req);
16✔
609
      const b58 = valid.str('address');
16✔
610
      const passphrase = valid.str('passphrase');
16✔
611

612
      enforce(b58, 'Address is required.');
16✔
613

614
      const addr = Address.fromString(b58, this.network);
16✔
615
      const key = await req.wallet.getPrivateKey(addr, passphrase);
16✔
616

617
      if (!key) {
16!
618
        res.json(404);
×
619
        return;
×
620
      }
621

622
      res.json(200, { privateKey: key.toSecret(this.network) });
16✔
623
    });
624

625
    // Create address
626
    this.post('/wallet/:id/address', async (req, res) => {
90✔
627
      const valid = Validator.fromRequest(req);
918✔
628
      const acct = valid.str('account');
918✔
629
      const addr = await req.wallet.createReceive(acct);
918✔
630

631
      res.json(200, addr.getJSON(this.network));
918✔
632
    });
633

634
    // Create change address
635
    this.post('/wallet/:id/change', async (req, res) => {
90✔
636
      const valid = Validator.fromRequest(req);
2✔
637
      const acct = valid.str('account');
2✔
638
      const addr = await req.wallet.createChange(acct);
2✔
639

640
      res.json(200, addr.getJSON(this.network));
2✔
641
    });
642

643
    // Wallet Balance
644
    this.get('/wallet/:id/balance', async (req, res) => {
90✔
645
      const valid = Validator.fromRequest(req);
57✔
646
      const acct = valid.str('account');
57✔
647
      const balance = await req.wallet.getBalance(acct);
57✔
648

649
      if (!balance) {
57!
650
        res.json(404);
×
651
        return;
×
652
      }
653

654
      res.json(200, balance.toJSON());
57✔
655
    });
656

657
    // Wallet UTXOs
658
    this.get('/wallet/:id/coin', async (req, res) => {
90✔
659
      const valid = Validator.fromRequest(req);
1✔
660
      const acct = valid.str('account');
1✔
661
      const coins = await req.wallet.getCoins(acct);
1✔
662
      const result = [];
1✔
663

664
      common.sortCoins(coins);
1✔
665

666
      for (const coin of coins)
1✔
667
        result.push(coin.getJSON(this.network));
20✔
668

669
      res.json(200, result);
1✔
670
    });
671

672
    // Locked coins
673
    this.get('/wallet/:id/locked', async (req, res) => {
90✔
674
      const locked = req.wallet.getLocked();
×
675
      const result = [];
×
676

677
      for (const outpoint of locked)
×
678
        result.push(outpoint.toJSON());
×
679

680
      res.json(200, result);
×
681
    });
682

683
    // Lock coin
684
    this.put('/wallet/:id/locked/:hash/:index', async (req, res) => {
90✔
685
      const valid = Validator.fromRequest(req);
×
686
      const hash = valid.bhash('hash');
×
687
      const index = valid.u32('index');
×
688

689
      enforce(hash, 'Hash is required.');
×
690
      enforce(index != null, 'Index is required.');
×
691

692
      const outpoint = new Outpoint(hash, index);
×
693

694
      req.wallet.lockCoin(outpoint);
×
695

696
      res.json(200, { success: true });
×
697
    });
698

699
    // Unlock coin
700
    this.del('/wallet/:id/locked/:hash/:index', async (req, res) => {
90✔
701
      const valid = Validator.fromRequest(req);
×
702
      const hash = valid.bhash('hash');
×
703
      const index = valid.u32('index');
×
704

705
      enforce(hash, 'Hash is required.');
×
706
      enforce(index != null, 'Index is required.');
×
707

708
      const outpoint = new Outpoint(hash, index);
×
709

710
      req.wallet.unlockCoin(outpoint);
×
711

712
      res.json(200, { success: true });
×
713
    });
714

715
    // Wallet Coin
716
    this.get('/wallet/:id/coin/:hash/:index', async (req, res) => {
90✔
717
      const valid = Validator.fromRequest(req);
2✔
718
      const hash = valid.bhash('hash');
2✔
719
      const index = valid.u32('index');
2✔
720

721
      enforce(hash, 'Hash is required.');
2✔
722
      enforce(index != null, 'Index is required.');
2✔
723

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

726
      if (!coin) {
2!
727
        res.json(404);
×
728
        return;
×
729
      }
730

731
      res.json(200, coin.getJSON(this.network));
2✔
732
    });
733

734
    // Wallet TXs
735
    this.get('/wallet/:id/tx/history', async (req, res) => {
90✔
736
      const valid = Validator.fromRequest(req);
×
737
      const acct = valid.str('account');
×
738
      const txs = await req.wallet.getHistory(acct);
×
739

740
      common.sortTX(txs);
×
741

742
      const details = await req.wallet.toDetails(txs);
×
743

744
      const result = [];
×
745

746
      for (const item of details)
×
747
        result.push(item.getJSON(this.network, this.wdb.height));
×
748

749
      res.json(200, result);
×
750
    });
751

752
    // Wallet Pending TXs
753
    this.get('/wallet/:id/tx/unconfirmed', async (req, res) => {
90✔
754
      const valid = Validator.fromRequest(req);
×
755
      const acct = valid.str('account');
×
756
      const txs = await req.wallet.getPending(acct);
×
757

758
      common.sortTX(txs);
×
759

760
      const details = await req.wallet.toDetails(txs);
×
761
      const result = [];
×
762

763
      for (const item of details)
×
764
        result.push(item.getJSON(this.network, this.wdb.height));
×
765

766
      res.json(200, result);
×
767
    });
768

769
    // Wallet TXs within time range
770
    this.get('/wallet/:id/tx/range', async (req, res) => {
90✔
771
      const valid = Validator.fromRequest(req);
×
772
      const acct = valid.str('account');
×
773

774
      const options = {
×
775
        start: valid.u32('start'),
776
        end: valid.u32('end'),
777
        limit: valid.u32('limit'),
778
        reverse: valid.bool('reverse')
779
      };
780

781
      const txs = await req.wallet.getRange(acct, options);
×
782
      const details = await req.wallet.toDetails(txs);
×
783
      const result = [];
×
784

785
      for (const item of details)
×
786
        result.push(item.getJSON(this.network, this.wdb.height));
×
787

788
      res.json(200, result);
×
789
    });
790

791
    // Last Wallet TXs
792
    this.get('/wallet/:id/tx/last', async (req, res) => {
90✔
793
      const valid = Validator.fromRequest(req);
×
794
      const acct = valid.str('account');
×
795
      const limit = valid.u32('limit');
×
796
      const txs = await req.wallet.getLast(acct, limit);
×
797
      const details = await req.wallet.toDetails(txs);
×
798
      const result = [];
×
799

800
      for (const item of details)
×
801
        result.push(item.getJSON(this.network, this.wdb.height));
×
802

803
      res.json(200, result);
×
804
    });
805

806
    // Wallet TX
807
    this.get('/wallet/:id/tx/:hash', async (req, res) => {
90✔
808
      const valid = Validator.fromRequest(req);
2✔
809
      const hash = valid.bhash('hash');
2✔
810

811
      enforce(hash, 'Hash is required.');
2✔
812

813
      const tx = await req.wallet.getTX(hash);
2✔
814

815
      if (!tx) {
2✔
816
        res.json(404);
1✔
817
        return;
1✔
818
      }
819

820
      const details = await req.wallet.toDetails(tx);
1✔
821

822
      res.json(200, details.getJSON(this.network, this.wdb.height));
1✔
823
    });
824

825
    // Resend
826
    this.post('/wallet/:id/resend', async (req, res) => {
90✔
827
      await req.wallet.resend();
×
828
      res.json(200, { success: true });
×
829
    });
830

831
    // Wallet Name States
832
    this.get('/wallet/:id/name', async (req, res) => {
90✔
833
      const valid = Validator.fromRequest(req);
5✔
834
      const own = valid.bool('own', false);
5✔
835

836
      const height = this.wdb.height;
5✔
837
      const network = this.network;
5✔
838

839
      const names = await req.wallet.getNames();
5✔
840
      const items = [];
5✔
841

842
      for (const ns of names) {
5✔
843
        if (own) {
50✔
844
          const {hash, index} = ns.owner;
18✔
845
          const coin = await req.wallet.getCoin(hash, index);
18✔
846

847
          if (coin)
18✔
848
            items.push(ns.getJSON(height, network));
7✔
849
        } else {
850
          items.push(ns.getJSON(height, network));
32✔
851
        }
852
      }
853

854
      res.json(200, items);
5✔
855
    });
856

857
    // Wallet Name State
858
    this.get('/wallet/:id/name/:name', async (req, res) => {
90✔
859
      const valid = Validator.fromRequest(req);
2✔
860
      const name = valid.str('name');
2✔
861

862
      enforce(name, 'Must pass name.');
2✔
863
      enforce(rules.verifyName(name), 'Must pass valid name.');
2✔
864

865
      const height = this.wdb.height;
2✔
866
      const network = this.network;
2✔
867
      const ns = await req.wallet.getNameStateByName(name);
2✔
868

869
      if (!ns)
2!
870
        return res.json(404);
×
871

872
      return res.json(200, ns.getJSON(height, network));
2✔
873
    });
874

875
    // Wallet Auctions
876
    this.get('/wallet/:id/auction', async (req, res) => {
90✔
877
      const height = this.wdb.height;
×
878
      const network = this.network;
×
879

880
      const names = await req.wallet.getNames();
×
881
      const items = [];
×
882

883
      for (const ns of names) {
×
884
        const bids = await req.wallet.getBidsByName(ns.name);
×
885
        const reveals = await req.wallet.getRevealsByName(ns.name);
×
886
        const info = ns.getJSON(height, network);
×
887

888
        info.bids = [];
×
889
        info.reveals = [];
×
890

891
        for (const bid of bids)
×
892
          info.bids.push(bid.toJSON());
×
893

894
        for (const reveal of reveals)
×
895
          info.reveals.push(reveal.toJSON());
×
896

897
        items.push(info);
×
898
      }
899

900
      return res.json(200, items);
×
901
    });
902

903
    // Wallet Auction
904
    this.get('/wallet/:id/auction/:name', async (req, res) => {
90✔
905
      const valid = Validator.fromRequest(req);
3✔
906
      const name = valid.str('name');
3✔
907

908
      enforce(name, 'Must pass name.');
3✔
909
      enforce(rules.verifyName(name), 'Must pass valid name.');
3✔
910

911
      const height = this.wdb.height;
3✔
912
      const network = this.network;
3✔
913

914
      const ns = await req.wallet.getNameStateByName(name);
3✔
915

916
      if (!ns)
3!
917
        return res.json(404);
×
918

919
      const bids = await req.wallet.getBidsByName(name);
3✔
920
      const reveals = await req.wallet.getRevealsByName(name);
3✔
921

922
      const info = ns.getJSON(height, network);
3✔
923
      info.bids = [];
3✔
924
      info.reveals = [];
3✔
925

926
      for (const bid of bids)
3✔
927
        info.bids.push(bid.toJSON());
4✔
928

929
      for (const reveal of reveals)
3✔
930
        info.reveals.push(reveal.toJSON());
3✔
931

932
      return res.json(200, info);
3✔
933
    });
934

935
    // All Wallet Bids
936
    this.get('/wallet/:id/bid', async (req, res) => {
90✔
937
      const valid = Validator.fromRequest(req);
1✔
938
      const own = valid.bool('own', false);
1✔
939

940
      const bids = await req.wallet.getBidsByName();
1✔
941
      const items = [];
1✔
942

943
      for (const bid of bids) {
1✔
944
        if (!own || bid.own)
3!
945
          items.push(bid.toJSON());
3✔
946
      }
947

948
      res.json(200, items);
1✔
949
    });
950

951
    // Wallet Bids by Name
952
    this.get('/wallet/:id/bid/:name', async (req, res) => {
90✔
953
      const valid = Validator.fromRequest(req);
2✔
954
      const name = valid.str('name');
2✔
955
      let own = valid.bool('own', false);
2✔
956

957
      if (name)
2!
958
        enforce(rules.verifyName(name), 'Must pass valid name.');
2✔
959

960
      if (!name)
2!
961
        own = true;
×
962

963
      const bids = await req.wallet.getBidsByName(name);
2✔
964
      const items = [];
2✔
965

966
      for (const bid of bids) {
2✔
967
        if (!own || bid.own)
4✔
968
          items.push(bid.toJSON());
3✔
969
      }
970

971
      res.json(200, items);
2✔
972
    });
973

974
    // All Wallet Reveals
975
    this.get('/wallet/:id/reveal', async (req, res) => {
90✔
976
      const valid = Validator.fromRequest(req);
1✔
977
      const own = valid.bool('own', false);
1✔
978

979
      const reveals = await req.wallet.getRevealsByName();
1✔
980
      const items = [];
1✔
981

982
      for (const brv of reveals) {
1✔
983
        if (!own || brv.own)
2!
984
          items.push(brv.toJSON());
2✔
985
      }
986

987
      res.json(200, items);
1✔
988
    });
989

990
    // Wallet Reveals by Name
991
    this.get('/wallet/:id/reveal/:name', async (req, res) => {
90✔
992
      const valid = Validator.fromRequest(req);
3✔
993
      const name = valid.str('name');
3✔
994
      let own = valid.bool('own', false);
3✔
995

996
      if (name)
3!
997
        enforce(rules.verifyName(name), 'Must pass valid name.');
3✔
998

999
      if (!name)
3!
1000
        own = true;
×
1001

1002
      const reveals = await req.wallet.getRevealsByName(name);
3✔
1003
      const items = [];
3✔
1004

1005
      for (const brv of reveals) {
3✔
1006
        if (!own || brv.own)
5✔
1007
          items.push(brv.toJSON());
4✔
1008
      }
1009

1010
      res.json(200, items);
3✔
1011
    });
1012

1013
    // Name Resource
1014
    this.get('/wallet/:id/resource/:name', async (req, res) => {
90✔
1015
      const valid = Validator.fromRequest(req);
2✔
1016
      const name = valid.str('name');
2✔
1017

1018
      enforce(name, 'Must pass name.');
2✔
1019
      enforce(rules.verifyName(name), 'Must pass valid name.');
2✔
1020

1021
      const ns = await req.wallet.getNameStateByName(name);
2✔
1022

1023
      if (!ns || ns.data.length === 0)
2✔
1024
        return res.json(404);
1✔
1025

1026
      try {
1✔
1027
        const resource = Resource.decode(ns.data);
1✔
1028
        return res.json(200, resource.toJSON());
1✔
1029
      } catch (e) {
1030
        return res.json(400);
×
1031
      }
1032
    });
1033

1034
    // Regenerate Nonce
1035
    this.get('/wallet/:id/nonce/:name', async (req, res) => {
90✔
1036
      const valid = Validator.fromRequest(req);
2✔
1037
      const name = valid.str('name');
2✔
1038
      const addr = valid.str('address');
2✔
1039
      const bid = valid.ufixed('bid');
2✔
1040

1041
      enforce(name, 'Name is required.');
2✔
1042
      enforce(rules.verifyName(name), 'Valid name is required.');
2✔
1043
      enforce(addr, 'Address is required.');
2✔
1044
      enforce(bid != null, 'Bid is required.');
2✔
1045

1046
      let address;
1047
      try {
2✔
1048
        address = Address.fromString(addr, this.network);
2✔
1049
      } catch (e) {
1050
        return req.json(400);
×
1051
      }
1052

1053
      const nameHash = rules.hashName(name);
2✔
1054
      const nonces = await req.wallet.generateNonces(nameHash, address, bid);
2✔
1055
      const blinds = nonces.map(nonce => rules.blind(bid, nonce));
2✔
1056

1057
      return res.json(200, {
2✔
1058
        address: address.toString(this.network),
1059
        blinds: blinds.map(blind => blind.toString('hex')),
2✔
1060
        nonces: nonces.map(nonce => nonce.toString('hex')),
2✔
1061
        bid: bid,
1062
        name: name,
1063
        nameHash: nameHash.toString('hex')
1064
      });
1065
    });
1066

1067
    // Create Open
1068
    this.post('/wallet/:id/open', async (req, res) => {
90✔
1069
      const valid = Validator.fromRequest(req);
31✔
1070
      const name = valid.str('name');
31✔
1071
      const broadcast = valid.bool('broadcast', true);
31✔
1072
      const sign = valid.bool('sign', true);
31✔
1073

1074
      enforce(name, 'Name is required.');
31✔
1075
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
31✔
1076

1077
      const options = TransactionOptions.fromValidator(valid);
30✔
1078

1079
      if (broadcast) {
30✔
1080
        // TODO: Add abort signal to close when request closes.
1081
        const tx = await req.wallet.sendOpen(name, options);
28✔
1082
        return res.json(200, tx.getJSON(this.network));
24✔
1083
      }
1084

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

1089
      if (sign)
2✔
1090
        await req.wallet.sign(mtx, options.passphrase);
1✔
1091

1092
      const json = mtx.getJSON(this.network);
2✔
1093

1094
      if (options.paths)
2!
1095
        await this.addOutputPaths(json, mtx, req.wallet);
×
1096

1097
      return res.json(200, json);
2✔
1098
    });
1099

1100
    // Create Bid
1101
    this.post('/wallet/:id/bid', async (req, res) => {
90✔
1102
      const valid = Validator.fromRequest(req);
39✔
1103
      const name = valid.str('name');
39✔
1104
      const bid = valid.u64('bid');
39✔
1105
      const lockup = valid.u64('lockup');
39✔
1106
      const broadcast = valid.bool('broadcast', true);
39✔
1107
      const sign = valid.bool('sign', true);
39✔
1108

1109
      enforce(name, 'Name is required.');
39✔
1110
      enforce(bid != null, 'Bid is required.');
39✔
1111
      enforce(lockup != null, 'Lockup is required.');
38✔
1112
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
37!
1113

1114
      const options = TransactionOptions.fromValidator(valid);
37✔
1115

1116
      if (broadcast) {
37!
1117
        // TODO: Add abort signal to close when request closes.
1118
        const tx = await req.wallet.sendBid(name, bid, lockup, options);
37✔
1119
        return res.json(200, tx.getJSON(this.network));
35✔
1120
      }
1121

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

1126
      if (sign)
×
1127
        await req.wallet.sign(mtx, options.passphrase);
×
1128

1129
      const json = mtx.getJSON(this.network);
×
1130

1131
      if (options.paths)
×
1132
        await this.addOutputPaths(json, mtx, req.wallet);
×
1133

1134
      return res.json(200, json);
×
1135
    });
1136

1137
    // Create auction-related transactions in advance (bid and reveal for now)
1138
    this.post('/wallet/:id/auction', async (req, res) => {
90✔
1139
      const valid = Validator.fromRequest(req);
1✔
1140
      const name = valid.str('name');
1✔
1141
      const bid = valid.u64('bid');
1✔
1142
      const lockup = valid.u64('lockup');
1✔
1143
      const passphrase = valid.str('passphrase');
1✔
1144
      const sign = valid.bool('sign', true);
1✔
1145
      const broadcastBid = valid.bool('broadcastBid');
1✔
1146

1147
      enforce(name, 'Name is required.');
1✔
1148
      enforce(bid != null, 'Bid is required.');
1✔
1149
      enforce(lockup != null, 'Lockup is required.');
1✔
1150
      enforce(broadcastBid != null, 'broadcastBid is required.');
1✔
1151
      enforce(broadcastBid ? sign : true, 'Must sign when broadcasting.');
1!
1152

1153
      const options = TransactionOptions.fromValidator(valid);
1✔
1154
      const auctionTXs = await req.wallet.createAuctionTXs(
1✔
1155
        name,
1156
        bid,
1157
        lockup,
1158
        options
1159
      );
1160

1161
      if (broadcastBid)
1!
1162
        auctionTXs.bid = await req.wallet.sendMTX(auctionTXs.bid, passphrase);
1✔
1163

1164
      if (sign) {
1!
1165
        if (!broadcastBid)
1!
NEW
1166
          await req.wallet.sign(auctionTXs.bid, passphrase);
×
1167

1168
        await req.wallet.sign(auctionTXs.reveal, passphrase);
1✔
1169
      }
1170

1171
      const jsonBid = auctionTXs.bid.getJSON(this.network);
1✔
1172
      const jsonReveal = auctionTXs.reveal.getJSON(this.network);
1✔
1173

1174
      if (options.paths) {
1!
NEW
1175
        await this.addOutputPaths(jsonBid, auctionTXs.bid, req.wallet);
×
NEW
1176
        await this.addOutputPaths(jsonReveal, auctionTXs.reveal, req.wallet);
×
1177
      }
1178

1179
      return res.json(200, {
1✔
1180
        bid: jsonBid,
1181
        reveal: jsonReveal
1182
      });
1183
    });
1184

1185
    // Create Reveal
1186
    this.post('/wallet/:id/reveal', async (req, res) => {
90✔
1187
      const valid = Validator.fromRequest(req);
20✔
1188
      const name = valid.str('name');
20✔
1189
      const broadcast = valid.bool('broadcast', true);
20✔
1190
      const sign = valid.bool('sign', true);
20✔
1191

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

1194
      const options = TransactionOptions.fromValidator(valid);
20✔
1195

1196
      if (broadcast) {
20!
1197
        let tx;
1198

1199
        if (name) {
20✔
1200
          // TODO: Add abort signal to close when request closes.
1201
          tx = await req.wallet.sendReveal(name, options);
18✔
1202
        } else {
1203
          // TODO: Add abort signal to close when request closes.
1204
          tx = await req.wallet.sendRevealAll(options);
2✔
1205
        }
1206

1207
        return res.json(200, tx.getJSON(this.network));
19✔
1208
      }
1209

1210
      let mtx;
1211

1212
      // TODO: Add create TX with locks for used Coins and/or
1213
      // adds to the pending list.
1214
      if (name) {
×
1215
        mtx = await req.wallet.createReveal(name, options);
×
1216
      } else {
1217
        mtx = await req.wallet.createRevealAll(options);
×
1218
      }
1219

1220
      if (sign)
×
1221
        await req.wallet.sign(mtx, options.passphrase);
×
1222

1223
      const json = mtx.getJSON(this.network);
×
1224

1225
      if (options.paths)
×
1226
        await this.addOutputPaths(json, mtx, req.wallet);
×
1227

1228
      return res.json(200, json);
×
1229
    });
1230

1231
    // Create Redeem
1232
    this.post('/wallet/:id/redeem', async (req, res) => {
90✔
1233
      const valid = Validator.fromRequest(req);
7✔
1234
      const name = valid.str('name');
7✔
1235
      const broadcast = valid.bool('broadcast', true);
7✔
1236
      const sign = valid.bool('sign', true);
7✔
1237

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

1240
      const options = TransactionOptions.fromValidator(valid);
7✔
1241

1242
      if (broadcast) {
7!
1243
        let tx;
1244

1245
        if (name) {
7✔
1246
          // TODO: Add abort signal to close when request closes.
1247
          tx = await req.wallet.sendRedeem(name, options);
4✔
1248
        } else {
1249
          // TODO: Add abort signal to close when request closes.
1250
          tx = await req.wallet.sendRedeemAll(options);
3✔
1251
        }
1252

1253
        return res.json(200, tx.getJSON(this.network));
5✔
1254
      }
1255

1256
      let mtx;
1257

1258
      // TODO: Add create TX with locks for used Coins and/or
1259
      // adds to the pending list.
1260
      if (!name) {
×
1261
        mtx = await req.wallet.createRedeemAll(options);
×
1262
      } else {
1263
        mtx = await req.wallet.createRedeem(name, options);
×
1264
      }
1265

1266
      if (sign)
×
1267
        await req.wallet.sign(mtx, options.passphrase);
×
1268

1269
      const json = mtx.getJSON(this.network);
×
1270

1271
      if (options.paths)
×
1272
        await this.addOutputPaths(json, mtx, req.wallet);
×
1273

1274
      return res.json(200, json);
×
1275
    });
1276

1277
    // Create Update
1278
    this.post('/wallet/:id/update', async (req, res) => {
90✔
1279
      const valid = Validator.fromRequest(req);
12✔
1280
      const name = valid.str('name');
12✔
1281
      const data = valid.obj('data');
12✔
1282
      const broadcast = valid.bool('broadcast', true);
12✔
1283
      const sign = valid.bool('sign', true);
12✔
1284

1285
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
12!
1286
      enforce(name, 'Must pass name.');
12✔
1287
      enforce(data, 'Must pass data.');
12✔
1288

1289
      let resource;
1290
      try {
12✔
1291
        resource = Resource.fromJSON(data);
12✔
1292
      } catch (e) {
1293
        return res.json(400);
×
1294
      }
1295

1296
      const options = TransactionOptions.fromValidator(valid);
12✔
1297

1298
      if (broadcast) {
12!
1299
        // TODO: Add abort signal to close when request closes.
1300
        const tx = await req.wallet.sendUpdate(name, resource, options);
12✔
1301
        return res.json(200, tx.getJSON(this.network));
11✔
1302
      }
1303

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

1308
      if (sign)
×
1309
        await req.wallet.sign(mtx, options.passphrase);
×
1310

1311
      const json = mtx.getJSON(this.network);
×
1312

1313
      if (options.paths)
×
1314
        await this.addOutputPaths(json, mtx, req.wallet);
×
1315

1316
      return res.json(200, json);
×
1317
    });
1318

1319
    // Create Renewal
1320
    this.post('/wallet/:id/renewal', async (req, res) => {
90✔
1321
      const valid = Validator.fromRequest(req);
4✔
1322
      const name = valid.str('name');
4✔
1323
      const broadcast = valid.bool('broadcast', true);
4✔
1324
      const sign = valid.bool('sign', true);
4✔
1325

1326
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
4!
1327
      enforce(name, 'Must pass name.');
4✔
1328

1329
      const options = TransactionOptions.fromValidator(valid);
4✔
1330

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

1337
      const mtx = await req.wallet.createRenewal(name, 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 Transfer
1351
    this.post('/wallet/:id/transfer', async (req, res) => {
90✔
1352
      const valid = Validator.fromRequest(req);
12✔
1353
      const name = valid.str('name');
12✔
1354
      const address = valid.str('address');
12✔
1355
      const broadcast = valid.bool('broadcast', true);
12✔
1356
      const sign = valid.bool('sign', true);
12✔
1357

1358
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
12!
1359
      enforce(name, 'Must pass name.');
12✔
1360
      enforce(address, 'Must pass address.');
12✔
1361

1362
      const addr = Address.fromString(address, this.network);
12✔
1363
      const options = TransactionOptions.fromValidator(valid);
12✔
1364

1365
      if (broadcast) {
12!
1366
        // TODO: Add abort signal to close when request closes.
1367
        const tx = await req.wallet.sendTransfer(name, addr, options);
12✔
1368
        return res.json(200, tx.getJSON(this.network));
12✔
1369
      }
1370

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

1375
      if (sign)
×
1376
        await req.wallet.sign(mtx, options.passphrase);
×
1377

1378
      const json = mtx.getJSON(this.network);
×
1379

1380
      if (options.paths)
×
1381
        await this.addOutputPaths(json, mtx, req.wallet);
×
1382

1383
      return res.json(200, json);
×
1384
    });
1385

1386
    // Create Cancel
1387
    this.post('/wallet/:id/cancel', async (req, res) => {
90✔
1388
      const valid = Validator.fromRequest(req);
4✔
1389
      const name = valid.str('name');
4✔
1390
      const broadcast = valid.bool('broadcast', true);
4✔
1391
      const sign = valid.bool('sign', true);
4✔
1392

1393
      enforce(broadcast ? sign : true, 'Must sign when broadcasting.');
4!
1394
      enforce(name, 'Must pass name.');
4✔
1395

1396
      const options = TransactionOptions.fromValidator(valid);
4✔
1397

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

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

1408
      if (sign)
×
1409
        await req.wallet.sign(mtx, options.passphrase);
×
1410

1411
      const json = mtx.getJSON(this.network);
×
1412

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

1416
      return res.json(200, json);
×
1417
    });
1418

1419
    // Create Finalize
1420
    this.post('/wallet/:id/finalize', async (req, res) => {
90✔
1421
      const valid = Validator.fromRequest(req);
4✔
1422
      const name = valid.str('name');
4✔
1423
      const broadcast = valid.bool('broadcast', true);
4✔
1424
      const sign = valid.bool('sign', true);
4✔
1425

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

1429
      const options = TransactionOptions.fromValidator(valid);
4✔
1430

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

1437
      const mtx = await req.wallet.createFinalize(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 Revoke
1451
    this.post('/wallet/:id/revoke', async (req, res) => {
90✔
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);
4✔
1461

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

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

1472
      if (sign)
×
1473
        await req.wallet.sign(mtx, options.passphrase);
×
1474

1475
      const json = mtx.getJSON(this.network);
×
1476

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

1480
      return res.json(200, json);
×
1481
    });
1482
  }
1483

1484
  /**
1485
   * Add wallet path information to JSON outputs
1486
   * @private
1487
   */
1488

1489
   async addOutputPaths(json, tx, wallet) {
1490
    for (let i = 0; i < tx.outputs.length; i++) {
2✔
1491
      const {address} = tx.outputs[i];
4✔
1492
      const path = await wallet.getPath(address);
4✔
1493

1494
      if (!path)
4✔
1495
        continue;
1✔
1496

1497
      json.outputs[i].path = path.getJSON(this.network);
3✔
1498
    }
1499

1500
    return json;
2✔
1501
   }
1502

1503
  /**
1504
   * Initialize websockets.
1505
   * @private
1506
   */
1507

1508
  initSockets() {
1509
    const handleTX = (event, wallet, tx, details) => {
90✔
1510
      const name = `w:${wallet.id}`;
12,467✔
1511

1512
      if (!this.channel(name) && !this.channel('w:*'))
12,467✔
1513
        return;
12,118✔
1514

1515
      const json = details.getJSON(this.network, this.wdb.liveHeight());
349✔
1516

1517
      if (this.channel(name))
349!
1518
        this.to(name, event, wallet.id, json);
349✔
1519

1520
      if (this.channel('w:*'))
349!
1521
        this.to('w:*', event, wallet.id, json);
×
1522
    };
1523

1524
    this.wdb.on('tx', (wallet, tx, details) => {
90✔
1525
      handleTX('tx', wallet, tx, details);
9,263✔
1526
    });
1527

1528
    this.wdb.on('confirmed', (wallet, tx, details) => {
90✔
1529
      handleTX('confirmed', wallet, tx, details);
2,296✔
1530
    });
1531

1532
    this.wdb.on('unconfirmed', (wallet, tx, details) => {
90✔
1533
      handleTX('unconfirmed', wallet, tx, details);
907✔
1534
    });
1535

1536
    this.wdb.on('conflict', (wallet, tx, details) => {
90✔
1537
      handleTX('conflict', wallet, tx, details);
1✔
1538
    });
1539

1540
    this.wdb.on('balance', (wallet, balance) => {
90✔
1541
      const name = `w:${wallet.id}`;
13,515✔
1542

1543
      if (!this.channel(name) && !this.channel('w:*'))
13,515✔
1544
        return;
13,166✔
1545

1546
      const json = balance.toJSON();
349✔
1547

1548
      if (this.channel(name))
349!
1549
        this.to(name, 'balance', wallet.id, json);
349✔
1550

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

1555
    this.wdb.on('address', (wallet, receive) => {
90✔
1556
      const name = `w:${wallet.id}`;
1,499✔
1557

1558
      if (!this.channel(name) && !this.channel('w:*'))
1,499✔
1559
        return;
1,445✔
1560

1561
      const json = [];
54✔
1562

1563
      for (const addr of receive)
54✔
1564
        json.push(addr.getJSON(this.network));
54✔
1565

1566
      if (this.channel(name))
54!
1567
        this.to(name, 'address', wallet.id, json);
54✔
1568

1569
      if (this.channel('w:*'))
54!
1570
        this.to('w:*', 'address', wallet.id, json);
×
1571
    });
1572
  }
1573

1574
  /**
1575
   * Handle new websocket.
1576
   * @private
1577
   * @param {WebSocket} socket
1578
   */
1579

1580
  handleSocket(socket) {
1581
    socket.hook('auth', (...args) => {
54✔
1582
      if (socket.channel('auth'))
54!
1583
        throw new Error('Already authed.');
×
1584

1585
      if (!this.options.noAuth) {
54✔
1586
        const valid = new Validator(args);
14✔
1587
        const key = valid.str(0, '');
14✔
1588

1589
        if (key.length > 255)
14!
1590
          throw new Error('Invalid API key.');
×
1591

1592
        const data = Buffer.from(key, 'utf8');
14✔
1593
        const hash = sha256.digest(data);
14✔
1594

1595
        if (!safeEqual(hash, this.options.apiHash))
14!
1596
          throw new Error('Invalid API key.');
×
1597
      }
1598

1599
      socket.join('auth');
54✔
1600

1601
      this.logger.info('Successful auth from %s.', socket.host);
54✔
1602

1603
      this.handleAuth(socket);
54✔
1604

1605
      return null;
54✔
1606
    });
1607
  }
1608

1609
  /**
1610
   * Handle new auth'd websocket.
1611
   * @private
1612
   * @param {WebSocket} socket
1613
   */
1614

1615
  handleAuth(socket) {
1616
    socket.hook('join', async (...args) => {
54✔
1617
      const valid = new Validator(args);
10✔
1618
      const id = valid.str(0, '');
10✔
1619
      const token = valid.buf(1);
10✔
1620

1621
      if (!id)
10!
1622
        throw new Error('Invalid parameter.');
×
1623

1624
      if (!this.options.walletAuth) {
10!
1625
        socket.join('admin');
10✔
1626
      } else if (token) {
×
1627
        if (safeEqual(token, this.options.adminToken))
×
1628
          socket.join('admin');
×
1629
      }
1630

1631
      if (socket.channel('admin') || !this.options.walletAuth) {
10!
1632
        socket.join(`w:${id}`);
10✔
1633
        return null;
10✔
1634
      }
1635

1636
      if (id === '*')
×
1637
        throw new Error('Bad token.');
×
1638

1639
      if (!token)
×
1640
        throw new Error('Invalid parameter.');
×
1641

1642
      let wallet;
1643
      try {
×
1644
        wallet = await this.wdb.auth(id, token);
×
1645
      } catch (e) {
1646
        this.logger.info('Wallet auth failure for %s: %s.', id, e.message);
×
1647
        throw new Error('Bad token.');
×
1648
      }
1649

1650
      if (!wallet)
×
1651
        throw new Error('Wallet does not exist.');
×
1652

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

1655
      socket.join(`w:${id}`);
×
1656

1657
      return null;
×
1658
    });
1659

1660
    socket.hook('leave', (...args) => {
54✔
1661
      const valid = new Validator(args);
×
1662
      const id = valid.str(0, '');
×
1663

1664
      if (!id)
×
1665
        throw new Error('Invalid parameter.');
×
1666

1667
      socket.leave(`w:${id}`);
×
1668

1669
      return null;
×
1670
    });
1671
  }
1672
}
1673

1674
class HTTPOptions {
1675
  /**
1676
   * HTTPOptions
1677
   * @alias module:http.HTTPOptions
1678
   * @constructor
1679
   * @param {Object} options
1680
   */
1681

1682
  constructor(options) {
1683
    this.network = Network.primary;
90✔
1684
    this.logger = null;
90✔
1685
    this.node = null;
90✔
1686
    this.apiKey = base58.encode(random.randomBytes(20));
90✔
1687
    this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
90✔
1688
    this.adminToken = random.randomBytes(32);
90✔
1689
    this.serviceHash = this.apiHash;
90✔
1690
    this.noAuth = false;
90✔
1691
    this.cors = false;
90✔
1692
    this.walletAuth = false;
90✔
1693

1694
    this.prefix = null;
90✔
1695
    this.host = '127.0.0.1';
90✔
1696
    this.port = 8080;
90✔
1697
    this.ssl = false;
90✔
1698
    this.keyFile = null;
90✔
1699
    this.certFile = null;
90✔
1700

1701
    this.fromOptions(options);
90✔
1702
  }
1703

1704
  /**
1705
   * Inject properties from object.
1706
   * @private
1707
   * @param {Object} options
1708
   * @returns {HTTPOptions}
1709
   */
1710

1711
  fromOptions(options) {
1712
    assert(options);
90✔
1713
    assert(options.node && typeof options.node === 'object',
90✔
1714
      'HTTP Server requires a WalletDB.');
1715

1716
    this.node = options.node;
90✔
1717
    this.network = options.node.network;
90✔
1718
    this.logger = options.node.logger;
90✔
1719
    this.port = this.network.walletPort;
90✔
1720

1721
    if (options.logger != null) {
90!
1722
      assert(typeof options.logger === 'object');
90✔
1723
      this.logger = options.logger;
90✔
1724
    }
1725

1726
    if (options.apiKey != null) {
90✔
1727
      assert(typeof options.apiKey === 'string',
15✔
1728
        'API key must be a string.');
1729
      assert(options.apiKey.length <= 255,
15✔
1730
        'API key must be under 255 bytes.');
1731
      this.apiKey = options.apiKey;
15✔
1732
      this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
15✔
1733
    }
1734

1735
    if (options.adminToken != null) {
90!
1736
      if (typeof options.adminToken === 'string') {
×
1737
        assert(options.adminToken.length === 64,
×
1738
          'Admin token must be a 32 byte hex string.');
1739
        const token = Buffer.from(options.adminToken, 'hex');
×
1740
        assert(token.length === 32,
×
1741
          'Admin token must be a 32 byte hex string.');
1742
        this.adminToken = token;
×
1743
      } else {
1744
        assert(Buffer.isBuffer(options.adminToken),
×
1745
          'Admin token must be a hex string or buffer.');
1746
        assert(options.adminToken.length === 32,
×
1747
          'Admin token must be 32 bytes.');
1748
        this.adminToken = options.adminToken;
×
1749
      }
1750
    }
1751

1752
    if (options.noAuth != null) {
90!
1753
      assert(typeof options.noAuth === 'boolean');
×
1754
      this.noAuth = options.noAuth;
×
1755
    }
1756

1757
    if (options.cors != null) {
90!
1758
      assert(typeof options.cors === 'boolean');
×
1759
      this.cors = options.cors;
×
1760
    }
1761

1762
    if (options.walletAuth != null) {
90!
1763
      assert(typeof options.walletAuth === 'boolean');
×
1764
      this.walletAuth = options.walletAuth;
×
1765
    }
1766

1767
    if (options.prefix != null) {
90✔
1768
      assert(typeof options.prefix === 'string');
13✔
1769
      this.prefix = options.prefix;
13✔
1770
      this.keyFile = path.join(this.prefix, 'key.pem');
13✔
1771
      this.certFile = path.join(this.prefix, 'cert.pem');
13✔
1772
    }
1773

1774
    if (options.host != null) {
90!
1775
      assert(typeof options.host === 'string');
×
1776
      this.host = options.host;
×
1777
    }
1778

1779
    if (options.port != null) {
90✔
1780
      assert((options.port & 0xffff) === options.port,
39✔
1781
        'Port must be a number.');
1782
      this.port = options.port;
39✔
1783
    }
1784

1785
    if (options.ssl != null) {
90!
1786
      assert(typeof options.ssl === 'boolean');
×
1787
      this.ssl = options.ssl;
×
1788
    }
1789

1790
    if (options.keyFile != null) {
90!
1791
      assert(typeof options.keyFile === 'string');
×
1792
      this.keyFile = options.keyFile;
×
1793
    }
1794

1795
    if (options.certFile != null) {
90!
1796
      assert(typeof options.certFile === 'string');
×
1797
      this.certFile = options.certFile;
×
1798
    }
1799

1800
    // Allow no-auth implicitly
1801
    // if we're listening locally.
1802
    if (!options.apiKey) {
90✔
1803
      if (   this.host === '127.0.0.1'
75!
1804
          || this.host === '::1'
1805
          || this.host === 'localhost')
1806
        this.noAuth = true;
75✔
1807
    }
1808

1809
    return this;
90✔
1810
  }
1811

1812
  /**
1813
   * Instantiate http options from object.
1814
   * @param {Object} options
1815
   * @returns {HTTPOptions}
1816
   */
1817

1818
  static fromOptions(options) {
1819
    return new HTTPOptions().fromOptions(options);
×
1820
  }
1821
}
1822

1823
class TransactionOptions {
1824
  /**
1825
   * TransactionOptions
1826
   * @alias module:http.TransactionOptions
1827
   * @constructor
1828
   * @param {Validator} valid
1829
   */
1830

1831
  constructor(valid) {
1832
    if (valid)
238!
1833
      return this.fromValidator(valid);
×
1834
  }
1835

1836
  /**
1837
   * Inject properties from Validator.
1838
   * @private
1839
   * @param {Validator} valid
1840
   * @returns {TransactionOptions}
1841
   */
1842

1843
  fromValidator(valid) {
1844
    assert(valid);
238✔
1845

1846
    this.rate = valid.u64('rate');
238✔
1847
    this.maxFee = valid.u64('maxFee');
238✔
1848
    this.selection = valid.str('selection');
238✔
1849
    this.smart = valid.bool('smart');
238✔
1850
    this.account = valid.str('account');
238✔
1851
    this.locktime = valid.u64('locktime');
238✔
1852
    this.sort = valid.bool('sort');
238✔
1853
    this.subtractFee = valid.bool('subtractFee');
238✔
1854
    this.subtractIndex = valid.i32('subtractIndex');
238✔
1855
    this.depth = valid.u32(['confirmations', 'depth']);
238✔
1856
    this.paths = valid.bool('paths');
238✔
1857
    this.passphrase = valid.str('passphrase');
238✔
1858
    this.hardFee = valid.u64('hardFee'),
238✔
1859
    this.outputs = [];
1860

1861
    if (valid.has('outputs')) {
238✔
1862
      const outputs = valid.array('outputs');
103✔
1863

1864
      for (const output of outputs) {
103✔
1865
        const valid = new Validator(output);
127✔
1866

1867
        let addr = valid.str('address');
127✔
1868

1869
        if (addr)
127!
1870
          addr = Address.fromString(addr, this.network);
127✔
1871

1872
        let covenant = valid.obj('covenant');
127✔
1873

1874
        if (covenant)
127✔
1875
          covenant = Covenant.fromJSON(covenant);
2✔
1876

1877
        this.outputs.push({
127✔
1878
          value: valid.u64('value'),
1879
          address: addr,
1880
          covenant: covenant
1881
        });
1882
      }
1883
    }
1884

1885
    return this;
238✔
1886
  }
1887

1888
  /*
1889
   * Instantiate transaction options
1890
   * from Validator.
1891
   * @param {Validator} valid
1892
   * @returns {TransactionOptions}
1893
   */
1894

1895
  static fromValidator(valid) {
1896
    return new this().fromValidator(valid);
238✔
1897
  }
1898
}
1899

1900
/*
1901
 * Helpers
1902
 */
1903

1904
function enforce(value, msg) {
1905
  if (!value) {
430✔
1906
    const err = new Error(msg);
3✔
1907
    err.statusCode = 400;
3✔
1908
    throw err;
3✔
1909
  }
1910
}
1911

1912
/*
1913
 * Expose
1914
 */
1915

1916
exports = HTTP;
1✔
1917
exports.HTTP = HTTP;
1✔
1918
exports.TransactionOptions = TransactionOptions;
1✔
1919

1920
module.exports = exports;
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