Completed
Pull Request — master (#110)
by Tobias
03:25
created

WalletSweeper.discoverWalletFunds   B

Complexity

Conditions 2
Paths 24

Size

Total Lines 94

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
c 2
b 0
f 0
nc 24
dl 0
loc 94
rs 8.4378
nop 1

How to fix   Long Method   

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:

1
var UnspentOutputFinder = require('./unspent_output_finder');
2
var bitcoin = require('bitcoinjs-lib');
3
var bip39 = require("bip39");
4
var CryptoJS = require('crypto-js');
5
var blocktrail = require('./blocktrail');
6
var EncryptionMnemonic = require('./encryption_mnemonic');
7
var Encryption = require('./encryption');
8
var walletSDK = require('./wallet');
9
var _ = require('lodash');
10
var q = require('q');
11
var async = require('async');
12
13
/**
14
 *
15
 * @param backupData
16
 * @param bitcoinDataClient
17
 * @param options
18
 * @constructor
19
 */
20
var WalletSweeper = function(backupData, bitcoinDataClient, options) {
21
    /* jshint -W071, -W074 */
22
    var self = this;
23
    this.defaultSettings = {
24
        network: 'btc',
25
        testnet: false,
26
        regtest: false,
27
        logging: false,
28
        bitcoinCash: false,
29
        sweepBatchSize: 200
30
    };
31
    this.settings = _.merge({}, this.defaultSettings, options);
32
    this.bitcoinDataClient = bitcoinDataClient;
33
    this.utxoFinder = new UnspentOutputFinder(bitcoinDataClient, this.settings);
34
    this.sweepData = null;
35
36
    // set the bitcoinlib network
37
    if (typeof options.network === "object") {
38
        this.network = options.network;
39
    } else {
40
        this.network = this.getBitcoinNetwork(this.settings.network, this.settings.testnet, this.settings.regtest);
41
    }
42
43
    backupData.walletVersion = backupData.walletVersion || 2;   //default to version 2 wallets
44
45
    var usePassword = false;
46
47
    // validate backup data, cleanup input, and prepare seeds
48
    if (!Array.isArray(backupData.blocktrailKeys)) {
49
        throw new Error('blocktrail pub keys are required (must be type Array)');
50
    }
51
52
    switch (backupData.walletVersion) {
53
        case 1:
54
            if (typeof backupData.primaryMnemonic === "undefined" || !backupData.primaryMnemonic) {
55
                throw new Error('missing primary mnemonic for version 1 wallet');
56
            }
57
            if (typeof backupData.backupMnemonic === "undefined" || !backupData.backupMnemonic) {
58
                throw new Error('missing backup mnemonic for version 1 wallet');
59
            }
60
            if (typeof backupData.primaryPassphrase === "undefined") {
61
                throw new Error('missing primary passphrase for version 1 wallet');
62
            }
63
64
            // cleanup copy paste errors from mnemonics
65
            backupData.primaryMnemonic = backupData.primaryMnemonic.trim()
66
                .replace(new RegExp("\r\n", 'g'), " ")
67
                .replace(new RegExp("\n", 'g'), " ")
68
                .replace(/\s+/g, " ");
69
            backupData.backupMnemonic = backupData.backupMnemonic.trim()
70
                .replace(new RegExp("\r\n", 'g'), " ")
71
                .replace(new RegExp("\n", 'g'), " ")
72
                .replace(/\s+/g, " ");
73
        break;
74
75
        case 2:
76
        case 3:
77
            if (typeof backupData.encryptedPrimaryMnemonic === "undefined" || !backupData.encryptedPrimaryMnemonic) {
78
                throw new Error('missing encrypted primary seed for version 2 wallet');
79
            }
80
            if (typeof backupData.backupMnemonic === "undefined" || (!backupData.backupMnemonic && backupData.backupMnemonic !== false)) {
81
                throw new Error('missing backup seed for version 2 wallet');
82
            }
83
            //can either recover with password and password encrypted secret, or with encrypted recovery secret and a decryption key
84
            usePassword = typeof backupData.password !== "undefined" && backupData.password !== null;
85
            if (usePassword) {
86
                if (typeof backupData.passwordEncryptedSecretMnemonic === "undefined" || !backupData.passwordEncryptedSecretMnemonic) {
87
                    throw new Error('missing password encrypted secret for version 2 wallet');
88
                }
89
                if (typeof backupData.password === "undefined") {
90
                    throw new Error('missing primary passphrase for version 2 wallet');
91
                }
92
            } else {
93
                if (typeof backupData.encryptedRecoverySecretMnemonic === "undefined" || !backupData.encryptedRecoverySecretMnemonic) {
94
                    throw new Error('missing encrypted recovery secret for version 2 wallet (recovery without password)');
95
                }
96
                if (!backupData.recoverySecretDecryptionKey) {
97
                    throw new Error('missing recovery secret decryption key for version 2 wallet (recovery without password)');
98
                }
99
            }
100
101
            // cleanup copy paste errors from mnemonics
102
            backupData.encryptedPrimaryMnemonic = backupData.encryptedPrimaryMnemonic.trim()
103
                .replace(new RegExp("\r\n", 'g'), " ")
104
                .replace(new RegExp("\n", 'g'), " ")
105
                .replace(/\s+/g, " ");
106
            backupData.backupMnemonic = (backupData.backupMnemonic || "").trim()
107
                .replace(new RegExp("\r\n", 'g'), " ")
108
                .replace(new RegExp("\n", 'g'), " ")
109
                .replace(/\s+/g, " ");
110
            if (backupData.recoverySecretDecryptionKey) {
111
                backupData.recoverySecretDecryptionKey = backupData.recoverySecretDecryptionKey.trim()
112
                    .replace(new RegExp("\r\n", 'g'), " ")
113
                    .replace(new RegExp("\n", 'g'), " ")
114
                    .replace(/\s+/g, " ");
115
            }
116
            if (usePassword) {
117
                backupData.passwordEncryptedSecretMnemonic = backupData.passwordEncryptedSecretMnemonic.trim()
118
                    .replace(new RegExp("\r\n", 'g'), " ").replace(new RegExp("\n", 'g'), " ").replace(/\s+/g, " ");
119
            } else {
120
                backupData.encryptedRecoverySecretMnemonic = backupData.encryptedRecoverySecretMnemonic.trim()
121
                    .replace(new RegExp("\r\n", 'g'), " ").replace(new RegExp("\n", 'g'), " ").replace(/\s+/g, " ");
122
            }
123
124
        break;
125
126
        default:
127
            throw new Error('Wrong version [' + backupData.walletVersion + ']');
128
    }
129
130
131
    // create BIP32 HDNodes for the Blocktrail public keys
132
    this.blocktrailPublicKeys = {};
133
    _.each(backupData.blocktrailKeys, function(blocktrailKey) {
134
        self.blocktrailPublicKeys[blocktrailKey['keyIndex']] = bitcoin.HDNode.fromBase58(blocktrailKey['pubkey'], self.network);
135
    });
136
137
    // convert the primary and backup mnemonics to seeds (using BIP39)
138
    var primarySeed, backupSeed, secret;
139
    switch (backupData.walletVersion) {
140
        case 1:
141
            primarySeed = bip39.mnemonicToSeed(backupData.primaryMnemonic, backupData.primaryPassphrase);
142
            backupSeed = bip39.mnemonicToSeed(backupData.backupMnemonic, "");
143
        break;
144
145
        case 2:
146
            // convert mnemonics to hex (bip39) and then base64 for decryption
147
            backupData.encryptedPrimaryMnemonic = blocktrail.convert(bip39.mnemonicToEntropy(backupData.encryptedPrimaryMnemonic), 'hex', 'base64');
148
            if (usePassword) {
149
                backupData.passwordEncryptedSecretMnemonic = blocktrail.convert(
150
                    bip39.mnemonicToEntropy(backupData.passwordEncryptedSecretMnemonic), 'hex', 'base64');
151
            } else {
152
                backupData.encryptedRecoverySecretMnemonic = blocktrail.convert(
153
                    bip39.mnemonicToEntropy(backupData.encryptedRecoverySecretMnemonic), 'hex', 'base64');
154
            }
155
156
            // decrypt encryption secret
157
            if (usePassword) {
158
                secret = CryptoJS.AES.decrypt(backupData.passwordEncryptedSecretMnemonic, backupData.password).toString(CryptoJS.enc.Utf8);
159
            } else {
160
                secret = CryptoJS.AES.decrypt(backupData.encryptedRecoverySecretMnemonic, backupData.recoverySecretDecryptionKey).toString(CryptoJS.enc.Utf8);
161
            }
162
163
            if (!secret) {
164
                throw new Error("Could not decrypt secret with " + (usePassword ? "password" : "decryption key"));
165
            }
166
167
            // now finally decrypt the primary seed and convert to buffer (along with backup seed)
168
            primarySeed = new Buffer(CryptoJS.AES.decrypt(backupData.encryptedPrimaryMnemonic, secret).toString(CryptoJS.enc.Utf8), 'base64');
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...
169
170
            if (backupData.backupMnemonic) {
171
                backupSeed = new Buffer(bip39.mnemonicToEntropy(backupData.backupMnemonic), 'hex');
172
            }
173
174
        break;
175
176
        case 3:
177
            // convert mnemonics to hex (bip39) and then base64 for decryption
178
            backupData.encryptedPrimaryMnemonic = EncryptionMnemonic.decode(backupData.encryptedPrimaryMnemonic);
179
            if (usePassword) {
180
                backupData.passwordEncryptedSecretMnemonic = EncryptionMnemonic.decode(backupData.passwordEncryptedSecretMnemonic);
181
            } else {
182
                backupData.encryptedRecoverySecretMnemonic = EncryptionMnemonic.decode(backupData.encryptedRecoverySecretMnemonic);
183
            }
184
185
            // decrypt encryption secret
186
            if (usePassword) {
187
                secret = Encryption.decrypt(backupData.passwordEncryptedSecretMnemonic, new Buffer(backupData.password));
188
            } else {
189
                secret = Encryption.decrypt(backupData.encryptedRecoverySecretMnemonic, new Buffer(backupData.recoverySecretDecryptionKey, 'hex'));
190
            }
191
192
            if (!secret) {
193
                throw new Error("Could not decrypt secret with " + (usePassword ? "password" : "decryption key"));
194
            }
195
196
            // now finally decrypt the primary seed and convert to buffer (along with backup seed)
197
            primarySeed = Encryption.decrypt(backupData.encryptedPrimaryMnemonic, secret);
198
            if (backupData.backupMnemonic) {
199
                backupSeed = new Buffer(bip39.mnemonicToEntropy(backupData.backupMnemonic), 'hex');
200
            }
201
202
        break;
203
204
        default:
205
            throw new Error('Wrong version [' + backupData.walletVersion + ']');
206
    }
207
208
    // convert the primary and backup seeds to private keys (using BIP32)
209
    this.primaryPrivateKey = bitcoin.HDNode.fromSeedBuffer(primarySeed, this.network);
210
211
    if (backupSeed) {
212
        this.backupPrivateKey = bitcoin.HDNode.fromSeedBuffer(backupSeed, this.network);
213
        this.backupPublicKey = this.backupPrivateKey.neutered();
214
    } else {
215
        this.backupPrivateKey = false;
216
        this.backupPublicKey = bitcoin.HDNode.fromBase58(backupData.backupPublicKey, this.network);
217
    }
218
219
    if (this.settings.logging) {
220
        console.log('using password method: ' + usePassword);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
221
        console.log("Primary Prv Key: " + this.primaryPrivateKey.toBase58());
222
        console.log("Primary Pub Key: " + this.primaryPrivateKey.neutered().toBase58());
223
        console.log("Backup Prv Key: " + (this.backupPrivateKey ? this.backupPrivateKey.toBase58() : null));
224
        console.log("Backup Pub Key: " + this.backupPublicKey.toBase58());
225
    }
226
};
227
228
229
/**
230
 * returns an appropriate bitcoin-js lib network
231
 *
232
 * @param network
233
 * @param testnet
234
 * @param regtest
235
 * @returns {*[]}
236
 */
237
WalletSweeper.prototype.getBitcoinNetwork =  function(network, testnet, regtest) {
238
    switch (network.toLowerCase()) {
239
        case 'btc':
240
        case 'bitcoin':
241
            if (regtest) {
242
                return bitcoin.networks.regtest;
243
            } else if (testnet) {
244
                return bitcoin.networks.testnet;
245
            } else {
246
                return bitcoin.networks.bitcoin;
247
            }
248
        break;
0 ignored issues
show
Unused Code introduced by
This break statement is unnecessary and may be removed.
Loading history...
249
        case 'tbtc':
250
        case 'bitcoin-testnet':
251
            return bitcoin.networks.testnet;
252
        default:
253
            throw new Error("Unknown network " + network);
254
    }
255
};
256
257
/**
258
 * gets the blocktrail pub key for the given path from the stored array of pub keys
259
 *
260
 * @param path
261
 * @returns {boolean}
262
 */
263
WalletSweeper.prototype.getBlocktrailPublicKey = function(path) {
264
    path = path.replace("m", "M");
265
    var keyIndex = path.split("/")[1].replace("'", "");
266
267
    if (!this.blocktrailPublicKeys[keyIndex]) {
268
        throw new Error("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
269
    }
270
271
    return this.blocktrailPublicKeys[keyIndex];
272
};
273
274
/**
275
 * generate multisig address and redeem script for given path
276
 *
277
 * @param path
278
 * @returns {{address, redeem: *, witness: *}}
279
 */
280
WalletSweeper.prototype.createAddress = function(path) {
281
    //ensure a public path is used
282
    path = path.replace("m", "M");
283
    var keyIndex = path.split("/")[1].replace("'", "");
284
    var scriptType = parseInt(path.split("/")[2]);
285
286
    //derive the primary pub key directly from the primary priv key
287
    var primaryPubKey = walletSDK.deriveByPath(this.primaryPrivateKey, path, "m");
288
    //derive the backup pub key directly from the backup priv key (unharden path)
289
    var backupPubKey = walletSDK.deriveByPath(this.backupPublicKey, path.replace("'", ""), "M");
290
    //derive a pub key for this path from the blocktrail pub key
291
    var blocktrailPubKey = walletSDK.deriveByPath(this.getBlocktrailPublicKey(path), path, "M/" + keyIndex + "'");
292
293
    //sort the keys and generate a multisig redeem script and address
294
    var multisigKeys = walletSDK.sortMultiSigKeys([
295
        primaryPubKey.keyPair.getPublicKeyBuffer(),
296
        backupPubKey.keyPair.getPublicKeyBuffer(),
297
        blocktrailPubKey.keyPair.getPublicKeyBuffer()
298
    ]);
299
300
    var multisig = bitcoin.script.multisig.output.encode(2, multisigKeys);
301
    var redeemScript, witnessScript;
302
    if (this.network !== "bitcoincash" && scriptType === walletSDK.CHAIN_BTC_SEGWIT) {
303
        witnessScript = multisig;
304
        redeemScript = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(witnessScript));
305
    } else {
306
        witnessScript = null;
307
        redeemScript = multisig;
308
    }
309
    var scriptHash = bitcoin.crypto.hash160(redeemScript);
310
    var scriptPubKey = bitcoin.script.scriptHash.output.encode(scriptHash);
311
312
    var network = this.network;
313
    if (typeof this.network !== "undefined") {
314
        network = this.network;
315
    }
316
    var address = bitcoin.address.fromOutputScript(scriptPubKey, network, !!this.settings.bitcoinCash);
317
318
    // Insight nodes want nothing to do with 'bitcoin:' or 'bitcoincash:' prefixes
319
    address = address.replace('bitcoin:', '').replace('bitcoincash:', '');
320
321
    //@todo return as buffers
322
    return {address: address.toString(), redeem: redeemScript, witness: witnessScript};
323
};
324
325
/**
326
 * create a batch of multisig addresses
327
 *
328
 * @param start
329
 * @param count
330
 * @param keyIndex
331
 * @param chain
332
 * @returns {{}}
333
 */
334
WalletSweeper.prototype.createBatchAddresses = function(start, count, keyIndex, chain) {
335
    var self = this;
336
    var addresses = {};
337
338
    return q.all(_.range(0, count).map(function(i) {
339
        //create a path subsequent address
340
        var path =  "M/" + keyIndex + "'/" + chain + "/" + (start + i);
341
        var multisig = self.createAddress(path);
342
        addresses[multisig['address']] = {
343
            redeem: multisig['redeem'],
344
            witness: multisig['witness'],
345
            path: path
346
        };
347
    })).then(function() {
348
        return addresses;
349
    });
350
};
351
352
WalletSweeper.prototype.discoverWalletFunds = function(increment, cb) {
353
    var self = this;
354
    var totalBalance = 0;
355
    var totalUTXOs = 0;
356
    var totalAddressesGenerated = 0;
357
    var addressUTXOs = {};    //addresses and their utxos, paths and redeem scripts
358
    if (typeof increment === "undefined") {
359
        increment = this.settings.sweepBatchSize;
360
    }
361
362
    var deferred = q.defer();
363
    deferred.promise.nodeify(cb);
364
365
    var checkChain;
366
    if (this.network === "bitcoincash") {
367
        checkChain = [0, 1];
368
    } else {
369
        checkChain = [0, 1, 2];
370
    }
371
372
    async.nextTick(function() {
373
        //for each blocktrail pub key, do fund discovery on batches of addresses
374
        async.eachSeries(Object.keys(self.blocktrailPublicKeys), function(keyIndex, done) {
375
            async.eachSeries(checkChain, function(chain, done) {
376
                var i = 0;
377
                var hasTransactions = false;
378
379
                async.doWhilst(function(done) {
380
                    //do
381
                    if (self.settings.logging) {
382
                        console.log("generating addresses " + i + " -> " + (i + increment) + " using blocktrail key index: " + keyIndex + ", chain: " + chain);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
383
                    }
384
                    deferred.notify({
385
                        message: "generating addresses " + i + " -> " + (i + increment) + "",
386
                        increment: increment,
387
                        btPubKeyIndex: keyIndex,
388
                        chain: chain,
389
                        //addresses: [],
390
                        totalAddresses: totalAddressesGenerated,
391
                        addressUTXOs: addressUTXOs,
392
                        totalUTXOs: totalUTXOs,
393
                        totalBalance: totalBalance
394
                    });
395
396
                    async.nextTick(function() {
397
                        self.createBatchAddresses(i, increment, keyIndex, chain)
398
                            .then(function(batch) {
399
                                totalAddressesGenerated += Object.keys(batch).length;
400
401
                                if (self.settings.logging) {
402
                                    console.log("starting fund discovery for " + increment + " addresses...");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
403
                                }
404
405
                                deferred.notify({
406
                                    message: "starting fund discovery for " + increment + " addresses",
407
                                    increment: increment,
408
                                    btPubKeyIndex: keyIndex,
409
                                    //addresses: addresses,
410
                                    totalAddresses: totalAddressesGenerated,
411
                                    addressUTXOs: addressUTXOs,
412
                                    totalUTXOs: totalUTXOs,
413
                                    totalBalance: totalBalance
414
                                });
415
416
                                //get the unspent outputs for this batch of addresses
417
                                return self.bitcoinDataClient.batchAddressHasTransactions(_.keys(batch)).then(function(_hasTransactions) {
418
                                    hasTransactions = _hasTransactions;
419
                                    if (self.settings.logging) {
420
                                        console.log("batch " + (hasTransactions ? "has" : "does not have") + " transactions...");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
421
                                    }
422
423
                                    return q.when(hasTransactions)
424
                                        .then(function(hasTransactions) {
425
                                            if (!hasTransactions) {
426
                                                return;
427
                                            }
428
429
                                            //get the unspent outputs for this batch of addresses
430
                                            return self.utxoFinder.getUTXOs(_.keys(batch)).then(function(utxos) {
431
                                                // Do not evaluate 0-confirmation UTXOs
432
                                                // This would include double spends and other things Insight happily accepts
433
                                                // (and keeps in mempool - even when the parent UTXO gets spent otherwise)
434
                                                for (var address in utxos) {
435
                                                    if (utxos.hasOwnProperty(address) && Array.isArray(utxos[address])) {
436
                                                        var utxosPerAddress = utxos[address];
437
                                                        // Iterate over utxos per address
438
                                                        for (var idx = 0; idx < utxosPerAddress.length; idx++) {
439
                                                            if (utxosPerAddress[idx] &&
440
                                                                'confirmations' in utxosPerAddress[idx]
441
                                                                && utxosPerAddress[idx]['confirmations'] === 0) {
442
                                                                // Delete if unconfirmed
443
                                                                delete utxos[address][idx];
444
                                                                utxos[address].length--;
445
                                                                if (utxos[address].length <= 0) {
446
                                                                    delete utxos[address];
447
                                                                }
448
                                                            }
449
                                                        }
450
                                                    }
451
                                                }
452
453
                                                // save the address utxos, along with relevant path and redeem script
454
                                                _.each(utxos, function(outputs, address) {
455
                                                    var witnessScript = null;
456
                                                    if (typeof batch[address]['witness'] !== 'undefined') {
457
                                                        witnessScript = batch[address]['witness'];
458
                                                    }
459
                                                    addressUTXOs[address] = {
460
                                                        path: batch[address]['path'],
461
                                                        redeem: batch[address]['redeem'],
462
                                                        witness: witnessScript,
463
                                                        utxos: outputs
464
                                                    };
465
466
                                                    totalUTXOs += outputs.length;
467
468
                                                    //add up the total utxo value for all addresses
469
                                                    totalBalance = _.reduce(outputs, function(carry, output) {
470
                                                        return carry + output['value'];
471
                                                    }, totalBalance);
472
473
                                                    if (self.settings.logging) {
474
                                                        console.log("found " + outputs.length + " unspent outputs for address: " + address);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
475
                                                    }
476
                                                });
477
478
                                                deferred.notify({
479
                                                    message: "discovering funds",
480
                                                    increment: increment,
481
                                                    btPubKeyIndex: keyIndex,
482
                                                    totalAddresses: totalAddressesGenerated,
483
                                                    addressUTXOs: addressUTXOs,
484
                                                    totalUTXOs: totalUTXOs,
485
                                                    totalBalance: totalBalance
486
                                                });
487
                                            });
488
                                        })
489
                                        ;
490
                                });
491
                            })
492
                            .then(
493
                                function() {
494
                                    //ready for the next batch
495
                                    i += increment;
496
                                    async.nextTick(done);
497
                                },
498
                                function(err) {
499
                                    done(err);
500
                                }
501
                            )
502
                        ;
503
                    });
504
                }, function() {
505
                    //while
506
                    return hasTransactions;
507
                }, function(err) {
508
                    //all done
509
                    if (err) {
510
                        console.log("batch complete, but with errors", err.message);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
511
512
                        deferred.notify({
513
                            message: "batch complete, but with errors: " + err.message,
514
                            error: err,
515
                            increment: increment,
516
                            btPubKeyIndex: keyIndex,
517
                            totalAddresses: totalAddressesGenerated,
518
                            addressUTXOs: addressUTXOs,
519
                            totalUTXOs: totalUTXOs,
520
                            totalBalance: totalBalance
521
                        });
522
                    }
523
                    //ready for next Blocktrail pub key
524
                    async.nextTick(done);
525
                });
526
            }, function(err) {
527
                done(err);
528
            });
529
        }, function(err) {
530
            //callback
531
            if (err) {
532
                //perhaps we should also reject the promise, and stop everything?
533
                if (self.settings.logging) {
534
                    console.log("error encountered when discovering funds", err);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
535
                }
536
            }
537
538
            if (self.settings.logging) {
539
                console.log("finished fund discovery: " + totalBalance + " Satoshi (in " + totalUTXOs + " outputs) " +
540
                    "found when searching " + totalAddressesGenerated + " addresses");
541
            }
542
543
            self.sweepData = {
544
                utxos: addressUTXOs,
545
                count: totalUTXOs,
546
                balance: totalBalance,
547
                addressesSearched: totalAddressesGenerated
548
            };
549
550
            //resolve the promise
551
            deferred.resolve(self.sweepData);
552
        });
553
    });
554
555
    return deferred.promise;
556
};
557
558
WalletSweeper.prototype.sweepWallet = function(destinationAddress, cb) {
559
    var self = this;
560
    var deferred = q.defer();
561
    deferred.promise.nodeify(cb);
562
563
    if (self.settings.logging) {
564
        console.log("starting wallet sweeping to address " + destinationAddress);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
565
    }
566
567
    q.when(true)
568
        .then(function() {
569
            if (!self.sweepData) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if !self.sweepData 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...
570
                //do wallet fund discovery
571
                return self.discoverWalletFunds()
572
                    .progress(function(progress) {
573
                        deferred.notify(progress);
574
                    });
575
            }
576
        })
577
        .then(function() {
578
            return self.bitcoinDataClient.estimateFee();
579
        })
580
        .then(function(feePerKb) {
581
            // Insight reports 1000 sat/kByte, but this is too low
582
            if (self.settings.bitcoinCash && feePerKb < 5000) {
583
                feePerKb = 5000;
584
            }
585
586
            if (self.sweepData['balance'] === 0) {
587
                //no funds found
588
                deferred.reject("No funds found after searching through " + self.sweepData['addressesSearched'] + " addresses");
589
                return deferred.promise;
590
            }
591
592
            //create and sign the transaction
593
            return self.createTransaction(destinationAddress, null, feePerKb, deferred);
594
        })
595
        .then(function(r) {
596
            deferred.resolve(r);
597
        }, function(e) {
598
            deferred.reject(e);
599
        });
600
601
    return deferred.promise;
602
};
603
604
/**
605
 * creates a raw transaction from the sweep data
606
 * @param destinationAddress        the destination address for the transaction
607
 * @param fee                       a specific transaction fee to use (optional: if null, fee will be estimated)
608
 * @param feePerKb                  fee per kb (optional: if null, use default value)
609
 * @param deferred                  a deferred promise object, used for giving progress updates (optional)
610
 */
611
WalletSweeper.prototype.createTransaction = function(destinationAddress, fee, feePerKb, deferred) {
612
    var self = this;
613
    if (this.settings.logging) {
614
        console.log("Creating transaction to address destinationAddress");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
615
    }
616
    if (deferred) {
617
        deferred.notify({
618
            message: "creating raw transaction to " + destinationAddress
619
        });
620
    }
621
622
    // create raw transaction
623
    var rawTransaction = new bitcoin.TransactionBuilder(this.network);
624
    if (this.settings.bitcoinCash) {
625
        rawTransaction.enableBitcoinCash();
626
    }
627
    var inputs = [];
628
    _.each(this.sweepData['utxos'], function(data, address) {
629
        _.each(data.utxos, function(utxo) {
630
            rawTransaction.addInput(utxo['hash'], utxo['index']);
631
            inputs.push({
632
                txid:         utxo['hash'],
633
                vout:         utxo['index'],
634
                scriptPubKey: utxo['script_hex'],
635
                value:        utxo['value'],
636
                address:      address,
637
                path:         data['path'],
638
                redeemScript: data['redeem'],
639
                witnessScript: data['witness']
640
            });
641
        });
642
    });
643
    if (!rawTransaction) {
644
        throw new Error("Failed to create raw transaction");
645
    }
646
647
    var sendAmount = self.sweepData['balance'];
648
    var outputIdx = rawTransaction.addOutput(destinationAddress, sendAmount);
649
650
    if (typeof fee === "undefined" || fee === null) {
651
        //estimate the fee and reduce it's value from the output
652
        if (deferred) {
653
            deferred.notify({
654
                message: "estimating transaction fee, based on " + blocktrail.toBTC(feePerKb) + " BTC/kb"
655
            });
656
        }
657
658
        var toHexString = function(byteArray) {
659
            return Array.prototype.map.call(byteArray, function(byte) {
660
                return ('0' + (byte & 0xFF).toString(16)).slice(-2);
661
            }).join('');
662
        };
663
664
        var calcUtxos = inputs.map(function(input) {
665
            var rs = (typeof input.redeemScript === "string" || !input.redeemScript)
666
                ? input.redeemScript : toHexString(input.redeemScript);
667
            var ws = (typeof input.witnessScript === "string" || !input.witnessScript)
668
                ? input.witnessScript : toHexString(input.witnessScript);
669
670
            return {
671
                txid: input.txid,
672
                vout: input.vout,
673
                address: input.address,
674
                scriptpubkey_hex: input.scriptPubKey,
675
                redeem_script: rs,
676
                witness_script: ws,
677
                path: input.path,
678
                value: input.value
679
            };
680
        });
681
        fee = walletSDK.estimateVsizeFee(rawTransaction.tx, calcUtxos, feePerKb);
682
    }
683
    rawTransaction.tx.outs[outputIdx].value -= fee;
684
685
    //sign and return the raw transaction
686
    if (deferred) {
687
        deferred.notify({
688
            message: "signing transaction"
689
        });
690
    }
691
    return this.signTransaction(rawTransaction, inputs);
692
};
693
694
WalletSweeper.prototype.signTransaction = function(rawTransaction, inputs) {
695
    var self = this;
696
    if (this.settings.logging) {
697
        console.log("Signing transaction");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
698
    }
699
700
    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
701
    if (this.settings.bitcoinCash) {
702
        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
703
    }
704
705
    //sign the transaction with the private key for each input
706
    _.each(inputs, function(input, index) {
707
        //create private keys for signing
708
        var primaryPrivKey =  walletSDK.deriveByPath(self.primaryPrivateKey, input['path'].replace("M", "m"), "m").keyPair;
709
        rawTransaction.sign(index, primaryPrivKey, input['redeemScript'], sigHash, input['value'], input['witnessScript']);
710
711
        if (self.backupPrivateKey) {
712
            var backupPrivKey = walletSDK.deriveByPath(self.backupPrivateKey, input['path'].replace("'", "").replace("M", "m"), "m").keyPair;
713
            rawTransaction.sign(index, backupPrivKey, input['redeemScript'], sigHash, input['value'], input['witnessScript']);
714
        }
715
    });
716
717
    if (self.backupPrivateKey) {
718
        return rawTransaction.build().toHex();
719
    } else {
720
        return rawTransaction.buildIncomplete().toHex();
721
    }
722
};
723
724
module.exports = WalletSweeper;
725