bitcore-wallet-client
Version:
Client for bitcore-wallet-service
1,835 lines (1,551 loc) • 57.2 kB
JavaScript
/** @namespace Client.API */
'use strict';
var _ = require('lodash');
var $ = require('preconditions').singleton();
var util = require('util');
var async = require('async');
var events = require('events');
var Bitcore = require('bitcore-lib');
var sjcl = require('sjcl');
var url = require('url');
var querystring = require('querystring');
var Stringify = require('json-stable-stringify');
var request;
if (process && !process.browser) {
request = require('request');
} else {
request = require('browser-request');
}
var Common = require('./common');
var Constants = Common.Constants;
var Defaults = Common.Defaults;
var Utils = Common.Utils;
var PayPro = require('./paypro');
var log = require('./log');
var Credentials = require('./credentials');
var Verifier = require('./verifier');
var Package = require('../package.json');
var Errors = require('./errors');
var BASE_URL = 'http://localhost:3232/bws/api';
/**
* @desc ClientAPI constructor.
*
* @param {Object} opts
* @constructor
*/
function API(opts) {
opts = opts || {};
this.verbose = !!opts.verbose;
this.request = opts.request || request;
this.baseUrl = opts.baseUrl || BASE_URL;
var parsedUrl = url.parse(this.baseUrl);
this.basePath = parsedUrl.path;
this.baseHost = parsedUrl.protocol + '//' + parsedUrl.host;
this.payProHttp = null; // Only for testing
this.doNotVerifyPayPro = opts.doNotVerifyPayPro;
this.timeout = opts.timeout || 50000;
if (this.verbose) {
log.setLevel('debug');
} else {
log.setLevel('info');
}
};
util.inherits(API, events.EventEmitter);
API.privateKeyEncryptionOpts = {
iter: 10000
};
API.prototype.initNotifications = function(cb) {
log.warn('DEPRECATED: use initialize() instead.');
this.initialize({}, cb);
};
API.prototype.initialize = function(opts, cb) {
$.checkState(this.credentials);
var self = this;
self._initNotifications(opts);
return cb();
};
API.prototype.dispose = function(cb) {
var self = this;
self._disposeNotifications();
return cb();
};
API.prototype._fetchLatestNotifications = function(interval, cb) {
var self = this;
cb = cb || function() {};
var opts = {
lastNotificationId: self.lastNotificationId,
};
if (!self.lastNotificationId) {
opts.timeSpan = interval + 1;
}
self.getNotifications(opts, function(err, notifications) {
if (err) {
log.warn('Error receiving notifications.');
log.debug(err);
return cb(err);
}
if (notifications.length > 0) {
self.lastNotificationId = _.last(notifications).id;
}
_.each(notifications, function(notification) {
self.emit('notification', notification);
});
return cb();
});
};
API.prototype._initNotifications = function(opts) {
var self = this;
opts = opts || {};
var interval = opts.notificationIntervalSeconds || 5;
self.notificationsIntervalId = setInterval(function() {
self._fetchLatestNotifications(interval, function(err) {
if (err) {
if (err instanceof Errors.NOT_FOUND || err instanceof Errors.NOT_AUTHORIZED) {
self._disposeNotifications();
}
}
});
}, interval * 1000);
};
API.prototype._disposeNotifications = function() {
var self = this;
if (self.notificationsIntervalId) {
clearInterval(self.notificationsIntervalId);
self.notificationsIntervalId = null;
}
};
/**
* Reset notification polling with new interval
* @memberof Client.API
* @param {Numeric} notificationIntervalSeconds - use 0 to pause notifications
*/
API.prototype.setNotificationsInterval = function(notificationIntervalSeconds) {
var self = this;
self._disposeNotifications();
if (notificationIntervalSeconds > 0) {
self._initNotifications({
notificationIntervalSeconds: notificationIntervalSeconds
});
}
};
/**
* Encrypt a message
* @private
* @static
* @memberof Client.API
* @param {String} message
* @param {String} encryptingKey
*/
API._encryptMessage = function(message, encryptingKey) {
if (!message) return null;
return Utils.encryptMessage(message, encryptingKey);
};
/**
* Decrypt a message
* @private
* @static
* @memberof Client.API
* @param {String} message
* @param {String} encryptingKey
*/
API._decryptMessage = function(message, encryptingKey) {
if (!message) return '';
try {
return Utils.decryptMessage(message, encryptingKey);
} catch (ex) {
return '<ECANNOTDECRYPT>';
}
};
/**
* Decrypt text fields in transaction proposals
* @private
* @static
* @memberof Client.API
* @param {Array} txps
* @param {String} encryptingKey
*/
API.prototype._processTxps = function(txps) {
var self = this;
if (!txps) return;
var encryptingKey = self.credentials.sharedEncryptingKey;
_.each([].concat(txps), function(txp) {
txp.encryptedMessage = txp.message;
txp.message = API._decryptMessage(txp.message, encryptingKey) || null;
_.each(txp.actions, function(action) {
action.comment = API._decryptMessage(action.comment, encryptingKey);
// TODO get copayerName from Credentials -> copayerId to copayerName
// action.copayerName = null;
});
_.each(txp.outputs, function(output) {
output.encryptedMessage = output.message;
output.message = API._decryptMessage(output.message, encryptingKey) || null;
});
txp.hasUnconfirmedInputs = _.any(txp.inputs, function(input) {
return input.confirmations == 0;
});
});
};
/**
* Parse errors
* @private
* @static
* @memberof Client.API
* @param {Object} body
*/
API._parseError = function(body) {
if (_.isString(body)) {
try {
body = JSON.parse(body);
} catch (e) {
body = {
error: body
};
}
}
var ret;
if (body && body.code) {
if (Errors[body.code]) {
ret = new Errors[body.code];
} else {
ret = new Error(body.code);
}
} else {
ret = new Error(body.error || body);
}
log.error(ret);
return ret;
};
/**
* Sign an HTTP request
* @private
* @static
* @memberof Client.API
* @param {String} method - The HTTP method
* @param {String} url - The URL for the request
* @param {Object} args - The arguments in case this is a POST/PUT request
* @param {String} privKey - Private key to sign the request
*/
API._signRequest = function(method, url, args, privKey) {
var message = [method.toLowerCase(), url, JSON.stringify(args)].join('|');
return Utils.signMessage(message, privKey);
};
/**
* Seed from random
*
* @param {Object} opts
* @param {String} opts.network - default 'livenet'
*/
API.prototype.seedFromRandom = function(opts) {
$.checkArgument(arguments.length <= 1, 'DEPRECATED: only 1 argument accepted.');
$.checkArgument(_.isUndefined(opts) || _.isObject(opts), 'DEPRECATED: argument should be an options object.');
opts = opts || {};
this.credentials = Credentials.create(opts.network || 'livenet');
};
/**
* Seed from random with mnemonic
*
* @param {Object} opts
* @param {String} opts.network - default 'livenet'
* @param {String} opts.passphrase
* @param {Number} opts.language - default 'en'
* @param {Number} opts.account - default 0
*/
API.prototype.seedFromRandomWithMnemonic = function(opts) {
$.checkArgument(arguments.length <= 1, 'DEPRECATED: only 1 argument accepted.');
$.checkArgument(_.isUndefined(opts) || _.isObject(opts), 'DEPRECATED: argument should be an options object.');
opts = opts || {};
this.credentials = Credentials.createWithMnemonic(opts.network || 'livenet', opts.passphrase, opts.language || 'en', opts.account || 0);
};
API.prototype.getMnemonic = function() {
return this.credentials.getMnemonic();
};
API.prototype.mnemonicHasPassphrase = function() {
return this.credentials.mnemonicHasPassphrase;
};
API.prototype.clearMnemonic = function() {
return this.credentials.clearMnemonic();
};
/**
* Seed from extended private key
*
* @param {String} xPrivKey
*/
API.prototype.seedFromExtendedPrivateKey = function(xPrivKey) {
this.credentials = Credentials.fromExtendedPrivateKey(xPrivKey);
};
/**
* Seed from Mnemonics (language autodetected)
* Can throw an error if mnemonic is invalid
*
* @param {String} BIP39 words
* @param {Object} opts
* @param {String} opts.network - default 'livenet'
* @param {String} opts.passphrase
* @param {Number} opts.account - default 0
* @param {String} opts.derivationStrategy - default 'BIP44'
*/
API.prototype.seedFromMnemonic = function(words, opts) {
$.checkArgument(_.isUndefined(opts) || _.isObject(opts), 'DEPRECATED: second argument should be an options object.');
opts = opts || {};
this.credentials = Credentials.fromMnemonic(opts.network || 'livenet', words, opts.passphrase, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44);
};
/**
* Seed from external wallet public key
*
* @param {String} xPubKey
* @param {String} source - A name identifying the source of the xPrivKey (e.g. ledger, TREZOR, ...)
* @param {String} entropySourceHex - A HEX string containing pseudo-random data, that can be deterministically derived from the xPrivKey, and should not be derived from xPubKey.
* @param {Object} opts
* @param {Number} opts.account - default 0
* @param {String} opts.derivationStrategy - default 'BIP44'
*/
API.prototype.seedFromExtendedPublicKey = function(xPubKey, source, entropySourceHex, opts) {
$.checkArgument(_.isUndefined(opts) || _.isObject(opts));
opts = opts || {};
this.credentials = Credentials.fromExtendedPublicKey(xPubKey, source, entropySourceHex, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44);
};
/**
* Export wallet
*
* @param {Object} opts
* @param {Boolean} opts.noSign
*/
API.prototype.export = function(opts) {
$.checkState(this.credentials);
opts = opts || {};
var output;
var c = Credentials.fromObj(this.credentials);
if (opts.noSign) {
c.setNoSign();
}
output = JSON.stringify(c.toObj());
return output;
};
/**
* Import wallet
*
* @param {Object} str
* @param {Object} opts
* @param {String} opts.password If the source has the private key encrypted, the password
* will be needed for derive credentials fields.
*/
API.prototype.import = function(str, opts) {
opts = opts || {};
try {
var credentials = Credentials.fromObj(JSON.parse(str));
this.credentials = credentials;
} catch (ex) {
throw new Errors.INVALID_BACKUP;
}
};
API.prototype._import = function(cb) {
$.checkState(this.credentials);
var self = this;
// First option, grab wallet info from BWS.
self.openWallet(function(err, ret) {
// it worked?
if (!err) return cb(null, ret);
// Is the error other than "copayer was not found"? || or no priv key.
if (err instanceof Errors.NOT_AUTHORIZED || self.isPrivKeyExternal())
return cb(err);
//Second option, lets try to add an access
log.info('Copayer not found, trying to add access');
self.addAccess({}, function(err) {
if (err) {
return cb(new Errors.WALLET_DOES_NOT_EXIST);
}
self.openWallet(cb);
});
});
};
/**
* Import from Mnemonics (language autodetected)
* Can throw an error if mnemonic is invalid
*
* @param {String} BIP39 words
* @param {Object} opts
* @param {String} opts.network - default 'livenet'
* @param {String} opts.passphrase
* @param {Number} opts.account - default 0
* @param {String} opts.derivationStrategy - default 'BIP44'
*/
API.prototype.importFromMnemonic = function(words, opts, cb) {
log.debug('Importing from 12 Words');
opts = opts || {};
try {
this.credentials = Credentials.fromMnemonic(opts.network || 'livenet', words, opts.passphrase, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44);
} catch (e) {
log.info('Mnemonic error:', e);
return cb(new Errors.INVALID_BACKUP);
};
this._import(cb);
};
API.prototype.importFromExtendedPrivateKey = function(xPrivKey, cb) {
log.debug('Importing from Extended Private Key');
try {
this.credentials = Credentials.fromExtendedPrivateKey(xPrivKey);
} catch (e) {
log.info('xPriv error:', e);
return cb(new Errors.INVALID_BACKUP);
};
this._import(cb);
};
/**
* Import from Extended Public Key
*
* @param {String} xPubKey
* @param {String} source - A name identifying the source of the xPrivKey
* @param {String} entropySourceHex - A HEX string containing pseudo-random data, that can be deterministically derived from the xPrivKey, and should not be derived from xPubKey.
* @param {Object} opts
* @param {Number} opts.account - default 0
* @param {String} opts.derivationStrategy - default 'BIP44'
*/
API.prototype.importFromExtendedPublicKey = function(xPubKey, source, entropySourceHex, opts, cb) {
$.checkArgument(arguments.length == 5, "DEPRECATED: should receive 5 arguments");
$.checkArgument(_.isUndefined(opts) || _.isObject(opts));
$.shouldBeFunction(cb);
opts = opts || {};
log.debug('Importing from Extended Private Key');
try {
this.credentials = Credentials.fromExtendedPublicKey(xPubKey, source, entropySourceHex, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44);
} catch (e) {
log.info('xPriv error:', e);
return cb(new Errors.INVALID_BACKUP);
};
this._import(cb);
};
API.prototype.decryptBIP38PrivateKey = function(encryptedPrivateKeyBase58, passphrase, opts, cb) {
var Bip38 = require('bip38');
var bip38 = new Bip38();
var privateKeyWif;
try {
privateKeyWif = bip38.decrypt(encryptedPrivateKeyBase58, passphrase);
} catch (ex) {
return cb(new Error('Could not decrypt BIP38 private key', ex));
}
var privateKey = new Bitcore.PrivateKey(privateKeyWif);
var address = privateKey.publicKey.toAddress().toString();
var addrBuff = new Buffer(address, 'ascii');
var actualChecksum = Bitcore.crypto.Hash.sha256sha256(addrBuff).toString('hex').substring(0, 8);
var expectedChecksum = Bitcore.encoding.Base58Check.decode(encryptedPrivateKeyBase58).toString('hex').substring(6, 14);
if (actualChecksum != expectedChecksum)
return cb(new Error('Incorrect passphrase'));
return cb(null, privateKeyWif);
};
API.prototype.getBalanceFromPrivateKey = function(privateKey, cb) {
var self = this;
var privateKey = new Bitcore.PrivateKey(privateKey);
var address = privateKey.publicKey.toAddress();
self.getUtxos({
addresses: address.toString(),
}, function(err, utxos) {
if (err) return cb(err);
return cb(null, _.sum(utxos, 'satoshis'));
});
};
API.prototype.buildTxFromPrivateKey = function(privateKey, destinationAddress, opts, cb) {
var self = this;
opts = opts || {};
var privateKey = new Bitcore.PrivateKey(privateKey);
var address = privateKey.publicKey.toAddress();
async.waterfall([
function(next) {
self.getUtxos({
addresses: address.toString(),
}, function(err, utxos) {
return next(err, utxos);
});
},
function(utxos, next) {
if (!_.isArray(utxos) || utxos.length == 0) return next(new Error('No utxos found'));
var fee = opts.fee || 10000;
var amount = _.sum(utxos, 'satoshis') - fee;
if (amount <= 0) return next(new Errors.INSUFFICIENT_FUNDS);
var tx;
try {
var toAddress = Bitcore.Address.fromString(destinationAddress);
tx = new Bitcore.Transaction()
.from(utxos)
.to(toAddress, amount)
.fee(fee)
.sign(privateKey);
// Make sure the tx can be serialized
tx.serialize();
} catch (ex) {
log.error('Could not build transaction from private key', ex);
return next(new Errors.COULD_NOT_BUILD_TRANSACTION);
}
return next(null, tx);
}
], cb);
};
/**
* Open a wallet and try to complete the public key ring.
*
* @param {Callback} cb - The callback that handles the response. It returns a flag indicating that the wallet is complete.
* @fires API#walletCompleted
*/
API.prototype.openWallet = function(cb) {
$.checkState(this.credentials);
var self = this;
if (self.credentials.isComplete() && self.credentials.hasWalletInfo())
return cb(null, true);
self._doGetRequest('/v2/wallets/?includeExtendedInfo=1', function(err, ret) {
if (err) return cb(err);
var wallet = ret.wallet;
if (!self.credentials.hasWalletInfo()) {
var me = _.find(wallet.copayers, {
id: self.credentials.copayerId
});
self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, null, me.name);
}
if (wallet.status != 'complete')
return cb();
if (self.credentials.walletPrivKey) {
if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) {
return cb(new Errors.SERVER_COMPROMISED);
}
} else {
// this should only happends in AIR-GAPPED flows
log.warn('Could not verify copayers key (missing wallet Private Key)');
}
// Wallet was not complete. We are completing it.
self.credentials.addPublicKeyRing(API._extractPublicKeyRing(wallet.copayers));
self.emit('walletCompleted', wallet);
self._processTxps(ret.pendingTxps);
self._processCustomData(ret);
return cb(null, ret);
});
};
/**
* Do an HTTP request
* @private
*
* @param {Object} method
* @param {String} url
* @param {Object} args
* @param {Callback} cb
*/
API.prototype._doRequest = function(method, url, args, cb) {
$.checkState(this.credentials);
var reqSignature;
var key = args._requestPrivKey || this.credentials.requestPrivKey;
if (key) {
delete args['_requestPrivKey'];
reqSignature = API._signRequest(method, url, args, key);
}
var absUrl = this.baseUrl + url;
var args = {
// relUrl: only for testing with `supertest`
relUrl: this.basePath + url,
headers: {
'x-identity': this.credentials.copayerId,
'x-signature': reqSignature,
'x-client-version': 'bwc-' + Package.version,
},
method: method,
url: absUrl,
body: args,
json: true,
withCredentials: false,
timeout: this.timeout,
};
log.debug('Request Args', util.inspect(args, {
depth: 10
}));
this.request(args, function(err, res, body) {
log.debug(util.inspect(body, {
depth: 10
}));
if (!res) {
return cb(new Errors.CONNECTION_ERROR);
}
if (res.statusCode !== 200) {
if (res.statusCode === 404)
return cb(new Errors.NOT_FOUND);
if (!res.statusCode)
return cb(new Errors.CONNECTION_ERROR);
return cb(API._parseError(body));
}
if (body === '{"error":"read ECONNRESET"}')
return cb(new Errors.ECONNRESET_ERROR(JSON.parse(body)));
return cb(null, body, res.header);
});
};
/**
* Do a POST request
* @private
*
* @param {String} url
* @param {Object} args
* @param {Callback} cb
*/
API.prototype._doPostRequest = function(url, args, cb) {
return this._doRequest('post', url, args, cb);
};
API.prototype._doPutRequest = function(url, args, cb) {
return this._doRequest('put', url, args, cb);
};
/**
* Do a GET request
* @private
*
* @param {String} url
* @param {Callback} cb
*/
API.prototype._doGetRequest = function(url, cb) {
url += url.indexOf('?') > 0 ? '&' : '?';
url += 'r=' + _.random(10000, 99999);
return this._doRequest('get', url, {}, cb);
};
/**
* Do a DELETE request
* @private
*
* @param {String} url
* @param {Callback} cb
*/
API.prototype._doDeleteRequest = function(url, cb) {
return this._doRequest('delete', url, {}, cb);
};
API._buildSecret = function(walletId, walletPrivKey, network) {
if (_.isString(walletPrivKey)) {
walletPrivKey = Bitcore.PrivateKey.fromString(walletPrivKey);
}
var widHex = new Buffer(walletId.replace(/-/g, ''), 'hex');
var widBase58 = new Bitcore.encoding.Base58(widHex).toString();
return _.padRight(widBase58, 22, '0') + walletPrivKey.toWIF() + (network == 'testnet' ? 'T' : 'L');
};
API.parseSecret = function(secret) {
$.checkArgument(secret);
function split(str, indexes) {
var parts = [];
indexes.push(str.length);
var i = 0;
while (i < indexes.length) {
parts.push(str.substring(i == 0 ? 0 : indexes[i - 1], indexes[i]));
i++;
};
return parts;
};
try {
var secretSplit = split(secret, [22, 74]);
var widBase58 = secretSplit[0].replace(/0/g, '');
var widHex = Bitcore.encoding.Base58.decode(widBase58).toString('hex');
var walletId = split(widHex, [8, 12, 16, 20]).join('-');
var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]);
var networkChar = secretSplit[2];
return {
walletId: walletId,
walletPrivKey: walletPrivKey,
network: networkChar == 'T' ? 'testnet' : 'livenet',
};
} catch (ex) {
throw new Error('Invalid secret');
}
};
API.buildTx = function(txp) {
var t = new Bitcore.Transaction();
$.checkState(_.contains(_.values(Constants.SCRIPT_TYPES), txp.addressType));
switch (txp.addressType) {
case Constants.SCRIPT_TYPES.P2SH:
_.each(txp.inputs, function(i) {
t.from(i, i.publicKeys, txp.requiredSignatures);
});
break;
case Constants.SCRIPT_TYPES.P2PKH:
t.from(txp.inputs);
break;
}
if (txp.toAddress && txp.amount && !txp.outputs) {
t.to(txp.toAddress, txp.amount);
} else if (txp.outputs) {
_.each(txp.outputs, function(o) {
$.checkState(o.script || o.toAddress, 'Output should have either toAddress or script specified');
if (o.script) {
t.addOutput(new Bitcore.Transaction.Output({
script: o.script,
satoshis: o.amount
}));
} else {
t.to(o.toAddress, o.amount);
}
});
}
if (_.startsWith(txp.version, '1.')) {
Bitcore.Transaction.FEE_SECURITY_MARGIN = 1;
t.feePerKb(txp.feePerKb);
} else {
t.fee(txp.fee);
}
t.change(txp.changeAddress.address);
// Shuffle outputs for improved privacy
if (t.outputs.length > 1) {
var outputOrder = _.reject(txp.outputOrder, function(order) {
return order >= t.outputs.length;
});
$.checkState(t.outputs.length == outputOrder.length);
t.sortOutputs(function(outputs) {
return _.map(outputOrder, function(i) {
return outputs[i];
});
});
}
// Validate inputs vs outputs independently of Bitcore
var totalInputs = _.reduce(txp.inputs, function(memo, i) {
return +i.satoshis + memo;
}, 0);
var totalOutputs = _.reduce(t.outputs, function(memo, o) {
return +o.satoshis + memo;
}, 0);
$.checkState(totalInputs - totalOutputs >= 0);
$.checkState(totalInputs - totalOutputs <= Defaults.MAX_TX_FEE);
return t;
};
API.signTxp = function(txp, derivedXPrivKey) {
//Derive proper key to sign, for each input
var privs = [];
var derived = {};
var xpriv = new Bitcore.HDPrivateKey(derivedXPrivKey);
_.each(txp.inputs, function(i) {
if (!derived[i.path]) {
derived[i.path] = xpriv.derive(i.path).privateKey;
privs.push(derived[i.path]);
}
});
var t = API.buildTx(txp);
var signatures = _.map(privs, function(priv, i) {
return t.getSignatures(priv);
});
signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) {
return s.signature.toDER().toString('hex');
});
return signatures;
};
API.prototype._signTxp = function(txp) {
return API.signTxp(txp, this.credentials.getDerivedXPrivKey());
};
API.prototype._getCurrentSignatures = function(txp) {
var acceptedActions = _.filter(txp.actions, {
type: 'accept'
});
return _.map(acceptedActions, function(x) {
return {
signatures: x.signatures,
xpub: x.xpub,
};
});
};
API.prototype._addSignaturesToBitcoreTx = function(txp, t, signatures, xpub) {
if (signatures.length != txp.inputs.length)
throw new Error('Number of signatures does not match number of inputs');
var i = 0,
x = new Bitcore.HDPublicKey(xpub);
_.each(signatures, function(signatureHex) {
var input = txp.inputs[i];
try {
var signature = Bitcore.crypto.Signature.fromString(signatureHex);
var pub = x.derive(txp.inputPaths[i]).publicKey;
var s = {
inputIndex: i,
signature: signature,
sigtype: Bitcore.crypto.Signature.SIGHASH_ALL,
publicKey: pub,
};
t.inputs[i].addSignature(t, s);
i++;
} catch (e) {};
});
if (i != txp.inputs.length)
throw new Error('Wrong signatures');
};
API.prototype._applyAllSignatures = function(txp, t) {
var self = this;
$.checkState(txp.status == 'accepted');
var sigs = self._getCurrentSignatures(txp);
_.each(sigs, function(x) {
self._addSignaturesToBitcoreTx(txp, t, x.signatures, x.xpub);
});
};
/**
* Join
* @private
*
* @param {String} walletId
* @param {String} walletPrivKey
* @param {String} xPubKey
* @param {String} requestPubKey
* @param {String} copayerName
* @param {Object} Optional args
* @param {String} opts.customData
* @param {Callback} cb
*/
API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, requestPubKey, copayerName, opts, cb) {
$.shouldBeFunction(cb);
opts = opts || {};
// Adds encrypted walletPrivateKey to CustomData
opts.customData = opts.customData || {};
opts.customData.walletPrivKey = walletPrivKey.toString();
var encCustomData = Utils.encryptMessage(JSON.stringify(opts.customData),
this.credentials.personalEncryptingKey);
var args = {
walletId: walletId,
name: copayerName,
xPubKey: xPubKey,
requestPubKey: requestPubKey,
customData: encCustomData,
};
if (opts.dryRun) args.dryRun = true;
if (_.isBoolean(opts.supportBIP44AndP2PKH))
args.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH;
var hash = Utils.getCopayerHash(args.name, args.xPubKey, args.requestPubKey);
args.copayerSignature = Utils.signMessage(hash, walletPrivKey);
var url = '/v2/wallets/' + walletId + '/copayers';
this._doPostRequest(url, args, function(err, body) {
if (err) return cb(err);
return cb(null, body.wallet);
});
};
/**
* Return if wallet is complete
*/
API.prototype.isComplete = function() {
return this.credentials && this.credentials.isComplete();
};
/**
* Is private key currently encrypted? (ie, locked)
*
* @return {Boolean}
*/
API.prototype.isPrivKeyEncrypted = function() {
return this.credentials && this.credentials.isPrivKeyEncrypted();
};
/**
* Is private key encryption setup?
*
* @return {Boolean}
*/
API.prototype.hasPrivKeyEncrypted = function() {
return this.credentials && this.credentials.hasPrivKeyEncrypted();
};
/**
* Is private key external?
*
* @return {Boolean}
*/
API.prototype.isPrivKeyExternal = function() {
return this.credentials && this.credentials.hasExternalSource();
};
/**
* Get external wallet source name
*
* @return {String}
*/
API.prototype.getPrivKeyExternalSourceName = function() {
return this.credentials ? this.credentials.getExternalSourceName() : null;
};
/**
* unlocks the private key. `lock` need to be called explicity
* later to remove the unencrypted private key.
*
* @param password
*/
API.prototype.unlock = function(password) {
try {
this.credentials.unlock(password);
} catch (e) {
throw new Error('Could not unlock:' + e);
}
};
/**
* Can this credentials sign a transaction?
* (Only returns fail on a 'proxy' setup for airgapped operation)
*
* @return {undefined}
*/
API.prototype.canSign = function() {
return this.credentials && this.credentials.canSign();
};
API._extractPublicKeyRing = function(copayers) {
return _.map(copayers, function(copayer) {
var pkr = _.pick(copayer, ['xPubKey', 'requestPubKey']);
pkr.copayerName = copayer.name;
return pkr;
});
};
/**
* sets up encryption for the extended private key
*
* @param {String} password Password used to encrypt
* @param {Object} opts optional: SJCL options to encrypt (.iter, .salt, etc).
* @return {undefined}
*/
API.prototype.setPrivateKeyEncryption = function(password, opts) {
this.credentials.setPrivateKeyEncryption(password, opts || API.privateKeyEncryptionOpts);
};
/**
* disables encryption for private key.
* wallet must be unlocked
*
*/
API.prototype.disablePrivateKeyEncryption = function(password, opts) {
return this.credentials.disablePrivateKeyEncryption();
};
/**
* Locks private key (removes the unencrypted version and keep only the encrypted)
*
* @return {undefined}
*/
API.prototype.lock = function() {
this.credentials.lock();
};
/**
* Get current fee levels for the specified network
*
* @param {string} network - 'livenet' (default) or 'testnet'
* @param {Callback} cb
* @returns {Callback} cb - Returns error or an object with status information
*/
API.prototype.getFeeLevels = function(network, cb) {
var self = this;
$.checkArgument(network || _.contains(['livenet', 'testnet'], network));
self._doGetRequest('/v1/feelevels/?network=' + (network || 'livenet'), function(err, result) {
if (err) return cb(err);
return cb(err, result);
});
};
/**
* Get service version
*
* @param {Callback} cb
*/
API.prototype.getVersion = function(cb) {
this._doGetRequest('/v1/version/', cb);
};
/**
*
* Create a wallet.
* @param {String} walletName
* @param {String} copayerName
* @param {Number} m
* @param {Number} n
* @param {object} opts (optional: advanced options)
* @param {string} opts.network - 'livenet' or 'testnet'
* @param {String} opts.walletPrivKey - set a walletPrivKey (instead of random)
* @param {String} opts.id - set a id for wallet (instead of server given)
* @param {String} opts.withMnemonics - generate credentials
* @param cb
* @return {undefined}
*/
API.prototype.createWallet = function(walletName, copayerName, m, n, opts, cb) {
var self = this;
if (opts) $.shouldBeObject(opts);
opts = opts || {};
var network = opts.network || 'livenet';
if (!_.contains(['testnet', 'livenet'], network)) return cb(new Error('Invalid network'));
if (!self.credentials) {
log.info('Generating new keys');
self.seedFromRandom({
network: network
});
} else {
log.info('Using existing keys');
}
if (network != self.credentials.network) {
return cb(new Error('Existing keys were created for a different network'));
}
var walletPrivKey = opts.walletPrivKey || new Bitcore.PrivateKey();
var args = {
name: walletName,
m: m,
n: n,
pubKey: (new Bitcore.PrivateKey(walletPrivKey)).toPublicKey().toString(),
network: network,
id: opts.id,
};
self._doPostRequest('/v2/wallets/', args, function(err, body) {
if (err) return cb(err);
var walletId = body.walletId;
self.credentials.addWalletInfo(walletId, walletName, m, n, walletPrivKey.toString(), copayerName);
var c = self.credentials;
var secret = API._buildSecret(c.walletId, c.walletPrivKey, c.network);
self._doJoinWallet(walletId, walletPrivKey, self.credentials.xPubKey, self.credentials.requestPubKey, copayerName, {},
function(err, wallet) {
if (err) return cb(err);
return cb(null, n > 1 ? secret : null);
});
});
};
/**
* Join an existent wallet
*
* @param {String} secret
* @param {String} copayerName
* @param {Object} opts
* @param {Boolean} opts.dryRun[=false] - Simulate wallet join
* @param {Callback} cb
* @returns {Callback} cb - Returns the wallet
*/
API.prototype.joinWallet = function(secret, copayerName, opts, cb) {
var self = this;
if (!cb) {
cb = opts;
opts = {};
log.warn('DEPRECATED WARN: joinWallet should receive 4 parameters.');
}
opts = opts || {};
try {
var secretData = API.parseSecret(secret);
} catch (ex) {
return cb(ex);
}
if (!self.credentials) {
self.seedFromRandom({
network: secretData.network
});
}
self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, self.credentials.xPubKey, self.credentials.requestPubKey, copayerName, {
dryRun: !!opts.dryRun,
}, function(err, wallet) {
if (err) return cb(err);
if (!opts.dryRun) {
self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, secretData.walletPrivKey.toString(), copayerName);
}
return cb(null, wallet);
});
};
/**
* Recreates a wallet, given credentials (with wallet id)
*
* @returns {Callback} cb - Returns the wallet
*/
API.prototype.recreateWallet = function(cb) {
$.checkState(this.credentials);
$.checkState(this.credentials.isComplete());
$.checkState(this.credentials.walletPrivKey);
//$.checkState(this.credentials.hasWalletInfo());
var self = this;
// First: Try to get the wallet with current credentials
this.getStatus({
includeExtendedInfo: true
}, function(err) {
// No error? -> Wallet is ready.
if (!err) {
log.info('Wallet is already created');
return cb();
};
var walletPrivKey = Bitcore.PrivateKey.fromString(self.credentials.walletPrivKey);
var walletId = self.credentials.walletId;
var supportBIP44AndP2PKH = self.credentials.derivationStrategy != Constants.DERIVATION_STRATEGIES.BIP45;
var args = {
name: self.credentials.walletName || 'recovered wallet',
m: self.credentials.m,
n: self.credentials.n,
pubKey: walletPrivKey.toPublicKey().toString(),
network: self.credentials.network,
id: walletId,
supportBIP44AndP2PKH: supportBIP44AndP2PKH,
};
self._doPostRequest('/v2/wallets/', args, function(err, body) {
if (err) {
if (!(err instanceof Errors.WALLET_ALREADY_EXISTS))
return cb(err);
return self.addAccess({}, function(err) {
if (err) return cb(err);
self.openWallet(function(err) {
return cb(err);
});
});
}
if (!walletId) {
walletId = body.walletId;
}
var i = 1;
async.each(self.credentials.publicKeyRing, function(item, next) {
var name = item.copayerName || ('copayer ' + i++);
self._doJoinWallet(walletId, walletPrivKey, item.xPubKey, item.requestPubKey, name, {
supportBIP44AndP2PKH: supportBIP44AndP2PKH,
}, function(err) {
//Ignore error is copayer already in wallet
if (err && err instanceof Errors.COPAYER_IN_WALLET) return next();
return next(err);
});
}, cb);
});
});
};
API.prototype._processCustomData = function(result) {
var copayers = result.wallet.copayers;
if (!copayers) return;
var me = _.find(copayers, {
'id': this.credentials.copayerId
});
if (!me || !me.customData) return;
var customData;
try {
customData = JSON.parse(Utils.decryptMessage(me.customData, this.credentials.personalEncryptingKey));
} catch (e) {
log.warn('Could not decrypt customData:', me.customData);
}
if (!customData) return;
// Add it to result
result.customData = customData;
// Update walletPrivateKey
if (!this.credentials.walletPrivKey && customData.walletPrivKey)
this.credentials.addWalletPrivateKey(customData.walletPrivKey)
}
/**
* Get latest notifications
*
* @param {object} opts
* @param {String} lastNotificationId (optional) - The ID of the last received notification
* @param {String} timeSpan (optional) - A time window on which to look for notifications (in seconds)
* @returns {Callback} cb - Returns error or an array of notifications
*/
API.prototype.getNotifications = function(opts, cb) {
$.checkState(this.credentials);
var self = this;
opts = opts || {};
var url = '/v1/notifications/';
if (opts.lastNotificationId) {
url += '?notificationId=' + opts.lastNotificationId;
} else if (opts.timeSpan) {
url += '?timeSpan=' + opts.timeSpan;
}
self._doGetRequest(url, function(err, result) {
if (err) return cb(err);
var notifications = _.filter(result, function(notification) {
return (notification.creatorId != self.credentials.copayerId);
});
return cb(null, notifications);
});
};
/**
* Get status of the wallet
*
* @param {Boolean} opts.twoStep[=false] - Optional: use 2-step balance computation for improved performance
* @param {Boolean} opts.includeExtendedInfo (optional: query extended status)
* @returns {Callback} cb - Returns error or an object with status information
*/
API.prototype.getStatus = function(opts, cb) {
$.checkState(this.credentials);
if (!cb) {
cb = opts;
opts = {};
log.warn('DEPRECATED WARN: getStatus should receive 2 parameters.')
}
var self = this;
opts = opts || {};
var qs = [];
qs.push('includeExtendedInfo=' + (opts.includeExtendedInfo ? '1' : '0'));
qs.push('twoStep=' + (opts.twoStep ? '1' : '0'));
self._doGetRequest('/v2/wallets/?' + qs.join('&'), function(err, result) {
if (err) return cb(err);
if (result.wallet.status == 'pending') {
var c = self.credentials;
result.wallet.secret = API._buildSecret(c.walletId, c.walletPrivKey, c.network);
}
self._processTxps(result.pendingTxps);
self._processCustomData(result);
return cb(err, result);
});
};
/**
* Get copayer preferences
*
* @param {Callback} cb
* @return {Callback} cb - Return error or object
*/
API.prototype.getPreferences = function(cb) {
$.checkState(this.credentials && this.credentials.isComplete());
$.checkArgument(cb);
var self = this;
self._doGetRequest('/v1/preferences/', function(err, preferences) {
if (err) return cb(err);
return cb(null, preferences);
});
};
/**
* Save copayer preferences
*
* @param {Object} preferences
* @param {Callback} cb
* @return {Callback} cb - Return error or object
*/
API.prototype.savePreferences = function(preferences, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
$.checkArgument(cb);
var self = this;
self._doPutRequest('/v1/preferences/', preferences, cb);
};
API.prototype._computeProposalSignature = function(args) {
var hash;
if (args.outputs) {
$.shouldBeArray(args.outputs);
// should match bws server createTx
var proposalHeader = {
outputs: _.map(args.outputs, function(output) {
$.shouldBeNumber(output.amount);
return _.pick(output, ['toAddress', 'amount', 'message']);
}),
message: args.message || null,
payProUrl: args.payProUrl || null,
};
hash = Utils.getProposalHash(proposalHeader);
} else {
$.shouldBeNumber(args.amount);
hash = Utils.getProposalHash(args.toAddress, args.amount, args.message || null, args.payProUrl || null);
}
return Utils.signMessage(hash, this.credentials.requestPrivKey);
};
/**
* fetchPayPro
*
* @param opts.payProUrl URL for paypro request
* @returns {Callback} cb - Return error or the parsed payment protocol request
* Returns (err,paypro)
* paypro.amount
* paypro.toAddress
* paypro.memo
*/
API.prototype.fetchPayPro = function(opts, cb) {
$.checkArgument(opts)
.checkArgument(opts.payProUrl);
PayPro.get({
url: opts.payProUrl,
http: this.payProHttp,
}, function(err, paypro) {
if (err)
return cb(err);
return cb(null, paypro);
});
};
/**
* Gets list of utxos
*
* @param {Function} cb
* @param {Object} opts
* @param {Array} opts.addresses (optional) - List of addresses from where to fetch UTXOs.
* @returns {Callback} cb - Return error or the list of utxos
*/
API.prototype.getUtxos = function(opts, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
opts = opts || {};
var url = '/v1/utxos/';
if (opts.addresses) {
url += '?' + querystring.stringify({
addresses: [].concat(opts.addresses).join(',')
});
}
this._doGetRequest(url, cb);
};
/**
* Send a transaction proposal
*
* @param {Object} opts
* @param {String} opts.toAddress | opts.outputs[].toAddress
* @param {Number} opts.amount | opts.outputs[].amount
* @param {String} opts.message | opts.outputs[].message
* @param {string} opts.feePerKb - Optional: Use an alternative fee per KB for this TX
* @param {String} opts.payProUrl - Optional: Tx is from a payment protocol URL
* @param {string} opts.excludeUnconfirmedUtxos - Optional: Do not use UTXOs of unconfirmed transactions as inputs
* @param {Object} opts.customData - Optional: Arbitrary data to store along with proposal
* @param {Array} opts.inputs - Optional: Inputs to be used in proposal.
* @param {Array} opts.outputs - Optional: Outputs to be used in proposal.
* @param {Array} opts.utxosToExclude - Optional: List of UTXOS (in form of txid:vout string)
* to exclude from coin selection for this proposal
* @returns {Callback} cb - Return error or the transaction proposal
*/
API.prototype.sendTxProposal = function(opts, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
$.checkArgument(!opts.message || this.credentials.sharedEncryptingKey, 'Cannot create transaction with message without shared Encrypting key');
$.checkArgument(opts);
var self = this;
var args = {
toAddress: opts.toAddress,
amount: opts.amount,
message: API._encryptMessage(opts.message, this.credentials.sharedEncryptingKey) || null,
feePerKb: opts.feePerKb,
payProUrl: opts.payProUrl || null,
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
type: opts.type,
customData: opts.customData,
inputs: opts.inputs,
utxosToExclude: opts.utxosToExclude
};
if (opts.outputs) {
args.outputs = _.map(opts.outputs, function(o) {
return {
toAddress: o.toAddress,
script: o.script,
amount: o.amount,
message: API._encryptMessage(o.message, self.credentials.sharedEncryptingKey) || null,
};
});
}
log.debug('Generating & signing tx proposal:', JSON.stringify(args));
args.proposalSignature = this._computeProposalSignature(args);
this._doPostRequest('/v1/txproposals/', args, function(err, txp) {
if (err) return cb(err);
return cb(null, txp);
});
};
/**
* Create a new address
*
* @param {Object} opts
* @param {Boolean} opts.ignoreMaxGap[=false]
* @param {Callback} cb
* @returns {Callback} cb - Return error or the address
*/
API.prototype.createAddress = function(opts, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
var self = this;
if (!cb) {
cb = opts;
opts = {};
log.warn('DEPRECATED WARN: createAddress should receive 2 parameters.')
}
opts = opts || {};
self._doPostRequest('/v2/addresses/', opts, function(err, address) {
if (err) return cb(err);
if (!Verifier.checkAddress(self.credentials, address)) {
return cb(new Errors.SERVER_COMPROMISED);
}
return cb(null, address);
});
};
/**
* Get your main addresses
*
* @param {Object} opts
* @param {Boolean} opts.doNotVerify
* @param {Numeric} opts.limit (optional) - Limit the resultset. Return all addresses by default.
* @param {Boolean} [opts.reverse=false] (optional) - Reverse the order of returned addresses.
* @param {Callback} cb
* @returns {Callback} cb - Return error or the array of addresses
*/
API.prototype.getMainAddresses = function(opts, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
var self = this;
opts = opts || {};
var args = [];
if (opts.limit) args.push('limit=' + opts.limit);
if (opts.reverse) args.push('reverse=1');
var qs = '';
if (args.length > 0) {
qs = '?' + args.join('&');
}
var url = '/v1/addresses/' + qs;
self._doGetRequest(url, function(err, addresses) {
if (err) return cb(err);
if (!opts.doNotVerify) {
var fake = _.any(addresses, function(address) {
return !Verifier.checkAddress(self.credentials, address);
});
if (fake)
return cb(new Errors.SERVER_COMPROMISED);
}
return cb(null, addresses);
});
};
/**
* Update wallet balance
*
* @param {Boolean} opts.twoStep[=false] - Optional: use 2-step balance computation for improved performance
* @param {Callback} cb
*/
API.prototype.getBalance = function(opts, cb) {
if (!cb) {
cb = opts;
opts = {};
log.warn('DEPRECATED WARN: getBalance should receive 2 parameters.')
}
var self = this;
opts = opts || {};
$.checkState(this.credentials && this.credentials.isComplete());
var url = '/v1/balance/';
if (opts.twoStep) url += '?twoStep=1';
this._doGetRequest(url, cb);
};
/**
* Get list of transactions proposals
*
* @param {Object} opts
* @param {Boolean} opts.doNotVerify
* @param {Boolean} opts.forAirGapped
* @return {Callback} cb - Return error or array of transactions proposals
*/
API.prototype.getTxProposals = function(opts, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
var self = this;
self._doGetRequest('/v1/txproposals/', function(err, txps) {
if (err) return cb(err);
self._processTxps(txps);
async.every(txps,
function(txp, acb) {
if (opts.doNotVerify) return acb(true);
self.getPayPro(txp, function(err, paypro) {
var isLegit = Verifier.checkTxProposal(self.credentials, txp, {
paypro: paypro,
});
return acb(isLegit);
});
},
function(isLegit) {
if (!isLegit)
return cb(new Errors.SERVER_COMPROMISED);
var result;
if (opts.forAirGapped) {
result = {
txps: JSON.parse(JSON.stringify(txps)),
encryptedPkr: Utils.encryptMessage(JSON.stringify(self.credentials.publicKeyRing), self.credentials.personalEncryptingKey),
m: self.credentials.m,
n: self.credentials.n,
};
} else {
result = txps;
}
return cb(null, result);
});
});
};
API.prototype.getPayPro = function(txp, cb) {
var self = this;
if (!txp.payProUrl || this.doNotVerifyPayPro)
return cb();
PayPro.get({
url: txp.payProUrl,
http: self.payProHttp,
}, function(err, paypro) {
if (err) return cb(new Error('Cannot check transaction now:' + err));
return cb(null, paypro);
});
};
/**
* Sign a transaction proposal
*
* @param {Object} txp
* @param {Callback} cb
* @return {Callback} cb - Return error or object
*/
API.prototype.signTxProposal = function(txp, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
$.checkArgument(txp.creatorId);
var self = this;
if (!self.canSign() && !txp.signatures)
return cb(new Error('You do not have the required keys to sign transactions'));
if (self.isPrivKeyEncrypted())
return cb(new Error('Private Key is encrypted, cannot sign'));
self.getPayPro(txp, function(err, paypro) {
if (err) return cb(err);
var isLegit = Verifier.checkTxProposal(self.credentials, txp, {
paypro: paypro,
});
if (!isLegit)
return cb(new Errors.SERVER_COMPROMISED);
var signatures = txp.signatures || self._signTxp(txp);
var url = '/v1/txproposals/' + txp.id + '/signatures/';
var args = {
signatures: signatures
};
self._doPostRequest(url, args, function(err, txp) {
if (err) return cb(err);
self._processTxps([txp]);
return cb(null, txp);
});
});
};
/**
* Sign transaction proposal from AirGapped
*
* @param {Object} txp
* @param {String} encryptedPkr
* @param {Number} m
* @param {Number} n
* @return {Object} txp - Return transaction
*/
API.prototype.signTxProposalFromAirGapped = function(txp, encryptedPkr, m, n) {
$.checkState(this.credentials);
var self = this;
if (!self.canSign())
throw new Errors.MISSING_PRIVATE_KEY;
if (self.isPrivKeyEncrypted())
throw new Errors.ENCRYPTED_PRIVATE_KEY;
var publicKeyRing;
try {
publicKeyRing = JSON.parse(Utils.decryptMessage(encryptedPkr, self.credentials.personalEncryptingKey));
} catch (ex) {
throw new Error('Could not decrypt public key ring');
}
if (!_.isArray(publicKeyRing) || publicKeyRing.length != n) {
throw new Error('Invalid public key ring');
}
self.credentials.m = m;
self.credentials.n = n;
self.credentials.addressType = txp.addressType;
self.credentials.addPublicKeyRing(publicKeyRing);
if (!Verifier.checkTxProposalBody(self.credentials, txp))
throw new Error('Fake transaction proposal');
return self._signTxp(txp);
};
/**
* Reject a transaction proposal
*
* @param {Object} txp
* @param {String} reason
* @param {Callback} cb
* @return {Callback} cb - Return error or object
*/
API.prototype.rejectTxProposal = function(txp, reason, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
$.checkArgument(cb);
var self = this;
var url = '/v1/txproposals/' + txp.id + '/rejections/';
var args = {
reason: API._encryptMessage(reason, self.credentials.sharedEncryptingKey) || '',
};
self._doPostRequest(url, args, function(err, txp) {
if (err) return cb(err);
self._processTxps([txp]);
return cb(null, txp);
});
};
/**
* Broadcast raw transaction
*
* @param {Object} opts
* @param {String} opts.network
* @param {String} opts.rawTx
* @param {Callback} cb
* @return {Callback} cb - Return error or txid
*/
API.prototype.broadcastRawTx = function(opts, cb) {
$.checkState(this.credentials);
$.checkArgument(cb);
var self = this;
opts = opts || {};
var url = '/v1/broadcast_raw/';
self._doPostRequest(url, opts, function(err, txid) {
if (err) return cb(err);
return cb(null, txid);
});
};
API.prototype._doBroadcast = function(txp, cb) {
var self = this;
var url = '/v1/txproposals/' + txp.id + '/broadcast/';
self._doPostRequest(url, {}, function(err, txp) {
if (err) return cb(err);
return cb(null, txp);
});
};
/**
* Broadcast a transaction proposal
*
* @param {Object} txp
* @param {Callback} cb
* @return {Callback} cb - Return error or object
*/
API.prototype.broadcastTxProposal = function(txp, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
var