UNPKG

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