UNPKG

86.2 kBPlain TextView Raw
1/**
2 * @hidden
3 */
4
5/**
6 */
7//
8// Wallet Object
9// BitGo accessor for a specific wallet
10//
11// Copyright 2014, BitGo, Inc. All Rights Reserved.
12//
13
14import { Codes, VirtualSizes } from '@bitgo/unspents';
15
16const TransactionBuilder = require('./transactionBuilder');
17import * as bitcoin from '@bitgo/utxo-lib';
18const PendingApproval = require('./pendingapproval');
19
20import * as common from './common';
21import * as Bluebird from 'bluebird';
22const co = Bluebird.coroutine;
23import * as _ from 'lodash';
24import { hdPath, makeRandomKey, getNetwork } from './bitcoin';
25const request = require('superagent');
26
27//
28// Constructor
29//
30const Wallet = function(bitgo, wallet) {
31 (this.bitgo as any) = bitgo;
32 this.wallet = wallet;
33 this.keychains = [];
34
35 if (wallet.private) {
36 this.keychains = wallet.private.keychains;
37 }
38};
39
40Wallet.prototype.toJSON = function() {
41 return this.wallet;
42};
43
44//
45// id
46// Get the id of this wallet.
47//
48Wallet.prototype.id = function() {
49 return this.wallet.id;
50};
51
52//
53// label
54// Get the label of this wallet.
55//
56Wallet.prototype.label = function() {
57 return this.wallet.label;
58};
59
60//
61// balance
62// Get the balance of this wallet.
63//
64Wallet.prototype.balance = function() {
65 return this.wallet.balance;
66};
67
68//
69// balance
70// Get the spendable balance of this wallet.
71// This is the total of all unspents except those that are unconfirmed and external
72//
73Wallet.prototype.spendableBalance = function() {
74 return this.wallet.spendableBalance;
75};
76
77//
78// confirmedBalance
79// Get the confirmedBalance of this wallet.
80//
81Wallet.prototype.confirmedBalance = function() {
82 return this.wallet.confirmedBalance;
83};
84
85//
86// canSendInstant
87// Returns if the wallet can send instant transactions
88// This is impacted by the choice of backup key provider
89//
90Wallet.prototype.canSendInstant = function() {
91 return this.wallet && this.wallet.canSendInstant;
92};
93
94//
95// instant balance
96// Get the instant balance of this wallet.
97// This is the total of all unspents that may be spent instantly.
98//
99Wallet.prototype.instantBalance = function() {
100 if (!this.canSendInstant()) {
101 throw new Error('not an instant wallet');
102 }
103 return this.wallet.instantBalance;
104};
105
106//
107// unconfirmedSends
108// Get the balance of unconfirmedSends of this wallet.
109//
110Wallet.prototype.unconfirmedSends = function() {
111 return this.wallet.unconfirmedSends;
112};
113
114//
115// unconfirmedReceives
116// Get the balance of unconfirmedReceives balance of this wallet.
117//
118Wallet.prototype.unconfirmedReceives = function() {
119 return this.wallet.unconfirmedReceives;
120};
121
122//
123// type
124// Get the type of this wallet, e.g. 'safehd'
125//
126Wallet.prototype.type = function() {
127 return this.wallet.type;
128};
129
130Wallet.prototype.url = function(extra) {
131 extra = extra || '';
132 return this.bitgo.url('/wallet/' + this.id() + extra);
133};
134
135//
136// pendingApprovals
137// returns the pending approvals list for this wallet as pending approval objects
138//
139Wallet.prototype.pendingApprovals = function() {
140 const self = this;
141 return this.wallet.pendingApprovals.map(function(p) {
142 return new PendingApproval(self.bitgo, p, self);
143 });
144};
145
146//
147// approvalsRequired
148// returns the number of approvals required to approve pending approvals involving this wallet
149//
150Wallet.prototype.approvalsRequired = function() {
151 return this.wallet.approvalsRequired || 1;
152};
153
154//
155// get
156// Refetches this wallet and returns it
157//
158Wallet.prototype.get = function(params, callback): Bluebird<any> {
159 params = params || {};
160 common.validateParams(params, [], [], callback);
161
162 const self = this;
163
164 return this.bitgo.get(this.url())
165 .result()
166 .then(function(res) {
167 self.wallet = res;
168 return self;
169 })
170 .nodeify(callback);
171};
172
173//
174// updateApprovalsRequired
175// Updates the number of approvals required on a pending approval involving this wallet.
176// The approvals required is by default 1, but this function allows you to update the
177// number such that 1 <= approvalsRequired <= walletAdmins.length - 1
178//
179Wallet.prototype.updateApprovalsRequired = function(params, callback): Bluebird<any> {
180 params = params || {};
181 common.validateParams(params, [], [], callback);
182 if (params.approvalsRequired === undefined ||
183 !_.isInteger(params.approvalsRequired) ||
184 params.approvalsRequired < 1
185 ) {
186 throw new Error('invalid approvalsRequired: must be a nonzero positive number');
187 }
188
189 const self = this;
190 const currentApprovalsRequired = this.approvalsRequired();
191 if (currentApprovalsRequired === params.approvalsRequired) {
192 // no-op, just return the current wallet
193 return Bluebird.try(function() {
194 return self.wallet;
195 })
196 .nodeify(callback);
197 }
198
199 return this.bitgo.put(this.url())
200 .send(params)
201 .result()
202 .nodeify(callback);
203};
204
205/**
206 * Returns the correct chain for change, taking into consideration segwit
207 */
208Wallet.prototype.getChangeChain = function(params) {
209 let useSegwitChange = !!this.bitgo.getConstants().enableSegwit;
210 if (!_.isUndefined(params.segwitChange)) {
211 if (!_.isBoolean(params.segwitChange)) {
212 throw new Error('segwitChange must be a boolean');
213 }
214
215 // if segwit is disabled through the constants, segwit change should still not be created
216 useSegwitChange = this.bitgo.getConstants().enableSegwit && params.segwitChange;
217 }
218 return useSegwitChange ? Codes.internal.p2shP2wsh : Codes.internal.p2sh;
219};
220
221//
222// createAddress
223// Creates a new address for use with this wallet.
224//
225Wallet.prototype.createAddress = function(params, callback) {
226 const self = this;
227 params = params || {};
228 common.validateParams(params, [], [], callback);
229 if (this.type() === 'safe') {
230 throw new Error('You are using a legacy wallet that cannot create a new address');
231 }
232
233 // Default to client-side address validation on, for safety. Use validate=false to disable.
234 const shouldValidate = params.validate !== undefined ? params.validate : this.bitgo.getValidate();
235
236 const allowExisting = params.allowExisting;
237 if (typeof allowExisting !== 'boolean') {
238 params.allowExisting = (allowExisting === 'true');
239 }
240
241 const isSegwit = this.bitgo.getConstants().enableSegwit;
242 const defaultChain = isSegwit ? Codes.external.p2shP2wsh : Codes.external.p2sh;
243
244 let chain = params.chain;
245 if (chain === null || chain === undefined) {
246 chain = defaultChain;
247 }
248 return this.bitgo.post(this.url('/address/' + chain))
249 .send(params)
250 .result()
251 .then(function(addr) {
252 if (shouldValidate) {
253 self.validateAddress(addr);
254 }
255 return addr;
256 })
257 .nodeify(callback);
258};
259
260/**
261 * Generate address locally without calling server
262 * @param params
263 *
264 */
265Wallet.prototype.generateAddress = function({ segwit, path, keychains, threshold }) {
266 const isSegwit = !!segwit;
267 let signatureThreshold = 2;
268 if (_.isInteger(threshold)) {
269 signatureThreshold = threshold;
270 if (signatureThreshold <= 0) {
271 throw new Error('threshold has to be positive');
272 }
273 }
274
275 const pathRegex = /^\/1?[01]\/\d+$/;
276 if (!path.match(pathRegex)) {
277 throw new Error('unsupported path: ' + path);
278 }
279
280 let rootKeys = this.keychains;
281 if (Array.isArray(keychains)) {
282 rootKeys = keychains;
283 }
284
285 const network = common.Environments[this.bitgo.getEnv()].network;
286
287 const derivedKeys = rootKeys.map(function(k) {
288 const hdnode = bitcoin.HDNode.fromBase58(k.xpub);
289 let derivationPath = k.path + path;
290 if (k.walletSubPath) {
291 // if a keychain has a wallet subpath, it should be used as an infix
292 derivationPath = k.path + k.walletSubPath + path;
293 }
294 if (!derivationPath.startsWith('m')) {
295 // all derivation paths need to start with m, but k.path may already contain that
296 derivationPath = `m/${derivationPath}`;
297 }
298 return hdPath(hdnode).deriveKey(derivationPath).getPublicKeyBuffer();
299 });
300
301 const pathComponents = path.split('/');
302 const normalizedPathComponents = _.map(pathComponents, (component) => {
303 if (component && component.length > 0) {
304 return parseInt(component, 10);
305 }
306 });
307 const pathDetails = _.filter(normalizedPathComponents, _.isInteger);
308
309 const addressDetails: any = {
310 chainPath: path,
311 path: path,
312 chain: pathDetails[0],
313 index: pathDetails[1],
314 wallet: this.id()
315 };
316
317 // redeem script normally, witness script for segwit
318 const inputScript = bitcoin.script.multisig.output.encode(signatureThreshold, derivedKeys);
319 const inputScriptHash = bitcoin.crypto.hash160(inputScript);
320 let outputScript = bitcoin.script.scriptHash.output.encode(inputScriptHash);
321 addressDetails.redeemScript = inputScript.toString('hex');
322
323 if (isSegwit) {
324 const witnessScriptHash = bitcoin.crypto.sha256(inputScript);
325 const redeemScript = bitcoin.script.witnessScriptHash.output.encode(witnessScriptHash);
326 const redeemScriptHash = bitcoin.crypto.hash160(redeemScript);
327 outputScript = bitcoin.script.scriptHash.output.encode(redeemScriptHash);
328 addressDetails.witnessScript = inputScript.toString('hex');
329 addressDetails.redeemScript = redeemScript.toString('hex');
330 }
331
332 addressDetails.outputScript = outputScript.toString('hex');
333 addressDetails.address = bitcoin.address.fromOutputScript(outputScript, getNetwork(network));
334
335 return addressDetails;
336};
337
338//
339// validateAddress
340// Validates an address and path by calculating it locally from the keychain xpubs
341//
342Wallet.prototype.validateAddress = function(params) {
343 common.validateParams(params, ['address', 'path'], []);
344 const isSegwit = !!params.witnessScript && params.witnessScript.length > 0;
345
346 const generatedAddress = this.generateAddress({ path: params.path, segwit: isSegwit });
347 if (generatedAddress.address !== params.address) {
348 throw new Error('address validation failure: ' + params.address + ' vs. ' + generatedAddress.address);
349 }
350};
351
352//
353// addresses
354// Gets the addresses of a HD wallet.
355// Options include:
356// limit: the number of addresses to get
357//
358Wallet.prototype.addresses = function(params, callback) {
359 params = params || {};
360 common.validateParams(params, [], [], callback);
361
362 const query: any = {};
363 if (params.details) {
364 query.details = 1;
365 }
366
367 const chain = params.chain;
368 if (chain !== null && chain !== undefined) {
369 if (Array.isArray(chain)) {
370 query.chain = _.uniq(_.filter(chain, _.isInteger));
371 } else {
372 if (chain !== 0 && chain !== 1) {
373 throw new Error('invalid chain argument, expecting 0 or 1');
374 }
375 query.chain = chain;
376 }
377 }
378 if (params.limit) {
379 if (!_.isInteger(params.limit)) {
380 throw new Error('invalid limit argument, expecting number');
381 }
382 query.limit = params.limit;
383 }
384 if (params.skip) {
385 if (!_.isInteger(params.skip)) {
386 throw new Error('invalid skip argument, expecting number');
387 }
388 query.skip = params.skip;
389 }
390 if (params.sort) {
391 if (!_.isNumber(params.sort)) {
392 throw new Error('invalid sort argument, expecting number');
393 }
394 query.sort = params.sort;
395 }
396
397 const url = this.url('/addresses');
398 return this.bitgo.get(url)
399 .query(query)
400 .result()
401 .nodeify(callback);
402};
403
404Wallet.prototype.stats = function(params, callback) {
405 params = params || {};
406 common.validateParams(params, [], [], callback);
407 const args: string[] = [];
408 if (params.limit) {
409 if (!_.isInteger(params.limit)) {
410 throw new Error('invalid limit argument, expecting number');
411 }
412 args.push('limit=' + params.limit);
413 }
414 let query = '';
415 if (args.length) {
416 query = '?' + args.join('&');
417 }
418
419 const url = this.url('/stats' + query);
420
421 return this.bitgo.get(url)
422 .result()
423 .nodeify(callback);
424};
425
426/**
427 * Refresh the wallet object by syncing with the back-end
428 * @param callback
429 * @returns {Wallet}
430 */
431Wallet.prototype.refresh = function(params, callback) {
432 return co(function *() {
433 // when set to true, gpk returns the private data of safe wallets
434 const query = _.extend({}, _.pick(params, ['gpk']));
435 const res = yield this.bitgo.get(this.url()).query(query).result();
436 this.wallet = res;
437 return this;
438 }).call(this).asCallback(callback);
439};
440
441//
442// address
443// Gets information about a single address on a HD wallet.
444// Information includes index, path, redeemScript, sent, received, txCount and balance
445// Options include:
446// address: the address on this wallet to get
447//
448Wallet.prototype.address = function(params, callback) {
449 params = params || {};
450 common.validateParams(params, ['address'], [], callback);
451
452 const url = this.url('/addresses/' + params.address);
453
454 return this.bitgo.get(url)
455 .result()
456 .nodeify(callback);
457};
458
459/**
460 * Freeze the wallet for a duration of choice, stopping BitGo from signing any transactions.
461 * @param {number} limit The duration to freeze the wallet for in seconds, defaults to 3600.
462 */
463Wallet.prototype.freeze = function(params, callback) {
464 params = params || {};
465 common.validateParams(params, [], [], callback);
466
467 if (params.duration) {
468 if (!_.isNumber(params.duration)) {
469 throw new Error('invalid duration - should be number of seconds');
470 }
471 }
472
473 return this.bitgo.post(this.url('/freeze'))
474 .send(params)
475 .result()
476 .nodeify(callback);
477};
478
479//
480// delete
481// Deletes the wallet
482//
483Wallet.prototype.delete = function(params, callback) {
484 params = params || {};
485 common.validateParams(params, [], [], callback);
486
487 return this.bitgo.del(this.url())
488 .result()
489 .nodeify(callback);
490};
491
492//
493// labels
494// List the labels for the addresses in a given wallet
495//
496Wallet.prototype.labels = function(params, callback) {
497 params = params || {};
498 common.validateParams(params, [], [], callback);
499
500 const url = this.bitgo.url('/labels/' + this.id());
501
502 return this.bitgo.get(url)
503 .result('labels')
504 .nodeify(callback);
505};
506
507/**
508 * Rename a wallet
509 * @param params
510 * - label: the wallet's intended new name
511 * @param callback
512 * @returns {*}
513 */
514Wallet.prototype.setWalletName = function(params, callback) {
515 params = params || {};
516 common.validateParams(params, ['label'], [], callback);
517
518 const url = this.bitgo.url('/wallet/' + this.id());
519 return this.bitgo.put(url)
520 .send({ label: params.label })
521 .result()
522 .nodeify(callback);
523};
524
525//
526// setLabel
527// Sets a label on the provided address
528//
529Wallet.prototype.setLabel = function(params, callback) {
530 params = params || {};
531 common.validateParams(params, ['address', 'label'], [], callback);
532
533 const self = this;
534
535 if (!self.bitgo.verifyAddress({ address: params.address })) {
536 throw new Error('Invalid bitcoin address: ' + params.address);
537 }
538
539 const url = this.bitgo.url('/labels/' + this.id() + '/' + params.address);
540
541 return this.bitgo.put(url)
542 .send({ label: params.label })
543 .result()
544 .nodeify(callback);
545};
546
547//
548// deleteLabel
549// Deletes the label associated with the provided address
550//
551Wallet.prototype.deleteLabel = function(params, callback) {
552 params = params || {};
553 common.validateParams(params, ['address'], [], callback);
554
555 const self = this;
556
557 if (!self.bitgo.verifyAddress({ address: params.address })) {
558 throw new Error('Invalid bitcoin address: ' + params.address);
559 }
560
561 const url = this.bitgo.url('/labels/' + this.id() + '/' + params.address);
562
563 return this.bitgo.del(url)
564 .result()
565 .nodeify(callback);
566};
567
568//
569// unspents
570// List ALL the unspents for a given wallet
571// This method will return a paged list of all unspents
572//
573// Parameters include:
574// limit: the optional limit of unspents to collect in BTC
575// minConf: only include results with this number of confirmations
576// target: the amount of btc to find to spend
577// instant: only find instant transactions (must specify a target)
578//
579Wallet.prototype.unspents = function(params, callback) {
580 params = params || {};
581 common.validateParams(params, [], [], callback);
582
583 const allUnspents: any[] = [];
584 const self = this;
585
586 const getUnspentsBatch = function(skip, limit?) {
587
588 const queryObject = _.cloneDeep(params);
589 if (skip > 0) {
590 queryObject.skip = skip;
591 }
592 if (limit && limit > 0) {
593 queryObject.limit = limit;
594 }
595
596 return self.unspentsPaged(queryObject)
597 .then(function(result) {
598 // The API has its own limit handling. For example, the API does not support limits bigger than 500. If the limit
599 // specified here is bigger than that, we will have to do multiple requests with necessary limit adjustment.
600 for (let i = 0; i < result.unspents.length; i++) {
601 const unspent = result.unspents[i];
602 allUnspents.push(unspent);
603 }
604
605 // Our limit adjustment makes sure that we never fetch more unspents than we need, meaning that if we hit the
606 // limit, we hit it precisely
607 if (allUnspents.length >= params.limit) {
608 return allUnspents; // we aren't interested in any further unspents
609 }
610
611 const totalUnspentCount = result.total;
612 // if no target is specified and the SDK indicates that there has been a limit, we need to fetch another batch
613 if (!params.target && totalUnspentCount && totalUnspentCount > allUnspents.length) {
614 // we need to fetch the next batch
615 // let's just offset the current skip by the count
616 const newSkip = skip + result.count;
617 let newLimit: number | undefined;
618 if (limit > 0) {
619 // we set the new limit to be precisely the number of missing unspents to hit our own limit
620 newLimit = limit - allUnspents.length;
621 }
622 return getUnspentsBatch(newSkip, newLimit);
623 }
624
625 return allUnspents;
626 });
627 };
628
629 return getUnspentsBatch(0, params.limit)
630 .nodeify(callback);
631};
632
633/**
634 * List the unspents (paged) for a given wallet, returning the result as an object of unspents, count, skip and total
635 * This method may not return all the unspents as the list is paged by the API
636 * @param params
637 * @param params.limit the optional limit of unspents to collect in BTC
638 * @param params.skip index in list of unspents to start paging from
639 * @param params.minConfirms only include results with this number of confirmations
640 * @param params.target the amount of btc to find to spend
641 * @param params.instant only find instant transactions (must specify a target)
642 * @param params.targetWalletUnspents desired number of unspents to have in the wallet after the tx goes through (requires target)
643 * @param params.minSize minimum unspent size in satoshis
644 * @param params.segwit request segwit unspents (defaults to true if undefined)
645 * @param params.allowLedgerSegwit allow segwit unspents for ledger devices (defaults to false if undefined)
646 * @param callback
647 * @returns {*}
648 */
649Wallet.prototype.unspentsPaged = function(params, callback) {
650 params = params || {};
651 common.validateParams(params, [], [], callback);
652
653 if (!_.isUndefined(params.limit) && !_.isInteger(params.limit)) {
654 throw new Error('invalid limit - should be number');
655 }
656 if (!_.isUndefined(params.skip) && !_.isInteger(params.skip)) {
657 throw new Error('invalid skip - should be number');
658 }
659 if (!_.isUndefined(params.minConfirms) && !_.isInteger(params.minConfirms)) {
660 throw new Error('invalid minConfirms - should be number');
661 }
662 if (!_.isUndefined(params.target) && !_.isNumber(params.target)) {
663 throw new Error('invalid target - should be number');
664 }
665 if (!_.isUndefined(params.instant) && !_.isBoolean(params.instant)) {
666 throw new Error('invalid instant flag - should be boolean');
667 }
668 if (!_.isUndefined(params.segwit) && !_.isBoolean(params.segwit)) {
669 throw new Error('invalid segwit flag - should be boolean');
670 }
671 if (!_.isUndefined(params.targetWalletUnspents) && !_.isInteger(params.targetWalletUnspents)) {
672 throw new Error('invalid targetWalletUnspents flag - should be number');
673 }
674 if (!_.isUndefined(params.minSize) && !_.isNumber(params.minSize)) {
675 throw new Error('invalid argument: minSize must be a number');
676 }
677 if (!_.isUndefined(params.instant) && !_.isUndefined(params.minConfirms)) {
678 throw new Error('only one of instant and minConfirms may be defined');
679 }
680 if (!_.isUndefined(params.targetWalletUnspents) && _.isUndefined(params.target)) {
681 throw new Error('targetWalletUnspents can only be specified in conjunction with a target');
682 }
683 if (!_.isUndefined(params.allowLedgerSegwit) && !_.isBoolean(params.allowLedgerSegwit)) {
684 throw new Error('invalid argument: allowLedgerSegwit must be a boolean');
685 }
686
687 const queryObject = _.cloneDeep(params);
688
689 if (!_.isUndefined(params.target)) {
690 // skip and limit are unavailable when a target is specified
691 delete queryObject.skip;
692 delete queryObject.limit;
693 }
694
695 queryObject.segwit = true;
696 if (!_.isUndefined(params.segwit)) {
697 queryObject.segwit = params.segwit;
698 }
699
700 if (!_.isUndefined(params.allowLedgerSegwit)) {
701 queryObject.allowLedgerSegwit = params.allowLedgerSegwit;
702 }
703
704 return this.bitgo.get(this.url('/unspents'))
705 .query(queryObject)
706 .result()
707 .nodeify(callback);
708};
709
710//
711// transactions
712// List the transactions for a given wallet
713// Options include:
714// TODO: Add iterators for start/count/etc
715Wallet.prototype.transactions = function(params, callback) {
716 params = params || {};
717 common.validateParams(params, [], [], callback);
718
719 const args: string[] = [];
720 if (params.limit) {
721 if (!_.isInteger(params.limit)) {
722 throw new Error('invalid limit argument, expecting number');
723 }
724 args.push('limit=' + params.limit);
725 }
726 if (params.skip) {
727 if (!_.isInteger(params.skip)) {
728 throw new Error('invalid skip argument, expecting number');
729 }
730 args.push('skip=' + params.skip);
731 }
732 if (params.minHeight) {
733 if (!_.isInteger(params.minHeight)) {
734 throw new Error('invalid minHeight argument, expecting number');
735 }
736 args.push('minHeight=' + params.minHeight);
737 }
738 if (params.maxHeight) {
739 if (!_.isInteger(params.maxHeight) || params.maxHeight < 0) {
740 throw new Error('invalid maxHeight argument, expecting positive integer');
741 }
742 args.push('maxHeight=' + params.maxHeight);
743 }
744 if (params.minConfirms) {
745 if (!_.isInteger(params.minConfirms) || params.minConfirms < 0) {
746 throw new Error('invalid minConfirms argument, expecting positive integer');
747 }
748 args.push('minConfirms=' + params.minConfirms);
749 }
750 if (!_.isUndefined(params.compact)) {
751 if (!_.isBoolean(params.compact)) {
752 throw new Error('invalid compact argument, expecting boolean');
753 }
754 args.push('compact=' + params.compact);
755 }
756 let query = '';
757 if (args.length) {
758 query = '?' + args.join('&');
759 }
760
761 const url = this.url('/tx' + query);
762
763 return this.bitgo.get(url)
764 .result()
765 .nodeify(callback);
766};
767
768//
769// transaction
770// Get a transaction by ID for a given wallet
771Wallet.prototype.getTransaction = function(params, callback) {
772 params = params || {};
773 common.validateParams(params, ['id'], [], callback);
774
775 const url = this.url('/tx/' + params.id);
776
777 return this.bitgo.get(url)
778 .result()
779 .nodeify(callback);
780};
781
782//
783// pollForTransaction
784// Poll a transaction until successful or times out
785// Parameters:
786// id: the txid
787// delay: delay between polls in ms (default: 1000)
788// timeout: timeout in ms (default: 10000)
789Wallet.prototype.pollForTransaction = function(params, callback) {
790 const self = this;
791 params = params || {};
792 common.validateParams(params, ['id'], [], callback);
793 if (params.delay && !_.isNumber(params.delay)) {
794 throw new Error('invalid delay parameter');
795 }
796 if (params.timeout && !_.isNumber(params.timeout)) {
797 throw new Error('invalid timeout parameter');
798 }
799 params.delay = params.delay || 1000;
800 params.timeout = params.timeout || 10000;
801
802 const start = new Date();
803
804 const doNextPoll = function() {
805 return self.getTransaction(params)
806 .then(function(res) {
807 return res;
808 })
809 .catch(function(err) {
810 if (err.status !== 404 || new Date().valueOf() - start.valueOf() > params.timeout) {
811 throw err;
812 }
813 return Bluebird.delay(params.delay)
814 .then(function() {
815 return doNextPoll();
816 });
817 });
818 };
819
820 return doNextPoll();
821};
822
823//
824// transaction by sequence id
825// Get a transaction by sequence id for a given wallet
826Wallet.prototype.getWalletTransactionBySequenceId = function(params, callback) {
827 params = params || {};
828 common.validateParams(params, ['sequenceId'], [], callback);
829
830 const url = this.url('/tx/sequence/' + params.sequenceId);
831
832 return this.bitgo.get(url)
833 .result()
834 .nodeify(callback);
835};
836
837//
838// Key chains
839// Gets the user key chain for this wallet
840// The user key chain is typically the first keychain of the wallet and has the encrypted xpriv stored on BitGo.
841// Useful when trying to get the users' keychain from the server before decrypting to sign a transaction.
842Wallet.prototype.getEncryptedUserKeychain = function(params, callback) {
843 return co(function *() {
844 params = params || {};
845 common.validateParams(params, [], [], callback);
846 const self = this;
847
848 const tryKeyChain = co(function *(index) {
849 if (!self.keychains || index >= self.keychains.length) {
850 const error: any = new Error('No encrypted keychains on this wallet.');
851 error.code = 'no_encrypted_keychain_on_wallet';
852 throw error;
853 }
854
855 const params = { xpub: self.keychains[index].xpub };
856
857 const keychain = yield self.bitgo.keychains().get(params);
858 // If we find the xprv, then this is probably the user keychain we're looking for
859 keychain.walletSubPath = self.keychains[index].path;
860 if (keychain.encryptedXprv) {
861 return keychain;
862 }
863 return tryKeyChain(index + 1);
864 });
865
866 return tryKeyChain(0);
867 }).call(this).asCallback(callback);
868};
869
870//
871// createTransaction
872// Create a transaction (unsigned). To sign it, do signTransaction
873// Parameters:
874// recipients - object of recipient addresses and the amount to send to each e.g. {address:1500, address2:1500}
875// fee - the blockchain fee to send (optional)
876// feeRate - the fee per kb to send (optional)
877// minConfirms - minimum number of confirms to use when gathering unspents
878// forceChangeAtEnd - force change address to be last output (optional)
879// noSplitChange - disable automatic change splitting for purposes of unspent management
880// changeAddress - override the change address (optional)
881// validate - extra verification of change addresses (which are always verified server-side) (defaults to global config)
882// Returns:
883// callback(err, { transactionHex: string, unspents: [inputs], fee: satoshis })
884Wallet.prototype.createTransaction = function(params, callback) {
885 params = _.extend({}, params);
886 common.validateParams(params, [], [], callback);
887
888 if ((!_.isNumber(params.fee) && !_.isUndefined(params.fee)) ||
889 (!_.isNumber(params.feeRate) && !_.isUndefined(params.feeRate)) ||
890 (!_.isNumber(params.minConfirms) && !_.isUndefined(params.minConfirms)) ||
891 (!_.isBoolean(params.forceChangeAtEnd) && !_.isUndefined(params.forceChangeAtEnd)) ||
892 (!_.isString(params.changeAddress) && !_.isUndefined(params.changeAddress)) ||
893 (!_.isBoolean(params.validate) && !_.isUndefined(params.validate)) ||
894 (!_.isBoolean(params.instant) && !_.isUndefined(params.instant))) {
895 throw new Error('invalid argument');
896 }
897
898 if (!_.isObject(params.recipients)) {
899 throw new Error('expecting recipients object');
900 }
901
902 params.validate = params.validate !== undefined ? params.validate : this.bitgo.getValidate();
903 params.wallet = this;
904
905 return TransactionBuilder.createTransaction(params)
906 .nodeify(callback);
907};
908
909
910//
911// signTransaction
912// Sign a previously created transaction with a keychain
913// Parameters:
914// transactionHex - serialized form of the transaction in hex
915// unspents - array of unspent information, where each unspent is a chainPath
916// and redeemScript with the same index as the inputs in the
917// transactionHex
918// keychain - Keychain containing the xprv to sign with.
919// signingKey - For legacy safe wallets, the private key string.
920// validate - extra verification of signatures (which are always verified server-side) (defaults to global config)
921// Returns:
922// callback(err, transaction)
923Wallet.prototype.signTransaction = function(params, callback) {
924 params = _.extend({}, params);
925 common.validateParams(params, ['transactionHex'], [], callback);
926
927 if (!Array.isArray(params.unspents)) {
928 throw new Error('expecting the unspents array');
929 }
930
931 if ((!_.isObject(params.keychain) || !params.keychain.xprv) && !_.isString(params.signingKey)) {
932 // allow passing in a WIF private key for legacy safe wallet support
933 const error: any = new Error('expecting keychain object with xprv or signingKey WIF');
934 error.code = 'missing_keychain_or_signingKey';
935 throw error;
936 }
937
938 params.validate = params.validate !== undefined ? params.validate : this.bitgo.getValidate();
939 params.bitgo = this.bitgo;
940 return TransactionBuilder.signTransaction(params)
941 .then(function(result) {
942 return {
943 tx: result.transactionHex
944 };
945 })
946 .nodeify(callback);
947};
948
949//
950// send
951// Send a transaction to the Bitcoin network via BitGo.
952// One of the keys is typically signed, and BitGo will sign the other (if approved) and relay it to the P2P network.
953// Parameters:
954// tx - the hex encoded, signed transaction to send
955// Returns:
956//
957Wallet.prototype.sendTransaction = function(params, callback) {
958 params = params || {};
959 common.validateParams(params, ['tx'], ['message', 'otp'], callback);
960
961 return this.bitgo.post(this.bitgo.url('/tx/send'))
962 .send(params)
963 .result()
964 .then(function(body) {
965 if (body.pendingApproval) {
966 return _.extend(body, { status: 'pendingApproval' });
967 }
968
969 if (body.otp) {
970 return _.extend(body, { status: 'otp' });
971 }
972
973 return {
974 status: 'accepted',
975 tx: body.transaction,
976 hash: body.transactionHash,
977 instant: body.instant,
978 instantId: body.instantId
979 };
980 })
981 .nodeify(callback);
982};
983
984/**
985 * Share the wallet with an existing BitGo user.
986 * @param {string} user The recipient's user id, must have a corresponding user record in our database.
987 * @param {keychain} keychain The keychain to be shared with the recipient.
988 * @param {string} permissions A comma-separated value string that specifies the recipient's permissions if the share is accepted.
989 * @param {string} message The message to be used for this share.
990 */
991Wallet.prototype.createShare = function(params, callback) {
992 params = params || {};
993 common.validateParams(params, ['user', 'permissions'], [], callback);
994
995 if (params.keychain && !_.isEmpty(params.keychain)) {
996 if (!params.keychain.xpub || !params.keychain.encryptedXprv || !params.keychain.fromPubKey || !params.keychain.toPubKey || !params.keychain.path) {
997 throw new Error('requires keychain parameters - xpub, encryptedXprv, fromPubKey, toPubKey, path');
998 }
999 }
1000
1001 return this.bitgo.post(this.url('/share'))
1002 .send(params)
1003 .result()
1004 .nodeify(callback);
1005};
1006
1007//
1008// createInvite
1009// invite a non BitGo customer to join a wallet
1010// Parameters:
1011// email - the recipient's email address
1012// permissions - the recipient's permissions if the share is accepted
1013// Returns:
1014//
1015Wallet.prototype.createInvite = function(params, callback) {
1016 params = params || {};
1017 common.validateParams(params, ['email', 'permissions'], ['message'], callback);
1018
1019 const options: any = {
1020 toEmail: params.email,
1021 permissions: params.permissions
1022 };
1023
1024 if (params.message) {
1025 options.message = params.message;
1026 }
1027
1028 return this.bitgo.post(this.url('/invite'))
1029 .send(options)
1030 .result()
1031 .nodeify(callback);
1032};
1033
1034//
1035// confirmInviteAndShareWallet
1036// confirm my invite on this wallet to a recipient who has
1037// subsequently signed up by creating the actual wallet share
1038// Parameters:
1039// walletInviteId - the wallet invite id
1040// walletPassphrase - required if the wallet share success is expected
1041// Returns:
1042//
1043Wallet.prototype.confirmInviteAndShareWallet = function(params, callback) {
1044 params = params || {};
1045 common.validateParams(params, ['walletInviteId'], ['walletPassphrase'], callback);
1046
1047 const self = this;
1048 return this.bitgo.wallets().listInvites()
1049 .then(function(invites) {
1050 const outgoing = invites.outgoing;
1051 const invite = _.find(outgoing, function(out) {
1052 return out.id === params.walletInviteId;
1053 });
1054 if (!invite) {
1055 throw new Error('wallet invite not found');
1056 }
1057
1058 const options = {
1059 email: invite.toEmail,
1060 permissions: invite.permissions,
1061 message: invite.message,
1062 walletPassphrase: params.walletPassphrase
1063 };
1064
1065 return self.shareWallet(options);
1066 })
1067 .then(function() {
1068 return this.bitgo.put(this.bitgo.url('/walletinvite/' + params.walletInviteId));
1069 })
1070 .nodeify(callback);
1071};
1072
1073//
1074// sendCoins
1075// Send coins to a destination address from this wallet using the user key.
1076// 1. Gets the user keychain by checking the wallet for a key which has an encrypted xpriv
1077// 2. Decrypts user key
1078// 3. Creates the transaction with default fee
1079// 4. Signs transaction with decrypted user key
1080// 3. Sends the transaction to BitGo
1081//
1082// Parameters:
1083// address - the destination address
1084// amount - the amount in satoshis to be sent
1085// message - optional message to attach to transaction
1086// walletPassphrase - the passphrase to be used to decrypt the user key on this wallet
1087// xprv - the private key in string form, if walletPassphrase is not available
1088// (See transactionBuilder.createTransaction for other passthrough params)
1089// Returns:
1090//
1091Wallet.prototype.sendCoins = function(params, callback) {
1092 params = params || {};
1093 common.validateParams(params, ['address'], ['message'], callback);
1094
1095 if (!_.isNumber(params.amount)) {
1096 throw new Error('invalid argument for amount - number expected');
1097 }
1098
1099 params.recipients = {};
1100 params.recipients[params.address] = params.amount;
1101
1102 return this.sendMany(params)
1103 .nodeify(callback);
1104};
1105
1106//
1107// sendMany
1108// Send coins to multiple destination addresses from this wallet using the user key.
1109// 1. Gets the user keychain by checking the wallet for a key which has an encrypted xpriv
1110// 2. Decrypts user key
1111// 3. Creates the transaction with default fee
1112// 4. Signs transaction with decrypted user key
1113// 3. Sends the transaction to BitGo
1114//
1115// Parameters:
1116// recipients - array of { address: string, amount: number, travelInfo: object } to send to
1117// walletPassphrase - the passphrase to be used to decrypt the user key on this wallet
1118// xprv - the private key in string form, if walletPassphrase is not available
1119// (See transactionBuilder.createTransaction for other passthrough params)
1120// Returns:
1121//
1122Wallet.prototype.sendMany = function(params, callback) {
1123 params = params || {};
1124 common.validateParams(params, [], ['message', 'otp'], callback);
1125 const self = this;
1126
1127 if (!_.isObject(params.recipients)) {
1128 throw new Error('expecting recipients object');
1129 }
1130
1131 if (params.fee && !_.isNumber(params.fee)) {
1132 throw new Error('invalid argument for fee - number expected');
1133 }
1134
1135 if (params.feeRate && !_.isNumber(params.feeRate)) {
1136 throw new Error('invalid argument for feeRate - number expected');
1137 }
1138
1139 if (params.instant && !_.isBoolean(params.instant)) {
1140 throw new Error('invalid argument for instant - boolean expected');
1141 }
1142
1143 let bitgoFee;
1144 let travelInfos;
1145 let finalResult;
1146 let unspentsUsed;
1147
1148 const acceptedBuildParams = [
1149 'numBlocks', 'feeRate', 'minConfirms', 'enforceMinConfirmsForChange',
1150 'targetWalletUnspents', 'message', 'minValue', 'maxValue',
1151 'noSplitChange', 'comment'
1152 ];
1153 const preservedBuildParams = _.pick(params, acceptedBuildParams);
1154
1155 // Get the user keychain
1156 const retPromise = this.createAndSignTransaction(params)
1157 .then(function(transaction) {
1158 // Send the transaction
1159 bitgoFee = transaction.bitgoFee;
1160 travelInfos = transaction.travelInfos;
1161 unspentsUsed = transaction.unspents;
1162 return self.sendTransaction({
1163 tx: transaction.tx,
1164 message: params.message,
1165 sequenceId: params.sequenceId,
1166 instant: params.instant,
1167 otp: params.otp,
1168 // The below params are for logging only, and do not impact the API call
1169 estimatedSize: transaction.estimatedSize,
1170 buildParams: preservedBuildParams
1171 });
1172 })
1173 .then(function(result) {
1174 const tx = bitcoin.Transaction.fromHex(result.tx);
1175 const inputsSum = _.sumBy(unspentsUsed, 'value');
1176 const outputsSum = _.sumBy(tx.outs, 'value');
1177 const feeUsed = inputsSum - outputsSum;
1178 if (isNaN(feeUsed)) {
1179 throw new Error('invalid feeUsed');
1180 }
1181 result.fee = feeUsed,
1182 result.feeRate = feeUsed * 1000 / tx.virtualSize();
1183 result.travelInfos = travelInfos;
1184 if (bitgoFee) {
1185 result.bitgoFee = bitgoFee;
1186 }
1187 finalResult = result;
1188
1189 // Handle sending travel infos if they exist, but make sure we never fail here.
1190 // Error or result (with possible sub-errors) will be provided in travelResult
1191 if (travelInfos && travelInfos.length) {
1192 try {
1193 return self.pollForTransaction({ id: result.hash })
1194 .then(function() {
1195 return self.bitgo.travelRule().sendMany(result);
1196 })
1197 .then(function(res) {
1198 finalResult.travelResult = res;
1199 })
1200 .catch(function(err) {
1201 // catch async errors
1202 finalResult.travelResult = { error: err.message };
1203 });
1204 } catch (err) {
1205 // catch synchronous errors
1206 finalResult.travelResult = { error: err.message };
1207 }
1208 }
1209 })
1210 .then(function() {
1211 return finalResult;
1212 });
1213 return Bluebird.resolve(retPromise).nodeify(callback);
1214};
1215
1216/**
1217 * Accelerate a stuck transaction using Child-Pays-For-Parent (CPFP).
1218 *
1219 * This should only be used for stuck transactions which have no unconfirmed inputs.
1220 *
1221 * @param {Object} params - Input parameters
1222 * @param {String} params.transactionID - ID of transaction to accelerate
1223 * @param {Number} params.feeRate - New effective fee rate for stuck transaction (sat per 1000 bytes)
1224 * @param {Number} params.maxAdditionalUnspents - Maximum additional unspents to use from the wallet to cover any child fees that the parent unspent output cannot cover. Defaults to 100.
1225 * @param {String} params.walletPassphrase - The passphrase which should be used to decrypt the wallet private key. One of either walletPassphrase or xprv is required.
1226 * @param {String} params.xprv - The private key for the wallet. One of either walletPassphrase or xprv is required.
1227 * @param {Function} callback
1228 * @returns Result of sendTransaction() on the child transaction
1229 */
1230Wallet.prototype.accelerateTransaction = function accelerateTransaction(params, callback) {
1231
1232 const self = this;
1233 /**
1234 * Helper function to estimate a transactions size in virtual bytes.
1235 * Actual transactions may be slightly fewer virtual bytes, due to
1236 * the fact that valid ECSDA signatures have a variable length
1237 * between 8 and 73 virtual bytes.
1238 *
1239 * @param inputs.segwit The number of segwit inputs to the transaction
1240 * @param inputs.P2SH The number of P2SH inputs to the transaction
1241 * @param inputs.P2PKH The number of P2PKH inputs to the transaction
1242 */
1243 const estimateTxVSize = (inputs) => {
1244 const segwit = inputs.segwit || 0;
1245 const P2SH = inputs.P2SH || 0;
1246 const P2PKH = inputs.P2PKH || 0;
1247
1248 const childFeeInfo = TransactionBuilder.calculateMinerFeeInfo({
1249 nP2shInputs: P2SH,
1250 nP2pkhInputs: P2PKH,
1251 nP2shP2wshInputs: segwit,
1252 nOutputs: 1,
1253 feeRate: 1
1254 });
1255
1256 return childFeeInfo.size;
1257 };
1258
1259 /**
1260 * Calculate the number of satoshis that should be paid in fees by the child transaction
1261 *
1262 * @param inputs Inputs to the child transaction which are passed to estimateTxVSize
1263 * @param parentFee The number of satoshis the parent tx originally paid in fees
1264 * @param parentVSize The number of virtual bytes in the parent tx
1265 * @param feeRate The new fee rate which should be paid by the combined CPFP transaction
1266 * @returns {number} The number of satoshis the child tx should pay in fees
1267 */
1268 const estimateChildFee = ({ inputs, parentFee, parentVSize, feeRate }) => {
1269 // calculate how much more we *should* have paid in parent fees,
1270 // had the parent been originally sent with the new fee rate
1271 const additionalParentFee = _.ceil(parentVSize * feeRate / 1000) - parentFee;
1272
1273 // calculate how much we would pay in fees for the child,
1274 // if it were only paying for itself at the new fee rate
1275 const childFee = estimateTxVSize(inputs) * feeRate / 1000;
1276
1277 return _.ceil(childFee + additionalParentFee);
1278 };
1279
1280 /**
1281 * Helper function to find additional unspents to use to pay the child tx fees.
1282 * This function is called when the the parent tx output is not sufficient to
1283 * cover the total fees which should be paid by the child tx.
1284 *
1285 * @param inputs Inputs to the child transaction which are passed to estimateTxVSize
1286 * @param parentOutputValue The value of the output from the parent tx which we are using as an input to the child tx
1287 * @param parentFee The number of satoshis the parent tx originally paid in fees
1288 * @param parentVSize The number of virtual bytes in the parent tx
1289 * @param maxUnspents The maximum number of additional unspents which should be used to cover the remaining child fees
1290 * @returns An object with the additional unspents to use, the updated number of satoshis which should be paid by
1291 * the child tx, and the updated inputs for the child tx.
1292 */
1293 const findAdditionalUnspents = ({ inputs, parentOutputValue, parentFee, parentVSize, maxUnspents }) => {
1294 return co(function *coFindAdditionalUnspents() {
1295
1296 const additionalUnspents: any[] = [];
1297
1298 // ask the server for enough unspents to cover the child fee, assuming
1299 // that it can be done without additional unspents (which is not possible,
1300 // since if that were the case, findAdditionalUnspents would not have been
1301 // called in the first place. This will be corrected before returning)
1302 let currentChildFeeEstimate = estimateChildFee({ inputs, parentFee, parentVSize, feeRate: params.feeRate });
1303 let uncoveredChildFee = currentChildFeeEstimate - parentOutputValue;
1304
1305 while (uncoveredChildFee > 0 && additionalUnspents.length < maxUnspents) {
1306 // try to get enough unspents to cover the rest of the child fee
1307 const unspents = (yield this.unspents({
1308 minConfirms: 1,
1309 target: uncoveredChildFee,
1310 limit: maxUnspents - additionalUnspents.length
1311 })) as any;
1312
1313 if (unspents.length === 0) {
1314 // no more unspents are available
1315 break;
1316 }
1317
1318 let additionalUnspentValue = 0;
1319
1320 // consume all unspents returned by the server, even if we don't need
1321 // all of them to cover the child fee. This is because the server will
1322 // return enough unspent value to ensure that the minimum change amount
1323 // is achieved for the child tx, and we can't leave out those unspents
1324 // or else the minimum change amount constraint could be violated
1325 _.forEach(unspents, (unspent) => {
1326 // update the child tx inputs
1327 const unspentChain = getChain(unspent);
1328 if (unspentChain === Codes.p2shP2wsh.external || unspentChain === Codes.p2shP2wsh.internal) {
1329 inputs.segwit++;
1330 } else {
1331 inputs.P2SH++;
1332 }
1333
1334 additionalUnspents.push(unspent);
1335 additionalUnspentValue += unspent.value;
1336 });
1337
1338 currentChildFeeEstimate = estimateChildFee({ inputs, parentFee, parentVSize, feeRate: params.feeRate });
1339 uncoveredChildFee = currentChildFeeEstimate - parentOutputValue - additionalUnspentValue;
1340 }
1341
1342 if (uncoveredChildFee > 0) {
1343 // Unable to find enough unspents to cover the child fee
1344 throw new Error(`Insufficient confirmed unspents available to cover the child fee`);
1345 }
1346
1347 // found enough unspents
1348 return {
1349 additional: additionalUnspents,
1350 newChildFee: currentChildFeeEstimate,
1351 newInputs: inputs
1352 };
1353 }).call(this);
1354 };
1355
1356 /**
1357 * Helper function to get a full copy (including witness data) of an arbitrary tx using only the tx id.
1358 *
1359 * We have to use an external service for this (currently blockstream.info), since
1360 * the v1 indexer service (based on bitcoinj) does not have segwit support and
1361 * does not return any segwit related fields in the tx hex.
1362 *
1363 * @param parentTxId The ID of the transaction to get the full hex of
1364 * @returns {Bluebird<any>} The full hex for the specified transaction
1365 */
1366 async function getParentTxHex({ parentTxId }: { parentTxId: string }): Promise<string> {
1367 const explorerBaseUrl = common.Environments[self.bitgo.getEnv()].btcExplorerBaseUrl;
1368 const result = await request.get(`${explorerBaseUrl}/tx/${parentTxId}/hex`);
1369
1370 if (!result.text || !/([a-f0-9]{2})+/.test(result.text)) {
1371 throw new Error(`Did not successfully receive parent tx hex. Received '${_.truncate(result.text, { length: 100 })}' instead.`);
1372 }
1373
1374 return result.text;
1375 }
1376
1377 /**
1378 * Helper function to get the chain from an unspent or tx output.
1379 *
1380 * @param outputOrUnspent The output or unspent whose chain should be determined
1381 * @returns {number} The chain for the given output or unspent
1382 */
1383 const getChain = (outputOrUnspent) => {
1384 if (outputOrUnspent.chain !== undefined) {
1385 return outputOrUnspent.chain;
1386 }
1387
1388 if (outputOrUnspent.chainPath !== undefined) {
1389 return _.toNumber(outputOrUnspent.chainPath.split('/')[1]);
1390 }
1391
1392 // no way to tell the chain, let's just blow up now instead
1393 // of blowing up later when the undefined return value is used.
1394 // Note: for unspents the field to use is 'address', but for outputs
1395 // the field to use is 'account'
1396 throw Error(`Could not get chain for output on account ${outputOrUnspent.account || outputOrUnspent.address}`);
1397 };
1398
1399 /**
1400 * Helper function to calculate the actual value contribution an output or unspent will
1401 * contribute to a transaction, were it to be used. Each type of output or unspent
1402 * will have a different value contribution since each type has a different number
1403 * of virtual bytes, and thus will cause a different fee to be paid.
1404 *
1405 * @param outputOrUnspent Output or unspent whose effective value should be determined
1406 * @returns {number} The actual number of satoshis that this unspent or output
1407 * would contribute to a transaction, were it to be used.
1408 */
1409 const effectiveValue = (outputOrUnspent) => {
1410 const chain = getChain(outputOrUnspent);
1411 if (chain === Codes.p2shP2wsh.external || chain === Codes.p2shP2wsh.internal) {
1412 // VirtualSizes.txP2shP2wshInputSize is in bytes, so we need to convert to kB
1413 return outputOrUnspent.value - (VirtualSizes.txP2shP2wshInputSize * params.feeRate / 1000);
1414 }
1415 // VirtualSizes.txP2shInputSize is in bytes, so we need to convert to kB
1416 return outputOrUnspent.value - (VirtualSizes.txP2shInputSize * params.feeRate / 1000);
1417 };
1418
1419 /**
1420 * Coroutine which actually implements the accelerateTransaction algorithm
1421 *
1422 * Described at a high level, the algorithm is as follows:
1423 * 1) Find appropriate output from parent transaction to use as child transaction input
1424 * 2) Find unspent corresponding to parent transaction output. If not found, return to step 1.
1425 * 3) Determine if parent transaction unspent can cover entire child fee, plus minimum change
1426 * 4) If yes, go to step 6
1427 * 5) Otherwise, find additional unspents from the wallet to use to cover the remaining child fee
1428 * 6) Create and sign the child transaction, using the parent transaction output
1429 * (and, if necessary, additional wallet unspents) as inputs
1430 * 7) Broadcast the new child transaction
1431 */
1432 return co(function *coAccelerateTransaction(): any {
1433 params = params || {};
1434 common.validateParams(params, ['transactionID'], [], callback);
1435
1436 // validate fee rate
1437 if (params.feeRate === undefined) {
1438 throw new Error('Missing parameter: feeRate');
1439 }
1440 if (!_.isFinite(params.feeRate) || params.feeRate <= 0) {
1441 throw new Error('Expecting positive finite number for parameter: feeRate');
1442 }
1443
1444 // validate maxUnspents
1445 if (params.maxAdditionalUnspents === undefined) {
1446 // by default, use at most 100 additional unspents (not including the unspent output from the parent tx)
1447 params.maxAdditionalUnspents = 100;
1448 }
1449
1450 if (!_.isInteger(params.maxAdditionalUnspents) || params.maxAdditionalUnspents <= 0) {
1451 throw Error('Expecting positive integer for parameter: maxAdditionalUnspents');
1452 }
1453
1454 const parentTx = yield this.getTransaction({ id: params.transactionID });
1455 if (parentTx.confirmations > 0) {
1456 throw new Error(`Transaction ${params.transactionID} is already confirmed and cannot be accelerated`);
1457 }
1458
1459 // get the outputs from the parent tx which are to our wallet
1460 const walletOutputs = _.filter(parentTx.outputs, (output) => output.isMine);
1461
1462 if (walletOutputs.length === 0) {
1463 throw new Error(`Transaction ${params.transactionID} contains no outputs to this wallet, and thus cannot be accelerated`);
1464 }
1465
1466 // use an output from the parent with largest effective value,
1467 // but check to make sure the output is actually unspent.
1468 // An output could be spent already if the output was used in a
1469 // child tx which itself has become stuck due to low fees and is
1470 // also unconfirmed.
1471 const sortedOutputs = _.sortBy(walletOutputs, effectiveValue);
1472 let parentUnspentToUse;
1473 let outputToUse;
1474
1475 while (sortedOutputs.length > 0 && parentUnspentToUse === undefined) {
1476 outputToUse = sortedOutputs.pop();
1477
1478 // find the unspent corresponding to this particular output
1479 // TODO: is there a better way to get this unspent?
1480 // TODO: The best we can do here is set minSize = maxSize = outputToUse.value
1481 const unspentsResult = yield this.unspents({
1482 minSize: outputToUse.value,
1483 maxSize: outputToUse.value
1484 });
1485
1486 parentUnspentToUse = _.find(unspentsResult, (unspent) => {
1487 // make sure unspent belongs to the given txid
1488 if (unspent.tx_hash !== params.transactionID) {
1489 return false;
1490 }
1491 // make sure unspent has correct v_out index
1492 return unspent.tx_output_n === outputToUse.vout;
1493 });
1494 }
1495
1496 if (parentUnspentToUse === undefined) {
1497 throw new Error(`Could not find unspent output from parent tx to use as child input`);
1498 }
1499
1500 // get the full hex for the parent tx and decode it to get its vsize
1501 const parentTxHex = yield getParentTxHex({ parentTxId: params.transactionID });
1502 const decodedParent = bitcoin.Transaction.fromHex(parentTxHex);
1503 const parentVSize = decodedParent.virtualSize();
1504
1505 // make sure id from decoded tx and given tx id match
1506 // this should catch problems emanating from the use of an external service
1507 // for getting the complete parent tx hex
1508 if (decodedParent.getId() !== params.transactionID) {
1509 throw new Error(`Decoded transaction id is ${decodedParent.getId()}, which does not match given txid ${params.transactionID}`);
1510 }
1511
1512 // make sure new fee rate is greater than the parent's current fee rate
1513 // virtualSize is returned in vbytes, so we need to convert to kvB
1514 const parentRate = 1000 * parentTx.fee / parentVSize;
1515 if (params.feeRate <= parentRate) {
1516 throw new Error(`Cannot lower fee rate! (Parent tx fee rate is ${parentRate} sat/kB, and requested fee rate was ${params.feeRate} sat/kB)`);
1517 }
1518
1519 // determine if parent output can cover child fee
1520 const isParentOutputSegwit =
1521 outputToUse.chain === Codes.p2shP2wsh.external ||
1522 outputToUse.chain === Codes.p2shP2wsh.internal;
1523
1524 let childInputs = {
1525 segwit: isParentOutputSegwit ? 1 : 0,
1526 P2SH: isParentOutputSegwit ? 0 : 1
1527 };
1528
1529 let childFee = estimateChildFee({
1530 inputs: childInputs,
1531 parentFee: parentTx.fee,
1532 feeRate: params.feeRate,
1533 parentVSize
1534 });
1535
1536 const unspentsToUse = [parentUnspentToUse];
1537
1538 // try to get the min change size from the server, otherwise default to 0.1 BTC
1539 // TODO: minChangeSize is not currently a constant defined on the client and should be added
1540 const minChangeSize = this.bitgo.getConstants().minChangeSize || 1e7;
1541
1542 if (outputToUse.value < childFee + minChangeSize) {
1543 // parent output cannot cover child fee plus the minimum change,
1544 // must find additional unspents to cover the difference
1545 const { additional, newChildFee, newInputs } = yield findAdditionalUnspents({
1546 inputs: childInputs,
1547 parentOutputValue: outputToUse.value,
1548 parentFee: parentTx.fee,
1549 maxUnspents: params.maxAdditionalUnspents,
1550 parentVSize
1551 });
1552 childFee = newChildFee;
1553 childInputs = newInputs;
1554 unspentsToUse.push(... additional);
1555 }
1556
1557 // sanity check the fee rate we're paying for the combined tx
1558 // to make sure it's under the max fee rate. Only the child tx
1559 // can break this limit, but the combined tx shall not
1560 const maxFeeRate = this.bitgo.getConstants().maxFeeRate;
1561 const childVSize = estimateTxVSize(childInputs);
1562 const combinedVSize = childVSize + parentVSize;
1563 const combinedFee = parentTx.fee + childFee;
1564 // combined fee rate must be in sat/kB, so we need to convert
1565 const combinedFeeRate = 1000 * combinedFee / combinedVSize;
1566
1567 if (combinedFeeRate > maxFeeRate) {
1568 throw new Error(`Transaction cannot be accelerated. Combined fee rate of ${combinedFeeRate} sat/kB exceeds maximum fee rate of ${maxFeeRate} sat/kB`);
1569 }
1570
1571 // create a new change address and determine change amount.
1572 // the tx builder will reject transactions which have no recipients,
1573 // and such zero-output transactions are forbidden by the Bitcoin protocol,
1574 // so we need at least a single recipient for the change which won't be pruned.
1575 const changeAmount = _.sumBy(unspentsToUse, (unspent) => unspent.value) - childFee;
1576 const changeChain = this.getChangeChain({});
1577 const changeAddress = yield this.createAddress({ chain: changeChain });
1578
1579 // create the child tx and broadcast
1580 const tx = yield this.createAndSignTransaction({
1581 unspents: unspentsToUse,
1582 recipients: [{
1583 address: changeAddress.address,
1584 amount: changeAmount
1585 }],
1586 fee: childFee,
1587 bitgoFee: {
1588 amount: 0,
1589 address: ''
1590 },
1591 xprv: params.xprv,
1592 walletPassphrase: params.walletPassphrase
1593 });
1594
1595
1596 // child fee rate must be in sat/kB, so we need to convert
1597 const childFeeRate = 1000 * childFee / childVSize;
1598 if (childFeeRate > maxFeeRate) {
1599 // combined tx is within max fee rate limits, but the child tx is not.
1600 // in this case, we need to use the ignoreMaxFeeRate flag to get the child tx to be accepted
1601 tx.ignoreMaxFeeRate = true;
1602 }
1603
1604 return this.sendTransaction(tx);
1605 }).call(this).asCallback(callback);
1606};
1607
1608//
1609// createAndSignTransaction
1610// INTERNAL function to create and sign a transaction
1611//
1612// Parameters:
1613// recipients - array of { address, amount } to send to
1614// walletPassphrase - the passphrase to be used to decrypt the user key on this wallet
1615// (See transactionBuilder.createTransaction for other passthrough params)
1616// Returns:
1617//
1618Wallet.prototype.createAndSignTransaction = function(params, callback) {
1619 return co(function *() {
1620 params = params || {};
1621 common.validateParams(params, [], [], callback);
1622
1623 if (!_.isObject(params.recipients)) {
1624 throw new Error('expecting recipients object');
1625 }
1626
1627 if (params.fee && !_.isNumber(params.fee)) {
1628 throw new Error('invalid argument for fee - number expected');
1629 }
1630
1631 if (params.feeRate && !_.isNumber(params.feeRate)) {
1632 throw new Error('invalid argument for feeRate - number expected');
1633 }
1634
1635 if (params.dynamicFeeConfirmTarget && !_.isNumber(params.dynamicFeeConfirmTarget)) {
1636 throw new Error('invalid argument for confirmTarget - number expected');
1637 }
1638
1639 if (params.instant && !_.isBoolean(params.instant)) {
1640 throw new Error('invalid argument for instant - boolean expected');
1641 }
1642
1643 const transaction = (yield this.createTransaction(params)) as any;
1644 const fee = transaction.fee;
1645 const feeRate = transaction.feeRate;
1646 const estimatedSize = transaction.estimatedSize;
1647 const bitgoFee = transaction.bitgoFee;
1648 const travelInfos = transaction.travelInfos;
1649 const unspents = transaction.unspents;
1650
1651 // Sign the transaction
1652 try {
1653 const keychain = yield this.getAndPrepareSigningKeychain(params);
1654 transaction.keychain = keychain;
1655 } catch (e) {
1656 if (e.code !== 'no_encrypted_keychain_on_wallet') {
1657 throw e;
1658 }
1659 // this might be a safe wallet, so let's retrieve the private key info
1660 yield this.refresh({ gpk: true });
1661 const safeUserKey = _.get(this.wallet, 'private.userPrivKey');
1662 if (_.isString(safeUserKey) && _.isString(params.walletPassphrase)) {
1663 transaction.signingKey = this.bitgo.decrypt({ password: params.walletPassphrase, input: safeUserKey });
1664 } else {
1665 throw e;
1666 }
1667 }
1668
1669 transaction.feeSingleKeyWIF = params.feeSingleKeyWIF;
1670 const result = yield this.signTransaction(transaction);
1671 return _.extend(result, {
1672 fee,
1673 feeRate,
1674 instant: params.instant,
1675 bitgoFee,
1676 travelInfos,
1677 estimatedSize,
1678 unspents
1679 });
1680 }).call(this).asCallback(callback);
1681};
1682
1683//
1684// getAndPrepareSigningKeychain
1685// INTERNAL function to get the user keychain for signing.
1686// Caller must provider either a keychain, or walletPassphrase or xprv as a string
1687// If the caller provides the keychain with xprv, it is simply returned.
1688// If the caller provides the encrypted xprv (walletPassphrase), then fetch the keychain object and decrypt
1689// Otherwise if the xprv is provided, fetch the keychain object and augment it with the xprv.
1690//
1691// Parameters:
1692// keychain - keychain with xprv
1693// xprv - the private key in string form
1694// walletPassphrase - the passphrase to be used to decrypt the user key on this wallet
1695// Returns:
1696// Keychain object containing xprv, xpub and paths
1697//
1698Wallet.prototype.getAndPrepareSigningKeychain = function(params, callback) {
1699 params = params || {};
1700
1701 // If keychain with xprv is already provided, use it
1702 if (_.isObject(params.keychain) && params.keychain.xprv) {
1703 return Bluebird.resolve(params.keychain);
1704 }
1705
1706 common.validateParams(params, [], ['walletPassphrase', 'xprv'], callback);
1707
1708 if ((params.walletPassphrase && params.xprv) || (!params.walletPassphrase && !params.xprv)) {
1709 throw new Error('must provide exactly one of xprv or walletPassphrase');
1710 }
1711
1712 const self = this;
1713
1714 // Caller provided a wallet passphrase
1715 if (params.walletPassphrase) {
1716 return self.getEncryptedUserKeychain()
1717 .then(function(keychain) {
1718 // Decrypt the user key with a passphrase
1719 try {
1720 keychain.xprv = self.bitgo.decrypt({ password: params.walletPassphrase, input: keychain.encryptedXprv });
1721 } catch (e) {
1722 throw new Error('Unable to decrypt user keychain');
1723 }
1724 return keychain;
1725 });
1726 }
1727
1728 // Caller provided an xprv - validate and construct keychain object
1729 let xpub;
1730 try {
1731 xpub = bitcoin.HDNode.fromBase58(params.xprv).neutered().toBase58();
1732 } catch (e) {
1733 throw new Error('Unable to parse the xprv');
1734 }
1735
1736 if (xpub === params.xprv) {
1737 throw new Error('xprv provided was not a private key (found xpub instead)');
1738 }
1739
1740 const walletXpubs = _.map(self.keychains, 'xpub');
1741 if (!_.includes(walletXpubs, xpub)) {
1742 throw new Error('xprv provided was not a keychain on this wallet!');
1743 }
1744
1745 // get the keychain object from bitgo to find the path and (potential) wallet structure
1746 return self.bitgo.keychains().get({ xpub: xpub })
1747 .then(function(keychain) {
1748 keychain.xprv = params.xprv;
1749 return keychain;
1750 });
1751};
1752
1753/**
1754 * Takes a wallet's unspents and fans them out into a larger number of equally sized unspents
1755 * @param params
1756 * target: set how many unspents you want to have in the end
1757 * minConfirms: minimum number of confirms the unspents must have
1758 * xprv: private key to sign transaction
1759 * walletPassphrase: wallet passphrase to decrypt the wallet's private key
1760 * @param callback
1761 * @returns {*}
1762 */
1763Wallet.prototype.fanOutUnspents = function(params, callback) {
1764 const self = this;
1765 return Bluebird.coroutine(function *() {
1766 // maximum number of inputs for fanout transaction
1767 // (when fanning out, we take all the unspents and make a bigger number of outputs)
1768 const MAX_FANOUT_INPUT_COUNT = 80;
1769 // maximum number of outputs for fanout transaction
1770 const MAX_FANOUT_OUTPUT_COUNT = 300;
1771 params = params || {};
1772 common.validateParams(params, [], ['walletPassphrase', 'xprv'], callback);
1773 const validate = params.validate === undefined ? true : params.validate;
1774
1775 const target = params.target;
1776 // the target must be defined, be a number, be at least two, and be a natural number
1777 if (!_.isNumber(target) || target < 2 || (target % 1) !== 0) {
1778 throw new Error('Target needs to be a positive integer');
1779 }
1780 if (target > MAX_FANOUT_OUTPUT_COUNT) {
1781 throw new Error('Fan out target too high');
1782 }
1783
1784 let minConfirms = params.minConfirms;
1785 if (minConfirms === undefined) {
1786 minConfirms = 1;
1787 }
1788 if (!_.isNumber(minConfirms) || minConfirms < 0) {
1789 throw new Error('minConfirms needs to be an integer >= 0');
1790 }
1791
1792 /**
1793 * Split a natural number N into n almost equally sized (±1) natural numbers.
1794 * In order to calculate the sizes of the parts, we calculate floor(N/n), and thus have the base size of all parts.
1795 * If N % n !== 0, this leaves us with a remainder r where r < n. We distribute r equally among the n parts by
1796 * adding 1 to the first r parts.
1797 * @param total
1798 * @param partCount
1799 * @returns {Array}
1800 */
1801 const splitNumberIntoCloseNaturalNumbers = function(total, partCount) {
1802 const partSize = Math.floor(total / partCount);
1803 const remainder = total - partSize * partCount;
1804 // initialize placeholder array
1805 const almostEqualParts = new Array(partCount);
1806 // fill the first remainder parts with the value partSize+1
1807 _.fill(almostEqualParts, partSize + 1, 0, remainder);
1808 // fill the remaining parts with the value partSize
1809 _.fill(almostEqualParts, partSize, remainder);
1810 // assert the correctness of the almost equal parts
1811 // TODO: add check for the biggest deviation between any two parts and make sure it's <= 1
1812 if (_(almostEqualParts).sum() !== total || _(almostEqualParts).size() !== partCount) {
1813 throw new Error('part sum or part count mismatch');
1814 }
1815 return almostEqualParts;
1816 };
1817
1818 // first, let's take all the wallet's unspents (with min confirms if necessary)
1819 const allUnspents = (yield self.unspents({ minConfirms: minConfirms })) as any;
1820 if (allUnspents.length < 1) {
1821 throw new Error('No unspents to branch out');
1822 }
1823
1824 // this consolidation is essentially just a waste of money
1825 if (allUnspents.length >= target) {
1826 throw new Error('Fan out target has to be bigger than current number of unspents');
1827 }
1828
1829 // we have at the very minimum 81 inputs, and 81 outputs. That transaction will be big
1830 // in the medium run, this algorithm could be reworked to only work with a subset of the transactions
1831 if (allUnspents.length > MAX_FANOUT_INPUT_COUNT) {
1832 throw new Error('Too many unspents');
1833 }
1834
1835 // this is all the money that is currently in the wallet
1836 const grossAmount = _(allUnspents).map('value').sum();
1837
1838 // in order to not modify the params object, we create a copy
1839 const txParams = _.extend({}, params);
1840 txParams.unspents = allUnspents;
1841 txParams.recipients = {};
1842
1843 // create target amount of new addresses for this wallet
1844 const newAddressPromises = _.range(target)
1845 .map(() => self.createAddress({ chain: self.getChangeChain(params), validate: validate }));
1846 const newAddresses = yield Bluebird.all(newAddressPromises);
1847 // let's find a nice, equal distribution of our Satoshis among the new addresses
1848 const splitAmounts = splitNumberIntoCloseNaturalNumbers(grossAmount, target);
1849 // map the newly created addresses to the almost components amounts we just calculated
1850 txParams.recipients = _.zipObject(_.map(newAddresses, 'address'), splitAmounts);
1851 txParams.noSplitChange = true;
1852 // attempt to create a transaction. As it is a wallet-sweeping transaction with no fee, we expect it to fail
1853 try {
1854 yield self.sendMany(txParams);
1855 } catch (error) {
1856 // as expected, the transaction creation did indeed fail due to insufficient fees
1857 // the error suggests a fee value which we then use for the transaction
1858 // however, let's make sure it wasn't something else
1859 if (!error.fee && (!error.result || !error.result.fee)) {
1860 // if the error does not contain a fee property, it is something else that has gone awry, and we throw it
1861 const debugParams = _.omit(txParams, ['walletPassphrase', 'xprv']);
1862 error.message += `\n\nTX PARAMS:\n ${JSON.stringify(debugParams, null, 4)}`;
1863 throw error;
1864 }
1865 const baseFee = error.fee || error.result.fee;
1866 let totalFee = baseFee;
1867 if (error.result.bitgoFee && error.result.bitgoFee.amount) {
1868 totalFee += error.result.bitgoFee.amount;
1869 txParams.bitgoFee = error.result.bitgoFee;
1870 }
1871
1872 // Need to clear these out since only 1 may be set
1873 delete txParams.fee;
1874 txParams.originalFeeRate = txParams.feeRate;
1875 delete txParams.feeRate;
1876 delete txParams.feeTxConfirmTarget;
1877 txParams.fee = baseFee;
1878 // in order to maintain the equal distribution, we need to subtract the fee from the cumulative funds
1879 // in case some unspents got pruned, we need to use error.result.available
1880 const netAmount = error.result.available - totalFee; // after fees
1881 // that means that the distribution has to be recalculated
1882 const remainingSplitAmounts = splitNumberIntoCloseNaturalNumbers(netAmount, target);
1883 // and the distribution again mapped to the new addresses
1884 txParams.recipients = _.zipObject(_.map(newAddresses, 'address'), remainingSplitAmounts);
1885 }
1886
1887 // this time, the transaction creation should work
1888 let fanoutTx;
1889 try {
1890 fanoutTx = yield self.sendMany(txParams);
1891 } catch (e) {
1892 const debugParams = _.omit(txParams, ['walletPassphrase', 'xprv']);
1893 e.message += `\n\nTX PARAMS:\n ${JSON.stringify(debugParams, null, 4)}`;
1894 throw e;
1895 }
1896
1897 return Bluebird.resolve(fanoutTx).asCallback(callback);
1898 })().asCallback(callback);
1899};
1900
1901/**
1902 * Determine whether to fan out or coalesce a wallet's unspents
1903 * @param params
1904 * @param callback
1905 * @returns {Request|Promise.<T>|*}
1906 */
1907Wallet.prototype.regroupUnspents = function(params, callback) {
1908 params = params || {};
1909 const target = params.target;
1910 if (!_.isNumber(target) || target < 1 || (target % 1) !== 0) {
1911 // the target must be defined, be a number, be at least one, and be a natural number
1912 throw new Error('Target needs to be a positive integer');
1913 }
1914
1915 let minConfirms = params.minConfirms;
1916 if (minConfirms === undefined) {
1917 minConfirms = 1;
1918 }
1919 if ((!_.isNumber(minConfirms) || minConfirms < 0)) {
1920 throw new Error('minConfirms needs to be an integer equal to or bigger than 0');
1921 }
1922
1923 const self = this;
1924 return self.unspents({ minConfirms: minConfirms })
1925 .then(function(unspents) {
1926 if (unspents.length === target) {
1927 return unspents;
1928 } else if (unspents.length > target) {
1929 return self.consolidateUnspents(params, callback);
1930 } else if (unspents.length < target) {
1931 return self.fanOutUnspents(params, callback);
1932 }
1933 });
1934};
1935
1936/**
1937 * Consolidate a wallet's unspents into fewer unspents
1938 * @param params
1939 * target: set how many unspents you want to have in the end
1940 * maxInputCountPerConsolidation: set how many maximum inputs are to be permitted per consolidation batch
1941 * xprv: private key to sign transaction
1942 * walletPassphrase: wallet passphrase to decrypt the wallet's private key
1943 * maxIterationCount: maximum number of iterations to be performed until function stops
1944 * progressCallback: method to be called with object outlining current progress details
1945 * @param callback
1946 * @returns {*}
1947 */
1948Wallet.prototype.consolidateUnspents = function(params, callback) {
1949 params = params || {};
1950 common.validateParams(params, [], ['walletPassphrase', 'xprv'], callback);
1951 const validate = params.validate === undefined ? true : params.validate;
1952
1953 let target = params.target;
1954 if (target === undefined) {
1955 target = 1;
1956 } else if (!_.isNumber(target) || target < 1 || (target % 1) !== 0) {
1957 // the target must be defined, be a number, be at least one, and be a natural number
1958 throw new Error('Target needs to be a positive integer');
1959 }
1960
1961 if (params.maxSize && !_.isNumber(params.maxSize)) {
1962 throw new Error('maxSize should be a number');
1963 }
1964
1965 if (params.minSize && !_.isNumber(params.minSize)) {
1966 throw new Error('minSize should be a number');
1967 }
1968
1969 // maximum number of inputs per transaction for consolidation
1970 const MAX_INPUT_COUNT = 200;
1971 let maxInputCount = params.maxInputCountPerConsolidation;
1972 if (maxInputCount === undefined) { // null or unidentified, because equality to zero returns true in if(! clause
1973 maxInputCount = MAX_INPUT_COUNT;
1974 }
1975 if (typeof (maxInputCount) !== 'number' || maxInputCount < 2 || (maxInputCount % 1) !== 0) {
1976 throw new Error('Maximum consolidation input count needs to be an integer equal to or bigger than 2');
1977 } else if (maxInputCount > MAX_INPUT_COUNT) {
1978 throw new Error('Maximum consolidation input count cannot be bigger than ' + MAX_INPUT_COUNT);
1979 }
1980
1981 const maxIterationCount = params.maxIterationCount || -1;
1982 if (params.maxIterationCount && (!_.isNumber(maxIterationCount) || maxIterationCount < 1) || (maxIterationCount % 1) !== 0) {
1983 throw new Error('Maximum iteration count needs to be an integer equal to or bigger than 1');
1984 }
1985
1986 let minConfirms = params.minConfirms;
1987 if (minConfirms === undefined) {
1988 minConfirms = 1;
1989 }
1990 if ((!_.isNumber(minConfirms) || minConfirms < 0)) {
1991 throw new Error('minConfirms needs to be an integer equal to or bigger than 0');
1992 }
1993
1994 let minSize = params.minSize || 0;
1995 if (params.feeRate) {
1996 // fee rate is in satoshis per kB, input size in bytes
1997 const feeBasedMinSize = Math.ceil(VirtualSizes.txP2shInputSize * params.feeRate / 1000);
1998 if (params.minSize && minSize < feeBasedMinSize) {
1999 throw new Error('provided minSize too low due to too high fee rate');
2000 }
2001 minSize = Math.max(feeBasedMinSize, minSize);
2002
2003 if (!params.minSize) {
2004 // fee rate-based min size needs no logging if it was set explicitly
2005 console.log('Only consolidating unspents larger than ' + minSize + ' satoshis to avoid wasting money on fees. To consolidate smaller unspents, use a lower fee rate.');
2006 }
2007 }
2008
2009 let iterationCount = 0;
2010
2011 const self = this;
2012 let consolidationIndex = 0;
2013
2014 /**
2015 * Consolidate one batch of up to MAX_INPUT_COUNT unspents.
2016 * @returns {*}
2017 */
2018 const runNextConsolidation = co(function *() {
2019 const consolidationTransactions: any[] = [];
2020 let isFinalConsolidation = false;
2021 iterationCount++;
2022 /*
2023 We take a maximum of unspentBulkSizeLimit unspents from the wallet. We want to make sure that we swipe the wallet
2024 clean of all excessive unspents, so we add 1 to the target unspent count to make sure we haven't missed anything.
2025 In case there are even more unspents than that, to make the consolidation as fast as possible, we expand our
2026 selection to include as many as the maximum permissible number of inputs per consolidation batch.
2027 Should the target number of unspents be higher than the maximum number if inputs per consolidation,
2028 we still want to fetch them all simply to be able to determine whether or not a consolidation can be performed
2029 at all, which is dependent on the number of all unspents being higher than the target.
2030 In the next version of the unspents version SDK, we will know the total number of unspents without having to fetch
2031 them, and therefore will be able to simplify this method.
2032 */
2033
2034 const queryParams: any = {
2035 limit: target + maxInputCount,
2036 minConfirms: minConfirms,
2037 minSize: minSize
2038 };
2039 if (params.maxSize) {
2040 queryParams.maxSize = params.maxSize;
2041 }
2042 const allUnspents = (yield self.unspents(queryParams)) as any;
2043 // this consolidation is essentially just a waste of money
2044 if (allUnspents.length <= target) {
2045 if (iterationCount <= 1) {
2046 // this is the first iteration, so the method is incorrect
2047 throw new Error('Fewer unspents than consolidation target. Use fanOutUnspents instead.');
2048 } else {
2049 // it's a later iteration, so the target may have been surpassed (due to confirmations in the background)
2050 throw new Error('Done');
2051 }
2052 }
2053
2054 const allUnspentsCount = allUnspents.length;
2055
2056 // how many of the unspents do we want to consolidate?
2057 // the +1 is because the consolidated block becomes a new unspent later
2058 let targetInputCount = allUnspentsCount - target + 1;
2059 targetInputCount = Math.min(targetInputCount, allUnspents.length);
2060
2061 // if the targetInputCount requires more inputs than we allow per batch, we reduce the number
2062 const inputCount = Math.min(targetInputCount, maxInputCount);
2063
2064 // if either the number of inputs left to coalesce equals the number we will coalesce in this iteration
2065 // or if the number of iterations matches the maximum permitted number
2066 isFinalConsolidation = (inputCount === targetInputCount || iterationCount === maxIterationCount);
2067
2068 const currentChunk = allUnspents.splice(0, inputCount);
2069 const changeChain = self.getChangeChain(params);
2070 const newAddress = (yield self.createAddress({ chain: changeChain, validate: validate })) as any;
2071 const txParams = _.extend({}, params);
2072 const currentAddress = newAddress;
2073 // the total amount that we are consolidating within this batch
2074 const grossAmount = _(currentChunk).map('value').sum(); // before fees
2075
2076 txParams.unspents = currentChunk;
2077 txParams.recipients = {};
2078 txParams.recipients[newAddress.address] = grossAmount;
2079 txParams.noSplitChange = true;
2080
2081 if (txParams.unspents.length <= 1) {
2082 throw new Error('Done');
2083 }
2084
2085 // let's attempt to create this transaction. We expect it to fail because no fee is set.
2086 try {
2087 yield self.sendMany(txParams);
2088 } catch (error) {
2089 // this error should occur due to insufficient funds
2090 // however, let's make sure it wasn't something else
2091 if (!error.fee && (!error.result || !error.result.fee)) {
2092 // if the error does not contain a fee property, it is something else that has gone awry, and we throw it
2093 const debugParams = _.omit(txParams, ['walletPassphrase', 'xprv']);
2094 error.message += `\n\nTX PARAMS:\n ${JSON.stringify(debugParams, null, 4)}`;
2095 throw error;
2096 }
2097 const baseFee = error.fee || error.result.fee;
2098 let bitgoFee = 0;
2099 let totalFee = baseFee;
2100 if (error.result.bitgoFee && error.result.bitgoFee.amount) {
2101 bitgoFee = error.result.bitgoFee.amount;
2102 totalFee += bitgoFee;
2103 txParams.bitgoFee = error.result.bitgoFee;
2104 }
2105
2106 // if the net amount is negative, it should be replaced with the minimum output size
2107 const netAmount = Math.max(error.result.available - totalFee, self.bitgo.getConstants().minOutputSize);
2108 // Need to clear these out since only 1 may be set
2109 delete txParams.fee;
2110 txParams.originalFeeRate = txParams.feeRate;
2111 delete txParams.feeRate;
2112 delete txParams.feeTxConfirmTarget;
2113
2114 // we set the fee explicitly
2115 txParams.fee = error.result.available - netAmount - bitgoFee;
2116 txParams.recipients[newAddress.address] = netAmount;
2117 }
2118 // this transaction, on the other hand, should be created with no issues, because an appropriate fee is set
2119 let sentTx;
2120 try {
2121 sentTx = yield self.sendMany(txParams);
2122 } catch (e) {
2123 const debugParams = _.omit(txParams, ['walletPassphrase', 'xprv']);
2124 e.message += `\n\nTX PARAMS:\n ${JSON.stringify(debugParams, null, 4)}`;
2125 throw e;
2126 }
2127 consolidationTransactions.push(sentTx);
2128 if (_.isFunction(params.progressCallback)) {
2129 params.progressCallback({
2130 txid: sentTx.hash,
2131 destination: currentAddress,
2132 amount: grossAmount,
2133 fee: sentTx.fee,
2134 inputCount: inputCount,
2135 index: consolidationIndex
2136 });
2137 }
2138 consolidationIndex++;
2139 if (!isFinalConsolidation) {
2140 // this last consolidation has not yet brought the unspents count down to the target unspent count
2141 // therefore, we proceed by consolidating yet another batch
2142 // before we do that, we wait 1 second so that the newly created unspent will be fetched in the next batch
2143 yield Bluebird.delay(1000);
2144 consolidationTransactions.push(...((yield runNextConsolidation()) as any));
2145 }
2146 // this is the final consolidation transaction. We return all the ones we've had so far
2147 return consolidationTransactions;
2148 });
2149
2150 return runNextConsolidation(this, target)
2151 .catch(function(err) {
2152 if (err.message === 'Done') {
2153 return;
2154 }
2155 throw err;
2156 })
2157 .nodeify(callback);
2158};
2159
2160Wallet.prototype.shareWallet = function(params, callback) {
2161 params = params || {};
2162 common.validateParams(params, ['email', 'permissions'], ['walletPassphrase', 'message'], callback);
2163
2164 if (params.reshare !== undefined && !_.isBoolean(params.reshare)) {
2165 throw new Error('Expected reshare to be a boolean.');
2166 }
2167
2168 if (params.skipKeychain !== undefined && !_.isBoolean(params.skipKeychain)) {
2169 throw new Error('Expected skipKeychain to be a boolean. ');
2170 }
2171 const needsKeychain = !params.skipKeychain && params.permissions.indexOf('spend') !== -1;
2172
2173 if (params.disableEmail !== undefined && !_.isBoolean(params.disableEmail)) {
2174 throw new Error('Expected disableEmail to be a boolean.');
2175 }
2176
2177 const self = this;
2178 let sharing;
2179 let sharedKeychain;
2180 return this.bitgo.getSharingKey({ email: params.email.toLowerCase() })
2181 .then(function(result) {
2182 sharing = result;
2183
2184 if (needsKeychain) {
2185 return self.getEncryptedUserKeychain({})
2186 .then(function(keychain) {
2187 // Decrypt the user key with a passphrase
2188 if (keychain.encryptedXprv) {
2189 if (!params.walletPassphrase) {
2190 throw new Error('Missing walletPassphrase argument');
2191 }
2192 try {
2193 keychain.xprv = self.bitgo.decrypt({ password: params.walletPassphrase, input: keychain.encryptedXprv });
2194 } catch (e) {
2195 throw new Error('Unable to decrypt user keychain');
2196 }
2197
2198 const eckey = makeRandomKey();
2199 const secret = self.bitgo.getECDHSecret({ eckey: eckey, otherPubKeyHex: sharing.pubkey });
2200 const newEncryptedXprv = self.bitgo.encrypt({ password: secret, input: keychain.xprv });
2201
2202 sharedKeychain = {
2203 xpub: keychain.xpub,
2204 encryptedXprv: newEncryptedXprv,
2205 fromPubKey: eckey.getPublicKeyBuffer().toString('hex'),
2206 toPubKey: sharing.pubkey,
2207 path: sharing.path
2208 };
2209 }
2210 });
2211 }
2212 })
2213 .then(function() {
2214 interface Options {
2215 user: any;
2216 permissions: string;
2217 reshare: boolean;
2218 message: string;
2219 disableEmail: any;
2220 keychain?: any;
2221 skipKeychain?: boolean
2222 }
2223
2224 const options: Options = {
2225 user: sharing.userId,
2226 permissions: params.permissions,
2227 reshare: params.reshare,
2228 message: params.message,
2229 disableEmail: params.disableEmail
2230 };
2231 if (sharedKeychain) {
2232 options.keychain = sharedKeychain;
2233 } else if (params.skipKeychain) {
2234 options.keychain = {};
2235 }
2236
2237 return self.createShare(options);
2238 })
2239 .nodeify(callback);
2240};
2241
2242Wallet.prototype.removeUser = function(params, callback) {
2243 params = params || {};
2244 common.validateParams(params, ['user'], [], callback);
2245
2246 return this.bitgo.del(this.url('/user/' + params.user))
2247 .send()
2248 .result()
2249 .nodeify(callback);
2250};
2251
2252Wallet.prototype.getPolicy = function(params, callback) {
2253 params = params || {};
2254 common.validateParams(params, [], [], callback);
2255
2256 return this.bitgo.get(this.url('/policy'))
2257 .send()
2258 .result()
2259 .nodeify(callback);
2260};
2261
2262Wallet.prototype.getPolicyStatus = function(params, callback) {
2263 params = params || {};
2264 common.validateParams(params, [], [], callback);
2265
2266 return this.bitgo.get(this.url('/policy/status'))
2267 .send()
2268 .result()
2269 .nodeify(callback);
2270};
2271
2272Wallet.prototype.setPolicyRule = function(params, callback) {
2273 params = params || {};
2274 common.validateParams(params, ['id', 'type'], ['message'], callback);
2275
2276 if (!_.isObject(params.condition)) {
2277 throw new Error('missing parameter: conditions object');
2278 }
2279
2280 if (!_.isObject(params.action)) {
2281 throw new Error('missing parameter: action object');
2282 }
2283
2284 return this.bitgo.put(this.url('/policy/rule'))
2285 .send(params)
2286 .result()
2287 .nodeify(callback);
2288};
2289
2290Wallet.prototype.removePolicyRule = function(params, callback) {
2291 params = params || {};
2292 common.validateParams(params, ['id'], ['message'], callback);
2293
2294 return this.bitgo.del(this.url('/policy/rule'))
2295 .send(params)
2296 .result()
2297 .nodeify(callback);
2298};
2299
2300Wallet.prototype.listWebhooks = function(params, callback) {
2301 params = params || {};
2302 common.validateParams(params, [], [], callback);
2303
2304 return this.bitgo.get(this.url('/webhooks'))
2305 .send()
2306 .result()
2307 .nodeify(callback);
2308};
2309
2310/**
2311 * Simulate wallet webhook, currently for webhooks of type transaction and pending approval
2312 * @param params
2313 * - webhookId (required): id of the webhook to be simulated
2314 * - txHash (optional but required for transaction webhooks) hash of the simulated transaction
2315 * - pendingApprovalId (optional but required for pending approval webhooks) id of the simulated pending approval
2316 * @param callback
2317 * @returns {*}
2318 */
2319Wallet.prototype.simulateWebhook = function(params, callback) {
2320 params = params || {};
2321 common.validateParams(params, ['webhookId'], ['txHash', 'pendingApprovalId'], callback);
2322
2323 const hasTxHash = !!params.txHash;
2324 const hasPendingApprovalId = !!params.pendingApprovalId;
2325
2326 if ((hasTxHash && hasPendingApprovalId) || (!hasTxHash && !hasPendingApprovalId)) {
2327 throw new Error('must supply either txHash or pendingApprovalId, but not both');
2328 }
2329
2330 // depending on the coin type of the wallet, the txHash has to adhere to its respective format
2331 // but the server takes care of that
2332
2333 // only take the txHash and pendingApprovalId properties
2334 const filteredParams = _.pick(params, ['txHash', 'pendingApprovalId']);
2335
2336 const webhookId = params.webhookId;
2337 return this.bitgo.post(this.url('/webhooks/' + webhookId + '/simulate'))
2338 .send(filteredParams)
2339 .result()
2340 .nodeify(callback);
2341};
2342
2343Wallet.prototype.addWebhook = function(params, callback) {
2344 params = params || {};
2345 common.validateParams(params, ['url', 'type'], [], callback);
2346
2347 return this.bitgo.post(this.url('/webhooks'))
2348 .send(params)
2349 .result()
2350 .nodeify(callback);
2351};
2352
2353Wallet.prototype.removeWebhook = function(params, callback) {
2354 params = params || {};
2355 common.validateParams(params, ['url', 'type'], [], callback);
2356
2357 return this.bitgo.del(this.url('/webhooks'))
2358 .send(params)
2359 .result()
2360 .nodeify(callback);
2361};
2362
2363Wallet.prototype.estimateFee = function(params, callback) {
2364 common.validateParams(params, [], [], callback);
2365
2366 if (params.amount && params.recipients) {
2367 throw new Error('cannot specify both amount as well as recipients');
2368 }
2369 if (params.recipients && !_.isObject(params.recipients)) {
2370 throw new Error('recipients must be array of { address: abc, amount: 100000 } objects');
2371 }
2372 if (params.amount && !_.isNumber(params.amount)) {
2373 throw new Error('invalid amount argument, expecting number');
2374 }
2375
2376 const recipients = params.recipients || [];
2377
2378 if (params.amount) {
2379 // only the amount was passed in, so we need to make a false recipient to run createTransaction with
2380 recipients.push({
2381 address: common.Environments[this.bitgo.env].signingAddress, // any address will do
2382 amount: params.amount
2383 });
2384 }
2385
2386 const transactionParams = _.extend({}, params);
2387 transactionParams.amount = undefined;
2388 transactionParams.recipients = recipients;
2389
2390 return this.createTransaction(transactionParams)
2391 .then(function(tx) {
2392 return {
2393 estimatedSize: tx.estimatedSize,
2394 fee: tx.fee,
2395 feeRate: tx.feeRate
2396 };
2397 });
2398};
2399
2400// Not fully implemented / released on SDK. Testing for now.
2401Wallet.prototype.updatePolicyRule = function(params, callback) {
2402 params = params || {};
2403 common.validateParams(params, ['id', 'type'], [], callback);
2404
2405 return this.bitgo.put(this.url('/policy/rule'))
2406 .send(params)
2407 .result()
2408 .nodeify(callback);
2409};
2410
2411Wallet.prototype.deletePolicyRule = function(params, callback) {
2412 params = params || {};
2413 common.validateParams(params, ['id'], [], callback);
2414
2415 return this.bitgo.del(this.url('/policy/rule'))
2416 .send(params)
2417 .result()
2418 .nodeify(callback);
2419};
2420
2421//
2422// getBitGoFee
2423// Get the required on-transaction BitGo fee
2424//
2425Wallet.prototype.getBitGoFee = function(params, callback) {
2426 params = params || {};
2427 common.validateParams(params, [], [], callback);
2428 if (!_.isNumber(params.amount)) {
2429 throw new Error('invalid amount argument');
2430 }
2431 if (params.instant && !_.isBoolean(params.instant)) {
2432 throw new Error('invalid instant argument');
2433 }
2434 return this.bitgo.get(this.url('/billing/fee'))
2435 .query(params)
2436 .result()
2437 .nodeify(callback);
2438};
2439
2440export = Wallet;