blocktrail-sdk
Version:
BlockTrail's Developer Friendly API binding for NodeJS
767 lines (679 loc) • 33.6 kB
JavaScript
var UnspentOutputFinder = require('./unspent_output_finder');
var bitcoin = require('bitcoinjs-lib');
var bip39 = require("bip39");
var CryptoJS = require('crypto-js');
var blocktrail = require('./blocktrail');
var EncryptionMnemonic = require('./encryption_mnemonic');
var Encryption = require('./encryption');
var walletSDK = require('./wallet');
var _ = require('lodash');
var q = require('q');
var async = require('async');
/**
*
* @param backupData
* @param bitcoinDataClient
* @param options
* @constructor
*/
var WalletSweeper = function(backupData, bitcoinDataClient, options) {
/* jshint -W071, -W074 */
var self = this;
this.defaultSettings = {
network: 'btc',
testnet: false,
regtest: false,
logging: false,
bitcoinCash: false,
cashAddr: false,
bitcoinGold: false,
sweepBatchSize: 200
};
this.settings = _.merge({}, this.defaultSettings, options);
this.bitcoinDataClient = bitcoinDataClient;
this.utxoFinder = new UnspentOutputFinder(bitcoinDataClient, this.settings);
this.sweepData = null;
// set the bitcoinlib network
if (typeof options.network === "object") {
this.network = options.network;
} else {
this.network = this.getBitcoinNetwork(this.settings.network, this.settings.testnet, this.settings.regtest);
}
backupData.walletVersion = backupData.walletVersion || 2; //default to version 2 wallets
var usePassword = false;
// validate backup data, cleanup input, and prepare seeds
if (!Array.isArray(backupData.blocktrailKeys)) {
throw new Error('blocktrail pub keys are required (must be type Array)');
}
switch (backupData.walletVersion) {
case 1:
if (typeof backupData.primaryMnemonic === "undefined" || !backupData.primaryMnemonic) {
throw new Error('missing primary mnemonic for version 1 wallet');
}
if (typeof backupData.backupMnemonic === "undefined" || !backupData.backupMnemonic) {
throw new Error('missing backup mnemonic for version 1 wallet');
}
if (typeof backupData.primaryPassphrase === "undefined") {
throw new Error('missing primary passphrase for version 1 wallet');
}
// cleanup copy paste errors from mnemonics
backupData.primaryMnemonic = backupData.primaryMnemonic.trim()
.replace(new RegExp("\r\n", 'g'), " ")
.replace(new RegExp("\n", 'g'), " ")
.replace(/\s+/g, " ");
backupData.backupMnemonic = backupData.backupMnemonic.trim()
.replace(new RegExp("\r\n", 'g'), " ")
.replace(new RegExp("\n", 'g'), " ")
.replace(/\s+/g, " ");
break;
case 2:
case 3:
if (typeof backupData.encryptedPrimaryMnemonic === "undefined" || !backupData.encryptedPrimaryMnemonic) {
throw new Error('missing encrypted primary seed for version 2 wallet');
}
if (typeof backupData.backupMnemonic === "undefined" || (!backupData.backupMnemonic && backupData.backupMnemonic !== false)) {
throw new Error('missing backup seed for version 2 wallet');
}
//can either recover with password and password encrypted secret, or with encrypted recovery secret and a decryption key
usePassword = typeof backupData.password !== "undefined" && backupData.password !== null;
if (usePassword) {
if (typeof backupData.passwordEncryptedSecretMnemonic === "undefined" || !backupData.passwordEncryptedSecretMnemonic) {
throw new Error('missing password encrypted secret for version 2 wallet');
}
if (typeof backupData.password === "undefined") {
throw new Error('missing primary passphrase for version 2 wallet');
}
} else {
if (typeof backupData.encryptedRecoverySecretMnemonic === "undefined" || !backupData.encryptedRecoverySecretMnemonic) {
throw new Error('missing encrypted recovery secret for version 2 wallet (recovery without password)');
}
if (!backupData.recoverySecretDecryptionKey) {
throw new Error('missing recovery secret decryption key for version 2 wallet (recovery without password)');
}
}
// cleanup copy paste errors from mnemonics
backupData.encryptedPrimaryMnemonic = backupData.encryptedPrimaryMnemonic.trim()
.replace(new RegExp("\r\n", 'g'), " ")
.replace(new RegExp("\n", 'g'), " ")
.replace(/\s+/g, " ");
backupData.backupMnemonic = (backupData.backupMnemonic || "").trim()
.replace(new RegExp("\r\n", 'g'), " ")
.replace(new RegExp("\n", 'g'), " ")
.replace(/\s+/g, " ");
if (backupData.recoverySecretDecryptionKey) {
backupData.recoverySecretDecryptionKey = backupData.recoverySecretDecryptionKey.trim()
.replace(new RegExp("\r\n", 'g'), " ")
.replace(new RegExp("\n", 'g'), " ")
.replace(/\s+/g, " ");
}
if (usePassword) {
backupData.passwordEncryptedSecretMnemonic = backupData.passwordEncryptedSecretMnemonic.trim()
.replace(new RegExp("\r\n", 'g'), " ").replace(new RegExp("\n", 'g'), " ").replace(/\s+/g, " ");
} else {
backupData.encryptedRecoverySecretMnemonic = backupData.encryptedRecoverySecretMnemonic.trim()
.replace(new RegExp("\r\n", 'g'), " ").replace(new RegExp("\n", 'g'), " ").replace(/\s+/g, " ");
}
break;
default:
throw new Error('Wrong version [' + backupData.walletVersion + ']');
}
// create BIP32 HDNodes for the Blocktrail public keys
this.blocktrailPublicKeys = {};
_.each(backupData.blocktrailKeys, function(blocktrailKey) {
self.blocktrailPublicKeys[blocktrailKey['keyIndex']] = bitcoin.HDNode.fromBase58(blocktrailKey['pubkey'], self.network);
});
// convert the primary and backup mnemonics to seeds (using BIP39)
var primarySeed, backupSeed, secret;
switch (backupData.walletVersion) {
case 1:
primarySeed = bip39.mnemonicToSeed(backupData.primaryMnemonic, backupData.primaryPassphrase);
backupSeed = bip39.mnemonicToSeed(backupData.backupMnemonic, "");
break;
case 2:
// convert mnemonics to hex (bip39) and then base64 for decryption
backupData.encryptedPrimaryMnemonic = blocktrail.convert(bip39.mnemonicToEntropy(backupData.encryptedPrimaryMnemonic), 'hex', 'base64');
if (usePassword) {
backupData.passwordEncryptedSecretMnemonic = blocktrail.convert(
bip39.mnemonicToEntropy(backupData.passwordEncryptedSecretMnemonic), 'hex', 'base64');
} else {
backupData.encryptedRecoverySecretMnemonic = blocktrail.convert(
bip39.mnemonicToEntropy(backupData.encryptedRecoverySecretMnemonic), 'hex', 'base64');
}
// decrypt encryption secret
if (usePassword) {
secret = CryptoJS.AES.decrypt(backupData.passwordEncryptedSecretMnemonic, backupData.password).toString(CryptoJS.enc.Utf8);
} else {
secret = CryptoJS.AES.decrypt(backupData.encryptedRecoverySecretMnemonic, backupData.recoverySecretDecryptionKey).toString(CryptoJS.enc.Utf8);
}
if (!secret) {
throw new Error("Could not decrypt secret with " + (usePassword ? "password" : "decryption key"));
}
// now finally decrypt the primary seed and convert to buffer (along with backup seed)
primarySeed = new Buffer(CryptoJS.AES.decrypt(backupData.encryptedPrimaryMnemonic, secret).toString(CryptoJS.enc.Utf8), 'base64');
if (backupData.backupMnemonic) {
backupSeed = new Buffer(bip39.mnemonicToEntropy(backupData.backupMnemonic), 'hex');
}
break;
case 3:
// convert mnemonics to hex (bip39) and then base64 for decryption
backupData.encryptedPrimaryMnemonic = EncryptionMnemonic.decode(backupData.encryptedPrimaryMnemonic);
if (usePassword) {
backupData.passwordEncryptedSecretMnemonic = EncryptionMnemonic.decode(backupData.passwordEncryptedSecretMnemonic);
} else {
backupData.encryptedRecoverySecretMnemonic = EncryptionMnemonic.decode(backupData.encryptedRecoverySecretMnemonic);
}
// decrypt encryption secret
if (usePassword) {
secret = Encryption.decrypt(backupData.passwordEncryptedSecretMnemonic, new Buffer(backupData.password));
} else {
secret = Encryption.decrypt(backupData.encryptedRecoverySecretMnemonic, new Buffer(backupData.recoverySecretDecryptionKey, 'hex'));
}
if (!secret) {
throw new Error("Could not decrypt secret with " + (usePassword ? "password" : "decryption key"));
}
// now finally decrypt the primary seed and convert to buffer (along with backup seed)
primarySeed = Encryption.decrypt(backupData.encryptedPrimaryMnemonic, secret);
if (backupData.backupMnemonic) {
backupSeed = new Buffer(bip39.mnemonicToEntropy(backupData.backupMnemonic), 'hex');
}
break;
default:
throw new Error('Wrong version [' + backupData.walletVersion + ']');
}
// convert the primary and backup seeds to private keys (using BIP32)
this.primaryPrivateKey = bitcoin.HDNode.fromSeedBuffer(primarySeed, this.network);
if (backupSeed) {
this.backupPrivateKey = bitcoin.HDNode.fromSeedBuffer(backupSeed, this.network);
this.backupPublicKey = this.backupPrivateKey.neutered();
} else {
this.backupPrivateKey = false;
this.backupPublicKey = bitcoin.HDNode.fromBase58(backupData.backupPublicKey, this.network);
}
if (this.settings.logging) {
console.log('using password method: ' + usePassword);
console.log("Primary Prv Key: " + this.primaryPrivateKey.toBase58());
console.log("Primary Pub Key: " + this.primaryPrivateKey.neutered().toBase58());
console.log("Backup Prv Key: " + (this.backupPrivateKey ? this.backupPrivateKey.toBase58() : null));
console.log("Backup Pub Key: " + this.backupPublicKey.toBase58());
}
};
/**
* returns an appropriate bitcoin-js lib network
*
* @param network
* @param testnet
* @param regtest
* @returns {*[]}
*/
WalletSweeper.prototype.getBitcoinNetwork = function(network, testnet, regtest) {
switch (network.toLowerCase()) {
case 'btc':
case 'bitcoin':
if (regtest) {
return bitcoin.networks.regtest;
} else if (testnet) {
return bitcoin.networks.testnet;
} else {
return bitcoin.networks.bitcoin;
}
break;
case 'tbtc':
case 'bitcoin-testnet':
return bitcoin.networks.testnet;
case 'bcc':
case 'bch':
case 'bitcoincash':
if (regtest) {
return bitcoin.networks.bitcoincashregtest;
} else if (testnet) {
return bitcoin.networks.bitcoincashtestnet;
} else {
return bitcoin.networks.bitcoincash;
}
case 'btg':
case 'bitcoingold':
if (regtest) {
return bitcoin.networks.regtest;
} else if (testnet) {
return bitcoin.networks.testnet;
} else {
return bitcoin.networks.bitcoin;
}
default:
throw new Error("Unknown network " + network);
}
};
/**
* gets the blocktrail pub key for the given path from the stored array of pub keys
*
* @param path
* @returns {boolean}
*/
WalletSweeper.prototype.getBlocktrailPublicKey = function(path) {
path = path.replace("m", "M");
var keyIndex = path.split("/")[1].replace("'", "");
if (!this.blocktrailPublicKeys[keyIndex]) {
throw new Error("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
}
return this.blocktrailPublicKeys[keyIndex];
};
/**
* generate multisig address and redeem script for given path
*
* @param path
* @returns {{address, redeem: *, witness: *}}
*/
WalletSweeper.prototype.createAddress = function(path) {
//ensure a public path is used
path = path.replace("m", "M");
var keyIndex = path.split("/")[1].replace("'", "");
var scriptType = parseInt(path.split("/")[2]);
//derive the primary pub key directly from the primary priv key
var primaryPubKey = walletSDK.deriveByPath(this.primaryPrivateKey, path, "m");
//derive the backup pub key directly from the backup priv key (unharden path)
var backupPubKey = walletSDK.deriveByPath(this.backupPublicKey, path.replace("'", ""), "M");
//derive a pub key for this path from the blocktrail pub key
var blocktrailPubKey = walletSDK.deriveByPath(this.getBlocktrailPublicKey(path), path, "M/" + keyIndex + "'");
//sort the keys and generate a multisig redeem script and address
var multisigKeys = walletSDK.sortMultiSigKeys([
primaryPubKey.keyPair.getPublicKeyBuffer(),
backupPubKey.keyPair.getPublicKeyBuffer(),
blocktrailPubKey.keyPair.getPublicKeyBuffer()
]);
var multisig = bitcoin.script.multisig.output.encode(2, multisigKeys);
var redeemScript, witnessScript;
if (!(this.network.cashAddrPrefix || this.settings.bitcoinCash) && scriptType === walletSDK.CHAIN_BTC_SEGWIT) {
witnessScript = multisig;
redeemScript = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(witnessScript));
} else {
witnessScript = null;
redeemScript = multisig;
}
var scriptHash = bitcoin.crypto.hash160(redeemScript);
var scriptPubKey = bitcoin.script.scriptHash.output.encode(scriptHash);
var network = this.network;
if (typeof this.network !== "undefined") {
network = this.network;
}
var address = bitcoin.address.fromOutputScript(scriptPubKey, network, !!this.settings.bitcoinCash && !!this.settings.cashAddr);
// Insight nodes want nothing to do with 'bitcoin:' or 'bitcoincash:' prefixes
address = address.replace('bitcoin:', '').replace('bitcoincash:', '');
//@todo return as buffers
return {address: address.toString(), redeem: redeemScript, witness: witnessScript};
};
/**
* create a batch of multisig addresses
*
* @param start
* @param count
* @param keyIndex
* @param chain
* @returns {{}}
*/
WalletSweeper.prototype.createBatchAddresses = function(start, count, keyIndex, chain) {
var self = this;
var addresses = {};
return q.all(_.range(0, count).map(function(i) {
//create a path subsequent address
var path = "M/" + keyIndex + "'/" + chain + "/" + (start + i);
var multisig = self.createAddress(path);
addresses[multisig['address']] = {
redeem: multisig['redeem'],
witness: multisig['witness'],
path: path
};
})).then(function() {
return addresses;
});
};
WalletSweeper.prototype.discoverWalletFunds = function(increment, options, cb) {
var self = this;
var totalBalance = 0;
var totalUTXOs = 0;
var totalAddressesGenerated = 0;
if (typeof increment === "function") {
cb = increment;
increment = null;
} else if (typeof options === "function") {
cb = options;
options = {};
}
if(options && !(typeof options === "object")) {
console.warn("Wallet Sweeper discovery options is not an object, ignoring");
options = {};
}
var addressUTXOs = {}; //addresses and their utxos, paths and redeem scripts
if (typeof increment === "undefined" || !increment) {
increment = this.settings.sweepBatchSize;
}
var deferred = q.defer();
deferred.promise.nodeify(cb);
var checkChain;
if (
this.network.cashAddrPrefix ||
this.settings.bitcoinCash
) {
checkChain = [0, 1];
} else {
checkChain = [0, 1, 2];
}
async.nextTick(function() {
//for each blocktrail pub key, do fund discovery on batches of addresses
async.eachSeries(Object.keys(self.blocktrailPublicKeys), function(keyIndex, done) {
async.eachSeries(checkChain, function(chain, done) {
var i = 0;
var hasTransactions = false;
async.doWhilst(function(done) {
//do
if (self.settings.logging) {
console.log("generating addresses " + i + " -> " + (i + increment) + " using blocktrail key index: " + keyIndex + ", chain: " + chain);
}
deferred.notify({
message: "generating addresses " + i + " -> " + (i + increment) + "",
increment: increment,
btPubKeyIndex: keyIndex,
chain: chain,
//addresses: [],
totalAddresses: totalAddressesGenerated,
addressUTXOs: addressUTXOs,
totalUTXOs: totalUTXOs,
totalBalance: totalBalance
});
async.nextTick(function() {
self.createBatchAddresses(i, increment, keyIndex, chain)
.then(function(batch) {
totalAddressesGenerated += Object.keys(batch).length;
if (self.settings.logging) {
console.log("starting fund discovery for " + increment + " addresses...");
}
deferred.notify({
message: "starting fund discovery for " + increment + " addresses",
increment: increment,
btPubKeyIndex: keyIndex,
//addresses: addresses,
totalAddresses: totalAddressesGenerated,
addressUTXOs: addressUTXOs,
totalUTXOs: totalUTXOs,
totalBalance: totalBalance
});
//get the unspent outputs for this batch of addresses
return self.bitcoinDataClient.batchAddressHasTransactions(_.keys(batch)).then(function(_hasTransactions) {
hasTransactions = _hasTransactions;
if (self.settings.logging) {
console.log("batch " + (hasTransactions ? "has" : "does not have") + " transactions...");
}
return q.when(hasTransactions)
.then(function(hasTransactions) {
if (!hasTransactions) {
return;
}
//get the unspent outputs for this batch of addresses
return self.utxoFinder.getUTXOs(_.keys(batch)).then(function(utxos) {
if (options.excludeZeroConf) {
// Do not evaluate 0-confirmation UTXOs
// This would include double spends and other things Insight happily accepts
// (and keeps in mempool - even when the parent UTXO gets spent otherwise)
for (var address in utxos) {
if (utxos.hasOwnProperty(address) && Array.isArray(utxos[address])) {
var utxosPerAddress = utxos[address];
// Iterate over utxos per address
for (var idx = 0; idx < utxosPerAddress.length; idx++) {
if (utxosPerAddress[idx] &&
'confirmations' in utxosPerAddress[idx]
&& utxosPerAddress[idx]['confirmations'] === 0) {
// Delete if unconfirmed
delete utxos[address][idx];
utxos[address].length--;
if (utxos[address].length <= 0) {
delete utxos[address];
}
}
}
}
}
}
// save the address utxos, along with relevant path and redeem script
_.each(utxos, function(outputs, address) {
var witnessScript = null;
if (typeof batch[address]['witness'] !== 'undefined') {
witnessScript = batch[address]['witness'];
}
addressUTXOs[address] = {
path: batch[address]['path'],
redeem: batch[address]['redeem'],
witness: witnessScript,
utxos: outputs
};
totalUTXOs += outputs.length;
//add up the total utxo value for all addresses
totalBalance = _.reduce(outputs, function(carry, output) {
return carry + output['value'];
}, totalBalance);
if (self.settings.logging) {
console.log("found " + outputs.length + " unspent outputs for address: " + address);
}
});
deferred.notify({
message: "discovering funds",
increment: increment,
btPubKeyIndex: keyIndex,
totalAddresses: totalAddressesGenerated,
addressUTXOs: addressUTXOs,
totalUTXOs: totalUTXOs,
totalBalance: totalBalance
});
});
})
;
});
})
.then(
function() {
//ready for the next batch
i += increment;
async.nextTick(done);
},
function(err) {
done(err);
}
)
;
});
}, function() {
//while
return hasTransactions;
}, function(err) {
//all done
if (err) {
console.log("batch complete, but with errors", err.message);
deferred.notify({
message: "batch complete, but with errors: " + err.message,
error: err,
increment: increment,
btPubKeyIndex: keyIndex,
totalAddresses: totalAddressesGenerated,
addressUTXOs: addressUTXOs,
totalUTXOs: totalUTXOs,
totalBalance: totalBalance
});
}
//ready for next Blocktrail pub key
async.nextTick(done);
});
}, function(err) {
done(err);
});
}, function(err) {
//callback
if (err) {
//perhaps we should also reject the promise, and stop everything?
if (self.settings.logging) {
console.log("error encountered when discovering funds", err);
}
}
if (self.settings.logging) {
console.log("finished fund discovery: " + totalBalance + " Satoshi (in " + totalUTXOs + " outputs) " +
"found when searching " + totalAddressesGenerated + " addresses");
}
self.sweepData = {
utxos: addressUTXOs,
count: totalUTXOs,
balance: totalBalance,
addressesSearched: totalAddressesGenerated
};
//resolve the promise
deferred.resolve(self.sweepData);
});
});
return deferred.promise;
};
WalletSweeper.prototype.sweepWallet = function(destinationAddress, cb) {
var self = this;
var deferred = q.defer();
deferred.promise.nodeify(cb);
if (self.settings.logging) {
console.log("starting wallet sweeping to address " + destinationAddress);
}
q.when(true)
.then(function() {
if (!self.sweepData) {
//do wallet fund discovery
return self.discoverWalletFunds()
.progress(function(progress) {
deferred.notify(progress);
});
}
})
.then(function() {
return self.bitcoinDataClient.estimateFee();
})
.then(function(feePerKb) {
// Insight reports 1000 sat/kByte, but this is too low
if (self.settings.bitcoinCash && feePerKb < 5000) {
feePerKb = 5000;
}
if (self.sweepData['balance'] === 0) {
//no funds found
deferred.reject("No funds found after searching through " + self.sweepData['addressesSearched'] + " addresses");
return deferred.promise;
}
//create and sign the transaction
return self.createTransaction(destinationAddress, null, feePerKb, deferred);
})
.then(function(r) {
deferred.resolve(r);
}, function(e) {
deferred.reject(e);
});
return deferred.promise;
};
/**
* creates a raw transaction from the sweep data
* @param destinationAddress the destination address for the transaction
* @param fee a specific transaction fee to use (optional: if null, fee will be estimated)
* @param feePerKb fee per kb (optional: if null, use default value)
* @param deferred a deferred promise object, used for giving progress updates (optional)
*/
WalletSweeper.prototype.createTransaction = function(destinationAddress, fee, feePerKb, deferred) {
var self = this;
if (this.settings.logging) {
console.log("Creating transaction to address destinationAddress");
}
if (deferred) {
deferred.notify({
message: "creating raw transaction to " + destinationAddress
});
}
// create raw transaction
var rawTransaction = new bitcoin.TransactionBuilder(this.network);
if (this.settings.bitcoinCash) {
rawTransaction.enableBitcoinCash();
} else if (this.settings.bitcoinGold) {
rawTransaction.enableBitcoinGold();
}
var inputs = [];
_.each(this.sweepData['utxos'], function(data, address) {
_.each(data.utxos, function(utxo) {
rawTransaction.addInput(utxo['hash'], utxo['index']);
inputs.push({
txid: utxo['hash'],
vout: utxo['index'],
scriptPubKey: utxo['script_hex'],
value: utxo['value'],
address: address,
path: data['path'],
redeemScript: data['redeem'],
witnessScript: data['witness']
});
});
});
if (!rawTransaction) {
throw new Error("Failed to create raw transaction");
}
var sendAmount = self.sweepData['balance'];
var outputIdx = rawTransaction.addOutput(destinationAddress, sendAmount);
if (typeof fee === "undefined" || fee === null) {
//estimate the fee and reduce it's value from the output
if (deferred) {
deferred.notify({
message: "estimating transaction fee, based on " + blocktrail.toBTC(feePerKb) + " BTC/kb"
});
}
var toHexString = function(byteArray) {
return Array.prototype.map.call(byteArray, function(byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
};
var calcUtxos = inputs.map(function(input) {
var rs = (typeof input.redeemScript === "string" || !input.redeemScript)
? input.redeemScript : toHexString(input.redeemScript);
var ws = (typeof input.witnessScript === "string" || !input.witnessScript)
? input.witnessScript : toHexString(input.witnessScript);
return {
txid: input.txid,
vout: input.vout,
address: input.address,
scriptpubkey_hex: input.scriptPubKey,
redeem_script: rs,
witness_script: ws,
path: input.path,
value: input.value
};
});
fee = walletSDK.estimateVsizeFee(rawTransaction.buildIncomplete(), calcUtxos, feePerKb);
}
rawTransaction.tx.outs[outputIdx].value -= fee;
//sign and return the raw transaction
if (deferred) {
deferred.notify({
message: "signing transaction"
});
}
return this.signTransaction(rawTransaction, inputs);
};
WalletSweeper.prototype.signTransaction = function(rawTransaction, inputs) {
var self = this;
if (this.settings.logging) {
console.log("Signing transaction");
}
var sigHash = bitcoin.Transaction.SIGHASH_ALL;
if (this.settings.bitcoinCash || this.settings.bitcoinGold) {
sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
}
//sign the transaction with the private key for each input
_.each(inputs, function(input, index) {
//create private keys for signing
var primaryPrivKey = walletSDK.deriveByPath(self.primaryPrivateKey, input['path'].replace("M", "m"), "m").keyPair;
rawTransaction.sign(index, primaryPrivKey, input['redeemScript'], sigHash, input['value'], input['witnessScript']);
if (self.backupPrivateKey) {
var backupPrivKey = walletSDK.deriveByPath(self.backupPrivateKey, input['path'].replace("'", "").replace("M", "m"), "m").keyPair;
rawTransaction.sign(index, backupPrivKey, input['redeemScript'], sigHash, input['value'], input['witnessScript']);
}
});
if (self.backupPrivateKey) {
return rawTransaction.build().toHex();
} else {
return rawTransaction.buildIncomplete().toHex();
}
};
module.exports = WalletSweeper;