Completed
Push — master ( 3c3114...9af846 )
by Ruben de
01:47 queued 01:01
created

wallet.js ➔ Wallet   C

Complexity

Conditions 8
Paths 36

Size

Total Lines 90

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
c 0
b 0
f 0
nc 36
dl 0
loc 90
rs 5.6109
nop 17

1 Function

Rating   Name   Duplication   Size   Complexity  
A wallet.js ➔ ... ➔ _.every 0 1 1

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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