Completed
Push — master ( 1b680e...1143cd )
by thomas
02:25
created

Wallet.convertPayToOutputs   B

Complexity

Conditions 10
Paths 3

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
c 0
b 0
f 0
nc 3
dl 0
loc 19
rs 7.2765
nop 3

How to fix   Complexity   

Complexity

Complex classes like Wallet.convertPayToOutputs 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.bitcoinCash) {
71
        if (self.testnet) {
72
            self.network = bitcoin.networks.bitcoincashtestnet;
73
        } else {
74
            self.network = bitcoin.networks.bitcoincash;
75
        }
76
    } else {
77
        if (self.testnet) {
78
            self.network = bitcoin.networks.testnet;
79
        } else {
80
            self.network = bitcoin.networks.bitcoin;
81
        }
82
    }
83
84
    assert(backupPublicKey instanceof bitcoin.HDNode);
85
    assert(_.every(primaryPublicKeys, function(primaryPublicKey) { return primaryPublicKey instanceof bitcoin.HDNode; }));
86
    assert(_.every(blocktrailPublicKeys, function(blocktrailPublicKey) { return blocktrailPublicKey instanceof bitcoin.HDNode; }));
87
88
    // v1
89
    self.primaryMnemonic = primaryMnemonic;
90
91
    // v2 & v3
92
    self.encryptedPrimarySeed = encryptedPrimarySeed;
93
    self.encryptedSecret = encryptedSecret;
94
95
    self.primaryPrivateKey = null;
96
    self.backupPrivateKey = null;
97
98
    self.backupPublicKey = backupPublicKey;
99
    self.blocktrailPublicKeys = blocktrailPublicKeys;
100
    self.primaryPublicKeys = primaryPublicKeys;
101
    self.keyIndex = keyIndex;
102
103
    var chainIdx;
104
    if (!self.bitcoinCash) {
105
        if (segwit) {
106
            chainIdx = Wallet.CHAIN_BTC_SEGWIT;
107
        } else {
108
            chainIdx = Wallet.CHAIN_BTC_DEFAULT;
109
        }
110
    } else {
111
        chainIdx = Wallet.CHAIN_BCC_DEFAULT;
112
    }
113
114
    self.chain = chainIdx;
115
    self.checksum = checksum;
116
    self.upgradeToKeyIndex = upgradeToKeyIndex;
117
118
    self.secret = null;
119
    self.seedHex = null;
120
};
121
122
Wallet.WALLET_VERSION_V1 = 'v1';
123
Wallet.WALLET_VERSION_V2 = 'v2';
124
Wallet.WALLET_VERSION_V3 = 'v3';
125
126
Wallet.WALLET_ENTROPY_BITS = 256;
127
128
Wallet.OP_RETURN = 'opreturn';
129
Wallet.DATA = Wallet.OP_RETURN; // alias
130
131
Wallet.PAY_PROGRESS_START = 0;
132
Wallet.PAY_PROGRESS_COIN_SELECTION = 10;
133
Wallet.PAY_PROGRESS_CHANGE_ADDRESS = 20;
134
Wallet.PAY_PROGRESS_SIGN = 30;
135
Wallet.PAY_PROGRESS_SEND = 40;
136
Wallet.PAY_PROGRESS_DONE = 100;
137
138
Wallet.CHAIN_BTC_DEFAULT = 0;
139
Wallet.CHAIN_BTC_SEGWIT = 2;
140
Wallet.CHAIN_BCC_DEFAULT = 1;
141
142
Wallet.FEE_STRATEGY_FORCE_FEE = blocktrail.FEE_STRATEGY_FORCE_FEE;
143
Wallet.FEE_STRATEGY_BASE_FEE = blocktrail.FEE_STRATEGY_BASE_FEE;
144
Wallet.FEE_STRATEGY_OPTIMAL = blocktrail.FEE_STRATEGY_OPTIMAL;
145
Wallet.FEE_STRATEGY_LOW_PRIORITY = blocktrail.FEE_STRATEGY_LOW_PRIORITY;
146
Wallet.FEE_STRATEGY_MIN_RELAY_FEE = blocktrail.FEE_STRATEGY_MIN_RELAY_FEE;
147
148
Wallet.prototype.isSegwit = function() {
149
    return Wallet.CHAIN_BTC_SEGWIT === this.chain;
150
};
151
152
Wallet.prototype.unlock = function(options, cb) {
153
    var self = this;
154
155
    var deferred = q.defer();
156
    deferred.promise.nodeify(cb);
157
158
    // avoid modifying passed options
159
    options = _.merge({}, options);
160
161
    q.fcall(function() {
162
        switch (self.walletVersion) {
163
            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...
164
                return self.unlockV1(options);
165
166
            case Wallet.WALLET_VERSION_V2:
167
                return self.unlockV2(options);
168
169
            case Wallet.WALLET_VERSION_V3:
170
                return self.unlockV3(options);
171
172
            default:
173
                return q.reject(new blocktrail.WalletInitError("Invalid wallet version"));
174
        }
175
    }).then(
176
        function(primaryPrivateKey) {
177
            self.primaryPrivateKey = primaryPrivateKey;
178
179
            // create a checksum of our private key which we'll later use to verify we used the right password
180
            var checksum = self.primaryPrivateKey.getAddress();
181
182
            // check if we've used the right passphrase
183
            if (checksum !== self.checksum) {
184
                throw new blocktrail.WalletChecksumError("Generated checksum [" + checksum + "] does not match " +
185
                    "[" + self.checksum + "], most likely due to incorrect password");
186
            }
187
188
            self.locked = false;
189
190
            // if the response suggests we should upgrade to a different blocktrail cosigning key then we should
191
            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...
192
                return self.upgradeKeyIndex(self.upgradeToKeyIndex);
193
            }
194
        }
195
    ).then(
196
        function(r) {
197
            deferred.resolve(r);
198
        },
199
        function(e) {
200
            deferred.reject(e);
201
        }
202
    );
203
204
    return deferred.promise;
205
};
206
207
Wallet.prototype.unlockV1 = function(options) {
208
    var self = this;
209
210
    options.primaryMnemonic = typeof options.primaryMnemonic !== "undefined" ? options.primaryMnemonic : self.primaryMnemonic;
211
    options.secretMnemonic = typeof options.secretMnemonic !== "undefined" ? options.secretMnemonic : self.secretMnemonic;
212
213
    return self.sdk.resolvePrimaryPrivateKeyFromOptions(options)
214
        .then(function(options) {
215
            self.primarySeed = options.primarySeed;
216
217
            return options.primaryPrivateKey;
218
        });
219
};
220
221
Wallet.prototype.unlockV2 = function(options, cb) {
222
    var self = this;
223
224
    var deferred = q.defer();
225
    deferred.promise.nodeify(cb);
226
227
    deferred.resolve(q.fcall(function() {
228
        /* jshint -W071, -W074 */
229
        options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
230
        options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
231
232
        if (options.secret) {
233
            self.secret = options.secret;
234
        }
235
236
        if (options.primaryPrivateKey) {
237
            throw new blocktrail.WalletDecryptError("specifying primaryPrivateKey has been deprecated");
238
        }
239
240
        if (options.primarySeed) {
241
            self.primarySeed = options.primarySeed;
242
        } else if (options.secret) {
243
            try {
244
                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...
245
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
246
                if (!self.primarySeed.length) {
247
                    throw new Error();
248
                }
249
            } catch (e) {
250
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
251
            }
252
253
        } else {
254
            // avoid conflicting options
255
            if (options.passphrase && options.password) {
256
                throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
257
            }
258
            // normalize passphrase/password
259
            options.passphrase = options.passphrase || options.password;
260
261
            try {
262
                self.secret = CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedSecret), options.passphrase).toString(CryptoJS.enc.Utf8);
263
                if (!self.secret.length) {
264
                    throw new Error();
265
                }
266
            } catch (e) {
267
                throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
268
            }
269
            try {
270
                self.primarySeed = new Buffer(
271
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
272
                if (!self.primarySeed.length) {
273
                    throw new Error();
274
                }
275
            } catch (e) {
276
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
277
            }
278
        }
279
280
        return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
281
    }));
282
283
    return deferred.promise;
284
};
285
286
Wallet.prototype.unlockV3 = function(options, cb) {
287
    var self = this;
288
289
    var deferred = q.defer();
290
    deferred.promise.nodeify(cb);
291
292
    deferred.resolve(q.fcall(function() {
293
        return q.when()
294
            .then(function() {
295
                /* jshint -W071, -W074 */
296
                options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
297
                options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
298
299
                if (options.secret) {
300
                    self.secret = options.secret;
301
                }
302
303
                if (options.primaryPrivateKey) {
304
                    throw new blocktrail.WalletInitError("specifying primaryPrivateKey has been deprecated");
305
                }
306
307
                if (options.primarySeed) {
308
                    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...
309
                } else if (options.secret) {
310
                    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...
311
                        .then(function(primarySeed) {
312
                            self.primarySeed = primarySeed;
313
                        }, function() {
314
                            throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
315
                        });
316
                } else {
317
                    // avoid conflicting options
318
                    if (options.passphrase && options.password) {
319
                        throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
320
                    }
321
                    // normalize passphrase/password
322
                    options.passphrase = options.passphrase || options.password;
323
                    delete options.password;
324
325
                    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...
326
                        .then(function(secret) {
327
                            self.secret = secret;
328
                        }, function() {
329
                            throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
330
                        })
331
                        .then(function() {
332
                            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...
333
                                .then(function(primarySeed) {
334
                                    self.primarySeed = primarySeed;
335
                                }, function() {
336
                                    throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
337
                                });
338
                        });
339
                }
340
            })
341
            .then(function() {
342
                return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
343
            })
344
        ;
345
    }));
346
347
    return deferred.promise;
348
};
349
350
Wallet.prototype.lock = function() {
351
    var self = this;
352
353
    self.secret = null;
354
    self.primarySeed = null;
355
    self.primaryPrivateKey = null;
356
    self.backupPrivateKey = null;
357
358
    self.locked = true;
359
};
360
361
/**
362
 * upgrade wallet to V3 encryption scheme
363
 *
364
 * @param passphrase is required again to reencrypt the data, important that it's the correct password!!!
365
 * @param cb
366
 * @returns {promise}
367
 */
368
Wallet.prototype.upgradeToV3 = function(passphrase, cb) {
369
    var self = this;
370
371
    var deferred = q.defer();
372
    deferred.promise.nodeify(cb);
373
374
    q.when(true)
375
        .then(function() {
376
            if (self.locked) {
377
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade");
378
            }
379
380
            if (self.walletVersion === Wallet.WALLET_VERSION_V3) {
381
                throw new blocktrail.WalletUpgradeError("Wallet is already V3");
382
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
383
                return self._upgradeV2ToV3(passphrase, deferred.notify.bind(deferred));
384
            } 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...
385
                return self._upgradeV1ToV3(passphrase, deferred.notify.bind(deferred));
386
            }
387
        })
388
        .then(function(r) { deferred.resolve(r); }, function(e) { deferred.reject(e); });
389
390
    return deferred.promise;
391
};
392
393
Wallet.prototype._upgradeV2ToV3 = function(passphrase, notify) {
394
    var self = this;
395
396
    return q.when(true)
397
        .then(function() {
398
            var options = {
399
                storeDataOnServer: true,
400
                passphrase: passphrase,
401
                primarySeed: self.primarySeed,
402
                recoverySecret: false // don't create new recovery secret, V2 already has ones
403
            };
404
405
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
406
                .then(function(options) {
407
                    return self.sdk.updateWallet(self.identifier, {
408
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
409
                        encrypted_secret: options.encryptedSecret.toString('base64'),
410
                        wallet_version: Wallet.WALLET_VERSION_V3
411
                    }).then(function() {
412
                        self.secret = options.secret;
413
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
414
                        self.encryptedSecret = options.encryptedSecret;
415
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
416 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
417
                        return self;
418
                    });
419
                });
420
        });
421
422
};
423
424
Wallet.prototype._upgradeV1ToV3 = function(passphrase, notify) {
425
    var self = this;
426
427
    return q.when(true)
428
        .then(function() {
429
            var options = {
430
                storeDataOnServer: true,
431
                passphrase: passphrase,
432
                primarySeed: self.primarySeed
433
            };
434
435
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
436
                .then(function(options) {
437
                    // store recoveryEncryptedSecret for printing on backup sheet
438
                    self.recoveryEncryptedSecret = options.recoveryEncryptedSecret;
439
440
                    return self.sdk.updateWallet(self.identifier, {
441
                        primary_mnemonic: '',
442
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
443
                        encrypted_secret: options.encryptedSecret.toString('base64'),
444
                        recovery_secret: options.recoverySecret.toString('hex'),
445
                        wallet_version: Wallet.WALLET_VERSION_V3
446
                    }).then(function() {
447
                        self.secret = options.secret;
448
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
449
                        self.encryptedSecret = options.encryptedSecret;
450
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
451
452
                        return self;
453
                    });
454
                });
455
        });
456
};
457
458
Wallet.prototype.doPasswordChange = function(newPassword) {
459
    var self = this;
460
461
    return q.when(null)
462
        .then(function() {
463
464
            if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
465
                throw new blocktrail.WalletLockedError("Wallet version does not support password change!");
466
            }
467
468
            if (self.locked) {
469
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to change password");
470
            }
471
472
            if (!self.secret) {
473
                throw new blocktrail.WalletLockedError("No secret");
474
            }
475
476
            var newEncryptedSecret;
477
            var newEncrypedWalletSecretMnemonic;
478
            if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
479
                newEncryptedSecret = CryptoJS.AES.encrypt(self.secret, newPassword).toString(CryptoJS.format.OpenSSL);
480
                newEncrypedWalletSecretMnemonic = bip39.entropyToMnemonic(blocktrail.convert(newEncryptedSecret, 'base64', 'hex'));
481
482
            } else {
483
                if (typeof newPassword === "string") {
484
                    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...
485
                } else {
486
                    if (!(newPassword instanceof Buffer)) {
487
                        throw new Error('New password must be provided as a string or a Buffer');
488
                    }
489
                }
490
491
                newEncryptedSecret = Encryption.encrypt(self.secret, newPassword);
492
                newEncrypedWalletSecretMnemonic = EncryptionMnemonic.encode(newEncryptedSecret);
493
494
                // It's a buffer, so convert it back to base64
495
                newEncryptedSecret = newEncryptedSecret.toString('base64');
496
            }
497
498
            return [newEncryptedSecret, newEncrypedWalletSecretMnemonic];
499
        });
500
};
501
502
Wallet.prototype.passwordChange = function(newPassword, cb) {
503
    var self = this;
504
505
    var deferred = q.defer();
506
    deferred.promise.nodeify(cb);
507
508
    q.fcall(function() {
509
        return self.doPasswordChange(newPassword)
510
            .then(function(r) {
511
                var newEncryptedSecret = r[0];
512
                var newEncrypedWalletSecretMnemonic = r[1];
513
514
                return self.sdk.updateWallet(self.identifier, {encrypted_secret: newEncryptedSecret}).then(function() {
515
                    self.encryptedSecret = newEncryptedSecret;
516
517
                    // backupInfo
518
                    return {
519
                        encryptedSecret: newEncrypedWalletSecretMnemonic
520
                    };
521
                });
522
            })
523
            .then(
524
                function(r) {
525
                    deferred.resolve(r);
526
                },
527
                function(e) {
528
                    deferred.reject(e);
529
                }
530
            );
531
    });
532
533
    return deferred.promise;
534
};
535
536
/**
537
 * get address for specified path
538
 *
539
 * @param path
540
 * @returns string
541
 */
542
Wallet.prototype.getAddressByPath = function(path) {
543
    return this.getWalletScriptByPath(path).address;
544
};
545
546
/**
547
 * get redeemscript for specified path
548 View Code Duplication
 *
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
549
 * @param path
550
 * @returns {bitcoin.Script}
551
 */
552
Wallet.prototype.getRedeemScriptByPath = function(path) {
553
    return this.getWalletScriptByPath(path).redeemScript;
554
};
555
556
Wallet.prototype.getWalletScriptByPath = function(path) {
557
    var self = this;
558
559
    // get derived primary key
560
    var derivedPrimaryPublicKey = self.getPrimaryPublicKey(path);
561
    // get derived blocktrail key
562
    var derivedBlocktrailPublicKey = self.getBlocktrailPublicKey(path);
563
    // derive the backup key
564
    var derivedBackupPublicKey = Wallet.deriveByPath(self.backupPublicKey, path.replace("'", ""), "M");
565
566
    // sort the pubkeys
567
    var pubKeys = Wallet.sortMultiSigKeys([
568
        derivedPrimaryPublicKey.keyPair.getPublicKeyBuffer(),
569
        derivedBackupPublicKey.keyPair.getPublicKeyBuffer(),
570
        derivedBlocktrailPublicKey.keyPair.getPublicKeyBuffer()
571
    ]);
572
573
    var multisig = bitcoin.script.multisig.output.encode(2, pubKeys);
574
    var scriptType = parseInt(path.split("/")[2]);
575
576
    var ws, rs;
577
    if (this.network !== "bitcoincash" && scriptType === Wallet.CHAIN_BTC_SEGWIT) {
578
        ws = multisig;
579
        rs = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(ws));
580
    } else {
581
        ws = null;
582
        rs = multisig;
583
    }
584
585
    var spk = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(rs));
586
    var addr = bitcoin.address.fromOutputScript(spk, this.network);
587
588
    return {
589
        witnessScript: ws,
590
        redeemScript: rs,
591
        scriptPubKey: spk,
592
        address: addr
593
    };
594
};
595
596
/**
597
 * get primary public key by path
598
 *  first level of the path is used as keyIndex to find the correct key in the dict
599
 *
600
 * @param path  string
601
 * @returns {bitcoin.HDNode}
602
 */
603
Wallet.prototype.getPrimaryPublicKey = function(path) {
604
    var self = this;
605
606
    path = path.replace("m", "M");
607
608
    var keyIndex = path.split("/")[1].replace("'", "");
609
610
    if (!self.primaryPublicKeys[keyIndex]) {
611
        if (self.primaryPrivateKey) {
612
            self.primaryPublicKeys[keyIndex] = Wallet.deriveByPath(self.primaryPrivateKey, "M/" + keyIndex + "'", "m");
613
        } else {
614
            throw new blocktrail.KeyPathError("Wallet.getPrimaryPublicKey keyIndex (" + keyIndex + ") is unknown to us");
615
        }
616
    }
617
618
    var primaryPublicKey = self.primaryPublicKeys[keyIndex];
619
    return Wallet.deriveByPath(primaryPublicKey, path, "M/" + keyIndex + "'");
620
};
621
622
/**
623
 * get blocktrail public key by path
624
 *  first level of the path is used as keyIndex to find the correct key in the dict
625
 *
626
 * @param path  string
627
 * @returns {bitcoin.HDNode}
628
 */
629
Wallet.prototype.getBlocktrailPublicKey = function(path) {
630
    var self = this;
631
632
    path = path.replace("m", "M");
633
634
    var keyIndex = path.split("/")[1].replace("'", "");
635
636
    if (!self.blocktrailPublicKeys[keyIndex]) {
637
        throw new blocktrail.KeyPathError("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
638
    }
639
640
    var blocktrailPublicKey = self.blocktrailPublicKeys[keyIndex];
641
642
    return Wallet.deriveByPath(blocktrailPublicKey, path, "M/" + keyIndex + "'");
643
};
644
645
/**
646
 * upgrade wallet to different blocktrail cosign key
647
 *
648
 * @param keyIndex  int
649
 * @param [cb]      function
650
 * @returns {q.Promise}
651
 */
652
Wallet.prototype.upgradeKeyIndex = function(keyIndex, cb) {
653
    var self = this;
654
655
    var deferred = q.defer();
656
    deferred.promise.nodeify(cb);
657
658
    if (self.locked) {
659
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade key index"));
660
        return deferred.promise;
661
    }
662
663
    var primaryPublicKey = self.primaryPrivateKey.deriveHardened(keyIndex).neutered();
664
665
    deferred.resolve(
666
        self.sdk.upgradeKeyIndex(self.identifier, keyIndex, [primaryPublicKey.toBase58(), "M/" + keyIndex + "'"])
667
            .then(function(result) {
668
                self.keyIndex = keyIndex;
669
                _.forEach(result.blocktrail_public_keys, function(publicKey, keyIndex) {
670
                    self.blocktrailPublicKeys[keyIndex] = bitcoin.HDNode.fromBase58(publicKey[0], self.network);
671
                });
672
673
                self.primaryPublicKeys[keyIndex] = primaryPublicKey;
674
675
                return true;
676
            })
677
    );
678
679
    return deferred.promise;
680
};
681
682
/**
683
 * generate a new derived private key and return the new address for it
684
 *
685
 * @param [cb]  function        callback(err, address)
686
 * @returns {q.Promise}
687
 */
688
Wallet.prototype.getNewAddress = function(cb) {
689
    var self = this;
690
    var deferred = q.defer();
691
    deferred.promise.spreadNodeify(cb);
692
693
    deferred.resolve(
694
        self.sdk.getNewDerivation(self.identifier, "M/" + self.keyIndex + "'/" + self.chain)
695
            .then(function(newDerivation) {
696
                var path = newDerivation.path;
697
                var address = newDerivation.address;
698
                if (!self.bypassNewAddressCheck) {
699
700
                    address = self.getAddressByPath(newDerivation.path);
701
                    // debug check
702
                    if (address !== newDerivation.address) {
703
                        throw new blocktrail.WalletAddressError("Failed to verify address [" + newDerivation.address + "] !== [" + address + "]");
704
                    }
705
                }
706
707
                return [address, path];
708
            })
709
    );
710
711
    return deferred.promise;
712
};
713
714
/**
715
 * get the balance for the wallet
716
 *
717
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
718
 * @returns {q.Promise}
719
 */
720
Wallet.prototype.getBalance = function(cb) {
721
    var self = this;
722
723
    var deferred = q.defer();
724
    deferred.promise.spreadNodeify(cb);
725
726
    deferred.resolve(
727
        self.sdk.getWalletBalance(self.identifier)
728
            .then(function(result) {
729
                return [result.confirmed, result.unconfirmed];
730
            })
731
    );
732
733
    return deferred.promise;
734
};
735
736
/**
737
 * get the balance for the wallet
738
 *
739
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
740
 * @returns {q.Promise}
741
 */
742
Wallet.prototype.getInfo = function(cb) {
743
    var self = this;
744
745
    var deferred = q.defer();
746
    deferred.promise.spreadNodeify(cb);
747
748
    deferred.resolve(
749
        self.sdk.getWalletBalance(self.identifier)
750
    );
751
752
    return deferred.promise;
753
};
754
755
/**
756
 * do wallet discovery (slow)
757
 *
758
 * @param [gap] int             gap limit
759
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
760
 * @returns {q.Promise}
761
 */
762
Wallet.prototype.doDiscovery = function(gap, cb) {
763
    var self = this;
764
765
    if (typeof gap === "function") {
766
        cb = gap;
767
        gap = null;
768
    }
769
770
    var deferred = q.defer();
771
    deferred.promise.spreadNodeify(cb);
772
773
    deferred.resolve(
774
        self.sdk.doWalletDiscovery(self.identifier, gap)
775
            .then(function(result) {
776
                return [result.confirmed, result.unconfirmed];
777
            })
778
    );
779
780
    return deferred.promise;
781
};
782
783
/**
784
 *
785
 * @param [force]   bool            ignore warnings (such as non-zero balance)
786
 * @param [cb]      function        callback(err, success)
787
 * @returns {q.Promise}
788
 */
789 View Code Duplication
Wallet.prototype.deleteWallet = function(force, cb) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
790
    var self = this;
791
792
    if (typeof force === "function") {
793
        cb = force;
794
        force = false;
795
    }
796
797
    var deferred = q.defer();
798
    deferred.promise.nodeify(cb);
799
800
    if (self.locked) {
801
        deferred.reject(new blocktrail.WalletDeleteError("Wallet needs to be unlocked to delete wallet"));
802
        return deferred.promise;
803
    }
804
805
    var checksum = self.primaryPrivateKey.getAddress();
806
    var privBuf = self.primaryPrivateKey.keyPair.d.toBuffer(32);
807
    var signature = bitcoinMessage.sign(checksum, self.network.messagePrefix, privBuf, true).toString('base64');
808
809
    deferred.resolve(
810
        self.sdk.deleteWallet(self.identifier, checksum, signature, force)
811
            .then(function(result) {
812
                return result.deleted;
813
            })
814
    );
815
816
    return deferred.promise;
817
};
818
819
/**
820
 * create, sign and send a transaction
821
 *
822
 * @param pay                   array       {'address': (int)value}     coins to send
823
 * @param [changeAddress]       bool        change address to use (auto generated if NULL)
824
 * @param [allowZeroConf]       bool        allow zero confirmation unspent outputs to be used in coin selection
825
 * @param [randomizeChangeIdx]  bool        randomize the index of the change output (default TRUE, only disable if you have a good reason to)
826
 * @param [feeStrategy]         string      defaults to Wallet.FEE_STRATEGY_OPTIMAL
827
 * @param [twoFactorToken]      string      2FA token
828
 * @param options
829
 * @param [cb]                  function    callback(err, txHash)
830
 * @returns {q.Promise}
831
 */
832
Wallet.prototype.pay = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, twoFactorToken, options, cb) {
833
834
    /* jshint -W071 */
835
    var self = this;
836
837
    if (typeof changeAddress === "function") {
838
        cb = changeAddress;
839
        changeAddress = null;
840
    } else if (typeof allowZeroConf === "function") {
841
        cb = allowZeroConf;
842
        allowZeroConf = false;
843
    } else if (typeof randomizeChangeIdx === "function") {
844
        cb = randomizeChangeIdx;
845
        randomizeChangeIdx = true;
846
    } else if (typeof feeStrategy === "function") {
847
        cb = feeStrategy;
848
        feeStrategy = null;
849
    } else if (typeof twoFactorToken === "function") {
850
        cb = twoFactorToken;
851
        twoFactorToken = null;
852
    } else if (typeof options === "function") {
853
        cb = options;
854
        options = {};
855
    }
856
857
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
858
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
859
    options = options || {};
860
    var checkFee = typeof options.checkFee !== "undefined" ? options.checkFee : true;
861
862
    var deferred = q.defer();
863
    deferred.promise.nodeify(cb);
864
865
    if (self.locked) {
866
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to send coins"));
867 View Code Duplication
        return deferred.promise;
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
868
    }
869
870
    q.nextTick(function() {
871
        deferred.notify(Wallet.PAY_PROGRESS_START);
872
        self.buildTransaction(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options)
873
            .then(
874
            function(r) { return r; },
875
            function(e) { deferred.reject(e); },
876
            function(progress) {
877
                deferred.notify(progress);
878
            }
879
        )
880
            .spread(
881
            function(tx, utxos) {
882
883
                deferred.notify(Wallet.PAY_PROGRESS_SEND);
884
885
                var data = {
886
                    signed_transaction: tx.toHex(),
887
                    base_transaction: tx.__toBuffer(null, null, false).toString('hex')
888
                };
889
890
                return self.sendTransaction(data, utxos.map(function(utxo) { return utxo['path']; }), checkFee, twoFactorToken, options.prioboost)
891
                    .then(function(result) {
892
                        deferred.notify(Wallet.PAY_PROGRESS_DONE);
893
894
                        if (!result || !result['complete'] || result['complete'] === 'false') {
895
                            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...
896
                        } else {
897
                            return result['txid'];
898
                        }
899
                    });
900
            },
901
            function(e) {
902
                throw e;
903
            }
904
        )
905
            .then(
906
            function(r) { deferred.resolve(r); },
907
            function(e) { deferred.reject(e); }
908
        )
909
        ;
910
    });
911
912
    return deferred.promise;
913
};
914
915
Wallet.prototype.decodeAddress = function(address) {
916
    return Wallet.getAddressAndType(address, this.network);
917
};
918
919
Wallet.getAddressAndType = function(address, network) {
920
    var addr;
921
    var type;
922
    var err;
923
924
    if (network === bitcoin.networks.bitcoin || network === bitcoin.networks.testnet) {
925
        try {
926
            addr = bitcoin.address.fromBech32(address, network);
927
            err = null;
928
            type = "bech32";
929
930
        } catch (_err) {
931
            err = _err;
932
        }
933
934
        if (!err) {
935
            // Valid bech32 but invalid network immediately alerts
936
            if (addr.prefix !== network.bech32) {
937
                throw new blocktrail.InvalidAddressError("Address invalid on this network");
938
            }
939
        }
940
    }
941
942
    if (!addr) {
943
        try {
944
            addr = bitcoin.address.fromBase58Check(address);
945
            err = null;
946
            type = "base58";
947
        } catch (_err) {
948
            err = _err;
949
        }
950
951
        if (!err) {
952
            // Valid base58 but invalid network immediately alerts
953
            if (addr.version !== network.pubKeyHash && addr.version !== network.scriptHash) {
954
                throw new blocktrail.InvalidAddressError("Address invalid on this network");
955
            }
956
        }
957
    }
958
959
    if (err) {
960
        throw new blocktrail.InvalidAddressError(err.message);
961
    }
962
963
    return {
964
        address: address,
965
        decoded: addr,
966
        type: type
0 ignored issues
show
Bug introduced by
The variable type seems to not be initialized for all possible execution paths.
Loading history...
967
    };
968
};
969
970
Wallet.convertPayToOutputs = function(pay, network) {
971
    var send = [];
972
973
    var readFunc;
974
975
    // Deal with two different forms
976
    if (Array.isArray(pay)) {
977
        // output[]
978
        readFunc = function(i, output, obj) {
979
            if (typeof output !== "object") {
980
                throw new Error("Invalid transaction output for numerically indexed list [1]");
981
            }
982
983
            var keys = Object.keys(output);
984
            if (keys.indexOf("scriptPubKey") !== -1 && keys.indexOf("value") !== -1) {
985
                obj.scriptPubKey = output["scriptPubKey"];
986
                obj.value = output["value"];
987
            } else if (keys.indexOf("address") !== -1 && keys.indexOf("value") !== -1) {
988
                obj.address = output["address"];
989
                obj.value = output["value"];
990
            } else if (keys.length !== 2 && output.length !== 2 && keys[0] !== 0 && keys[1] !== 1) {
991
                obj.address = output[0];
992
                obj.value = output[1];
993
            } else {
994
                throw new Error("Invalid transaction output for numerically indexed list [2]");
995
            }
996
        };
997
    } else if (typeof pay === "object") {
998
        // map[addr]amount
999
        readFunc = function(address, value, obj) {
1000
            obj.address = address.trim();
1001
            obj.value = value;
1002
            if (obj.address === Wallet.OP_RETURN) {
1003
                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...
1004
                obj.scriptPubKey = bitcoin.script.nullData.output.encode(datachunk).toString('hex');
1005
                obj.value = 0;
1006
                obj.address = null;
1007
            }
1008
        };
1009
    } else {
1010
        throw new Error("Invalid input");
1011
    }
1012
1013
    Object.keys(pay).forEach(function(key) {
1014
        var obj = {};
1015
        readFunc(key, pay[key], obj);
1016
1017
        if (parseInt(obj.value, 10).toString() !== obj.value.toString()) {
1018
            throw new blocktrail.WalletSendError("Values should be in Satoshis");
1019
        }
1020
1021
        // Remove address, replace with scriptPubKey
1022
        if (typeof obj.address === "string") {
1023
            try {
1024
                var addrAndType = Wallet.getAddressAndType(obj.address, network);
1025
                obj.scriptPubKey = bitcoin.address.toOutputScript(addrAndType.address, network).toString('hex');
1026
                delete obj.address;
1027
            } catch (e) {
1028
                throw new blocktrail.InvalidAddressError("Invalid address [" + obj.address + "] (" + e.message + ")");
1029
            }
1030
        }
1031
1032
        // Extra checks when the output isn't OP_RETURN
1033
        if (obj.scriptPubKey.slice(0, 2) !== "6a") {
1034
            if (!(obj.value = parseInt(obj.value, 10))) {
1035
                throw new blocktrail.WalletSendError("Values should be non zero");
1036
            } else if (obj.value <= blocktrail.DUST) {
1037
                throw new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")");
1038
            }
1039
        }
1040
1041
        // Value fully checked now
1042
        obj.value = parseInt(obj.value, 10);
1043
1044
        send.push(obj);
1045
    });
1046
1047
    return send;
1048
};
1049
1050
Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) {
1051
    /* jshint -W071 */
1052
    var self = this;
1053
1054
    if (typeof changeAddress === "function") {
1055
        cb = changeAddress;
1056
        changeAddress = null;
1057
    } else if (typeof allowZeroConf === "function") {
1058
        cb = allowZeroConf;
1059
        allowZeroConf = false;
1060
    } else if (typeof randomizeChangeIdx === "function") {
1061
        cb = randomizeChangeIdx;
1062
        randomizeChangeIdx = true;
1063
    } else if (typeof feeStrategy === "function") {
1064
        cb = feeStrategy;
1065
        feeStrategy = null;
1066
    } else if (typeof options === "function") {
1067
        cb = options;
1068
        options = {};
1069
    }
1070
1071
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
1072
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1073
    options = options || {};
1074
1075
    var deferred = q.defer();
1076
    deferred.promise.spreadNodeify(cb);
1077
1078
    q.nextTick(function() {
1079
        var send;
1080
        try {
1081
            send = Wallet.convertPayToOutputs(pay, self.network);
1082
        } catch (e) {
1083
            deferred.reject(e);
1084
            return deferred.promise;
1085
        }
1086
1087
        if (!send.length) {
1088
            deferred.reject(new blocktrail.WalletSendError("Need at least one recipient"));
1089
            return deferred.promise;
1090
        }
1091
1092
        deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION);
1093
1094
        deferred.resolve(
1095
            self.coinSelection(send, true, allowZeroConf, feeStrategy, options)
1096
            /**
1097
             *
1098
             * @param {Object[]} utxos
1099
             * @param fee
1100
             * @param change
1101
             * @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...
1102
             * @returns {*}
1103
             */
1104
                .spread(function(utxos, fee, change) {
1105
                    var tx, txb, outputs = [];
1106
1107
                    var deferred = q.defer();
1108
1109
                    async.waterfall([
1110
                        /**
1111
                         * prepare
1112
                         *
1113
                         * @param cb
1114
                         */
1115
                        function(cb) {
1116
                            var inputsTotal = utxos.map(function(utxo) {
1117
                                return utxo['value'];
1118
                            }).reduce(function(a, b) {
1119
                                return a + b;
1120
                            });
1121
                            var outputsTotal = send.map(function(output) {
1122
                                return output.value;
1123
                            }).reduce(function(a, b) {
1124
                                return a + b;
1125
                            });
1126
                            var estimatedChange = inputsTotal - outputsTotal - fee;
1127
1128
                            if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) {
1129
                                return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " +
1130
                                    "suggested by the coin selection seems incorrect (" + estimatedChange + ")"));
1131
                            }
1132
1133
                            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...
1134
                        },
1135
                        /**
1136
                         * init transaction builder
1137
                         *
1138
                         * @param cb
1139
                         */
1140
                        function(cb) {
1141
                            txb = new bitcoin.TransactionBuilder(self.network);
1142
                            if (self.bitcoinCash) {
1143
                                txb.enableBitcoinCash();
1144
                            }
1145
1146
                            cb();
1147
                        },
1148
                        /**
1149
                         * add UTXOs as inputs
1150
                         *
1151
                         * @param cb
1152
                         */
1153
                        function(cb) {
1154
                            var i;
1155
1156
                            for (i = 0; i < utxos.length; i++) {
1157
                                txb.addInput(utxos[i]['hash'], utxos[i]['idx']);
1158
                            }
1159
1160
                            cb();
1161
                        },
1162
                        /**
1163
                         * build desired outputs
1164
                         *
1165
                         * @param cb
1166
                         */
1167
                        function(cb) {
1168
                            send.forEach(function(_send) {
1169
                                if (_send.address) {
1170
                                    outputs.push({address: _send.address, value: _send.value});
1171
                                } else if (_send.scriptPubKey) {
1172
                                    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...
1173
                                } else {
1174
                                    throw new Error("Invalid send");
1175
                                }
1176
                            });
1177
                            cb();
1178
                        },
1179
                        /**
1180
                         * get change address if required
1181
                         *
1182
                         * @param cb
1183
                         */
1184
                        function(cb) {
1185
                            if (change > 0) {
1186
                                if (change <= blocktrail.DUST) {
1187
                                    change = 0; // don't do a change output if it would be a dust output
1188
1189
                                } else {
1190
                                    if (!changeAddress) {
1191
                                        deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS);
1192
1193
                                        return self.getNewAddress(function(err, address) {
1194
                                            if (err) {
1195
                                                return cb(err);
1196
                                            }
1197
                                            changeAddress = address;
1198
                                            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...
1199
                                        });
1200
                                    }
1201
                                }
1202
                            }
1203
1204
                            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...
1205
                        },
1206
                        /**
1207
                         * add change to outputs
1208
                         *
1209
                         * @param cb
1210
                         */
1211
                        function(cb) {
1212
                            if (change > 0) {
1213
                                if (randomizeChangeIdx) {
1214
                                    outputs.splice(_.random(0, outputs.length), 0, {
1215
                                        address: changeAddress,
1216
                                        value: change
1217
                                    });
1218
                                } else {
1219
                                    outputs.push({address: changeAddress, value: change});
1220
                                }
1221
                            }
1222
1223
                            cb();
1224
                        },
1225
                        /**
1226
                         * add outputs to txb
1227
                         *
1228
                         * @param cb
1229
                         */
1230
                        function(cb) {
1231
                            outputs.forEach(function(outputInfo) {
1232
                                txb.addOutput(outputInfo.scriptPubKey || outputInfo.address, outputInfo.value);
1233
                            });
1234
1235
                            cb();
1236
                        },
1237
                        /**
1238
                         * sign
1239
                         *
1240
                         * @param cb
1241
                         */
1242
                        function(cb) {
1243
                            var i, privKey, path, redeemScript, witnessScript;
1244
1245
                            deferred.notify(Wallet.PAY_PROGRESS_SIGN);
1246
1247
                            for (i = 0; i < utxos.length; i++) {
1248
                                var mode = SignMode.SIGN;
1249
                                if (utxos[i].sign_mode) {
1250
                                    mode = utxos[i].sign_mode;
1251
                                }
1252
1253
                                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...
1254
                                witnessScript = null;
1255
                                if (mode === SignMode.SIGN) {
1256
                                    path = utxos[i]['path'].replace("M", "m");
1257
1258
                                    // todo: regenerate scripts for path and compare for utxo (paranoid mode)
1259
                                    if (self.primaryPrivateKey) {
1260
                                        privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair;
1261
                                    } else if (self.backupPrivateKey) {
1262
                                        privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair;
1263
                                    } else {
1264
                                        throw new Error("No master privateKey present");
1265
                                    }
1266
1267
                                    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...
1268
                                    if (typeof utxos[i]['witness_script'] === 'string') {
1269
                                        witnessScript = new Buffer(utxos[i]['witness_script'], 'hex');
1270
                                    }
1271
1272
                                    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
1273
                                    if (self.bitcoinCash) {
1274
                                        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
1275
                                    }
1276
1277
                                    txb.sign(i, privKey, redeemScript, sigHash, utxos[i].value, witnessScript);
1278
                                }
1279
                            }
1280
1281
                            tx = txb.buildIncomplete();
1282
1283
                            cb();
1284
                        },
1285
                        /**
1286
                         * estimate fee to verify that the API is not providing us wrong data
1287
                         *
1288
                         * @param cb
1289
                         */
1290
                        function(cb) {
1291
                            var estimatedFee = Wallet.estimateVsizeFee(tx, utxos);
1292
1293
                            if (self.sdk.feeSanityCheck) {
1294
                                switch (feeStrategy) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
1295
                                    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...
1296
                                        if (Math.abs(estimatedFee - fee) > blocktrail.BASE_FEE) {
1297
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1298
                                                "seems incorrect (" + estimatedFee + ") for FEE_STRATEGY_BASE_FEE"));
1299
                                        }
1300
                                        break;
1301
1302
                                    case Wallet.FEE_STRATEGY_OPTIMAL:
1303
                                        if (fee > estimatedFee * self.feeSanityCheckBaseFeeMultiplier) {
1304
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1305
                                                "seems awefully high (" + estimatedFee + ") for FEE_STRATEGY_OPTIMAL"));
1306
                                        }
1307
                                        break;
1308
                                }
1309
                            }
1310
1311
                            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...
1312
                        }
1313
                    ], function(err) {
1314
                        if (err) {
1315
                            deferred.reject(new blocktrail.WalletSendError(err));
1316
                            return;
1317
                        }
1318
1319
                        deferred.resolve([tx, utxos]);
1320
                    });
1321
1322
                    return deferred.promise;
1323
                }
1324
            )
1325
        );
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...
1326
    });
1327
1328
    return deferred.promise;
1329
};
1330
1331
1332
/**
1333
 * use the API to get the best inputs to use based on the outputs
1334
 *
1335
 * @param pay               array       {'address': (int)value}     coins to send
1336
 * @param [lockUTXO]        bool        lock UTXOs for a few seconds to allow for transaction to be created
1337
 * @param [allowZeroConf]   bool        allow zero confirmation unspent outputs to be used in coin selection
1338
 * @param [feeStrategy]     string      defaults to FEE_STRATEGY_OPTIMAL
1339
 * @param [options]         object
1340
 * @param [cb]              function    callback(err, utxos, fee, change)
1341
 * @returns {q.Promise}
1342
 */
1343
Wallet.prototype.coinSelection = function(pay, lockUTXO, allowZeroConf, feeStrategy, options, cb) {
1344
    var self = this;
1345
1346
    if (typeof lockUTXO === "function") {
1347
        cb = lockUTXO;
1348
        lockUTXO = true;
1349
    } else if (typeof allowZeroConf === "function") {
1350
        cb = allowZeroConf;
1351
        allowZeroConf = false;
1352
    } else if (typeof feeStrategy === "function") {
1353
        cb = feeStrategy;
1354
        feeStrategy = null;
1355
    } else if (typeof options === "function") {
1356
        cb = options;
1357
        options = {};
1358
    }
1359
1360
    lockUTXO = typeof lockUTXO !== "undefined" ? lockUTXO : true;
1361
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1362
    options = options || {};
1363
1364
    var send;
1365
    try {
1366
        send = Wallet.convertPayToOutputs(pay, self.network);
1367
    } catch (e) {
1368
        var deferred = q.defer();
1369
        deferred.promise.nodeify(cb);
1370
        deferred.reject(e);
1371
        return deferred.promise;
1372
    }
1373
1374
    return self.sdk.coinSelection(self.identifier, send, lockUTXO, allowZeroConf, feeStrategy, options, cb);
1375
};
1376
1377
/**
1378
 * send the transaction using the API
1379
 *
1380
 * @param txHex             string      partially signed transaction as hex string
1381
 * @param paths             array       list of paths used in inputs which should be cosigned by the API
1382
 * @param checkFee          bool        when TRUE the API will verify if the fee is 100% correct and otherwise throw an exception
1383
 * @param [twoFactorToken]  string      2FA token
1384
 * @param prioboost         bool
1385
 * @param [cb]              function    callback(err, txHash)
1386
 * @returns {q.Promise}
1387
 */
1388
Wallet.prototype.sendTransaction = function(txHex, paths, checkFee, twoFactorToken, prioboost, cb) {
1389
    var self = this;
1390
1391
    if (typeof twoFactorToken === "function") {
1392
        cb = twoFactorToken;
1393
        twoFactorToken = null;
1394
        prioboost = false;
1395
    } else if (typeof prioboost === "function") {
1396
        cb = twoFactorToken;
1397
        prioboost = false;
1398
    }
1399
1400
    var deferred = q.defer();
1401
    deferred.promise.nodeify(cb);
1402
1403
    self.sdk.sendTransaction(self.identifier, txHex, paths, checkFee, twoFactorToken, prioboost)
1404
        .then(
1405
            function(result) {
1406
                deferred.resolve(result);
1407
            },
1408
            function(e) {
1409
                if (e.requires_2fa) {
1410
                    deferred.reject(new blocktrail.WalletMissing2FAError());
1411
                } else if (e.message.match(/Invalid two_factor_token/)) {
1412
                    deferred.reject(new blocktrail.WalletInvalid2FAError());
1413
                } else {
1414
                    deferred.reject(e);
1415
                }
1416
            }
1417
        )
1418
    ;
1419
1420
    return deferred.promise;
1421
};
1422
1423
/**
1424
 * setup a webhook for this wallet
1425
 *
1426
 * @param url           string      URL to receive webhook events
1427
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1428
 * @param [cb]          function    callback(err, webhook)
1429
 * @returns {q.Promise}
1430
 */
1431
Wallet.prototype.setupWebhook = function(url, identifier, cb) {
1432
    var self = this;
1433
1434
    if (typeof identifier === "function") {
1435
        cb = identifier;
1436
        identifier = null;
1437
    }
1438
1439
    identifier = identifier || ('WALLET-' + self.identifier);
1440
1441
    return self.sdk.setupWalletWebhook(self.identifier, identifier, url, cb);
1442
};
1443
1444
/**
1445
 * delete a webhook that was created for this wallet
1446
 *
1447
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1448
 * @param [cb]          function    callback(err, success)
1449
 * @returns {q.Promise}
1450
 */
1451
Wallet.prototype.deleteWebhook = function(identifier, cb) {
1452
    var self = this;
1453
1454
    if (typeof identifier === "function") {
1455
        cb = identifier;
1456
        identifier = null;
1457
    }
1458
1459
    identifier = identifier || ('WALLET-' + self.identifier);
1460
1461
    return self.sdk.deleteWalletWebhook(self.identifier, identifier, cb);
1462
};
1463
1464
/**
1465
 * get all transactions for the wallet (paginated)
1466
 *
1467
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1468
 * @param [cb]      function    callback(err, transactions)
1469
 * @returns {q.Promise}
1470
 */
1471
Wallet.prototype.transactions = function(params, cb) {
1472
    var self = this;
1473
1474
    return self.sdk.walletTransactions(self.identifier, params, cb);
1475
};
1476
1477
Wallet.prototype.maxSpendable = function(allowZeroConf, feeStrategy, options, cb) {
1478
    var self = this;
1479
1480
    if (typeof allowZeroConf === "function") {
1481
        cb = allowZeroConf;
1482
        allowZeroConf = false;
1483
    } else if (typeof feeStrategy === "function") {
1484
        cb = feeStrategy;
1485
        feeStrategy = null;
1486
    } else if (typeof options === "function") {
1487
        cb = options;
1488
        options = {};
1489
    }
1490
1491
    if (typeof allowZeroConf === "object") {
1492
        options = allowZeroConf;
1493
        allowZeroConf = false;
1494
    } else if (typeof feeStrategy === "object") {
1495
        options = feeStrategy;
1496
        feeStrategy = null;
1497
    }
1498
1499
    options = options || {};
1500
1501
    if (typeof options.allowZeroConf !== "undefined") {
1502
        allowZeroConf = options.allowZeroConf;
1503
    }
1504
    if (typeof options.feeStrategy !== "undefined") {
1505
        feeStrategy = options.feeStrategy;
1506
    }
1507
1508
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1509
1510
    return self.sdk.walletMaxSpendable(self.identifier, allowZeroConf, feeStrategy, options, cb);
1511
};
1512
1513
/**
1514
 * get all addresses for the wallet (paginated)
1515
 *
1516
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1517
 * @param [cb]      function    callback(err, addresses)
1518
 * @returns {q.Promise}
1519
 */
1520
Wallet.prototype.addresses = function(params, cb) {
1521
    var self = this;
1522
1523
    return self.sdk.walletAddresses(self.identifier, params, cb);
1524
};
1525
1526
/**
1527
 * @param address   string      the address to label
1528
 * @param label     string      the label
1529
 * @param [cb]      function    callback(err, res)
1530
 * @returns {q.Promise}
1531
 */
1532
Wallet.prototype.labelAddress = function(address, label, cb) {
1533
    var self = this;
1534
1535
    return self.sdk.labelWalletAddress(self.identifier, address, label, cb);
1536
};
1537
1538
/**
1539
 * get all UTXOs for the wallet (paginated)
1540
 *
1541
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1542
 * @param [cb]      function    callback(err, addresses)
1543
 * @returns {q.Promise}
1544
 */
1545
Wallet.prototype.utxos = function(params, cb) {
1546
    var self = this;
1547
1548
    return self.sdk.walletUTXOs(self.identifier, params, cb);
1549
};
1550
1551
Wallet.prototype.unspentOutputs = Wallet.prototype.utxos;
1552
1553
/**
1554
 * sort list of pubkeys to be used in a multisig redeemscript
1555
 *  sorted in lexicographical order on the hex of the pubkey
1556
 *
1557
 * @param pubKeys   {bitcoin.HDNode[]}
1558
 * @returns string[]
1559
 */
1560
Wallet.sortMultiSigKeys = function(pubKeys) {
1561
    pubKeys.sort(function(key1, key2) {
1562
        return key1.toString('hex').localeCompare(key2.toString('hex'));
1563
    });
1564
1565
    return pubKeys;
1566
};
1567
1568
/**
1569
 * determine how much fee is required based on the inputs and outputs
1570
 *  this is an estimation, not a proper 100% correct calculation
1571
 *
1572
 * @todo: mark deprecated in favor of estimations where UTXOS are known
1573
 * @param {bitcoin.Transaction} tx
1574
 * @param {int} feePerKb when not null use this feePerKb, otherwise use BASE_FEE legacy calculation
1575
 * @returns {number}
1576
 */
1577
Wallet.estimateIncompleteTxFee = function(tx, feePerKb) {
1578
    var size = Wallet.estimateIncompleteTxSize(tx);
1579
    var sizeKB = size / 1000;
1580
    var sizeKBCeil = Math.ceil(size / 1000);
1581
1582
    if (feePerKb) {
1583
        return parseInt(sizeKB * feePerKb, 10);
1584
    } else {
1585
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1586
    }
1587
};
1588
1589
/**
1590
 * Takes tx and utxos, computing their estimated vsize,
1591
 * and uses feePerKb (or BASEFEE as default) to estimate
1592
 * the number of satoshis in fee.
1593
 *
1594
 * @param {bitcoin.Transaction} tx
1595
 * @param {Array} utxos
1596
 * @param feePerKb
1597
 * @returns {Number}
1598
 */
1599
Wallet.estimateVsizeFee = function(tx, utxos, feePerKb) {
1600
    var vsize = SizeEstimation.estimateTxVsize(tx, utxos);
1601
    var sizeKB = vsize / 1000;
1602
    var sizeKBCeil = Math.ceil(vsize / 1000);
1603
1604
    if (feePerKb) {
1605
        return parseInt(sizeKB * feePerKb, 10);
1606
    } else {
1607
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1608
    }
1609
};
1610
1611
/**
1612
 * determine how much fee is required based on the inputs and outputs
1613
 *  this is an estimation, not a proper 100% correct calculation
1614
 *
1615
 * @param {bitcoin.Transaction} tx
1616
 * @returns {number}
1617
 */
1618
Wallet.estimateIncompleteTxSize = function(tx) {
1619
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1620
1621
    size += tx.outs.length * 34;
1622
1623
    tx.ins.forEach(function(txin) {
1624
        var scriptSig = txin.script,
1625
            scriptType = bitcoin.script.classifyInput(scriptSig);
1626
1627
        var multiSig = [2, 3]; // tmp hardcoded
1628
1629
        // Re-classify if P2SH
1630
        if (!multiSig && scriptType === 'scripthash') {
1631
            var sigChunks = bitcoin.script.decompile(scriptSig);
1632
            var redeemScript = sigChunks.slice(-1)[0];
1633
            scriptSig = bitcoin.script.compile(sigChunks.slice(0, -1));
1634
            scriptType = bitcoin.script.classifyInput(scriptSig);
1635
1636
            if (bitcoin.script.classifyOutput(redeemScript) !== scriptType) {
1637
                throw new blocktrail.TransactionInputError('Non-matching scriptSig and scriptPubKey in input');
1638
            }
1639
1640
            // figure out M of N for multisig (code from internal usage of bitcoinjs)
1641
            if (scriptType === 'multisig') {
1642
                var rsChunks = bitcoin.script.decompile(redeemScript);
1643
                var mOp = rsChunks[0];
1644
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1645
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1646
                }
1647
1648
                var nOp = rsChunks[redeemScript.chunks.length - 2];
1649
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1650
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1651
                }
1652
1653
                var m = mOp - (bitcoin.opcodes.OP_1 - 1);
1654
                var n = nOp - (bitcoin.opcodes.OP_1 - 1);
1655
                if (n < m) {
1656
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1657
                }
1658
1659
                multiSig = [m, n];
1660
            }
1661
        }
1662
1663
        if (multiSig) {
1664
            size += (
1665
                32 + // txhash
1666
                4 + // idx
1667
                3 + // scriptVarInt[>=253]
1668
                1 + // OP_0
1669
                ((1 + 72) * multiSig[0]) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1670
                (2 + 105) + // OP_PUSHDATA[>=75] + script
1671
                4 // sequence
1672
            );
1673
1674
        } else {
1675
            size += 32 + // txhash
1676
                4 + // idx
1677
                73 + // sig
1678
                34 + // script
1679
                4; // sequence
1680
        }
1681
    });
1682
1683
    return size;
1684
};
1685
1686
/**
1687
 * determine how much fee is required based on the amount of inputs and outputs
1688
 *  this is an estimation, not a proper 100% correct calculation
1689
 *  this asumes all inputs are 2of3 multisig
1690
 *
1691
 * @todo: mark deprecated in favor of situations where UTXOS are known
1692
 * @param txinCnt       {number}
1693
 * @param txoutCnt      {number}
1694
 * @returns {number}
1695
 */
1696
Wallet.estimateFee = function(txinCnt, txoutCnt) {
1697
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1698
1699
    size += txoutCnt * 34;
1700
1701
    size += (
1702
            32 + // txhash
1703
            4 + // idx
1704
            3 + // scriptVarInt[>=253]
1705
            1 + // OP_0
1706
            ((1 + 72) * 2) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1707
            (2 + 105) + // OP_PUSHDATA[>=75] + script
1708
            4 // sequence
1709
        ) * txinCnt;
1710
1711
    var sizeKB = Math.ceil(size / 1000);
1712
1713
    return sizeKB * blocktrail.BASE_FEE;
1714
};
1715
1716
/**
1717
 * create derived key from parent key by path
1718
 *
1719
 * @param hdKey     {bitcoin.HDNode}
1720
 * @param path      string
1721
 * @param keyPath   string
1722
 * @returns {bitcoin.HDNode}
1723
 */
1724
Wallet.deriveByPath = function(hdKey, path, keyPath) {
1725
    keyPath = keyPath || (!!hdKey.keyPair.d ? "m" : "M");
1726
1727
    if (path[0].toLowerCase() !== "m" || keyPath[0].toLowerCase() !== "m") {
1728
        throw new blocktrail.KeyPathError("Wallet.deriveByPath only works with absolute paths. (" + path + ", " + keyPath + ")");
1729
    }
1730
1731
    if (path[0] === "m" && keyPath[0] === "M") {
1732
        throw new blocktrail.KeyPathError("Wallet.deriveByPath can't derive private path from public parent. (" + path + ", " + keyPath + ")");
1733
    }
1734
1735
    // if the desired path is public while the input is private
1736
    var toPublic = path[0] === "M" && keyPath[0] === "m";
1737
    if (toPublic) {
1738
        // derive the private path, convert to public when returning
1739
        path[0] = "m";
1740
    }
1741
1742
    // keyPath should be the parent parent of path
1743
    if (path.toLowerCase().indexOf(keyPath.toLowerCase()) !== 0) {
1744
        throw new blocktrail.KeyPathError("Wallet.derivePath requires path (" + path + ") to be a child of keyPath (" + keyPath + ")");
1745
    }
1746
1747
    // remove the part of the path we already have
1748
    path = path.substr(keyPath.length);
1749
1750
    // iterate over the chunks and derive
1751
    var newKey = hdKey;
1752
    path.replace(/^\//, "").split("/").forEach(function(chunk) {
1753
        if (!chunk) {
1754
            return;
1755
        }
1756
1757
        if (chunk.indexOf("'") !== -1) {
1758
            chunk = parseInt(chunk.replace("'", ""), 10) + bitcoin.HDNode.HIGHEST_BIT;
1759
        }
1760
1761
        newKey = newKey.derive(parseInt(chunk, 10));
1762
    });
1763
1764
    if (toPublic) {
1765
        return newKey.neutered();
1766
    } else {
1767
        return newKey;
1768
    }
1769
};
1770
1771
module.exports = Wallet;
1772