Issues (615)

lib/wallet.js (2 issues)

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:
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) {
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(
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;
320
                } else if (options.secret) {
321
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
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))
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)
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) {
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
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
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);
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
    );
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
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 View Code Duplication
    if (typeof changeAddress === "function") {
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
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"));
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
    address = address.toLowerCase();
996
    try {
997
        addr = bitcoin.address.fromCashAddress(address);
998
        err = null;
999
    } catch (_err) {
1000
        err = _err;
1001
    }
1002
1003
    if (err) {
1004
        try {
1005
            addr = bitcoin.address.fromCashAddress(network.cashAddrPrefix + ':' + address);
1006
            err = null;
1007
        } catch (_err) {
1008
            err = _err;
1009
        }
1010
    }
1011
1012
    if (!err) {
1013
        // Valid base58 but invalid network immediately alerts
1014
        if (addr.prefix !== network.cashAddrPrefix) {
1015
            throw new Error(address + ' has an invalid prefix');
1016
        }
1017
    }
1018
1019
    return [err, addr];
1020
}
1021
1022
function readBase58Address(address, network) {
1023
    var addr;
1024
    var err;
1025
    try {
1026
        addr = bitcoin.address.fromBase58Check(address);
1027
        err = null;
1028
    } catch (_err) {
1029
        err = _err;
1030
    }
1031
1032
    if (!err) {
1033
        // Valid base58 but invalid network immediately alerts
1034
        if (addr.version !== network.pubKeyHash && addr.version !== network.scriptHash) {
1035
            throw new blocktrail.InvalidAddressError("Address invalid on this network");
1036
        }
1037
    }
1038
1039
    return [err, addr];
1040
}
1041
1042
Wallet.getAddressAndType = function(address, network, allowCashAddress) {
1043
    var addr;
1044
    var type;
1045
    var err;
1046
1047
    function readAddress(reader, readType) {
1048
        var decoded = reader(address, network);
1049
        if (decoded[0] === null) {
1050
            addr = decoded[1];
1051
            type = readType;
1052
        } else {
1053
            err = decoded[0];
1054
        }
1055
    }
1056
1057
    if (network === bitcoin.networks.bitcoin ||
1058
        network === bitcoin.networks.testnet ||
1059
        network === bitcoin.networks.regtest
1060
    ) {
1061
        readAddress(readBech32Address, "bech32");
1062
    }
1063
1064
    if (!addr && 'cashAddrPrefix' in network && allowCashAddress) {
1065
        readAddress(readCashAddress, "cashaddr");
1066
    }
1067
1068
    if (!addr) {
1069
        readAddress(readBase58Address, "base58");
1070
    }
1071
1072
    if (addr) {
1073
        return {
1074
            address: address,
1075
            decoded: addr,
1076
            type: type
1077
        };
1078
    } else {
1079
        throw new blocktrail.InvalidAddressError(err.message);
1080
    }
1081
};
1082
1083
Wallet.convertPayToOutputs = function(pay, network, allowCashAddr) {
1084
    var send = [];
1085
1086
    var readFunc;
1087
1088
    // Deal with two different forms
1089
    if (Array.isArray(pay)) {
1090
        // output[]
1091
        readFunc = function(i, output, obj) {
1092
            if (typeof output !== "object") {
1093
                throw new Error("Invalid transaction output for numerically indexed list [1]");
1094
            }
1095
1096
            var keys = Object.keys(output);
1097
            if (keys.indexOf("scriptPubKey") !== -1 && keys.indexOf("value") !== -1) {
1098
                obj.scriptPubKey = output["scriptPubKey"];
1099
                obj.value = output["value"];
1100
            } else if (keys.indexOf("address") !== -1 && keys.indexOf("value") !== -1) {
1101
                obj.address = output["address"];
1102
                obj.value = output["value"];
1103
            } else if (keys.length === 2 && output.length === 2 && keys[0] === '0' && keys[1] === '1') {
1104
                obj.address = output[0];
1105
                obj.value = output[1];
1106
            } else {
1107
                throw new Error("Invalid transaction output for numerically indexed list [2]");
1108
            }
1109
        };
1110
    } else if (typeof pay === "object") {
1111
        // map[addr]amount
1112
        readFunc = function(address, value, obj) {
1113
            obj.address = address.trim();
1114
            obj.value = value;
1115
            if (obj.address === Wallet.OP_RETURN) {
1116
                var datachunk = Buffer.isBuffer(value) ? value : new Buffer(value, 'utf-8');
1117
                obj.scriptPubKey = bitcoin.script.nullData.output.encode(datachunk).toString('hex');
1118
                obj.value = 0;
1119
                obj.address = null;
1120
            }
1121
        };
1122
    } else {
1123
        throw new Error("Invalid input");
1124
    }
1125
1126
    Object.keys(pay).forEach(function(key) {
1127
        var obj = {};
1128
        readFunc(key, pay[key], obj);
1129
1130
        if (parseInt(obj.value, 10).toString() !== obj.value.toString()) {
1131
            throw new blocktrail.WalletSendError("Values should be in Satoshis");
1132
        }
1133
1134
        // Remove address, replace with scriptPubKey
1135
        if (typeof obj.address === "string") {
1136
            try {
1137
                var addrAndType = Wallet.getAddressAndType(obj.address, network, allowCashAddr);
1138
                obj.scriptPubKey = bitcoin.address.toOutputScript(addrAndType.address, network, allowCashAddr).toString('hex');
1139
                delete obj.address;
1140
            } catch (e) {
1141
                throw new blocktrail.InvalidAddressError("Invalid address [" + obj.address + "] (" + e.message + ")");
1142
            }
1143
        }
1144
1145
        // Extra checks when the output isn't OP_RETURN
1146
        if (obj.scriptPubKey.slice(0, 2) !== "6a") {
1147
            if (!(obj.value = parseInt(obj.value, 10))) {
1148
                throw new blocktrail.WalletSendError("Values should be non zero");
1149
            } else if (obj.value <= blocktrail.DUST) {
1150
                throw new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")");
1151
            }
1152
        }
1153
1154
        // Value fully checked now
1155
        obj.value = parseInt(obj.value, 10);
1156
1157
        send.push(obj);
1158
    });
1159
1160
    return send;
1161
};
1162
1163
Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) {
1164
    /* jshint -W071 */
1165
    var self = this;
1166
1167 View Code Duplication
    if (typeof changeAddress === "function") {
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
1168
        cb = changeAddress;
1169
        changeAddress = null;
1170
    } else if (typeof allowZeroConf === "function") {
1171
        cb = allowZeroConf;
1172
        allowZeroConf = false;
1173
    } else if (typeof randomizeChangeIdx === "function") {
1174
        cb = randomizeChangeIdx;
1175
        randomizeChangeIdx = true;
1176
    } else if (typeof feeStrategy === "function") {
1177
        cb = feeStrategy;
1178
        feeStrategy = null;
1179
    } else if (typeof options === "function") {
1180
        cb = options;
1181
        options = {};
1182
    }
1183
1184
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
1185
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1186
    options = options || {};
1187
1188
    var deferred = q.defer();
1189
    deferred.promise.spreadNodeify(cb);
1190
1191
    q.nextTick(function() {
1192
        var send;
1193
        try {
1194
            send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1195
        } catch (e) {
1196
            deferred.reject(e);
1197
            return deferred.promise;
1198
        }
1199
1200
        if (!send.length) {
1201
            deferred.reject(new blocktrail.WalletSendError("Need at least one recipient"));
1202
            return deferred.promise;
1203
        }
1204
1205
        deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION);
1206
1207
        deferred.resolve(
1208
            self.coinSelection(send, true, allowZeroConf, feeStrategy, options)
1209
            /**
1210
             *
1211
             * @param {Object[]} utxos
1212
             * @param fee
1213
             * @param change
1214
             * @param randomizeChangeIdx
1215
             * @returns {*}
1216
             */
1217
                .spread(function(utxos, fee, change) {
1218
                    var tx, txb, outputs = [];
1219
1220
                    var deferred = q.defer();
1221
1222
                    async.waterfall([
1223
                        /**
1224
                         * prepare
1225
                         *
1226
                         * @param cb
1227
                         */
1228
                        function(cb) {
1229
                            var inputsTotal = utxos.map(function(utxo) {
1230
                                return utxo['value'];
1231
                            }).reduce(function(a, b) {
1232
                                return a + b;
1233
                            });
1234
                            var outputsTotal = send.map(function(output) {
1235
                                return output.value;
1236
                            }).reduce(function(a, b) {
1237
                                return a + b;
1238
                            });
1239
                            var estimatedChange = inputsTotal - outputsTotal - fee;
1240
1241
                            if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) {
1242
                                return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " +
1243
                                    "suggested by the coin selection seems incorrect (" + estimatedChange + ")"));
1244
                            }
1245
1246
                            cb();
1247
                        },
1248
                        /**
1249
                         * init transaction builder
1250
                         *
1251
                         * @param cb
1252
                         */
1253
                        function(cb) {
1254
                            txb = new bitcoin.TransactionBuilder(self.network);
1255
                            if (self.bitcoinCash) {
1256
                                txb.enableBitcoinCash();
1257
                            }
1258
1259
                            cb();
1260
                        },
1261
                        /**
1262
                         * add UTXOs as inputs
1263
                         *
1264
                         * @param cb
1265
                         */
1266
                        function(cb) {
1267
                            var i;
1268
1269
                            for (i = 0; i < utxos.length; i++) {
1270
                                txb.addInput(utxos[i]['hash'], utxos[i]['idx']);
1271
                            }
1272
1273
                            cb();
1274
                        },
1275
                        /**
1276
                         * build desired outputs
1277
                         *
1278
                         * @param cb
1279
                         */
1280
                        function(cb) {
1281
                            send.forEach(function(_send) {
1282
                                if (_send.scriptPubKey) {
1283
                                    outputs.push({scriptPubKey: new Buffer(_send.scriptPubKey, 'hex'), value: _send.value});
1284
                                } else {
1285
                                    throw new Error("Invalid send");
1286
                                }
1287
                            });
1288
                            cb();
1289
                        },
1290
                        /**
1291
                         * get change address if required
1292
                         *
1293
                         * @param cb
1294
                         */
1295
                        function(cb) {
1296
                            if (change > 0) {
1297
                                if (change <= blocktrail.DUST) {
1298
                                    change = 0; // don't do a change output if it would be a dust output
1299
1300
                                } else {
1301
                                    if (!changeAddress) {
1302
                                        deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS);
1303
1304
                                        return self.getNewAddress(self.changeChain, function(err, address) {
1305
                                            if (err) {
1306
                                                return cb(err);
1307
                                            }
1308
                                            changeAddress = address;
1309
                                            cb();
1310
                                        });
1311
                                    }
1312
                                }
1313
                            }
1314
1315
                            cb();
1316
                        },
1317
                        /**
1318
                         * add change to outputs
1319
                         *
1320
                         * @param cb
1321
                         */
1322
                        function(cb) {
1323
                            if (change > 0) {
1324
                                var changeOutput = {
1325
                                    scriptPubKey: bitcoin.address.toOutputScript(changeAddress, self.network, self.useNewCashAddr),
1326
                                    value: change
1327
                                };
1328
                                if (randomizeChangeIdx) {
1329
                                    outputs.splice(_.random(0, outputs.length), 0, changeOutput);
1330
                                } else {
1331
                                    outputs.push(changeOutput);
1332
                                }
1333
                            }
1334
1335
                            cb();
1336
                        },
1337
                        /**
1338
                         * add outputs to txb
1339
                         *
1340
                         * @param cb
1341
                         */
1342
                        function(cb) {
1343
                            outputs.forEach(function(outputInfo) {
1344
                                txb.addOutput(outputInfo.scriptPubKey, outputInfo.value);
1345
                            });
1346
1347
                            cb();
1348
                        },
1349
                        /**
1350
                         * sign
1351
                         *
1352
                         * @param cb
1353
                         */
1354
                        function(cb) {
1355
                            var i, privKey, path, redeemScript, witnessScript;
1356
1357
                            deferred.notify(Wallet.PAY_PROGRESS_SIGN);
1358
1359
                            for (i = 0; i < utxos.length; i++) {
1360
                                var mode = SignMode.SIGN;
1361
                                if (utxos[i].sign_mode) {
1362
                                    mode = utxos[i].sign_mode;
1363
                                }
1364
1365
                                redeemScript = null;
1366
                                witnessScript = null;
1367
                                if (mode === SignMode.SIGN) {
1368
                                    path = utxos[i]['path'].replace("M", "m");
1369
1370
                                    // todo: regenerate scripts for path and compare for utxo (paranoid mode)
1371
                                    if (self.primaryPrivateKey) {
1372
                                        privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair;
1373
                                    } else if (self.backupPrivateKey) {
1374
                                        privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair;
1375
                                    } else {
1376
                                        throw new Error("No master privateKey present");
1377
                                    }
1378
1379
                                    redeemScript = new Buffer(utxos[i]['redeem_script'], 'hex');
1380
                                    if (typeof utxos[i]['witness_script'] === 'string') {
1381
                                        witnessScript = new Buffer(utxos[i]['witness_script'], 'hex');
1382
                                    }
1383
1384
                                    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
1385
                                    if (self.bitcoinCash) {
1386
                                        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
1387
                                    }
1388
1389
                                    txb.sign(i, privKey, redeemScript, sigHash, utxos[i].value, witnessScript);
1390
                                }
1391
                            }
1392
1393
                            tx = txb.buildIncomplete();
1394
1395
                            cb();
1396
                        },
1397
                        /**
1398
                         * estimate fee to verify that the API is not providing us wrong data
1399
                         *
1400
                         * @param cb
1401
                         */
1402
                        function(cb) {
1403
                            var estimatedFee = Wallet.estimateVsizeFee(tx, utxos);
1404
1405
                            if (self.sdk.feeSanityCheck) {
1406
                                switch (feeStrategy) {
1407
                                    case Wallet.FEE_STRATEGY_BASE_FEE:
1408
                                        if (Math.abs(estimatedFee - fee) > blocktrail.BASE_FEE) {
1409
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1410
                                                "seems incorrect (" + estimatedFee + ") for FEE_STRATEGY_BASE_FEE"));
1411
                                        }
1412
                                    break;
1413
1414
                                    case Wallet.FEE_STRATEGY_HIGH_PRIORITY:
1415
                                    case Wallet.FEE_STRATEGY_OPTIMAL:
1416
                                    case Wallet.FEE_STRATEGY_LOW_PRIORITY:
1417
                                        if (fee > estimatedFee * self.feeSanityCheckBaseFeeMultiplier) {
1418
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1419
                                                "seems awefully high (" + estimatedFee + ") for FEE_STRATEGY_OPTIMAL"));
1420
                                        }
1421
                                    break;
1422
                                }
1423
                            }
1424
1425
                            cb();
1426
                        }
1427
                    ], function(err) {
1428
                        if (err) {
1429
                            deferred.reject(new blocktrail.WalletSendError(err));
1430
                            return;
1431
                        }
1432
1433
                        deferred.resolve([tx, utxos]);
1434
                    });
1435
1436
                    return deferred.promise;
1437
                }
1438
            )
1439
        );
1440
    });
1441
1442
    return deferred.promise;
1443
};
1444
1445
1446
/**
1447
 * use the API to get the best inputs to use based on the outputs
1448
 *
1449
 * @param pay               array       {'address': (int)value}     coins to send
1450
 * @param [lockUTXO]        bool        lock UTXOs for a few seconds to allow for transaction to be created
1451
 * @param [allowZeroConf]   bool        allow zero confirmation unspent outputs to be used in coin selection
1452
 * @param [feeStrategy]     string      defaults to FEE_STRATEGY_OPTIMAL
1453
 * @param [options]         object
1454
 * @param [cb]              function    callback(err, utxos, fee, change)
1455
 * @returns {q.Promise}
1456
 */
1457
Wallet.prototype.coinSelection = function(pay, lockUTXO, allowZeroConf, feeStrategy, options, cb) {
1458
    var self = this;
1459
1460
    if (typeof lockUTXO === "function") {
1461
        cb = lockUTXO;
1462
        lockUTXO = true;
1463
    } else if (typeof allowZeroConf === "function") {
1464
        cb = allowZeroConf;
1465
        allowZeroConf = false;
1466
    } else if (typeof feeStrategy === "function") {
1467
        cb = feeStrategy;
1468
        feeStrategy = null;
1469
    } else if (typeof options === "function") {
1470
        cb = options;
1471
        options = {};
1472
    }
1473
1474
    lockUTXO = typeof lockUTXO !== "undefined" ? lockUTXO : true;
1475
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1476
    options = options || {};
1477
1478
    var send;
1479
    try {
1480
        send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1481
    } catch (e) {
1482
        var deferred = q.defer();
1483
        deferred.promise.nodeify(cb);
1484
        deferred.reject(e);
1485
        return deferred.promise;
1486
    }
1487
1488
    return self.sdk.coinSelection(self.identifier, send, lockUTXO, allowZeroConf, feeStrategy, options, cb);
1489
};
1490
1491
/**
1492
 * send the transaction using the API
1493
 *
1494
 * @param txHex             string      partially signed transaction as hex string
1495
 * @param paths             array       list of paths used in inputs which should be cosigned by the API
1496
 * @param checkFee          bool        when TRUE the API will verify if the fee is 100% correct and otherwise throw an exception
1497
 * @param [twoFactorToken]  string      2FA token
1498
 * @param prioboost         bool
1499
 * @param [cb]              function    callback(err, txHash)
1500
 * @returns {q.Promise}
1501
 */
1502
Wallet.prototype.sendTransaction = function(txHex, paths, checkFee, twoFactorToken, prioboost, cb) {
1503
    var self = this;
1504
1505
    if (typeof twoFactorToken === "function") {
1506
        cb = twoFactorToken;
1507
        twoFactorToken = null;
1508
        prioboost = false;
1509
    } else if (typeof prioboost === "function") {
1510
        cb = twoFactorToken;
1511
        prioboost = false;
1512
    }
1513
1514
    var deferred = q.defer();
1515
    deferred.promise.nodeify(cb);
1516
1517
    self.sdk.sendTransaction(self.identifier, txHex, paths, checkFee, twoFactorToken, prioboost)
1518
        .then(
1519
            function(result) {
1520
                deferred.resolve(result);
1521
            },
1522
            function(e) {
1523
                if (e.requires_2fa) {
1524
                    deferred.reject(new blocktrail.WalletMissing2FAError());
1525
                } else if (e.message.match(/Invalid two_factor_token/)) {
1526
                    deferred.reject(new blocktrail.WalletInvalid2FAError());
1527
                } else {
1528
                    deferred.reject(e);
1529
                }
1530
            }
1531
        )
1532
    ;
1533
1534
    return deferred.promise;
1535
};
1536
1537
/**
1538
 * setup a webhook for this wallet
1539
 *
1540
 * @param url           string      URL to receive webhook events
1541
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1542
 * @param [cb]          function    callback(err, webhook)
1543
 * @returns {q.Promise}
1544
 */
1545
Wallet.prototype.setupWebhook = function(url, identifier, cb) {
1546
    var self = this;
1547
1548
    if (typeof identifier === "function") {
1549
        cb = identifier;
1550
        identifier = null;
1551
    }
1552
1553
    identifier = identifier || ('WALLET-' + self.identifier);
1554
1555
    return self.sdk.setupWalletWebhook(self.identifier, identifier, url, cb);
1556
};
1557
1558
/**
1559
 * delete a webhook that was created for this wallet
1560
 *
1561
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1562
 * @param [cb]          function    callback(err, success)
1563
 * @returns {q.Promise}
1564
 */
1565
Wallet.prototype.deleteWebhook = function(identifier, cb) {
1566
    var self = this;
1567
1568
    if (typeof identifier === "function") {
1569
        cb = identifier;
1570
        identifier = null;
1571
    }
1572
1573
    identifier = identifier || ('WALLET-' + self.identifier);
1574
1575
    return self.sdk.deleteWalletWebhook(self.identifier, identifier, cb);
1576
};
1577
1578
/**
1579
 * get all transactions for the wallet (paginated)
1580
 *
1581
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1582
 * @param [cb]      function    callback(err, transactions)
1583
 * @returns {q.Promise}
1584
 */
1585
Wallet.prototype.transactions = function(params, cb) {
1586
    var self = this;
1587
1588
    return self.sdk.walletTransactions(self.identifier, params, cb);
1589
};
1590
1591
Wallet.prototype.maxSpendable = function(allowZeroConf, feeStrategy, options, cb) {
1592
    var self = this;
1593
1594
    if (typeof allowZeroConf === "function") {
1595
        cb = allowZeroConf;
1596
        allowZeroConf = false;
1597
    } else if (typeof feeStrategy === "function") {
1598
        cb = feeStrategy;
1599
        feeStrategy = null;
1600
    } else if (typeof options === "function") {
1601
        cb = options;
1602
        options = {};
1603
    }
1604
1605
    if (typeof allowZeroConf === "object") {
1606
        options = allowZeroConf;
1607
        allowZeroConf = false;
1608
    } else if (typeof feeStrategy === "object") {
1609
        options = feeStrategy;
1610
        feeStrategy = null;
1611
    }
1612
1613
    options = options || {};
1614
1615
    if (typeof options.allowZeroConf !== "undefined") {
1616
        allowZeroConf = options.allowZeroConf;
1617
    }
1618
    if (typeof options.feeStrategy !== "undefined") {
1619
        feeStrategy = options.feeStrategy;
1620
    }
1621
1622
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1623
1624
    return self.sdk.walletMaxSpendable(self.identifier, allowZeroConf, feeStrategy, options, cb);
1625
};
1626
1627
/**
1628
 * get all addresses for the wallet (paginated)
1629
 *
1630
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1631
 * @param [cb]      function    callback(err, addresses)
1632
 * @returns {q.Promise}
1633
 */
1634
Wallet.prototype.addresses = function(params, cb) {
1635
    var self = this;
1636
1637
    return self.sdk.walletAddresses(self.identifier, params, cb);
1638
};
1639
1640
/**
1641
 * @param address   string      the address to label
1642
 * @param label     string      the label
1643
 * @param [cb]      function    callback(err, res)
1644
 * @returns {q.Promise}
1645
 */
1646
Wallet.prototype.labelAddress = function(address, label, cb) {
1647
    var self = this;
1648
1649
    return self.sdk.labelWalletAddress(self.identifier, address, label, cb);
1650
};
1651
1652
/**
1653
 * get all UTXOs for the wallet (paginated)
1654
 *
1655
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1656
 * @param [cb]      function    callback(err, addresses)
1657
 * @returns {q.Promise}
1658
 */
1659
Wallet.prototype.utxos = function(params, cb) {
1660
    var self = this;
1661
1662
    return self.sdk.walletUTXOs(self.identifier, params, cb);
1663
};
1664
1665
Wallet.prototype.unspentOutputs = Wallet.prototype.utxos;
1666
1667
/**
1668
 * sort list of pubkeys to be used in a multisig redeemscript
1669
 *  sorted in lexicographical order on the hex of the pubkey
1670
 *
1671
 * @param pubKeys   {bitcoin.HDNode[]}
1672
 * @returns string[]
1673
 */
1674
Wallet.sortMultiSigKeys = function(pubKeys) {
1675
    pubKeys.sort(function(key1, key2) {
1676
        return key1.toString('hex').localeCompare(key2.toString('hex'));
1677
    });
1678
1679
    return pubKeys;
1680
};
1681
1682
/**
1683
 * determine how much fee is required based on the inputs and outputs
1684
 *  this is an estimation, not a proper 100% correct calculation
1685
 *
1686
 * @todo: mark deprecated in favor of estimations where UTXOS are known
1687
 * @param {bitcoin.Transaction} tx
1688
 * @param {int} feePerKb when not null use this feePerKb, otherwise use BASE_FEE legacy calculation
1689
 * @returns {number}
1690
 */
1691
Wallet.estimateIncompleteTxFee = function(tx, feePerKb) {
1692
    var size = Wallet.estimateIncompleteTxSize(tx);
1693
    var sizeKB = size / 1000;
1694
    var sizeKBCeil = Math.ceil(size / 1000);
1695
1696
    if (feePerKb) {
1697
        return parseInt(sizeKB * feePerKb, 10);
1698
    } else {
1699
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1700
    }
1701
};
1702
1703
/**
1704
 * Takes tx and utxos, computing their estimated vsize,
1705
 * and uses feePerKb (or BASEFEE as default) to estimate
1706
 * the number of satoshis in fee.
1707
 *
1708
 * @param {bitcoin.Transaction} tx
1709
 * @param {Array} utxos
1710
 * @param feePerKb
1711
 * @returns {Number}
1712
 */
1713
Wallet.estimateVsizeFee = function(tx, utxos, feePerKb) {
1714
    var vsize = SizeEstimation.estimateTxVsize(tx, utxos);
1715
    var sizeKB = vsize / 1000;
1716
    var sizeKBCeil = Math.ceil(vsize / 1000);
1717
1718
    if (feePerKb) {
1719
        return parseInt(sizeKB * feePerKb, 10);
1720
    } else {
1721
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1722
    }
1723
};
1724
1725
/**
1726
 * determine how much fee is required based on the inputs and outputs
1727
 *  this is an estimation, not a proper 100% correct calculation
1728
 *
1729
 * @param {bitcoin.Transaction} tx
1730
 * @returns {number}
1731
 */
1732
Wallet.estimateIncompleteTxSize = function(tx) {
1733
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1734
1735
    size += tx.outs.length * 34;
1736
1737
    tx.ins.forEach(function(txin) {
1738
        var scriptSig = txin.script,
1739
            scriptType = bitcoin.script.classifyInput(scriptSig);
1740
1741
        var multiSig = [2, 3]; // tmp hardcoded
1742
1743
        // Re-classify if P2SH
1744
        if (!multiSig && scriptType === 'scripthash') {
1745
            var sigChunks = bitcoin.script.decompile(scriptSig);
1746
            var redeemScript = sigChunks.slice(-1)[0];
1747
            scriptSig = bitcoin.script.compile(sigChunks.slice(0, -1));
1748
            scriptType = bitcoin.script.classifyInput(scriptSig);
1749
1750
            if (bitcoin.script.classifyOutput(redeemScript) !== scriptType) {
1751
                throw new blocktrail.TransactionInputError('Non-matching scriptSig and scriptPubKey in input');
1752
            }
1753
1754
            // figure out M of N for multisig (code from internal usage of bitcoinjs)
1755
            if (scriptType === 'multisig') {
1756
                var rsChunks = bitcoin.script.decompile(redeemScript);
1757
                var mOp = rsChunks[0];
1758
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1759
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1760
                }
1761
1762
                var nOp = rsChunks[redeemScript.chunks.length - 2];
1763
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1764
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1765
                }
1766
1767
                var m = mOp - (bitcoin.opcodes.OP_1 - 1);
1768
                var n = nOp - (bitcoin.opcodes.OP_1 - 1);
1769
                if (n < m) {
1770
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1771
                }
1772
1773
                multiSig = [m, n];
1774
            }
1775
        }
1776
1777
        if (multiSig) {
1778
            size += (
1779
                32 + // txhash
1780
                4 + // idx
1781
                3 + // scriptVarInt[>=253]
1782
                1 + // OP_0
1783
                ((1 + 72) * multiSig[0]) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1784
                (2 + 105) + // OP_PUSHDATA[>=75] + script
1785
                4 // sequence
1786
            );
1787
1788
        } else {
1789
            size += 32 + // txhash
1790
                4 + // idx
1791
                73 + // sig
1792
                34 + // script
1793
                4; // sequence
1794
        }
1795
    });
1796
1797
    return size;
1798
};
1799
1800
/**
1801
 * determine how much fee is required based on the amount of inputs and outputs
1802
 *  this is an estimation, not a proper 100% correct calculation
1803
 *  this asumes all inputs are 2of3 multisig
1804
 *
1805
 * @todo: mark deprecated in favor of situations where UTXOS are known
1806
 * @param txinCnt       {number}
1807
 * @param txoutCnt      {number}
1808
 * @returns {number}
1809
 */
1810
Wallet.estimateFee = function(txinCnt, txoutCnt) {
1811
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1812
1813
    size += txoutCnt * 34;
1814
1815
    size += (
1816
            32 + // txhash
1817
            4 + // idx
1818
            3 + // scriptVarInt[>=253]
1819
            1 + // OP_0
1820
            ((1 + 72) * 2) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1821
            (2 + 105) + // OP_PUSHDATA[>=75] + script
1822
            4 // sequence
1823
        ) * txinCnt;
1824
1825
    var sizeKB = Math.ceil(size / 1000);
1826
1827
    return sizeKB * blocktrail.BASE_FEE;
1828
};
1829
1830
/**
1831
 * create derived key from parent key by path
1832
 *
1833
 * @param hdKey     {bitcoin.HDNode}
1834
 * @param path      string
1835
 * @param keyPath   string
1836
 * @returns {bitcoin.HDNode}
1837
 */
1838
Wallet.deriveByPath = function(hdKey, path, keyPath) {
1839
    keyPath = keyPath || (!!hdKey.keyPair.d ? "m" : "M");
1840
1841
    if (path[0].toLowerCase() !== "m" || keyPath[0].toLowerCase() !== "m") {
1842
        throw new blocktrail.KeyPathError("Wallet.deriveByPath only works with absolute paths. (" + path + ", " + keyPath + ")");
1843
    }
1844
1845
    if (path[0] === "m" && keyPath[0] === "M") {
1846
        throw new blocktrail.KeyPathError("Wallet.deriveByPath can't derive private path from public parent. (" + path + ", " + keyPath + ")");
1847
    }
1848
1849
    // if the desired path is public while the input is private
1850
    var toPublic = path[0] === "M" && keyPath[0] === "m";
1851
    if (toPublic) {
1852
        // derive the private path, convert to public when returning
1853
        path[0] = "m";
1854
    }
1855
1856
    // keyPath should be the parent parent of path
1857
    if (path.toLowerCase().indexOf(keyPath.toLowerCase()) !== 0) {
1858
        throw new blocktrail.KeyPathError("Wallet.derivePath requires path (" + path + ") to be a child of keyPath (" + keyPath + ")");
1859
    }
1860
1861
    // remove the part of the path we already have
1862
    path = path.substr(keyPath.length);
1863
1864
    // iterate over the chunks and derive
1865
    var newKey = hdKey;
1866
    path.replace(/^\//, "").split("/").forEach(function(chunk) {
1867
        if (!chunk) {
1868
            return;
1869
        }
1870
1871
        if (chunk.indexOf("'") !== -1) {
1872
            chunk = parseInt(chunk.replace("'", ""), 10) + bitcoin.HDNode.HIGHEST_BIT;
1873
        }
1874
1875
        newKey = newKey.derive(parseInt(chunk, 10));
1876
    });
1877
1878
    if (toPublic) {
1879
        return newKey.neutered();
1880
    } else {
1881
        return newKey;
1882
    }
1883
};
1884
1885
module.exports = Wallet;
1886