Completed
Push — master ( 7b524c...0d3c7c )
by Ruben de
05:15 queued 02:24
created

Wallet.buildTransaction   D

Complexity

Conditions 14
Paths 361

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
c 0
b 0
f 0
nc 361
dl 0
loc 57
rs 4.8083
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Wallet.buildTransaction often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
var _ = require('lodash');
2
var assert = require('assert');
3
var q = require('q');
4
var async = require('async');
5
var bitcoin = require('bitcoinjs-lib');
6
var bitcoinMessage = require('bitcoinjs-message');
7
var blocktrail = require('./blocktrail');
8
var CryptoJS = require('crypto-js');
9
var Encryption = require('./encryption');
10
var EncryptionMnemonic = require('./encryption_mnemonic');
11
var SizeEstimation = require('./size_estimation');
12
var bip39 = require('bip39');
13
14
var SignMode = {
15
    SIGN: "sign",
16
    DONT_SIGN: "dont_sign"
17
};
18
19
/**
20
 *
21
 * @param sdk                   APIClient       SDK instance used to do requests
22
 * @param identifier            string          identifier of the wallet
23
 * @param walletVersion         string
24
 * @param primaryMnemonic       string          primary mnemonic
25
 * @param encryptedPrimarySeed
26
 * @param encryptedSecret
27
 * @param primaryPublicKeys     string          primary mnemonic
28
 * @param backupPublicKey       string          BIP32 master pubKey M/
29
 * @param blocktrailPublicKeys  array           list of blocktrail pubKeys indexed by keyIndex
30
 * @param keyIndex              int             key index to use
31
 * @param chain                 int             chain to use
32
 * @param segwit                int             segwit toggle from server
33
 * @param testnet               bool            testnet
34
 * @param checksum              string
35
 * @param upgradeToKeyIndex     int
36
 * @param bypassNewAddressCheck bool            flag to indicate if wallet should/shouldn't derive new address locally to verify api
37
 * @constructor
38
 * @internal
39
 */
40
var Wallet = function(
41
    sdk,
42
    identifier,
43
    walletVersion,
44
    primaryMnemonic,
45
    encryptedPrimarySeed,
46
    encryptedSecret,
47
    primaryPublicKeys,
48
    backupPublicKey,
49
    blocktrailPublicKeys,
50
    keyIndex,
51
    chain,
52
    segwit,
53
    testnet,
54
    checksum,
55
    upgradeToKeyIndex,
56
    bypassNewAddressCheck
57
) {
58
    /* jshint -W071 */
59
    var self = this;
60
61
    self.sdk = sdk;
62
    self.identifier = identifier;
63
    self.walletVersion = walletVersion;
64
    self.locked = true;
65
    self.bypassNewAddressCheck = !!bypassNewAddressCheck;
66
    self.bitcoinCash = self.sdk.bitcoinCash;
67
    assert(segwit === 0 || !self.bitcoinCash);
68
69
    self.testnet = testnet;
70
    if (self.testnet) {
71
        self.network = bitcoin.networks.testnet;
72
    } else {
73
        self.network = bitcoin.networks.bitcoin;
74
    }
75
76
    assert(backupPublicKey instanceof bitcoin.HDNode);
77
    assert(_.every(primaryPublicKeys, function(primaryPublicKey) { return primaryPublicKey instanceof bitcoin.HDNode; }));
78
    assert(_.every(blocktrailPublicKeys, function(blocktrailPublicKey) { return blocktrailPublicKey instanceof bitcoin.HDNode; }));
79
80
    // v1
81
    self.primaryMnemonic = primaryMnemonic;
82
83
    // v2 & v3
84
    self.encryptedPrimarySeed = encryptedPrimarySeed;
85
    self.encryptedSecret = encryptedSecret;
86
87
    self.primaryPrivateKey = null;
88
    self.backupPrivateKey = null;
89
90
    self.backupPublicKey = backupPublicKey;
91
    self.blocktrailPublicKeys = blocktrailPublicKeys;
92
    self.primaryPublicKeys = primaryPublicKeys;
93
    self.keyIndex = keyIndex;
94
95
    var chainIdx;
96
    if (!self.bitcoinCash) {
97
        if (segwit) {
98
            chainIdx = Wallet.CHAIN_BTC_SEGWIT;
99
        } else {
100
            chainIdx = Wallet.CHAIN_BTC_DEFAULT;
101
        }
102
    } else {
103
        chainIdx = Wallet.CHAIN_BCC_DEFAULT;
104
    }
105
106
    self.chain = chainIdx;
107
    self.checksum = checksum;
108
    self.upgradeToKeyIndex = upgradeToKeyIndex;
109
110
    self.secret = null;
111
    self.seedHex = null;
112
};
113
114
Wallet.WALLET_VERSION_V1 = 'v1';
115
Wallet.WALLET_VERSION_V2 = 'v2';
116
Wallet.WALLET_VERSION_V3 = 'v3';
117
118
Wallet.WALLET_ENTROPY_BITS = 256;
119
120
Wallet.OP_RETURN = 'opreturn';
121
Wallet.DATA = Wallet.OP_RETURN; // alias
122
123
Wallet.PAY_PROGRESS_START = 0;
124
Wallet.PAY_PROGRESS_COIN_SELECTION = 10;
125
Wallet.PAY_PROGRESS_CHANGE_ADDRESS = 20;
126
Wallet.PAY_PROGRESS_SIGN = 30;
127
Wallet.PAY_PROGRESS_SEND = 40;
128
Wallet.PAY_PROGRESS_DONE = 100;
129
130
Wallet.CHAIN_BTC_DEFAULT = 0;
131
Wallet.CHAIN_BTC_SEGWIT = 2;
132
Wallet.CHAIN_BCC_DEFAULT = 1;
133
134
Wallet.FEE_STRATEGY_FORCE_FEE = blocktrail.FEE_STRATEGY_FORCE_FEE;
135
Wallet.FEE_STRATEGY_BASE_FEE = blocktrail.FEE_STRATEGY_BASE_FEE;
136
Wallet.FEE_STRATEGY_OPTIMAL = blocktrail.FEE_STRATEGY_OPTIMAL;
137
Wallet.FEE_STRATEGY_LOW_PRIORITY = blocktrail.FEE_STRATEGY_LOW_PRIORITY;
138
Wallet.FEE_STRATEGY_MIN_RELAY_FEE = blocktrail.FEE_STRATEGY_MIN_RELAY_FEE;
139
140
Wallet.prototype.isSegwit = function() {
141
    return Wallet.CHAIN_BTC_SEGWIT === this.chain;
142
};
143
144
Wallet.prototype.unlock = function(options, cb) {
145
    var self = this;
146
147
    var deferred = q.defer();
148
    deferred.promise.nodeify(cb);
149
150
    // avoid modifying passed options
151
    options = _.merge({}, options);
152
153
    q.fcall(function() {
154
        switch (self.walletVersion) {
155
            case Wallet.WALLET_VERSION_V1:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
156
                return self.unlockV1(options);
157
158
            case Wallet.WALLET_VERSION_V2:
159
                return self.unlockV2(options);
160
161
            case Wallet.WALLET_VERSION_V3:
162
                return self.unlockV3(options);
163
164
            default:
165
                return q.reject(new blocktrail.WalletInitError("Invalid wallet version"));
166
        }
167
    }).then(
168
        function(primaryPrivateKey) {
169
            self.primaryPrivateKey = primaryPrivateKey;
170
171
            // create a checksum of our private key which we'll later use to verify we used the right password
172
            var checksum = self.primaryPrivateKey.getAddress();
173
174
            // check if we've used the right passphrase
175
            if (checksum !== self.checksum) {
176
                throw new blocktrail.WalletChecksumError("Generated checksum [" + checksum + "] does not match " +
177
                    "[" + self.checksum + "], most likely due to incorrect password");
178
            }
179
180
            self.locked = false;
181
182
            // if the response suggests we should upgrade to a different blocktrail cosigning key then we should
183
            if (typeof self.upgradeToKeyIndex !== "undefined" && self.upgradeToKeyIndex !== null) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if typeof self.upgradeToKey...radeToKeyIndex !== null is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
184
                return self.upgradeKeyIndex(self.upgradeToKeyIndex);
185
            }
186
        }
187
    ).then(
188
        function(r) {
189
            deferred.resolve(r);
190
        },
191
        function(e) {
192
            deferred.reject(e);
193
        }
194
    );
195
196
    return deferred.promise;
197
};
198
199
Wallet.prototype.unlockV1 = function(options) {
200
    var self = this;
201
202
    options.primaryMnemonic = typeof options.primaryMnemonic !== "undefined" ? options.primaryMnemonic : self.primaryMnemonic;
203
    options.secretMnemonic = typeof options.secretMnemonic !== "undefined" ? options.secretMnemonic : self.secretMnemonic;
204
205
    return self.sdk.resolvePrimaryPrivateKeyFromOptions(options)
206
        .then(function(options) {
207
            self.primarySeed = options.primarySeed;
208
209
            return options.primaryPrivateKey;
210
        });
211
};
212
213
Wallet.prototype.unlockV2 = function(options, cb) {
214
    var self = this;
215
216
    var deferred = q.defer();
217
    deferred.promise.nodeify(cb);
218
219
    deferred.resolve(q.fcall(function() {
220
        /* jshint -W071, -W074 */
221
        options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
222
        options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
223
224
        if (options.secret) {
225
            self.secret = options.secret;
226
        }
227
228
        if (options.primaryPrivateKey) {
229
            throw new blocktrail.WalletDecryptError("specifying primaryPrivateKey has been deprecated");
230
        }
231
232
        if (options.primarySeed) {
233
            self.primarySeed = options.primarySeed;
234
        } else if (options.secret) {
235
            try {
236
                self.primarySeed = new Buffer(
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
237
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
238
                if (!self.primarySeed.length) {
239
                    throw new Error();
240
                }
241
            } catch (e) {
242
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
243
            }
244
245
        } else {
246
            // avoid conflicting options
247
            if (options.passphrase && options.password) {
248
                throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
249
            }
250
            // normalize passphrase/password
251
            options.passphrase = options.passphrase || options.password;
252
253
            try {
254
                self.secret = CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedSecret), options.passphrase).toString(CryptoJS.enc.Utf8);
255
                if (!self.secret.length) {
256
                    throw new Error();
257
                }
258
            } catch (e) {
259
                throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
260
            }
261
            try {
262
                self.primarySeed = new Buffer(
263
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
264
                if (!self.primarySeed.length) {
265
                    throw new Error();
266
                }
267
            } catch (e) {
268
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
269
            }
270
        }
271
272
        return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
273
    }));
274
275
    return deferred.promise;
276
};
277
278
Wallet.prototype.unlockV3 = function(options, cb) {
279
    var self = this;
280
281
    var deferred = q.defer();
282
    deferred.promise.nodeify(cb);
283
284
    deferred.resolve(q.fcall(function() {
285
        return q.when()
286
            .then(function() {
287
                /* jshint -W071, -W074 */
288
                options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
289
                options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
290
291
                if (options.secret) {
292
                    self.secret = options.secret;
293
                }
294
295
                if (options.primaryPrivateKey) {
296
                    throw new blocktrail.WalletInitError("specifying primaryPrivateKey has been deprecated");
297
                }
298
299
                if (options.primarySeed) {
300
                    self.primarySeed = options.primarySeed;
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
301
                } else if (options.secret) {
302
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
303
                        .then(function(primarySeed) {
304
                            self.primarySeed = primarySeed;
305
                        }, function() {
306
                            throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
307
                        });
308
                } else {
309
                    // avoid conflicting options
310
                    if (options.passphrase && options.password) {
311
                        throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
312
                    }
313
                    // normalize passphrase/password
314
                    options.passphrase = options.passphrase || options.password;
315
                    delete options.password;
316
317
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedSecret, 'base64'), new Buffer(options.passphrase))
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
318
                        .then(function(secret) {
319
                            self.secret = secret;
320
                        }, function() {
321
                            throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
322
                        })
323
                        .then(function() {
324
                            return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
325
                                .then(function(primarySeed) {
326
                                    self.primarySeed = primarySeed;
327
                                }, function() {
328
                                    throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
329
                                });
330
                        });
331
                }
332
            })
333
            .then(function() {
334
                return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
335
            })
336
        ;
337
    }));
338
339
    return deferred.promise;
340
};
341
342
Wallet.prototype.lock = function() {
343
    var self = this;
344
345
    self.secret = null;
346
    self.primarySeed = null;
347
    self.primaryPrivateKey = null;
348
    self.backupPrivateKey = null;
349
350
    self.locked = true;
351
};
352
353
/**
354
 * upgrade wallet to V3 encryption scheme
355
 *
356
 * @param passphrase is required again to reencrypt the data, important that it's the correct password!!!
357
 * @param cb
358
 * @returns {promise}
359
 */
360
Wallet.prototype.upgradeToV3 = function(passphrase, cb) {
361
    var self = this;
362
363
    var deferred = q.defer();
364
    deferred.promise.nodeify(cb);
365
366
    q.when(true)
367
        .then(function() {
368
            if (self.locked) {
369
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade");
370
            }
371
372
            if (self.walletVersion === Wallet.WALLET_VERSION_V3) {
373
                throw new blocktrail.WalletUpgradeError("Wallet is already V3");
374
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
375
                return self._upgradeV2ToV3(passphrase, deferred.notify.bind(deferred));
376
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if self.walletVersion === Wallet.WALLET_VERSION_V1 is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
377
                return self._upgradeV1ToV3(passphrase, deferred.notify.bind(deferred));
378
            }
379
        })
380
        .then(function(r) { deferred.resolve(r); }, function(e) { deferred.reject(e); });
381
382
    return deferred.promise;
383
};
384
385
Wallet.prototype._upgradeV2ToV3 = function(passphrase, notify) {
386
    var self = this;
387
388
    return q.when(true)
389
        .then(function() {
390
            var options = {
391
                storeDataOnServer: true,
392
                passphrase: passphrase,
393
                primarySeed: self.primarySeed,
394
                recoverySecret: false // don't create new recovery secret, V2 already has ones
395
            };
396
397
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
398
                .then(function(options) {
399
                    return self.sdk.updateWallet(self.identifier, {
400
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
401
                        encrypted_secret: options.encryptedSecret.toString('base64'),
402
                        wallet_version: Wallet.WALLET_VERSION_V3
403
                    }).then(function() {
404
                        self.secret = options.secret;
405
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
406
                        self.encryptedSecret = options.encryptedSecret;
407
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
408
409
                        return self;
410
                    });
411
                });
412
        });
413
414
};
415
416
Wallet.prototype._upgradeV1ToV3 = function(passphrase, notify) {
417
    var self = this;
418
419
    return q.when(true)
420
        .then(function() {
421
            var options = {
422
                storeDataOnServer: true,
423
                passphrase: passphrase,
424
                primarySeed: self.primarySeed
425
            };
426
427
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
428
                .then(function(options) {
429
                    // store recoveryEncryptedSecret for printing on backup sheet
430
                    self.recoveryEncryptedSecret = options.recoveryEncryptedSecret;
431
432
                    return self.sdk.updateWallet(self.identifier, {
433
                        primary_mnemonic: '',
434
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
435
                        encrypted_secret: options.encryptedSecret.toString('base64'),
436
                        recovery_secret: options.recoverySecret.toString('hex'),
437
                        wallet_version: Wallet.WALLET_VERSION_V3
438
                    }).then(function() {
439
                        self.secret = options.secret;
440
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
441
                        self.encryptedSecret = options.encryptedSecret;
442
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
443
444
                        return self;
445
                    });
446
                });
447
        });
448
};
449
450
Wallet.prototype.doPasswordChange = function(newPassword) {
451
    var self = this;
452
453
    return q.when(null)
454
        .then(function() {
455
456
            if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
457
                throw new blocktrail.WalletLockedError("Wallet version does not support password change!");
458
            }
459
460
            if (self.locked) {
461
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to change password");
462
            }
463
464
            if (!self.secret) {
465
                throw new blocktrail.WalletLockedError("No secret");
466
            }
467
468
            var newEncryptedSecret;
469
            var newEncrypedWalletSecretMnemonic;
470
            if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
471
                newEncryptedSecret = CryptoJS.AES.encrypt(self.secret, newPassword).toString(CryptoJS.format.OpenSSL);
472
                newEncrypedWalletSecretMnemonic = bip39.entropyToMnemonic(blocktrail.convert(newEncryptedSecret, 'base64', 'hex'));
473
474
            } else {
475
                if (typeof newPassword === "string") {
476
                    newPassword = new Buffer(newPassword);
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
477
                } else {
478
                    if (!(newPassword instanceof Buffer)) {
479
                        throw new Error('New password must be provided as a string or a Buffer');
480
                    }
481
                }
482
483
                newEncryptedSecret = Encryption.encrypt(self.secret, newPassword);
484
                newEncrypedWalletSecretMnemonic = EncryptionMnemonic.encode(newEncryptedSecret);
485
486
                // It's a buffer, so convert it back to base64
487
                newEncryptedSecret = newEncryptedSecret.toString('base64');
488
            }
489
490
            return [newEncryptedSecret, newEncrypedWalletSecretMnemonic];
491
        });
492
};
493
494
Wallet.prototype.passwordChange = function(newPassword, cb) {
495
    var self = this;
496
497
    var deferred = q.defer();
498
    deferred.promise.nodeify(cb);
499
500
    q.fcall(function() {
501
        return self.doPasswordChange(newPassword)
502
            .then(function(r) {
503
                var newEncryptedSecret = r[0];
504
                var newEncrypedWalletSecretMnemonic = r[1];
505
506
                return self.sdk.updateWallet(self.identifier, {encrypted_secret: newEncryptedSecret}).then(function() {
507
                    self.encryptedSecret = newEncryptedSecret;
508
509
                    // backupInfo
510
                    return {
511
                        encryptedSecret: newEncrypedWalletSecretMnemonic
512
                    };
513
                });
514
            })
515
            .then(
516
                function(r) {
517
                    deferred.resolve(r);
518
                },
519
                function(e) {
520
                    deferred.reject(e);
521
                }
522
            );
523
    });
524
525
    return deferred.promise;
526
};
527
528
/**
529
 * get address for specified path
530
 *
531
 * @param path
532
 * @returns string
533
 */
534
Wallet.prototype.getAddressByPath = function(path) {
535
    return this.getWalletScriptByPath(path).address;
536
};
537
538
/**
539
 * get redeemscript for specified path
540
 *
541
 * @param path
542
 * @returns {bitcoin.Script}
543
 */
544
Wallet.prototype.getRedeemScriptByPath = function(path) {
545
    return this.getWalletScriptByPath(path).redeemScript;
546
};
547
548
Wallet.prototype.getWalletScriptByPath = function(path) {
549
    var self = this;
550
551
    // get derived primary key
552
    var derivedPrimaryPublicKey = self.getPrimaryPublicKey(path);
553
    // get derived blocktrail key
554
    var derivedBlocktrailPublicKey = self.getBlocktrailPublicKey(path);
555
    // derive the backup key
556
    var derivedBackupPublicKey = Wallet.deriveByPath(self.backupPublicKey, path.replace("'", ""), "M");
557
558
    // sort the pubkeys
559
    var pubKeys = Wallet.sortMultiSigKeys([
560
        derivedPrimaryPublicKey.keyPair.getPublicKeyBuffer(),
561
        derivedBackupPublicKey.keyPair.getPublicKeyBuffer(),
562
        derivedBlocktrailPublicKey.keyPair.getPublicKeyBuffer()
563
    ]);
564
565
    var multisig = bitcoin.script.multisig.output.encode(2, pubKeys);
566
    var scriptType = parseInt(path.split("/")[2]);
567
568
    var ws, rs;
569
    if (this.network !== "bitcoincash" && scriptType === Wallet.CHAIN_BTC_SEGWIT) {
570
        ws = multisig;
571
        rs = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(ws));
572
    } else {
573
        ws = null;
574
        rs = multisig;
575
    }
576
577
    var spk = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(rs));
578
    var addr = bitcoin.address.fromOutputScript(spk, this.network);
579
580
    return {
581
        witnessScript: ws,
582
        redeemScript: rs,
583
        scriptPubKey: spk,
584
        address: addr
585
    };
586
};
587
588
/**
589
 * get primary public key by path
590
 *  first level of the path is used as keyIndex to find the correct key in the dict
591
 *
592
 * @param path  string
593
 * @returns {bitcoin.HDNode}
594
 */
595
Wallet.prototype.getPrimaryPublicKey = function(path) {
596
    var self = this;
597
598
    path = path.replace("m", "M");
599
600
    var keyIndex = path.split("/")[1].replace("'", "");
601
602
    if (!self.primaryPublicKeys[keyIndex]) {
603
        if (self.primaryPrivateKey) {
604
            self.primaryPublicKeys[keyIndex] = Wallet.deriveByPath(self.primaryPrivateKey, "M/" + keyIndex + "'", "m");
605
        } else {
606
            throw new blocktrail.KeyPathError("Wallet.getPrimaryPublicKey keyIndex (" + keyIndex + ") is unknown to us");
607
        }
608
    }
609
610
    var primaryPublicKey = self.primaryPublicKeys[keyIndex];
611
    return Wallet.deriveByPath(primaryPublicKey, path, "M/" + keyIndex + "'");
612
};
613
614
/**
615
 * get blocktrail public key by path
616
 *  first level of the path is used as keyIndex to find the correct key in the dict
617
 *
618
 * @param path  string
619
 * @returns {bitcoin.HDNode}
620
 */
621
Wallet.prototype.getBlocktrailPublicKey = function(path) {
622
    var self = this;
623
624
    path = path.replace("m", "M");
625
626
    var keyIndex = path.split("/")[1].replace("'", "");
627
628
    if (!self.blocktrailPublicKeys[keyIndex]) {
629
        throw new blocktrail.KeyPathError("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
630
    }
631
632
    var blocktrailPublicKey = self.blocktrailPublicKeys[keyIndex];
633
634
    return Wallet.deriveByPath(blocktrailPublicKey, path, "M/" + keyIndex + "'");
635
};
636
637
/**
638
 * upgrade wallet to different blocktrail cosign key
639
 *
640
 * @param keyIndex  int
641
 * @param [cb]      function
642
 * @returns {q.Promise}
643
 */
644
Wallet.prototype.upgradeKeyIndex = function(keyIndex, cb) {
645
    var self = this;
646
647
    var deferred = q.defer();
648
    deferred.promise.nodeify(cb);
649
650
    if (self.locked) {
651
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade key index"));
652
        return deferred.promise;
653
    }
654
655
    var primaryPublicKey = self.primaryPrivateKey.deriveHardened(keyIndex).neutered();
656
657
    deferred.resolve(
658
        self.sdk.upgradeKeyIndex(self.identifier, keyIndex, [primaryPublicKey.toBase58(), "M/" + keyIndex + "'"])
659
            .then(function(result) {
660
                self.keyIndex = keyIndex;
661
                _.forEach(result.blocktrail_public_keys, function(publicKey, keyIndex) {
662
                    self.blocktrailPublicKeys[keyIndex] = bitcoin.HDNode.fromBase58(publicKey[0], self.network);
663
                });
664
665
                self.primaryPublicKeys[keyIndex] = primaryPublicKey;
666
667
                return true;
668
            })
669
    );
670
671
    return deferred.promise;
672
};
673
674
/**
675
 * generate a new derived private key and return the new address for it
676
 *
677
 * @param [cb]  function        callback(err, address)
678
 * @returns {q.Promise}
679
 */
680
Wallet.prototype.getNewAddress = function(cb) {
681
    var self = this;
682
    var deferred = q.defer();
683
    deferred.promise.spreadNodeify(cb);
684
685
    deferred.resolve(
686
        self.sdk.getNewDerivation(self.identifier, "M/" + self.keyIndex + "'/" + self.chain)
687
            .then(function(newDerivation) {
688
                var path = newDerivation.path;
689
                var address = newDerivation.address;
690
                if (!self.bypassNewAddressCheck) {
691
692
                    address = self.getAddressByPath(newDerivation.path);
693
                    // debug check
694
                    if (address !== newDerivation.address) {
695
                        throw new blocktrail.WalletAddressError("Failed to verify address [" + newDerivation.address + "] !== [" + address + "]");
696
                    }
697
                }
698
699
                return [address, path];
700
            })
701
    );
702
703
    return deferred.promise;
704
};
705
706
/**
707
 * get the balance for the wallet
708
 *
709
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
710
 * @returns {q.Promise}
711
 */
712
Wallet.prototype.getBalance = function(cb) {
713
    var self = this;
714
715
    var deferred = q.defer();
716
    deferred.promise.spreadNodeify(cb);
717
718
    deferred.resolve(
719
        self.sdk.getWalletBalance(self.identifier)
720
            .then(function(result) {
721
                return [result.confirmed, result.unconfirmed];
722
            })
723
    );
724
725
    return deferred.promise;
726
};
727
728
/**
729
 * get the balance for the wallet
730
 *
731
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
732
 * @returns {q.Promise}
733
 */
734
Wallet.prototype.getInfo = function(cb) {
735
    var self = this;
736
737
    var deferred = q.defer();
738
    deferred.promise.spreadNodeify(cb);
739
740
    deferred.resolve(
741
        self.sdk.getWalletBalance(self.identifier)
742
    );
743
744
    return deferred.promise;
745
};
746
747
/**
748
 * do wallet discovery (slow)
749
 *
750
 * @param [gap] int             gap limit
751
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
752
 * @returns {q.Promise}
753
 */
754
Wallet.prototype.doDiscovery = function(gap, cb) {
755
    var self = this;
756
757
    if (typeof gap === "function") {
758
        cb = gap;
759
        gap = null;
760
    }
761
762
    var deferred = q.defer();
763
    deferred.promise.spreadNodeify(cb);
764
765
    deferred.resolve(
766
        self.sdk.doWalletDiscovery(self.identifier, gap)
767
            .then(function(result) {
768
                return [result.confirmed, result.unconfirmed];
769
            })
770
    );
771
772
    return deferred.promise;
773
};
774
775
/**
776
 *
777
 * @param [force]   bool            ignore warnings (such as non-zero balance)
778
 * @param [cb]      function        callback(err, success)
779
 * @returns {q.Promise}
780
 */
781
Wallet.prototype.deleteWallet = function(force, cb) {
782
    var self = this;
783
784
    if (typeof force === "function") {
785
        cb = force;
786
        force = false;
787
    }
788
789 View Code Duplication
    var deferred = q.defer();
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
790
    deferred.promise.nodeify(cb);
791
792
    if (self.locked) {
793
        deferred.reject(new blocktrail.WalletDeleteError("Wallet needs to be unlocked to delete wallet"));
794
        return deferred.promise;
795
    }
796
797
    var checksum = self.primaryPrivateKey.getAddress();
798
    var privBuf = self.primaryPrivateKey.keyPair.d.toBuffer(32);
799
    var signature = bitcoinMessage.sign(checksum, self.network.messagePrefix, privBuf, true).toString('base64');
800
801
    deferred.resolve(
802
        self.sdk.deleteWallet(self.identifier, checksum, signature, force)
803
            .then(function(result) {
804
                return result.deleted;
805
            })
806
    );
807
808
    return deferred.promise;
809
};
810
811
/**
812
 * create, sign and send a transaction
813
 *
814
 * @param pay                   array       {'address': (int)value}     coins to send
815
 * @param [changeAddress]       bool        change address to use (auto generated if NULL)
816
 * @param [allowZeroConf]       bool        allow zero confirmation unspent outputs to be used in coin selection
817
 * @param [randomizeChangeIdx]  bool        randomize the index of the change output (default TRUE, only disable if you have a good reason to)
818
 * @param [feeStrategy]         string      defaults to Wallet.FEE_STRATEGY_OPTIMAL
819
 * @param [twoFactorToken]      string      2FA token
820
 * @param options
821
 * @param [cb]                  function    callback(err, txHash)
822
 * @returns {q.Promise}
823
 */
824
Wallet.prototype.pay = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, twoFactorToken, options, cb) {
825
826
    /* jshint -W071 */
827
    var self = this;
828
829
    if (typeof changeAddress === "function") {
830
        cb = changeAddress;
831
        changeAddress = null;
832
    } else if (typeof allowZeroConf === "function") {
833
        cb = allowZeroConf;
834
        allowZeroConf = false;
835
    } else if (typeof randomizeChangeIdx === "function") {
836
        cb = randomizeChangeIdx;
837
        randomizeChangeIdx = true;
838
    } else if (typeof feeStrategy === "function") {
839
        cb = feeStrategy;
840
        feeStrategy = null;
841
    } else if (typeof twoFactorToken === "function") {
842
        cb = twoFactorToken;
843
        twoFactorToken = null;
844
    } else if (typeof options === "function") {
845
        cb = options;
846
        options = {};
847
    }
848
849
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
850
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
851
    options = options || {};
852
    var checkFee = typeof options.checkFee !== "undefined" ? options.checkFee : true;
853
854
    var deferred = q.defer();
855
    deferred.promise.nodeify(cb);
856
857
    if (self.locked) {
858
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to send coins"));
859
        return deferred.promise;
860
    }
861
862
    q.nextTick(function() {
863
        deferred.notify(Wallet.PAY_PROGRESS_START);
864
865
        self.buildTransaction(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options)
866
            .then(
867 View Code Duplication
            function(r) { return r; },
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
868
            function(e) { deferred.reject(e); },
869
            function(progress) {
870
                deferred.notify(progress);
871
            }
872
        )
873
            .spread(
874
            function(tx, utxos) {
875
876
                deferred.notify(Wallet.PAY_PROGRESS_SEND);
877
878
                var data = {
879
                    signed_transaction: tx.toHex(),
880
                    base_transaction: tx.__toBuffer(null, null, false).toString('hex')
881
                };
882
883
                return self.sendTransaction(data, utxos.map(function(utxo) { return utxo['path']; }), checkFee, twoFactorToken, options.prioboost)
884
                    .then(function(result) {
885
                        deferred.notify(Wallet.PAY_PROGRESS_DONE);
886
887
                        if (!result || !result['complete'] || result['complete'] === 'false') {
888
                            deferred.reject(new blocktrail.TransactionSignError("Failed to completely sign transaction"));
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
889
                        } else {
890
                            return result['txid'];
891
                        }
892
                    });
893
            },
894
            function(e) {
895
                throw e;
896
            }
897
        )
898
            .then(
899
            function(r) { deferred.resolve(r); },
900
            function(e) { deferred.reject(e); }
901
        )
902
        ;
903
    });
904
905
    return deferred.promise;
906
};
907
908
Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) {
909
    /* jshint -W071 */
910
    var self = this;
911
912
    if (typeof changeAddress === "function") {
913
        cb = changeAddress;
914
        changeAddress = null;
915
    } else if (typeof allowZeroConf === "function") {
916
        cb = allowZeroConf;
917
        allowZeroConf = false;
918
    } else if (typeof randomizeChangeIdx === "function") {
919
        cb = randomizeChangeIdx;
920
        randomizeChangeIdx = true;
921
    } else if (typeof feeStrategy === "function") {
922
        cb = feeStrategy;
923
        feeStrategy = null;
924
    } else if (typeof options === "function") {
925
        cb = options;
926
        options = {};
927
    }
928
929
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
930
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
931
    options = options || {};
932
933
    var deferred = q.defer();
934
    deferred.promise.spreadNodeify(cb);
935
936
    q.nextTick(function() {
937
        var send = [];
938
939
        // normalize / validate sends
940
        Object.keys(pay).forEach(function(address) {
941
            address = address.trim();
942
            var value = pay[address];
943
            var err = null;
0 ignored issues
show
Unused Code introduced by
The assignment to err seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
944
945
            if (address === Wallet.OP_RETURN) {
946
                var datachunk = Buffer.isBuffer(value) ? value : new Buffer(value, 'utf-8');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
947
                send.push({scriptPubKey: bitcoin.script.nullData.output.encode(datachunk).toString('hex'), value: 0});
948
                return;
949
            }
950
951
            var addr;
952
            var type;
953
954
            try {
955
                addr = bitcoin.address.fromBech32(address, self.network);
956
                err = null;
957
                type = "bech32";
958
            } catch (_err) {
959
                err = _err;
960
            }
961
962
            if (!addr) {
963
                try {
964
                    addr = bitcoin.address.fromBase58Check(address, self.network);
965
                    err = null;
966
                    type = "base58";
967
                } catch (_err) {
968
                    err = _err;
969
                }
970
            }
971
972
            if (!addr || err) {
973
                err = new blocktrail.InvalidAddressError("Invalid address [" + address + "]" + (err ? " (" + err.message + ")" : ""));
974
            } else if (parseInt(value, 10).toString() !== value.toString()) {
975
                err = new blocktrail.WalletSendError("Values should be in Satoshis");
976
            } else if (!(value = parseInt(value, 10))) {
977
                err = new blocktrail.WalletSendError("Values should be non zero");
978
            } else if (value <= blocktrail.DUST) {
979
                err = new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")");
980
            }
981
982
            if (err) {
983
                deferred.reject(err);
984
                return deferred.promise;
985
            }
986
987
            var payload = {
988
                value: parseInt(value, 10)
989
            };
990
            if (type === "bech32") {
0 ignored issues
show
Bug introduced by
The variable type does not seem to be initialized in case addr = bitcoin.address.f...(address, self.network) on line 955 throws an error. Are you sure this can never be the case?
Loading history...
991
                payload.scriptPubKey = bitcoin.address.toOutputScript(address, self.network).toString('hex');
992
            } else {
993
                payload.address = address;
994
            }
995
            send.push(payload);
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
996
        });
997
998
        if (!send.length) {
999
            deferred.reject(new blocktrail.WalletSendError("Need at least one recipient"));
1000
            return deferred.promise;
1001
        }
1002
1003
        deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION);
1004
1005
        deferred.resolve(
1006
            self.coinSelection(send, true, allowZeroConf, feeStrategy, options)
1007
            /**
1008
             *
1009
             * @param {Object[]} utxos
1010
             * @param fee
1011
             * @param change
1012
             * @param randomizeChangeIdx
0 ignored issues
show
Documentation introduced by
The parameter randomizeChangeIdx does not exist. Did you maybe forget to remove this comment?
Loading history...
1013
             * @returns {*}
1014
             */
1015
                .spread(function(utxos, fee, change) {
1016
                    var tx, txb, outputs = [];
1017
1018
                    var deferred = q.defer();
1019
1020
                    async.waterfall([
1021
                        /**
1022
                         * prepare
1023
                         *
1024
                         * @param cb
1025
                         */
1026
                        function(cb) {
1027
                            var inputsTotal = utxos.map(function(utxo) {
1028
                                return utxo['value'];
1029
                            }).reduce(function(a, b) {
1030
                                return a + b;
1031
                            });
1032
                            var outputsTotal = Object.keys(send).map(function(address) {
1033
                                return send[address];
1034
                            }).reduce(function(a, b) {
1035
                                return a + b;
1036
                            });
1037
                            var estimatedChange = inputsTotal - outputsTotal - fee;
1038
1039
                            if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) {
1040
                                return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " +
1041
                                    "suggested by the coin selection seems incorrect (" + estimatedChange + ")"));
1042
                            }
1043
1044
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1045
                        },
1046
                        /**
1047
                         * init transaction builder
1048
                         *
1049
                         * @param cb
1050
                         */
1051
                        function(cb) {
1052
                            txb = new bitcoin.TransactionBuilder(self.network);
1053
                            if (self.bitcoinCash) {
1054
                                txb.enableBitcoinCash();
1055
                            }
1056
1057
                            cb();
1058
                        },
1059
                        /**
1060
                         * add UTXOs as inputs
1061
                         *
1062
                         * @param cb
1063
                         */
1064
                        function(cb) {
1065
                            var i;
1066
1067
                            for (i = 0; i < utxos.length; i++) {
1068
                                txb.addInput(utxos[i]['hash'], utxos[i]['idx']);
1069
                            }
1070
1071
                            cb();
1072
                        },
1073
                        /**
1074
                         * build desired outputs
1075
                         *
1076
                         * @param cb
1077
                         */
1078
                        function(cb) {
1079
                            send.forEach(function(_send) {
1080
                                if (_send.address) {
1081
                                    outputs.push({address: _send.address, value: _send.value});
1082
                                } else if (_send.scriptPubKey) {
1083
                                    outputs.push({scriptPubKey: new Buffer(_send.scriptPubKey, 'hex'), value: _send.value});
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1084
                                } else {
1085
                                    throw new Error("Invalid send");
1086
                                }
1087
                            });
1088
                            cb();
1089
                        },
1090
                        /**
1091
                         * get change address if required
1092
                         *
1093
                         * @param cb
1094
                         */
1095
                        function(cb) {
1096
                            if (change > 0) {
1097
                                if (change <= blocktrail.DUST) {
1098
                                    change = 0; // don't do a change output if it would be a dust output
1099
1100
                                } else {
1101
                                    if (!changeAddress) {
1102
                                        deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS);
1103
1104
                                        return self.getNewAddress(function(err, address) {
1105
                                            if (err) {
1106
                                                return cb(err);
1107
                                            }
1108
                                            changeAddress = address;
1109
                                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1110
                                        });
1111
                                    }
1112
                                }
1113
                            }
1114
1115
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1116
                        },
1117
                        /**
1118
                         * add change to outputs
1119
                         *
1120
                         * @param cb
1121
                         */
1122
                        function(cb) {
1123
                            if (change > 0) {
1124
                                if (randomizeChangeIdx) {
1125
                                    outputs.splice(_.random(0, outputs.length), 0, {
1126
                                        address: changeAddress,
1127
                                        value: change
1128
                                    });
1129
                                } else {
1130
                                    outputs.push({address: changeAddress, value: change});
1131
                                }
1132
                            }
1133
1134
                            cb();
1135
                        },
1136
                        /**
1137
                         * add outputs to txb
1138
                         *
1139
                         * @param cb
1140
                         */
1141
                        function(cb) {
1142
                            outputs.forEach(function(outputInfo) {
1143
                                txb.addOutput(outputInfo.scriptPubKey || outputInfo.address, outputInfo.value);
1144
                            });
1145
1146
                            cb();
1147
                        },
1148
                        /**
1149
                         * sign
1150
                         *
1151
                         * @param cb
1152
                         */
1153
                        function(cb) {
1154
                            var i, privKey, path, redeemScript, witnessScript;
1155
1156
                            deferred.notify(Wallet.PAY_PROGRESS_SIGN);
1157
1158
                            for (i = 0; i < utxos.length; i++) {
1159
                                var mode = SignMode.SIGN;
1160
                                if (utxos[i].sign_mode) {
1161
                                    mode = utxos[i].sign_mode;
1162
                                }
1163
1164
                                redeemScript = null;
0 ignored issues
show
Unused Code introduced by
The assignment to redeemScript seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
1165
                                witnessScript = null;
1166
                                if (mode === SignMode.SIGN) {
1167
                                    path = utxos[i]['path'].replace("M", "m");
1168
1169
                                    // todo: regenerate scripts for path and compare for utxo (paranoid mode)
1170
                                    if (self.primaryPrivateKey) {
1171
                                        privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair;
1172
                                    } else if (self.backupPrivateKey) {
1173
                                        privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair;
1174
                                    } else {
1175
                                        throw new Error("No master privateKey present");
1176
                                    }
1177
1178
                                    redeemScript = new Buffer(utxos[i]['redeem_script'], 'hex');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1179
                                    if (typeof utxos[i]['witness_script'] === 'string') {
1180
                                        witnessScript = new Buffer(utxos[i]['witness_script'], 'hex');
1181
                                    }
1182
1183
                                    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
1184
                                    if (self.bitcoinCash) {
1185
                                        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
1186
                                    }
1187
1188
                                    txb.sign(i, privKey, redeemScript, sigHash, utxos[i].value, witnessScript);
1189
                                }
1190
                            }
1191
1192
                            tx = txb.buildIncomplete();
1193
1194
                            cb();
1195
                        },
1196
                        /**
1197
                         * estimate fee to verify that the API is not providing us wrong data
1198
                         *
1199
                         * @param cb
1200
                         */
1201
                        function(cb) {
1202
                            var estimatedFee = Wallet.estimateVsizeFee(tx, utxos);
1203
1204
                            switch (feeStrategy) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
1205
                                case Wallet.FEE_STRATEGY_BASE_FEE:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1206
                                    if (Math.abs(estimatedFee - fee) > blocktrail.BASE_FEE) {
1207
                                        return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1208
                                            "seems incorrect (" + estimatedFee + ") for FEE_STRATEGY_BASE_FEE"));
1209
                                    }
1210
                                break;
1211
1212
                                case Wallet.FEE_STRATEGY_OPTIMAL:
1213
                                    if (fee > estimatedFee * 50) {
1214
                                        return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1215
                                            "seems awefully high (" + estimatedFee + ") for FEE_STRATEGY_OPTIMAL"));
1216
                                    }
1217
                                break;
1218
                            }
1219
1220
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1221
                        }
1222
                    ], function(err) {
1223
                        if (err) {
1224
                            deferred.reject(new blocktrail.WalletSendError(err));
1225
                            return;
1226
                        }
1227
1228
                        deferred.resolve([tx, utxos]);
1229
                    });
1230
1231
                    return deferred.promise;
1232
                }
1233
            )
1234
        );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1235
    });
1236
1237
    return deferred.promise;
1238
};
1239
1240
/**
1241
 * use the API to get the best inputs to use based on the outputs
1242
 *
1243
 * @param pay               array       {'address': (int)value}     coins to send
1244
 * @param [lockUTXO]        bool        lock UTXOs for a few seconds to allow for transaction to be created
1245
 * @param [allowZeroConf]   bool        allow zero confirmation unspent outputs to be used in coin selection
1246
 * @param [feeStrategy]     string      defaults to FEE_STRATEGY_OPTIMAL
1247
 * @param [options]         object
1248
 * @param [cb]              function    callback(err, utxos, fee, change)
1249
 * @returns {q.Promise}
1250
 */
1251
Wallet.prototype.coinSelection = function(pay, lockUTXO, allowZeroConf, feeStrategy, options, cb) {
1252
    var self = this;
1253
1254
    if (typeof lockUTXO === "function") {
1255
        cb = lockUTXO;
1256
        lockUTXO = true;
1257
    } else if (typeof allowZeroConf === "function") {
1258
        cb = allowZeroConf;
1259
        allowZeroConf = false;
1260
    } else if (typeof feeStrategy === "function") {
1261
        cb = feeStrategy;
1262
        feeStrategy = null;
1263
    } else if (typeof options === "function") {
1264
        cb = options;
1265
        options = {};
1266
    }
1267
1268
    lockUTXO = typeof lockUTXO !== "undefined" ? lockUTXO : true;
1269
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1270
    options = options || {};
1271
1272
    return self.sdk.coinSelection(self.identifier, pay, lockUTXO, allowZeroConf, feeStrategy, options, cb);
1273
};
1274
1275
/**
1276
 * send the transaction using the API
1277
 *
1278
 * @param txHex             string      partially signed transaction as hex string
1279
 * @param paths             array       list of paths used in inputs which should be cosigned by the API
1280
 * @param checkFee          bool        when TRUE the API will verify if the fee is 100% correct and otherwise throw an exception
1281
 * @param [twoFactorToken]  string      2FA token
1282
 * @param prioboost         bool
1283
 * @param [cb]              function    callback(err, txHash)
1284
 * @returns {q.Promise}
1285
 */
1286
Wallet.prototype.sendTransaction = function(txHex, paths, checkFee, twoFactorToken, prioboost, cb) {
1287
    var self = this;
1288
1289
    if (typeof twoFactorToken === "function") {
1290
        cb = twoFactorToken;
1291
        twoFactorToken = null;
1292
        prioboost = false;
1293
    } else if (typeof prioboost === "function") {
1294
        cb = twoFactorToken;
1295
        prioboost = false;
1296
    }
1297
1298
    var deferred = q.defer();
1299
    deferred.promise.nodeify(cb);
1300
1301
    self.sdk.sendTransaction(self.identifier, txHex, paths, checkFee, twoFactorToken, prioboost)
1302
        .then(
1303
            function(result) {
1304
                deferred.resolve(result);
1305
            },
1306
            function(e) {
1307
                if (e.requires_2fa) {
1308
                    deferred.reject(new blocktrail.WalletMissing2FAError());
1309
                } else if (e.message.match(/Invalid two_factor_token/)) {
1310
                    deferred.reject(new blocktrail.WalletInvalid2FAError());
1311
                } else {
1312
                    deferred.reject(e);
1313
                }
1314
            }
1315
        )
1316
    ;
1317
1318
    return deferred.promise;
1319
};
1320
1321
/**
1322
 * setup a webhook for this wallet
1323
 *
1324
 * @param url           string      URL to receive webhook events
1325
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1326
 * @param [cb]          function    callback(err, webhook)
1327
 * @returns {q.Promise}
1328
 */
1329
Wallet.prototype.setupWebhook = function(url, identifier, cb) {
1330
    var self = this;
1331
1332
    if (typeof identifier === "function") {
1333
        cb = identifier;
1334
        identifier = null;
1335
    }
1336
1337
    identifier = identifier || ('WALLET-' + self.identifier);
1338
1339
    return self.sdk.setupWalletWebhook(self.identifier, identifier, url, cb);
1340
};
1341
1342
/**
1343
 * delete a webhook that was created for this wallet
1344
 *
1345
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1346
 * @param [cb]          function    callback(err, success)
1347
 * @returns {q.Promise}
1348
 */
1349
Wallet.prototype.deleteWebhook = function(identifier, cb) {
1350
    var self = this;
1351
1352
    if (typeof identifier === "function") {
1353
        cb = identifier;
1354
        identifier = null;
1355
    }
1356
1357
    identifier = identifier || ('WALLET-' + self.identifier);
1358
1359
    return self.sdk.deleteWalletWebhook(self.identifier, identifier, cb);
1360
};
1361
1362
/**
1363
 * get all transactions for the wallet (paginated)
1364
 *
1365
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1366
 * @param [cb]      function    callback(err, transactions)
1367
 * @returns {q.Promise}
1368
 */
1369
Wallet.prototype.transactions = function(params, cb) {
1370
    var self = this;
1371
1372
    return self.sdk.walletTransactions(self.identifier, params, cb);
1373
};
1374
1375
Wallet.prototype.maxSpendable = function(allowZeroConf, feeStrategy, options, cb) {
1376
    var self = this;
1377
1378
    if (typeof allowZeroConf === "function") {
1379
        cb = allowZeroConf;
1380
        allowZeroConf = false;
1381
    } else if (typeof feeStrategy === "function") {
1382
        cb = feeStrategy;
1383
        feeStrategy = null;
1384
    } else if (typeof options === "function") {
1385
        cb = options;
1386
        options = {};
1387
    }
1388
1389
    if (typeof allowZeroConf === "object") {
1390
        options = allowZeroConf;
1391
        allowZeroConf = false;
1392
    } else if (typeof feeStrategy === "object") {
1393
        options = feeStrategy;
1394
        feeStrategy = null;
1395
    }
1396
1397
    options = options || {};
1398
1399
    if (typeof options.allowZeroConf !== "undefined") {
1400
        allowZeroConf = options.allowZeroConf;
1401
    }
1402
    if (typeof options.feeStrategy !== "undefined") {
1403
        feeStrategy = options.feeStrategy;
1404
    }
1405
1406
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1407
1408
    return self.sdk.walletMaxSpendable(self.identifier, allowZeroConf, feeStrategy, options, cb);
1409
};
1410
1411
/**
1412
 * get all addresses for the wallet (paginated)
1413
 *
1414
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1415
 * @param [cb]      function    callback(err, addresses)
1416
 * @returns {q.Promise}
1417
 */
1418
Wallet.prototype.addresses = function(params, cb) {
1419
    var self = this;
1420
1421
    return self.sdk.walletAddresses(self.identifier, params, cb);
1422
};
1423
1424
/**
1425
 * @param address   string      the address to label
1426
 * @param label     string      the label
1427
 * @param [cb]      function    callback(err, res)
1428
 * @returns {q.Promise}
1429
 */
1430
Wallet.prototype.labelAddress = function(address, label, cb) {
1431
    var self = this;
1432
1433
    return self.sdk.labelWalletAddress(self.identifier, address, label, cb);
1434
};
1435
1436
/**
1437
 * get all UTXOs for the wallet (paginated)
1438
 *
1439
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1440
 * @param [cb]      function    callback(err, addresses)
1441
 * @returns {q.Promise}
1442
 */
1443
Wallet.prototype.utxos = function(params, cb) {
1444
    var self = this;
1445
1446
    return self.sdk.walletUTXOs(self.identifier, params, cb);
1447
};
1448
1449
Wallet.prototype.unspentOutputs = Wallet.prototype.utxos;
1450
1451
/**
1452
 * sort list of pubkeys to be used in a multisig redeemscript
1453
 *  sorted in lexicographical order on the hex of the pubkey
1454
 *
1455
 * @param pubKeys   {bitcoin.HDNode[]}
1456
 * @returns string[]
1457
 */
1458
Wallet.sortMultiSigKeys = function(pubKeys) {
1459
    pubKeys.sort(function(key1, key2) {
1460
        return key1.toString('hex').localeCompare(key2.toString('hex'));
1461
    });
1462
1463
    return pubKeys;
1464
};
1465
1466
/**
1467
 * determine how much fee is required based on the inputs and outputs
1468
 *  this is an estimation, not a proper 100% correct calculation
1469
 *
1470
 * @todo: mark deprecated in favor of estimations where UTXOS are known
1471
 * @param {bitcoin.Transaction} tx
1472
 * @param {int} feePerKb when not null use this feePerKb, otherwise use BASE_FEE legacy calculation
1473
 * @returns {number}
1474
 */
1475
Wallet.estimateIncompleteTxFee = function(tx, feePerKb) {
1476
    var size = Wallet.estimateIncompleteTxSize(tx);
1477
    var sizeKB = size / 1000;
1478
    var sizeKBCeil = Math.ceil(size / 1000);
1479
1480
    if (feePerKb) {
1481
        return parseInt(sizeKB * feePerKb, 10);
1482
    } else {
1483
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1484
    }
1485
};
1486
1487
/**
1488
 * Takes tx and utxos, computing their estimated vsize,
1489
 * and uses feePerKb (or BASEFEE as default) to estimate
1490
 * the number of satoshis in fee.
1491
 *
1492
 * @param {bitcoin.Transaction} tx
1493
 * @param {Array} utxos
1494
 * @param feePerKb
1495
 * @returns {Number}
1496
 */
1497
Wallet.estimateVsizeFee = function(tx, utxos, feePerKb) {
1498
    var vsize = SizeEstimation.estimateTxVsize(tx, utxos);
1499
    var sizeKB = vsize / 1000;
1500
    var sizeKBCeil = Math.ceil(vsize / 1000);
1501
1502
    if (feePerKb) {
1503
        return parseInt(sizeKB * feePerKb, 10);
1504
    } else {
1505
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1506
    }
1507
};
1508
1509
/**
1510
 * determine how much fee is required based on the inputs and outputs
1511
 *  this is an estimation, not a proper 100% correct calculation
1512
 *
1513
 * @param {bitcoin.Transaction} tx
1514
 * @returns {number}
1515
 */
1516
Wallet.estimateIncompleteTxSize = function(tx) {
1517
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1518
1519
    size += tx.outs.length * 34;
1520
1521
    tx.ins.forEach(function(txin) {
1522
        var scriptSig = txin.script,
1523
            scriptType = bitcoin.script.classifyInput(scriptSig);
1524
1525
        var multiSig = [2, 3]; // tmp hardcoded
1526
1527
        // Re-classify if P2SH
1528
        if (!multiSig && scriptType === 'scripthash') {
1529
            var sigChunks = bitcoin.script.decompile(scriptSig);
1530
            var redeemScript = sigChunks.slice(-1)[0];
1531
            scriptSig = bitcoin.script.compile(sigChunks.slice(0, -1));
1532
            scriptType = bitcoin.script.classifyInput(scriptSig);
1533
1534
            if (bitcoin.script.classifyOutput(redeemScript) !== scriptType) {
1535
                throw new blocktrail.TransactionInputError('Non-matching scriptSig and scriptPubKey in input');
1536
            }
1537
1538
            // figure out M of N for multisig (code from internal usage of bitcoinjs)
1539
            if (scriptType === 'multisig') {
1540
                var rsChunks = bitcoin.script.decompile(redeemScript);
1541
                var mOp = rsChunks[0];
1542
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1543
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1544
                }
1545
1546
                var nOp = rsChunks[redeemScript.chunks.length - 2];
1547
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1548
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1549
                }
1550
1551
                var m = mOp - (bitcoin.opcodes.OP_1 - 1);
1552
                var n = nOp - (bitcoin.opcodes.OP_1 - 1);
1553
                if (n < m) {
1554
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1555
                }
1556
1557
                multiSig = [m, n];
1558
            }
1559
        }
1560
1561
        if (multiSig) {
1562
            size += (
1563
                32 + // txhash
1564
                4 + // idx
1565
                3 + // scriptVarInt[>=253]
1566
                1 + // OP_0
1567
                ((1 + 72) * multiSig[0]) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1568
                (2 + 105) + // OP_PUSHDATA[>=75] + script
1569
                4 // sequence
1570
            );
1571
1572
        } else {
1573
            size += 32 + // txhash
1574
                4 + // idx
1575
                73 + // sig
1576
                34 + // script
1577
                4; // sequence
1578
        }
1579
    });
1580
1581
    return size;
1582
};
1583
1584
/**
1585
 * determine how much fee is required based on the amount of inputs and outputs
1586
 *  this is an estimation, not a proper 100% correct calculation
1587
 *  this asumes all inputs are 2of3 multisig
1588
 *
1589
 * @todo: mark deprecated in favor of situations where UTXOS are known
1590
 * @param txinCnt       {number}
1591
 * @param txoutCnt      {number}
1592
 * @returns {number}
1593
 */
1594
Wallet.estimateFee = function(txinCnt, txoutCnt) {
1595
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1596
1597
    size += txoutCnt * 34;
1598
1599
    size += (
1600
            32 + // txhash
1601
            4 + // idx
1602
            3 + // scriptVarInt[>=253]
1603
            1 + // OP_0
1604
            ((1 + 72) * 2) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1605
            (2 + 105) + // OP_PUSHDATA[>=75] + script
1606
            4 // sequence
1607
        ) * txinCnt;
1608
1609
    var sizeKB = Math.ceil(size / 1000);
1610
1611
    return sizeKB * blocktrail.BASE_FEE;
1612
};
1613
1614
/**
1615
 * create derived key from parent key by path
1616
 *
1617
 * @param hdKey     {bitcoin.HDNode}
1618
 * @param path      string
1619
 * @param keyPath   string
1620
 * @returns {bitcoin.HDNode}
1621
 */
1622
Wallet.deriveByPath = function(hdKey, path, keyPath) {
1623
    keyPath = keyPath || (!!hdKey.keyPair.d ? "m" : "M");
1624
1625
    if (path[0].toLowerCase() !== "m" || keyPath[0].toLowerCase() !== "m") {
1626
        throw new blocktrail.KeyPathError("Wallet.deriveByPath only works with absolute paths. (" + path + ", " + keyPath + ")");
1627
    }
1628
1629
    if (path[0] === "m" && keyPath[0] === "M") {
1630
        throw new blocktrail.KeyPathError("Wallet.deriveByPath can't derive private path from public parent. (" + path + ", " + keyPath + ")");
1631
    }
1632
1633
    // if the desired path is public while the input is private
1634
    var toPublic = path[0] === "M" && keyPath[0] === "m";
1635
    if (toPublic) {
1636
        // derive the private path, convert to public when returning
1637
        path[0] = "m";
1638
    }
1639
1640
    // keyPath should be the parent parent of path
1641
    if (path.toLowerCase().indexOf(keyPath.toLowerCase()) !== 0) {
1642
        throw new blocktrail.KeyPathError("Wallet.derivePath requires path (" + path + ") to be a child of keyPath (" + keyPath + ")");
1643
    }
1644
1645
    // remove the part of the path we already have
1646
    path = path.substr(keyPath.length);
1647
1648
    // iterate over the chunks and derive
1649
    var newKey = hdKey;
1650
    path.replace(/^\//, "").split("/").forEach(function(chunk) {
1651
        if (!chunk) {
1652
            return;
1653
        }
1654
1655
        if (chunk.indexOf("'") !== -1) {
1656
            chunk = parseInt(chunk.replace("'", ""), 10) + bitcoin.HDNode.HIGHEST_BIT;
1657
        }
1658
1659
        newKey = newKey.derive(parseInt(chunk, 10));
1660
    });
1661
1662
    if (toPublic) {
1663
        return newKey.neutered();
1664
    } else {
1665
        return newKey;
1666
    }
1667
};
1668
1669
module.exports = Wallet;
1670