blocktrail-sdk
Version:
BlockTrail's Developer Friendly API binding for NodeJS
1,384 lines (1,172 loc) • 67 kB
JavaScript
var _ = require('lodash');
var assert = require('assert');
var q = require('q');
var async = require('async');
var bitcoin = require('bitcoinjs-lib');
var bitcoinMessage = require('bitcoinjs-message');
var blocktrail = require('./blocktrail');
var CryptoJS = require('crypto-js');
var Encryption = require('./encryption');
var EncryptionMnemonic = require('./encryption_mnemonic');
var SizeEstimation = require('./size_estimation');
var bip39 = require('bip39');
var SignMode = {
SIGN: "sign",
DONT_SIGN: "dont_sign"
};
/**
*
* @param sdk APIClient SDK instance used to do requests
* @param identifier string identifier of the wallet
* @param walletVersion string
* @param primaryMnemonic string primary mnemonic
* @param encryptedPrimarySeed
* @param encryptedSecret
* @param primaryPublicKeys string primary mnemonic
* @param backupPublicKey string BIP32 master pubKey M/
* @param blocktrailPublicKeys array list of blocktrail pubKeys indexed by keyIndex
* @param keyIndex int key index to use
* @param segwit int segwit toggle from server
* @param testnet bool testnet
* @param regtest bool regtest
* @param checksum string
* @param upgradeToKeyIndex int
* @param useNewCashAddr bool flag to opt in to bitcoin cash cashaddr's
* @param bypassNewAddressCheck bool flag to indicate if wallet should/shouldn't derive new address locally to verify api
* @constructor
* @internal
*/
var Wallet = function(
sdk,
identifier,
walletVersion,
primaryMnemonic,
encryptedPrimarySeed,
encryptedSecret,
primaryPublicKeys,
backupPublicKey,
blocktrailPublicKeys,
keyIndex,
segwit,
testnet,
regtest,
checksum,
upgradeToKeyIndex,
useNewCashAddr,
bypassNewAddressCheck
) {
/* jshint -W071 */
var self = this;
self.sdk = sdk;
self.identifier = identifier;
self.walletVersion = walletVersion;
self.locked = true;
self.bypassNewAddressCheck = !!bypassNewAddressCheck;
self.bitcoinCash = self.sdk.bitcoinCash;
self.segwit = !!segwit;
self.useNewCashAddr = !!useNewCashAddr;
assert(!self.segwit || !self.bitcoinCash);
self.testnet = testnet;
self.regtest = regtest;
if (self.bitcoinCash) {
if (self.regtest) {
self.network = bitcoin.networks.bitcoincashregtest;
} else if (self.testnet) {
self.network = bitcoin.networks.bitcoincashtestnet;
} else {
self.network = bitcoin.networks.bitcoincash;
}
} else {
if (self.regtest) {
self.network = bitcoin.networks.regtest;
} else if (self.testnet) {
self.network = bitcoin.networks.testnet;
} else {
self.network = bitcoin.networks.bitcoin;
}
}
assert(backupPublicKey instanceof bitcoin.HDNode);
assert(_.every(primaryPublicKeys, function(primaryPublicKey) { return primaryPublicKey instanceof bitcoin.HDNode; }));
assert(_.every(blocktrailPublicKeys, function(blocktrailPublicKey) { return blocktrailPublicKey instanceof bitcoin.HDNode; }));
// v1
self.primaryMnemonic = primaryMnemonic;
// v2 & v3
self.encryptedPrimarySeed = encryptedPrimarySeed;
self.encryptedSecret = encryptedSecret;
self.primaryPrivateKey = null;
self.backupPrivateKey = null;
self.backupPublicKey = backupPublicKey;
self.blocktrailPublicKeys = blocktrailPublicKeys;
self.primaryPublicKeys = primaryPublicKeys;
self.keyIndex = keyIndex;
if (!self.bitcoinCash) {
if (self.segwit) {
self.chain = Wallet.CHAIN_BTC_SEGWIT;
self.changeChain = Wallet.CHAIN_BTC_SEGWIT;
} else {
self.chain = Wallet.CHAIN_BTC_DEFAULT;
self.changeChain = Wallet.CHAIN_BTC_DEFAULT;
}
} else {
self.chain = Wallet.CHAIN_BCC_DEFAULT;
self.changeChain = Wallet.CHAIN_BCC_DEFAULT;
}
self.checksum = checksum;
self.upgradeToKeyIndex = upgradeToKeyIndex;
self.secret = null;
self.seedHex = null;
};
Wallet.WALLET_VERSION_V1 = 'v1';
Wallet.WALLET_VERSION_V2 = 'v2';
Wallet.WALLET_VERSION_V3 = 'v3';
Wallet.WALLET_ENTROPY_BITS = 256;
Wallet.OP_RETURN = 'opreturn';
Wallet.DATA = Wallet.OP_RETURN; // alias
Wallet.PAY_PROGRESS_START = 0;
Wallet.PAY_PROGRESS_COIN_SELECTION = 10;
Wallet.PAY_PROGRESS_CHANGE_ADDRESS = 20;
Wallet.PAY_PROGRESS_SIGN = 30;
Wallet.PAY_PROGRESS_SEND = 40;
Wallet.PAY_PROGRESS_DONE = 100;
Wallet.CHAIN_BTC_DEFAULT = 0;
Wallet.CHAIN_BTC_SEGWIT = 2;
Wallet.CHAIN_BCC_DEFAULT = 1;
Wallet.FEE_STRATEGY_FORCE_FEE = blocktrail.FEE_STRATEGY_FORCE_FEE;
Wallet.FEE_STRATEGY_BASE_FEE = blocktrail.FEE_STRATEGY_BASE_FEE;
Wallet.FEE_STRATEGY_HIGH_PRIORITY = blocktrail.FEE_STRATEGY_HIGH_PRIORITY;
Wallet.FEE_STRATEGY_OPTIMAL = blocktrail.FEE_STRATEGY_OPTIMAL;
Wallet.FEE_STRATEGY_LOW_PRIORITY = blocktrail.FEE_STRATEGY_LOW_PRIORITY;
Wallet.FEE_STRATEGY_MIN_RELAY_FEE = blocktrail.FEE_STRATEGY_MIN_RELAY_FEE;
Wallet.prototype.isSegwit = function() {
return !!this.segwit;
};
Wallet.prototype.unlock = function(options, cb) {
var self = this;
var deferred = q.defer();
deferred.promise.nodeify(cb);
// avoid modifying passed options
options = _.merge({}, options);
q.fcall(function() {
switch (self.walletVersion) {
case Wallet.WALLET_VERSION_V1:
return self.unlockV1(options);
case Wallet.WALLET_VERSION_V2:
return self.unlockV2(options);
case Wallet.WALLET_VERSION_V3:
return self.unlockV3(options);
default:
return q.reject(new blocktrail.WalletInitError("Invalid wallet version"));
}
}).then(
function(primaryPrivateKey) {
self.primaryPrivateKey = primaryPrivateKey;
// create a checksum of our private key which we'll later use to verify we used the right password
var checksum = self.primaryPrivateKey.getAddress();
// check if we've used the right passphrase
if (checksum !== self.checksum) {
throw new blocktrail.WalletChecksumError("Generated checksum [" + checksum + "] does not match " +
"[" + self.checksum + "], most likely due to incorrect password");
}
self.locked = false;
// if the response suggests we should upgrade to a different blocktrail cosigning key then we should
if (typeof self.upgradeToKeyIndex !== "undefined" && self.upgradeToKeyIndex !== null) {
return self.upgradeKeyIndex(self.upgradeToKeyIndex);
}
}
).then(
function(r) {
deferred.resolve(r);
},
function(e) {
deferred.reject(e);
}
);
return deferred.promise;
};
Wallet.prototype.unlockV1 = function(options) {
var self = this;
options.primaryMnemonic = typeof options.primaryMnemonic !== "undefined" ? options.primaryMnemonic : self.primaryMnemonic;
options.secretMnemonic = typeof options.secretMnemonic !== "undefined" ? options.secretMnemonic : self.secretMnemonic;
return self.sdk.resolvePrimaryPrivateKeyFromOptions(options)
.then(function(options) {
self.primarySeed = options.primarySeed;
return options.primaryPrivateKey;
});
};
Wallet.prototype.unlockV2 = function(options, cb) {
var self = this;
var deferred = q.defer();
deferred.promise.nodeify(cb);
deferred.resolve(q.fcall(function() {
/* jshint -W071, -W074 */
options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
if (options.secret) {
self.secret = options.secret;
}
if (options.primaryPrivateKey) {
throw new blocktrail.WalletDecryptError("specifying primaryPrivateKey has been deprecated");
}
if (options.primarySeed) {
self.primarySeed = options.primarySeed;
} else if (options.secret) {
try {
self.primarySeed = new Buffer(
CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
if (!self.primarySeed.length) {
throw new Error();
}
} catch (e) {
throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
}
} else {
// avoid conflicting options
if (options.passphrase && options.password) {
throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
}
// normalize passphrase/password
options.passphrase = options.passphrase || options.password;
try {
self.secret = CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedSecret), options.passphrase).toString(CryptoJS.enc.Utf8);
if (!self.secret.length) {
throw new Error();
}
} catch (e) {
throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
}
try {
self.primarySeed = new Buffer(
CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
if (!self.primarySeed.length) {
throw new Error();
}
} catch (e) {
throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
}
}
return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
}));
return deferred.promise;
};
Wallet.prototype.unlockV3 = function(options, cb) {
var self = this;
var deferred = q.defer();
deferred.promise.nodeify(cb);
deferred.resolve(q.fcall(function() {
return q.when()
.then(function() {
/* jshint -W071, -W074 */
options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
if (options.secret) {
self.secret = options.secret;
}
if (options.primaryPrivateKey) {
throw new blocktrail.WalletInitError("specifying primaryPrivateKey has been deprecated");
}
if (options.primarySeed) {
self.primarySeed = options.primarySeed;
} else if (options.secret) {
return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
.then(function(primarySeed) {
self.primarySeed = primarySeed;
}, function() {
throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
});
} else {
// avoid conflicting options
if (options.passphrase && options.password) {
throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
}
// normalize passphrase/password
options.passphrase = options.passphrase || options.password;
delete options.password;
return self.sdk.promisedDecrypt(new Buffer(options.encryptedSecret, 'base64'), new Buffer(options.passphrase))
.then(function(secret) {
self.secret = secret;
}, function() {
throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
})
.then(function() {
return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
.then(function(primarySeed) {
self.primarySeed = primarySeed;
}, function() {
throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
});
});
}
})
.then(function() {
return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
})
;
}));
return deferred.promise;
};
Wallet.prototype.lock = function() {
var self = this;
self.secret = null;
self.primarySeed = null;
self.primaryPrivateKey = null;
self.backupPrivateKey = null;
self.locked = true;
};
/**
* upgrade wallet to V3 encryption scheme
*
* @param passphrase is required again to reencrypt the data, important that it's the correct password!!!
* @param cb
* @returns {promise}
*/
Wallet.prototype.upgradeToV3 = function(passphrase, cb) {
var self = this;
var deferred = q.defer();
deferred.promise.nodeify(cb);
q.when(true)
.then(function() {
if (self.locked) {
throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade");
}
if (self.walletVersion === Wallet.WALLET_VERSION_V3) {
throw new blocktrail.WalletUpgradeError("Wallet is already V3");
} else if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
return self._upgradeV2ToV3(passphrase, deferred.notify.bind(deferred));
} else if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
return self._upgradeV1ToV3(passphrase, deferred.notify.bind(deferred));
}
})
.then(function(r) { deferred.resolve(r); }, function(e) { deferred.reject(e); });
return deferred.promise;
};
Wallet.prototype._upgradeV2ToV3 = function(passphrase, notify) {
var self = this;
return q.when(true)
.then(function() {
var options = {
storeDataOnServer: true,
passphrase: passphrase,
primarySeed: self.primarySeed,
recoverySecret: false // don't create new recovery secret, V2 already has ones
};
return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
.then(function(options) {
return self.sdk.updateWallet(self.identifier, {
encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
encrypted_secret: options.encryptedSecret.toString('base64'),
wallet_version: Wallet.WALLET_VERSION_V3
}).then(function() {
self.secret = options.secret;
self.encryptedPrimarySeed = options.encryptedPrimarySeed;
self.encryptedSecret = options.encryptedSecret;
self.walletVersion = Wallet.WALLET_VERSION_V3;
return self;
});
});
});
};
Wallet.prototype._upgradeV1ToV3 = function(passphrase, notify) {
var self = this;
return q.when(true)
.then(function() {
var options = {
storeDataOnServer: true,
passphrase: passphrase,
primarySeed: self.primarySeed
};
return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
.then(function(options) {
// store recoveryEncryptedSecret for printing on backup sheet
self.recoveryEncryptedSecret = options.recoveryEncryptedSecret;
return self.sdk.updateWallet(self.identifier, {
primary_mnemonic: '',
encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
encrypted_secret: options.encryptedSecret.toString('base64'),
recovery_secret: options.recoverySecret.toString('hex'),
wallet_version: Wallet.WALLET_VERSION_V3
}).then(function() {
self.secret = options.secret;
self.encryptedPrimarySeed = options.encryptedPrimarySeed;
self.encryptedSecret = options.encryptedSecret;
self.walletVersion = Wallet.WALLET_VERSION_V3;
return self;
});
});
});
};
Wallet.prototype.doPasswordChange = function(newPassword) {
var self = this;
return q.when(null)
.then(function() {
if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
throw new blocktrail.WalletLockedError("Wallet version does not support password change!");
}
if (self.locked) {
throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to change password");
}
if (!self.secret) {
throw new blocktrail.WalletLockedError("No secret");
}
var newEncryptedSecret;
var newEncrypedWalletSecretMnemonic;
if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
newEncryptedSecret = CryptoJS.AES.encrypt(self.secret, newPassword).toString(CryptoJS.format.OpenSSL);
newEncrypedWalletSecretMnemonic = bip39.entropyToMnemonic(blocktrail.convert(newEncryptedSecret, 'base64', 'hex'));
} else {
if (typeof newPassword === "string") {
newPassword = new Buffer(newPassword);
} else {
if (!(newPassword instanceof Buffer)) {
throw new Error('New password must be provided as a string or a Buffer');
}
}
newEncryptedSecret = Encryption.encrypt(self.secret, newPassword);
newEncrypedWalletSecretMnemonic = EncryptionMnemonic.encode(newEncryptedSecret);
// It's a buffer, so convert it back to base64
newEncryptedSecret = newEncryptedSecret.toString('base64');
}
return [newEncryptedSecret, newEncrypedWalletSecretMnemonic];
});
};
Wallet.prototype.passwordChange = function(newPassword, cb) {
var self = this;
var deferred = q.defer();
deferred.promise.nodeify(cb);
q.fcall(function() {
return self.doPasswordChange(newPassword)
.then(function(r) {
var newEncryptedSecret = r[0];
var newEncrypedWalletSecretMnemonic = r[1];
return self.sdk.updateWallet(self.identifier, {encrypted_secret: newEncryptedSecret}).then(function() {
self.encryptedSecret = newEncryptedSecret;
// backupInfo
return {
encryptedSecret: newEncrypedWalletSecretMnemonic
};
});
})
.then(
function(r) {
deferred.resolve(r);
},
function(e) {
deferred.reject(e);
}
);
});
return deferred.promise;
};
/**
* get address for specified path
*
* @param path
* @returns string
*/
Wallet.prototype.getAddressByPath = function(path) {
return this.getWalletScriptByPath(path).address;
};
/**
* get redeemscript for specified path
*
* @param path
* @returns {Buffer}
*/
Wallet.prototype.getRedeemScriptByPath = function(path) {
return this.getWalletScriptByPath(path).redeemScript;
};
/**
* Generate scripts, and address.
* @param path
* @returns {{witnessScript: *, redeemScript: *, scriptPubKey, address: *}}
*/
Wallet.prototype.getWalletScriptByPath = function(path) {
var self = this;
// get derived primary key
var derivedPrimaryPublicKey = self.getPrimaryPublicKey(path);
// get derived blocktrail key
var derivedBlocktrailPublicKey = self.getBlocktrailPublicKey(path);
// derive the backup key
var derivedBackupPublicKey = Wallet.deriveByPath(self.backupPublicKey, path.replace("'", ""), "M");
// sort the pubkeys
var pubKeys = Wallet.sortMultiSigKeys([
derivedPrimaryPublicKey.keyPair.getPublicKeyBuffer(),
derivedBackupPublicKey.keyPair.getPublicKeyBuffer(),
derivedBlocktrailPublicKey.keyPair.getPublicKeyBuffer()
]);
var multisig = bitcoin.script.multisig.output.encode(2, pubKeys);
var scriptType = parseInt(path.split("/")[2]);
var ws, rs;
if (this.network !== "bitcoincash" && scriptType === Wallet.CHAIN_BTC_SEGWIT) {
ws = multisig;
rs = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(ws));
} else {
ws = null;
rs = multisig;
}
var spk = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(rs));
var addr = bitcoin.address.fromOutputScript(spk, this.network, self.useNewCashAddr);
return {
witnessScript: ws,
redeemScript: rs,
scriptPubKey: spk,
address: addr
};
};
/**
* get primary public key by path
* first level of the path is used as keyIndex to find the correct key in the dict
*
* @param path string
* @returns {bitcoin.HDNode}
*/
Wallet.prototype.getPrimaryPublicKey = function(path) {
var self = this;
path = path.replace("m", "M");
var keyIndex = path.split("/")[1].replace("'", "");
if (!self.primaryPublicKeys[keyIndex]) {
if (self.primaryPrivateKey) {
self.primaryPublicKeys[keyIndex] = Wallet.deriveByPath(self.primaryPrivateKey, "M/" + keyIndex + "'", "m");
} else {
throw new blocktrail.KeyPathError("Wallet.getPrimaryPublicKey keyIndex (" + keyIndex + ") is unknown to us");
}
}
var primaryPublicKey = self.primaryPublicKeys[keyIndex];
return Wallet.deriveByPath(primaryPublicKey, path, "M/" + keyIndex + "'");
};
/**
* get blocktrail public key by path
* first level of the path is used as keyIndex to find the correct key in the dict
*
* @param path string
* @returns {bitcoin.HDNode}
*/
Wallet.prototype.getBlocktrailPublicKey = function(path) {
var self = this;
path = path.replace("m", "M");
var keyIndex = path.split("/")[1].replace("'", "");
if (!self.blocktrailPublicKeys[keyIndex]) {
throw new blocktrail.KeyPathError("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
}
var blocktrailPublicKey = self.blocktrailPublicKeys[keyIndex];
return Wallet.deriveByPath(blocktrailPublicKey, path, "M/" + keyIndex + "'");
};
/**
* upgrade wallet to different blocktrail cosign key
*
* @param keyIndex int
* @param [cb] function
* @returns {q.Promise}
*/
Wallet.prototype.upgradeKeyIndex = function(keyIndex, cb) {
var self = this;
var deferred = q.defer();
deferred.promise.nodeify(cb);
if (self.locked) {
deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade key index"));
return deferred.promise;
}
var primaryPublicKey = self.primaryPrivateKey.deriveHardened(keyIndex).neutered();
deferred.resolve(
self.sdk.upgradeKeyIndex(self.identifier, keyIndex, [primaryPublicKey.toBase58(), "M/" + keyIndex + "'"])
.then(function(result) {
self.keyIndex = keyIndex;
_.forEach(result.blocktrail_public_keys, function(publicKey, keyIndex) {
self.blocktrailPublicKeys[keyIndex] = bitcoin.HDNode.fromBase58(publicKey[0], self.network);
});
self.primaryPublicKeys[keyIndex] = primaryPublicKey;
return true;
})
);
return deferred.promise;
};
/**
* generate a new derived private key and return the new address for it
*
* @param [chainIdx] int
* @param [cb] function callback(err, address)
* @returns {q.Promise}
*/
Wallet.prototype.getNewAddress = function(chainIdx, cb) {
var self = this;
// chainIdx is optional
if (typeof chainIdx === "function") {
cb = chainIdx;
chainIdx = null;
}
var deferred = q.defer();
deferred.promise.spreadNodeify(cb);
// Only enter if it's not an integer
if (chainIdx !== parseInt(chainIdx, 10)) {
// deal with undefined or null, assume defaults
if (typeof chainIdx === "undefined" || chainIdx === null) {
chainIdx = self.chain;
} else {
// was a variable but not integer
deferred.reject(new Error("Invalid chain index"));
return deferred.promise;
}
}
deferred.resolve(
self.sdk.getNewDerivation(self.identifier, "M/" + self.keyIndex + "'/" + chainIdx)
.then(function(newDerivation) {
var path = newDerivation.path;
var addressFromServer = newDerivation.address;
var decodedFromServer;
try {
// Decode the address the serer gave us
decodedFromServer = self.decodeAddress(addressFromServer);
if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") {
self.bypassNewAddressCheck = false;
}
} catch (e) {
throw new blocktrail.WalletAddressError("Failed to decode address [" + newDerivation.address + "]");
}
if (!self.bypassNewAddressCheck) {
// We need to reproduce this address with the same path,
// but the server (for BCH cashaddrs) uses base58?
var verifyAddress = self.getAddressByPath(newDerivation.path);
// If this occasion arises:
if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") {
// Decode our the address we produced for the path
var decodeOurs;
try {
decodeOurs = self.decodeAddress(verifyAddress);
} catch (e) {
throw new blocktrail.WalletAddressError("Error while verifying address from server [" + e.message + "]");
}
// Peek beyond the encoding - the hashes must match at least
if (decodeOurs.decoded.hash.toString('hex') !== decodedFromServer.decoded.hash.toString('hex')) {
throw new blocktrail.WalletAddressError("Failed to verify legacy address [hash mismatch]");
}
var matchedP2PKH = decodeOurs.decoded.version === bitcoin.script.types.P2PKH &&
decodedFromServer.decoded.version === self.network.pubKeyHash;
var matchedP2SH = decodeOurs.decoded.version === bitcoin.script.types.P2SH &&
decodedFromServer.decoded.version === self.network.scriptHash;
if (!(matchedP2PKH || matchedP2SH)) {
throw new blocktrail.WalletAddressError("Failed to verify legacy address [prefix mismatch]");
}
// We are satisfied that the address is for the same
// destination, so substitute addressFromServer with our
// 'reencoded' form.
addressFromServer = decodeOurs.address;
}
// debug check
if (verifyAddress !== addressFromServer) {
throw new blocktrail.WalletAddressError("Failed to verify address [" + newDerivation.address + "] !== [" + addressFromServer + "]");
}
}
return [addressFromServer, path];
})
);
return deferred.promise;
};
/**
* get the balance for the wallet
*
* @param [cb] function callback(err, confirmed, unconfirmed)
* @returns {q.Promise}
*/
Wallet.prototype.getBalance = function(cb) {
var self = this;
var deferred = q.defer();
deferred.promise.spreadNodeify(cb);
deferred.resolve(
self.sdk.getWalletBalance(self.identifier)
.then(function(result) {
return [result.confirmed, result.unconfirmed];
})
);
return deferred.promise;
};
/**
* get the balance for the wallet
*
* @param [cb] function callback(err, confirmed, unconfirmed)
* @returns {q.Promise}
*/
Wallet.prototype.getInfo = function(cb) {
var self = this;
var deferred = q.defer();
deferred.promise.spreadNodeify(cb);
deferred.resolve(
self.sdk.getWalletBalance(self.identifier)
);
return deferred.promise;
};
/**
*
* @param [force] bool ignore warnings (such as non-zero balance)
* @param [cb] function callback(err, success)
* @returns {q.Promise}
*/
Wallet.prototype.deleteWallet = function(force, cb) {
var self = this;
if (typeof force === "function") {
cb = force;
force = false;
}
var deferred = q.defer();
deferred.promise.nodeify(cb);
if (self.locked) {
deferred.reject(new blocktrail.WalletDeleteError("Wallet needs to be unlocked to delete wallet"));
return deferred.promise;
}
var checksum = self.primaryPrivateKey.getAddress();
var privBuf = self.primaryPrivateKey.keyPair.d.toBuffer(32);
var signature = bitcoinMessage.sign(checksum, self.network.messagePrefix, privBuf, true).toString('base64');
deferred.resolve(
self.sdk.deleteWallet(self.identifier, checksum, signature, force)
.then(function(result) {
return result.deleted;
})
);
return deferred.promise;
};
/**
* create, sign and send a transaction
*
* @param pay array {'address': (int)value} coins to send
* @param [changeAddress] bool change address to use (auto generated if NULL)
* @param [allowZeroConf] bool allow zero confirmation unspent outputs to be used in coin selection
* @param [randomizeChangeIdx] bool randomize the index of the change output (default TRUE, only disable if you have a good reason to)
* @param [feeStrategy] string defaults to Wallet.FEE_STRATEGY_OPTIMAL
* @param [twoFactorToken] string 2FA token
* @param options string Options for BIP70 broadcast (only for bitpay/BCH currently)
* @param [cb] function callback(err, txHash)
* @returns {q.Promise}
*/
Wallet.prototype.pay = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, twoFactorToken, options, cb) {
/* jshint -W071 */
var self = this;
if (typeof changeAddress === "function") {
cb = changeAddress;
changeAddress = null;
} else if (typeof allowZeroConf === "function") {
cb = allowZeroConf;
allowZeroConf = false;
} else if (typeof randomizeChangeIdx === "function") {
cb = randomizeChangeIdx;
randomizeChangeIdx = true;
} else if (typeof feeStrategy === "function") {
cb = feeStrategy;
feeStrategy = null;
} else if (typeof twoFactorToken === "function") {
cb = twoFactorToken;
twoFactorToken = null;
} else if (typeof options === "function") {
cb = options;
options = {};
}
randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
options = options || {};
var checkFee = typeof options.checkFee !== "undefined" ? options.checkFee : true;
var deferred = q.defer();
deferred.promise.nodeify(cb);
if (self.locked) {
deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to send coins"));
return deferred.promise;
}
q.nextTick(function() {
deferred.notify(Wallet.PAY_PROGRESS_START);
self.buildTransaction(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options)
.then(
function(r) { return r; },
function(e) { deferred.reject(e); },
function(progress) {
deferred.notify(progress);
}
)
.spread(
function(tx, utxos) {
deferred.notify(Wallet.PAY_PROGRESS_SEND);
var data = {
signed_transaction: tx.toHex(),
base_transaction: tx.__toBuffer(null, null, false).toString('hex')
};
return self.sendTransaction(data, utxos.map(function(utxo) { return utxo['path']; }), checkFee, twoFactorToken, options.prioboost, options)
.then(function(result) {
deferred.notify(Wallet.PAY_PROGRESS_DONE);
if (!result || !result['complete'] || result['complete'] === 'false') {
deferred.reject(new blocktrail.TransactionSignError("Failed to completely sign transaction"));
} else {
return result['txid'];
}
});
},
function(e) {
throw e;
}
)
.then(
function(r) { deferred.resolve(r); },
function(e) { deferred.reject(e); }
)
;
});
return deferred.promise;
};
Wallet.prototype.decodeAddress = function(address) {
return Wallet.getAddressAndType(address, this.network, this.useNewCashAddr);
};
function readBech32Address(address, network) {
var addr;
var err;
try {
addr = bitcoin.address.fromBech32(address, network);
err = null;
} catch (_err) {
err = _err;
}
if (!err) {
// Valid bech32 but invalid network immediately alerts
if (addr.prefix !== network.bech32) {
throw new blocktrail.InvalidAddressError("Address invalid on this network");
}
}
return [err, addr];
}
function readCashAddress(address, network) {
var addr;
var err;
address = address.toLowerCase();
try {
addr = bitcoin.address.fromCashAddress(address);
err = null;
} catch (_err) {
err = _err;
}
if (err) {
try {
addr = bitcoin.address.fromCashAddress(network.cashAddrPrefix + ':' + address);
err = null;
} catch (_err) {
err = _err;
}
}
if (!err) {
// Valid base58 but invalid network immediately alerts
if (addr.prefix !== network.cashAddrPrefix) {
throw new Error(address + ' has an invalid prefix');
}
}
return [err, addr];
}
function readBase58Address(address, network) {
var addr;
var err;
try {
addr = bitcoin.address.fromBase58Check(address);
err = null;
} catch (_err) {
err = _err;
}
if (!err) {
// Valid base58 but invalid network immediately alerts
if (addr.version !== network.pubKeyHash && addr.version !== network.scriptHash) {
throw new blocktrail.InvalidAddressError("Address invalid on this network");
}
}
return [err, addr];
}
Wallet.getAddressAndType = function(address, network, allowCashAddress) {
var addr;
var type;
var err;
function readAddress(reader, readType) {
var decoded = reader(address, network);
if (decoded[0] === null) {
addr = decoded[1];
type = readType;
} else {
err = decoded[0];
}
}
if (network === bitcoin.networks.bitcoin ||
network === bitcoin.networks.testnet ||
network === bitcoin.networks.regtest
) {
readAddress(readBech32Address, "bech32");
}
if (!addr && 'cashAddrPrefix' in network && allowCashAddress) {
readAddress(readCashAddress, "cashaddr");
}
if (!addr) {
readAddress(readBase58Address, "base58");
}
if (addr) {
return {
address: address,
decoded: addr,
type: type
};
} else {
throw new blocktrail.InvalidAddressError(err.message);
}
};
Wallet.convertPayToOutputs = function(pay, network, allowCashAddr) {
var send = [];
var readFunc;
// Deal with two different forms
if (Array.isArray(pay)) {
// output[]
readFunc = function(i, output, obj) {
if (typeof output !== "object") {
throw new Error("Invalid transaction output for numerically indexed list [1]");
}
var keys = Object.keys(output);
if (keys.indexOf("scriptPubKey") !== -1 && keys.indexOf("value") !== -1) {
obj.scriptPubKey = output["scriptPubKey"];
obj.value = output["value"];
} else if (keys.indexOf("address") !== -1 && keys.indexOf("value") !== -1) {
obj.address = output["address"];
obj.value = output["value"];
} else if (keys.length === 2 && output.length === 2 && keys[0] === '0' && keys[1] === '1') {
obj.address = output[0];
obj.value = output[1];
} else {
throw new Error("Invalid transaction output for numerically indexed list [2]");
}
};
} else if (typeof pay === "object") {
// map[addr]amount
readFunc = function(address, value, obj) {
obj.address = address.trim();
obj.value = value;
if (obj.address === Wallet.OP_RETURN) {
var datachunk = Buffer.isBuffer(value) ? value : new Buffer(value, 'utf-8');
obj.scriptPubKey = bitcoin.script.nullData.output.encode(datachunk).toString('hex');
obj.value = 0;
obj.address = null;
}
};
} else {
throw new Error("Invalid input");
}
Object.keys(pay).forEach(function(key) {
var obj = {};
readFunc(key, pay[key], obj);
if (parseInt(obj.value, 10).toString() !== obj.value.toString()) {
throw new blocktrail.WalletSendError("Values should be in Satoshis");
}
// Remove address, replace with scriptPubKey
if (typeof obj.address === "string") {
try {
var addrAndType = Wallet.getAddressAndType(obj.address, network, allowCashAddr);
obj.scriptPubKey = bitcoin.address.toOutputScript(addrAndType.address, network, allowCashAddr).toString('hex');
delete obj.address;
} catch (e) {
throw new blocktrail.InvalidAddressError("Invalid address [" + obj.address + "] (" + e.message + ")");
}
}
// Extra checks when the output isn't OP_RETURN
if (obj.scriptPubKey.slice(0, 2) !== "6a") {
if (!(obj.value = parseInt(obj.value, 10))) {
throw new blocktrail.WalletSendError("Values should be non zero");
} else if (obj.value <= blocktrail.DUST) {
throw new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")");
}
}
// Value fully checked now
obj.value = parseInt(obj.value, 10);
send.push(obj);
});
return send;
};
Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) {
/* jshint -W071 */
var self = this;
if (typeof changeAddress === "function") {
cb = changeAddress;
changeAddress = null;
} else if (typeof allowZeroConf === "function") {
cb = allowZeroConf;
allowZeroConf = false;
} else if (typeof randomizeChangeIdx === "function") {
cb = randomizeChangeIdx;
randomizeChangeIdx = true;
} else if (typeof feeStrategy === "function") {
cb = feeStrategy;
feeStrategy = null;
} else if (typeof options === "function") {
cb = options;
options = {};
}
randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
options = options || {};
var deferred = q.defer();
deferred.promise.spreadNodeify(cb);
q.nextTick(function() {
var send;
try {
send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
} catch (e) {
deferred.reject(e);
return deferred.promise;
}
if (!send.length) {
deferred.reject(new blocktrail.WalletSendError("Need at least one recipient"));
return deferred.promise;
}
deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION);
deferred.resolve(
self.coinSelection(send, true, allowZeroConf, feeStrategy, options)
/**
*
* @param {Object[]} utxos
* @param fee
* @param change
* @param randomizeChangeIdx
* @returns {*}
*/
.spread(function(utxos, fee, change) {
var tx, txb, outputs = [];
var deferred = q.defer();
async.waterfall([
/**
* prepare
*
* @param cb
*/
function(cb) {
var inputsTotal = utxos.map(function(utxo) {
return utxo['value'];
}).reduce(function(a, b) {
return a + b;
});
var outputsTotal = send.map(function(output) {
return output.value;
}).reduce(function(a, b) {
return a + b;
});
var estimatedChange = inputsTotal - outputsTotal - fee;
if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) {
return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " +
"suggested by the coin selection seems incorrect (" + estimatedChange + ")"));
}
cb();
},
/**
* init transaction builder
*
* @param cb
*/
function(cb) {
txb = new bitcoin.TransactionBuilder(self.network);
if (self.bitcoinCash) {
txb.enableBitcoinCash();
}
cb();
},
/**
* add UTXOs as inputs
*
* @param cb
*/
function(cb) {
var i;
for (i = 0; i < utxos.length; i++) {
txb.addInput(utxos[i]['hash'], utxos[i]['idx']);
}
cb();
},
/**
* build desired outputs
*
* @param cb
*/
function(cb) {
send.forEach(function(_send) {
if (_send.scriptPubKey) {
outputs.push({scriptPubKey: new Buffer(_send.scriptPubKey, 'hex'), value: _send.value});
} else {
throw new Error("Invalid send");
}
});
cb();
},
/**
* get change address if required
*
* @param cb
*/
function(cb) {
if (change > 0) {
if (change <= blocktrail.DUST) {
change = 0; // don't do a change output if it would be a dust output
} else {
if (!changeAddress) {
deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS);
return self.getNewAddress(self.changeChain, function(err, address) {
if (err) {
return cb(err);
}
changeAddress = address;
cb();
});
}
}
}
cb();
},
/**
* add change to outputs
*
* @param cb
*/
function(cb) {
if (change > 0) {
var changeOutput = {
scriptPubKey: bitcoin.address.toOutputScript(changeAddress, self.network, self.useNewCashAddr),
value: change
};
if (randomizeChangeIdx) {
outputs.splice(_.random(0, outputs.length), 0, changeOutput);
} else {
outputs.push(changeOutput);
}
}
cb();
},
/**
* add outputs to txb
*
* @param cb
*/
function(cb) {
outputs.forEach(function(outputInfo) {
txb.addOutput(outputInfo.scriptPubKey, outputInfo.value);
});
cb();
},
/**
* sign
*
* @param cb
*/
function(cb) {
var i, privKey, path, redeemScript, witnessScript;
deferred.notify(Wallet.PAY_PROGRESS_SIGN);
for (i = 0; i < utxos.length; i++) {
var mode = SignMode.SIGN;
if (utxos[i].sign_mode) {
mode = utxos[i].sign_mode;
}
redeemScript = null;
witnessScript = null;
if (mode === SignMode.SIGN) {
path = utxos[i]['path'].replace("M", "m");
// todo: regenerate scripts for path and compare for utxo (paranoid mode)
if (self.primaryPrivateKey) {
privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair;
} else if (self.backupPrivateKey) {
privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair;
} else {
throw new Error("No master privateKey present");
}
redeemScript = new Buffer(utxos[i]['redeem_script'], 'hex');
if (typeof utxos[i]['witness_script'] === 'string') {
witnessScript = new Buffer(utxos[i]['witness_script'], 'hex');
}
var sigHash = bitcoin.Transaction.SIG