UNPKG

bitcore-wallet-client-dash

Version:
1,740 lines (1,455 loc) 75.1 kB
'use strict'; var _ = require('lodash'); var $ = require('preconditions').singleton(); var util = require('util'); var async = require('async'); var events = require('events'); var Bitcore = require('@dashevo/dashcore-lib'); var Bitcore_ = { btc: Bitcore, bch: require('bitcore-lib-cash'), }; var Mnemonic = require('@dashevo/dashcore-mnemonic'); var sjcl = require('sjcl'); var url = require('url'); var querystring = require('querystring'); var Stringify = require('json-stable-stringify'); var request = require('superagent'); 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 = 'https://bws.dashevo.org/bws/api'; /** * @desc ClientAPI constructor. * * @param {Object} opts * @constructor */ function API(opts) { opts = opts || {}; this.request = opts.request || request; this.baseUrl = opts.baseUrl || BASE_URL; this.payProHttp = null; // Only for testing this.doNotVerifyPayPro = opts.doNotVerifyPayPro; this.timeout = opts.timeout || 50000; this.logLevel = opts.logLevel || 'silent'; this.supportStaffWalletId = opts.supportStaffWalletId; log.setLevel(this.logLevel); }; 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.notificationIncludeOwn = !!opts.notificationIncludeOwn; self._initNotifications(opts); return cb(); }; API.prototype.dispose = function(cb) { var self = this; self._disposeNotifications(); self._logout(cb); }; API.prototype._fetchLatestNotifications = function(interval, cb) { var self = this; cb = cb || function() {}; var opts = { lastNotificationId: self.lastNotificationId, includeOwn: self.notificationIncludeOwn, }; 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 * @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>'; } }; API.prototype._processTxNotes = function(notes) { var self = this; if (!notes) return; var encryptingKey = self.credentials.sharedEncryptingKey; _.each([].concat(notes), function(note) { note.encryptedBody = note.body; note.body = API._decryptMessage(note.body, encryptingKey); note.encryptedEditedByName = note.editedByName; note.editedByName = API._decryptMessage(note.editedByName, encryptingKey); }); }; /** * 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; txp.creatorName = API._decryptMessage(txp.creatorName, encryptingKey); _.each(txp.actions, function(action) { action.copayerName = API._decryptMessage(action.copayerName, encryptingKey); 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 = _.some(txp.inputs, function(input) { return input.confirmations == 0; }); self._processTxNotes(txp.note); }); }; /** * Parse errors * @private * @static * @memberof Client.API * @param {Object} body */ API._parseError = function(body) { if (!body) return; if (_.isString(body)) { try { body = JSON.parse(body); } catch (e) { body = { error: body }; } } var ret; if (body.code) { if (Errors[body.code]) { ret = new Errors[body.code]; if (body.message) ret.message = body.message; } else { ret = new Error(body.code + ': ' + body.message); } } else { ret = new Error(body.error || JSON.stringify(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.coin - default 'btc' * @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.coin || 'btc', opts.network || 'livenet'); }; var _deviceValidated; /** * Seed from random * * @param {Object} opts * @param {String} opts.passphrase * @param {String} opts.skipDeviceValidation */ API.prototype.validateKeyDerivation = function(opts, cb) { var self = this; opts = opts || {}; var c = self.credentials; function testMessageSigning(xpriv, xpub) { var nonHardenedPath = 'm/0/0'; var message = 'Lorem ipsum dolor sit amet, ne amet urbanitas percipitur vim, libris disputando his ne, et facer suavitate qui. Ei quidam laoreet sea. Cu pro dico aliquip gubergren, in mundi postea usu. Ad labitur posidonium interesset duo, est et doctus molestie adipiscing.'; var priv = xpriv.deriveChild(nonHardenedPath).privateKey; var signature = Utils.signMessage(message, priv); var pub = xpub.deriveChild(nonHardenedPath).publicKey; return Utils.verifyMessage(message, signature, pub); }; function testHardcodedKeys() { var words = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; var xpriv = Mnemonic(words).toHDPrivateKey(); if (xpriv.toString() != 'xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu') return false; xpriv = xpriv.deriveChild("m/44'/5'/0'"); if (xpriv.toString() != 'xprv9yYtLMwDA1nkeg8xvt4tfScuivNGdjMUDNpc2QfFC34YPMwzoaYAvVjkDvJ1APVeGMJnbJ6gPZMq4G7UfExoP5PpAS3UCF9utjeWa5eXq23') return false; var xpub = Bitcore.HDPublicKey.fromString('xpub6CYEjsU6zPM3sADS2ubu2aZeGxCm3C5KabkCpo4rkNbXGAH9M7rRUJ4E5CKiyUddmRzrSCopPzisTBrXkfCD4o577XKM9mzyZtP1Xdbizyk'); return testMessageSigning(xpriv, xpub); }; function testLiveKeys() { var words; try { words = c.getMnemonic(); } catch (ex) {} var xpriv; if (words && (!c.mnemonicHasPassphrase || opts.passphrase)) { var m = new Mnemonic(words); xpriv = m.toHDPrivateKey(opts.passphrase, c.network); } if (!xpriv) { xpriv = new Bitcore.HDPrivateKey(c.xPrivKey); } xpriv = xpriv.deriveChild(c.getBaseAddressDerivationPath()); var xpub = new Bitcore.HDPublicKey(c.xPubKey); return testMessageSigning(xpriv, xpub); }; var hardcodedOk = true; if (!_deviceValidated && !opts.skipDeviceValidation) { hardcodedOk = testHardcodedKeys(); _deviceValidated = true; } var liveOk = (c.canSign() && !c.isPrivKeyEncrypted()) ? testLiveKeys() : true; self.keyDerivationOk = hardcodedOk && liveOk; return cb(null, self.keyDerivationOk); }; /** * Seed from random with mnemonic * * @param {Object} opts * @param {String} opts.coin - default 'btc' * @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.coin || 'btc', 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 * @param {String} opts.coin - default 'btc' * @param {Number} opts.account - default 0 * @param {String} opts.derivationStrategy - default 'BIP44' */ API.prototype.seedFromExtendedPrivateKey = function(xPrivKey, opts) { opts = opts || {}; this.credentials = Credentials.fromExtendedPrivateKey(opts.coin || 'btc', xPrivKey, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44, opts); }; /** * Seed from Mnemonics (language autodetected) * Can throw an error if mnemonic is invalid * * @param {String} BIP39 words * @param {Object} opts * @param {String} opts.coin - default 'btc' * @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.coin || 'btc', opts.network || 'livenet', words, opts.passphrase, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44, opts); }; /** * 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 {String} opts.coin - default 'btc' * @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(opts.coin || 'btc', xPubKey, source, entropySourceHex, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44); }; /** * Export wallet * * @param {Object} opts * @param {Boolean} opts.password * @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(); } else if (opts.password) { c.decryptPrivateKey(opts.password); } output = JSON.stringify(c.toObj()); return output; }; /** * Import wallet * * @param {Object} str - The serialized JSON created with #export */ API.prototype.import = function(str) { 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.coin - default 'btc' * @param {String} opts.network - default 'livenet' * @param {String} opts.passphrase * @param {Number} opts.account - default 0 * @param {String} opts.derivationStrategy - default 'BIP44' * @param {String} opts.entropySourcePath - Only used if the wallet was created on a HW wallet, in which that private keys was not available for all the needed derivations * @param {String} opts.walletPrivKey - if available, walletPrivKey for encrypting metadata */ API.prototype.importFromMnemonic = function(words, opts, cb) { log.debug('Importing from 12 Words'); var self = this; opts = opts || {}; function derive(nonCompliantDerivation) { return Credentials.fromMnemonic(opts.coin || 'btc', opts.network || 'livenet', words, opts.passphrase, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44, { nonCompliantDerivation: nonCompliantDerivation, entropySourcePath: opts.entropySourcePath, walletPrivKey: opts.walletPrivKey, }); }; try { self.credentials = derive(false); } catch (e) { log.info('Mnemonic error:', e); return cb(new Errors.INVALID_BACKUP); } self._import(function(err, ret) { if (!err) return cb(null, ret); if (err instanceof Errors.INVALID_BACKUP) return cb(err); if (err instanceof Errors.NOT_AUTHORIZED || err instanceof Errors.WALLET_DOES_NOT_EXIST) { var altCredentials = derive(true); if (altCredentials.xPubKey.toString() == self.credentials.xPubKey.toString()) return cb(err); self.credentials = altCredentials; return self._import(cb); } return cb(err); }); }; /* * Import from extended private key * * @param {String} xPrivKey * @param {String} opts.coin - default 'btc' * @param {Number} opts.account - default 0 * @param {String} opts.derivationStrategy - default 'BIP44' * @param {String} opts.compliantDerivation - default 'true' * @param {String} opts.walletPrivKey - if available, walletPrivKey for encrypting metadata * @param {Callback} cb - The callback that handles the response. It returns a flag indicating that the wallet is imported. */ API.prototype.importFromExtendedPrivateKey = function(xPrivKey, opts, cb) { log.debug('Importing from Extended Private Key'); if (!cb) { cb = opts; opts = {}; log.warn('DEPRECATED WARN: importFromExtendedPrivateKey should receive 3 parameters.'); } try { this.credentials = Credentials.fromExtendedPrivateKey(opts.coin || 'btc', xPrivKey, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44, opts); } 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 {String} opts.coin - default 'btc' * @param {Number} opts.account - default 0 * @param {String} opts.derivationStrategy - default 'BIP44' * @param {String} opts.compliantDerivation - default 'true' */ 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(opts.coin || 'btc', xPubKey, source, entropySourceHex, opts.account || 0, opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP44, opts); } 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, coin, cb) { var self = this; if (_.isFunction(coin)) { cb = coin; coin = 'btc'; } var B = Bitcore_[coin]; var privateKey = new B.PrivateKey(privateKey); var address = privateKey.publicKey.toAddress(); self.getUtxos({ addresses: address.toString(), }, function(err, utxos) { if (err) return cb(err); return cb(null, _.sumBy(utxos, 'satoshis')); }); }; API.prototype.buildTxFromPrivateKey = function(privateKey, destinationAddress, opts, cb) { var self = this; opts = opts || {}; var coin = opts.coin || 'btc'; var B = Bitcore_[coin]; var privateKey = B.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 = _.sumBy(utxos, 'satoshis') - fee; if (amount <= 0) return next(new Errors.INSUFFICIENT_FUNDS); var tx; try { var toAddress = B.Address.fromString(destinationAddress); tx = new B.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; self._processStatus(ret); if (!self.credentials.hasWalletInfo()) { var me = _.find(wallet.copayers, { id: self.credentials.copayerId }); self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, 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 happen in AIR-GAPPED flows log.warn('Could not verify copayers key (missing wallet Private Key)'); } self.credentials.addPublicKeyRing(API._extractPublicKeyRing(wallet.copayers)); self.emit('walletCompleted', wallet); return cb(null, ret); }); }; API.prototype._getHeaders = function(method, url, args) { var headers = { 'x-client-version': 'bwc-' + Package.version, }; if (this.supportStaffWalletId) { headers['x-wallet-id'] = this.supportStaffWalletId; } return headers; }; /** * Do an HTTP request * @private * * @param {Object} method * @param {String} url * @param {Object} args * @param {Callback} cb */ API.prototype._doRequest = function(method, url, args, useSession, cb) { var self = this; var headers = self._getHeaders(method, url, args); if (self.credentials) { headers['x-identity'] = self.credentials.copayerId; if (useSession && self.session) { headers['x-session'] = self.session; } else { var reqSignature; var key = args._requestPrivKey || self.credentials.requestPrivKey; if (key) { delete args['_requestPrivKey']; reqSignature = API._signRequest(method, url, args, key); } headers['x-signature'] = reqSignature; } } var r = self.request[method](self.baseUrl + url); r.accept('json'); _.each(headers, function(v, k) { if (v) r.set(k, v); }); if (args) { if (method == 'post' || method == 'put') { r.send(args); } else { r.query(args); } } r.timeout(self.timeout); r.end(function(err, res) { if (!res) { return cb(new Errors.CONNECTION_ERROR); } if (res.body) log.debug(util.inspect(res.body, { depth: 10 })); if (res.status !== 200) { if (res.status === 404) return cb(new Errors.NOT_FOUND); if (!res.status) return cb(new Errors.CONNECTION_ERROR); log.error('HTTP Error:' + res.status); if (!res.body) return cb(new Error(res.status)); return cb(API._parseError(res.body)); } if (res.body === '{"error":"read ECONNRESET"}') return cb(new Errors.ECONNRESET_ERROR(JSON.parse(res.body))); return cb(null, res.body, res.header); }); }; API.prototype._login = function(cb) { this._doPostRequest('/v1/login', {}, cb); }; API.prototype._logout = function(cb) { this._doPostRequest('/v1/logout', {}, cb); }; /** * Do an HTTP request * @private * * @param {Object} method * @param {String} url * @param {Object} args * @param {Callback} cb */ API.prototype._doRequestWithLogin = function(method, url, args, cb) { var self = this; function doLogin(cb) { self._login(function(err, s) { if (err) return cb(err); if (!s) return cb(new Errors.NOT_AUTHORIZED); self.session = s; cb(); }); }; async.waterfall([ function(next) { if (self.session) return next(); doLogin(next); }, function(next) { self._doRequest(method, url, args, true, function(err, body, header) { if (err && err instanceof Errors.NOT_AUTHORIZED) { doLogin(function(err) { if (err) return next(err); return self._doRequest(method, url, args, true, next); }); } next(null, body, header); }); }, ], cb); }; /** * 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, false, cb); }; API.prototype._doPutRequest = function(url, args, cb) { return this._doRequest('put', url, args, false, 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, {}, false, cb); }; API.prototype._doGetRequestWithLogin = function(url, cb) { url += url.indexOf('?') > 0 ? '&' : '?'; url += 'r=' + _.random(10000, 99999); return this._doRequestWithLogin('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, {}, false, cb); }; API._buildSecret = function(walletId, walletPrivKey, coin, 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 _.padEnd(widBase58, 22, '0') + walletPrivKey.toWIF() + (network == 'testnet' ? 'T' : 'L') + coin; }; 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, 75]); 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]; var coin = secretSplit[3] || 'btc'; return { walletId: walletId, walletPrivKey: walletPrivKey, coin: coin, network: networkChar == 'T' ? 'testnet' : 'livenet', }; } catch (ex) { throw new Error('Invalid secret'); } }; API.getRawTx = function(txp) { var t = Utils.buildTx(txp); return t.uncheckedSerialize(); }; API.signTxp = function(txp, derivedXPrivKey) { //Derive proper key to sign, for each input var privs = []; var derived = {}; if (txp.coin == 'bch') { var xpriv = new Bitcore_.bch.HDPrivateKey(derivedXPrivKey); } else { var xpriv = new Bitcore.HDPrivateKey(derivedXPrivKey); } _.each(txp.inputs, function(i) { $.checkState(i.path, "Input derivation path not available (signing transaction)") if (!derived[i.path]) { derived[i.path] = xpriv.deriveChild(i.path).privateKey; privs.push(derived[i.path]); } }); var t = Utils.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, password) { var derived = this.credentials.getDerivedXPrivKey(password, txp.coin); return API.signTxp(txp, derived); }; 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'); $.checkState(txp.coin); var bitcore = Bitcore_[txp.coin]; 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.deriveChild(txp.inputPaths[i]).publicKey; var s = { inputIndex: i, signature: signature, sigtype: bitcore.crypto.Signature.SIGHASH_ALL | bitcore.crypto.Signature.SIGHASH_FORKID, 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 {String} opts.coin * @param {Callback} cb */ API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, requestPubKey, copayerName, opts, cb) { $.shouldBeFunction(cb); var self = this; 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 encCopayerName = Utils.encryptMessage(copayerName, this.credentials.sharedEncryptingKey); var args = { walletId: walletId, coin: opts.coin, name: encCopayerName, 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); self._processWallet(body.wallet); 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? * * @return {Boolean} */ API.prototype.isPrivKeyEncrypted = function() { return this.credentials && this.credentials.isPrivKeyEncrypted(); }; /** * 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; }; /** * Returns unencrypted extended private key and mnemonics * * @param password */ API.prototype.getKeys = function(password) { return this.credentials.getKeys(password); }; /** * Checks is password is valid * Returns null (keys not encrypted), true or false. * * @param password */ API.prototype.checkPassword = function(password) { if (!this.isPrivKeyEncrypted()) return; try { var keys = this.getKeys(password); return !!keys.xPrivKey; } catch (e) { return false; }; }; /** * 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.encryptPrivateKey = function(password, opts) { this.credentials.encryptPrivateKey(password, opts || API.privateKeyEncryptionOpts); }; /** * disables encryption for private key. * * @param {String} password Password used to encrypt */ API.prototype.decryptPrivateKey = function(password) { return this.credentials.decryptPrivateKey(password); }; /** * Get current fee levels for the specified network * * @param {string} coin - 'btc' (default) or 'bch' * @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(coin, network, cb) { var self = this; $.checkArgument(coin || _.includes(['btc', 'bch'], coin)); $.checkArgument(network || _.includes(['livenet', 'testnet'], network)); self._doGetRequest('/v2/feelevels/?coin=' + (coin || 'btc') + '&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); }; API.prototype._checkKeyDerivation = function() { var isInvalid = (this.keyDerivationOk === false); if (isInvalid) { log.error('Key derivation for this device is not working as expected'); } return !isInvalid; }; /** * * Create a wallet. * @param {String} walletName * @param {String} copayerName * @param {Number} m * @param {Number} n * @param {object} opts (optional: advanced options) * @param {string} opts.coin[='btc'] - The coin for this wallet (btc, bch). * @param {string} opts.network[='livenet'] * @param {string} opts.singleAddress[=false] - The wallet will only ever have one address. * @param {String} opts.walletPrivKey - set a walletPrivKey (instead of random) * @param {String} opts.id - set a id for wallet (instead of server given) * @param cb * @return {undefined} */ API.prototype.createWallet = function(walletName, copayerName, m, n, opts, cb) { var self = this; if (!self._checkKeyDerivation()) return cb(new Error('Cannot create new wallet')); if (opts) $.shouldBeObject(opts); opts = opts || {}; var coin = opts.coin || 'btc'; if (!_.includes(['btc', 'bch'], coin)) return cb(new Error('Invalid coin')); var network = opts.network || 'livenet'; if (!_.includes(['testnet', 'livenet'], network)) return cb(new Error('Invalid network')); if (!self.credentials) { log.info('Generating new keys'); self.seedFromRandom({ coin: coin, network: network }); } else { log.info('Using existing keys'); } if (coin != self.credentials.coin) { return cb(new Error('Existing keys were created for a different coin')); } 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 c = self.credentials; c.addWalletPrivateKey(walletPrivKey.toString()); var encWalletName = Utils.encryptMessage(walletName, c.sharedEncryptingKey); var args = { name: encWalletName, m: m, n: n, pubKey: (new Bitcore.PrivateKey(walletPrivKey)).toPublicKey().toString(), coin: coin, network: network, singleAddress: !!opts.singleAddress, id: opts.id, }; self._doPostRequest('/v2/wallets/', args, function(err, res) { if (err) return cb(err); var walletId = res.walletId; c.addWalletInfo(walletId, walletName, m, n, copayerName); var secret = API._buildSecret(c.walletId, c.walletPrivKey, c.coin, c.network); self._doJoinWallet(walletId, walletPrivKey, c.xPubKey, c.requestPubKey, copayerName, { coin: coin }, 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 {string} opts.coin[='btc'] - The expected coin for this wallet (btc, bch). * @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.'); } if (!self._checkKeyDerivation()) return cb(new Error('Cannot join wallet')); opts = opts || {}; var coin = opts.coin || 'btc'; if (!_.includes(['btc', 'bch'], coin)) return cb(new Error('Invalid coin')); try { var secretData = API.parseSecret(secret); } catch (ex) { return cb(ex); } if (!self.credentials) { self.seedFromRandom({ coin: coin, network: secretData.network }); } self.credentials.addWalletPrivateKey(secretData.walletPrivKey.toString()); self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, self.credentials.xPubKey, self.credentials.requestPubKey, copayerName, { coin: coin, 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, 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 c = self.credentials; var walletPrivKey = Bitcore.PrivateKey.fromString(c.walletPrivKey); var walletId = c.walletId; var supportBIP44AndP2PKH = c.derivationStrategy != Constants.DERIVATION_STRATEGIES.BIP45; var encWalletName = Utils.encryptMessage(c.walletName || 'recovered wallet', c.sharedEncryptingKey); var coin = c.coin; var args = { name: encWalletName, m: c.m, n: c.n, pubKey: walletPrivKey.toPublicKey().toString(), coin: c.coin, network: c.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, { coin: c.coin, 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._processWallet = function(wallet) { var self = this; var encryptingKey = self.credentials.sharedEncryptingKey; var name = Utils.decryptMessage(wallet.name, encryptingKey); if (name != wallet.name) { wallet.encryptedName = wallet.name; } wallet.name = name; _.each(wallet.copayers, function(copayer) { var name = Utils.decryptMessage(copayer.name, encryptingKey); if (name != copayer.name) { copayer.encryptedName = copayer.name; } copayer.name = name; _.each(copayer.requestPubKeys, function(access) { if (!access.name) return; var name = Utils.decryptMessage(access.name, encryptingKey); if (name != access.name) { access.encryptedName = access.name; } access.name = name; }); }); }; API.prototype._processStatus = function(status) { var self = this; function processCustomData(data) { var copayers = data.wallet.copayers; if (!copayers) return; var me = _.find(copayers, { 'id': self.credentials.copayerId }); if (!me || !me.customData) return; var customData; try { customData = JSON.parse(Utils.decryptMessage(me.customData, self.credentials.personalEncryptingKey)); } catch (e) { log.warn('Could not decrypt customData:', me.customData); } if (!customData) return; // Add it to result data.customData = customData; // Update walletPrivateKey if (!self.credentials.walletPrivKey && customData.walletPrivKey) self.credentials.addWalletPrivateKey(customData.walletPrivKey); }; processCustomData(status); self._processWallet(status.wallet); self._processTxps(status.pendingTxps); } /** * Get latest notifications * * @param {object} opts * @param {String} opts.lastNotificationId (optional) - The ID of the last received notification * @param {String} opts.timeSpan (optional) - A time window on which to look for notifications (in seconds) * @param {String} opts.includeOwn[=false] (optional) - Do not ignore notifications generated by the current copayer * @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._doGetRequestWithLogin(url, function(err, result) { if (err) return cb(err); var notifications = _.filter(result, function(notification) { return opts.includeOwn || (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.coin, c.network); } self._processStatus(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); $.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); $.checkArgument(cb); var self = this; self._doPutRequest('/v1/preferences/', preferences, cb); }; /** * 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); }; API.prototype._getCreateTxProposalArgs = function(opts) { var self = this; var args = _.cloneDeep(opts); args.message = API._encryptMessage(opts.message, this.credentials.sharedEncryptingKey) || null; args.payProUrl = opts.payProUrl || null; _.each(args.outputs, function(o) { o.message = API._encryptMessage(o.message, self.credentials.sharedEncryptingKey) || null; }); return args; }; /** * Create a transaction proposal * * @param {Object} opts * @param {string} opts.txProposalId - Optional. If provided it will be used as this TX proposal ID. Should be unique in the scope of the wallet. * @param {Array} opts.outputs - List of outputs. * @param {string} opts.outputs[].toAddress - Destination address. * @param {number} opts.outputs[].amount - Amount to transfer in satoshi. * @param {string} opts.outputs[].message - A message to attach to this output. * @param {string} opts.message - A message to attach to this transaction. * @param {number} opts.feeLevel[='normal'] - Optional. Specify the fee level for this TX ('priority', 'normal', 'economy', 'superEconomy'). * @param {number} opts.feePerKb - Optional. Specify the fee per KB for this TX (in satoshi). * @param {string} opts.changeAddress - Optional. Use this address as the change address for the tx. The address should belong to the wallet. In the case of singleAddress wallets, the first main address will be used. * @param {Boolean} opts.sendMax - Optional. Send maximum amount of funds that make sense under the specified fee/feePerKb conditions. (defaults to false). * @param {string} opts.payProUrl - Optional. Paypro URL for peers to verify TX * @param {Boolean} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs * @param {Boolean} opts.validateOutputs[=true] - Optional. Perform validation on outputs. * @param {Boolean} opts.dryRun[=false] - Optional. Simulate the action but do not change server state. * @param {Array} opts.inputs - Optional. Inputs for this TX * @param {number} opts.fee - Optional. Use an fixed fee for this TX (only when opts.inputs is specified) * @param {Boolean} opts.noShuffleOutputs - Optional. If set, TX outputs won't be shuffled. Defaults to false * @returns {Callback} cb - Return error or the transaction proposal */ API.prototype.createTxProposal = function(opts, cb) { $.chec