diff --git a/bitcore-wallet-client.js b/bitcore-wallet-client.js new file mode 100644 index 00000000..1b0aa95e --- /dev/null +++ b/bitcore-wallet-client.js @@ -0,0 +1,106065 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o'; + } +}; + +/** + * 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; + var encryptingKey = self.credentials.sharedEncryptingKey; + + if (!txps) return; + _.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) { + ret = new ClientError(body.code, body.message); + } else { + ret = { + code: 'ERROR', + error: body ? body.error : 'There was an unknown error processing the request', + }; + } + 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 WalletUtils.signMessage(message, privKey); +}; + + +/** + * Seed from random + * + * @param {String} network + */ +API.prototype.seedFromRandom = function(network) { + this.credentials = Credentials.create(network); +}; + +/** + * Seed from extended private key + * + * @param {String} xPrivKey + */ +API.prototype.seedFromExtendedPrivateKey = function(xPrivKey) { + this.credentials = Credentials.fromExtendedPrivateKey(xPrivKey); +}; + +/** + * Seed from external wallet public key + * + * @param {String} xPubKey - Extended public key + * @param {String} source - name of external wallet source (ex: ledger) + * @params{Number} index - index of the external key + */ +API.prototype.seedFromExternalWalletPublicKey = function(xPubKey, source, index) { + this.credentials = Credentials.fromExternalWalletPublicKey(xPubKey, source, index); +} + + +/** + * Export wallet + * + * @param {Object} opts + * @param {Boolean} opts.compressed + * @param {Boolean} opts.noSign + */ +API.prototype.export = function(opts) { + $.checkState(this.credentials); + + opts = opts || {}; + + var output; + + var cred = Credentials.fromObj(this.credentials); + if (opts.noSign) { + delete cred.xPrivKey; + delete cred.xPrivKeyEncrypted; + } + + if (opts.compressed) { + output = cred.exportCompressed(); + } else { + output = JSON.stringify(cred.toObj()); + } + + return output; +} + + +/** + * Import wallet + * + * @param {Object} opts + * @param {Boolean} opts.compressed + * @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 || {}; + + var credentials; + try { + if (opts.compressed) { + credentials = Credentials.importCompressed(str, opts.password); + // HACK: simulate incomplete credentials + delete credentials.m; + } else { + credentials = Credentials.fromObj(JSON.parse(str)); + } + } catch (ex) { + throw new Error('Error importing from source:' + ex); + } + this.credentials = credentials; +}; + +/** + * 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': 'bws-' + Package.version, + }, + method: method, + url: absUrl, + body: args, + json: true, + withCredentials: false, + timeout: 10000 + }; + + 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({ + code: 'CONNERROR', + }); + } + + if (res.statusCode != 200) { + if (res.statusCode == 404) + return cb({ + code: 'NOTFOUND' + }); + + if (!res.statusCode) + return cb({ + code: 'CONNERROR', + }); + + return cb(API._parseError(body)); + } + + if (body === '{"error":"read ECONNRESET"}') + return cb(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); +}; + +/** + * Join + * @private + * + * @param {String} walletId + * @param {String} walletPrivKey + * @param {String} xPubKey + * @param {String} requestPubKey + * @param {String} copayerName + * @param {Object} Optional args + * @param {Object} .isTemporaryRequestKey + * @param {Callback} cb + */ +API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, requestPubKey, copayerName, opts, cb) { + opts = opts || {}; + $.shouldBeFunction(cb); + + var args = { + walletId: walletId, + name: copayerName, + xPubKey: xPubKey, + requestPubKey: requestPubKey, + isTemporaryRequestKey: !!opts.isTemporaryRequestKey, + }; + var hash = WalletUtils.getCopayerHash(args.name, args.xPubKey, args.requestPubKey); + args.copayerSignature = WalletUtils.signMessage(hash, walletPrivKey); + + var url = '/v1/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; +}; + +/** + * Get external wallet key index + * + * @return {Number} + */ +API.prototype.getExternalIndex = function() { + return this.credentials ? this.credentials.getExternalIndex() : 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', 'isTemporaryRequestKey']); + pkr.copayerName = copayer.name; + return pkr; + }); +}; + +/** + * 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; + + var wasComplete = self.credentials.isComplete(); + + if (wasComplete && !self.credentials.hasTemporaryRequestKeys()) + return cb(null, true); + + self._doGetRequest('/v1/wallets/', function(err, ret) { + if (err) return cb(err); + var wallet = ret.wallet; + + if (wallet.status != 'complete') + return cb(); + + if (self.credentials.walletPrivKey) { + + if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) { + return cb(new ServerCompromisedError( + 'Copayers in the wallet could not be verified to have known the wallet secret')); + } + } else { + log.warn('Could not verify copayers key (missing wallet Private Key)'); + } + + if (wasComplete) { + + // Wallet was completed. We are just updating temporary request keys + + self.credentials.updatePublicKeyRing(API._extractPublicKeyRing(wallet.copayers)); + if (!self.credentials.hasTemporaryRequestKeys()) + self.emit('walletCompleted', wallet); + } else { + + + // Wallet was not complete. We are completing it. + + self.credentials.addPublicKeyRing(API._extractPublicKeyRing(wallet.copayers)); + + 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); + } + self.emit('walletCompleted', wallet); + } + if (ret.pendingTxps) + self._processTxps(ret.pendingTxps); + + return cb(null, ret); + }); +}; + + +/** + * 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); + }); +}; + +/** + * + * 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 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); + } 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('/v1/wallets/', args, function(err, body) { + if (err) return cb(err); + + var walletId = body.walletId; + var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); + self.credentials.addWalletInfo(walletId, walletName, m, n, walletPrivKey.toString(), copayerName); + + 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 to an existent wallet + * + * @param {String} secret + * @param {String} copayerName + * @param {Callback} cb + * @returns {Callback} cb - Returns the wallet + */ +API.prototype.joinWallet = function(secret, copayerName, cb) { + var self = this; + + try { + var secretData = WalletUtils.fromSecret(secret); + } catch (ex) { + return cb(ex); + } + + if (!self.credentials) { + self.seedFromRandom(secretData.network); + } + + self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, self.credentials.xPubKey, self.credentials.requestPubKey, copayerName, {}, + function(err, wallet) { + if (err) return cb(err); + self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, secretData.walletPrivKey.toString(), copayerName); + return cb(null, wallet); + }); +}; + +/** + * Recreate a wallet + * + * @returns {Callback} cb - Returns the wallet + */ +API.prototype.recreateWallet = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete() && this.credentials.hasWalletInfo()); + + var self = this; + + var walletPrivKey = Bitcore.PrivateKey.fromString(self.credentials.walletPrivKey); + var walletId = self.credentials.walletId; + 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, + }; + self._doPostRequest('/v1/wallets/', args, function(err, body) { + // Ignore error is wallet already exist + if (err && err.code != 'WEXISTS') return cb(err); + + + 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, { + isTemporaryRequestKey: item.isTemporaryRequestKey, + }, function(err) { + //Ignore error is copayer already in wallet + if (err && err.code == 'CINWALLET') return next(); + return next(err); + }); + }, cb); + }); +}; + + +/** + * Get status of the wallet + * + * @param {Callback} cb + * @returns {Callback} cb - Returns error or an object with status information + */ +API.prototype.getStatus = function(cb) { + $.checkState(this.credentials); + var self = this; + + self._doGetRequest('/v1/wallets/', function(err, result) { + if (err) return cb(err); + if (result.wallet.status == 'pending') { + var cred = self.credentials; + result.wallet.secret = WalletUtils.toSecret(cred.walletId, cred.walletPrivKey, cred.network); + } + self._processTxps(result.pendingTxps); + 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 + }; + hash = WalletUtils.getProposalHash(proposalHeader); + } else { + $.shouldBeNumber(args.amount); + hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message || null, args.payProUrl); + } + return WalletUtils.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 || 'Could not fetch PayPro request'); + + return cb(null, paypro); + }); +}; + +/** + * Gets list of utxos + * + * @param {Function} cb + * @returns {Callback} cb - Return error or the list of utxos + */ +API.prototype.getUtxos = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + this._doGetRequest('/v1/utxos/', 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 + * @returns {Callback} cb - Return error or the transaction proposal + */ +API.prototype.sendTxProposal = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + $.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, + excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos, + type: opts.type, + outputs: opts.outputs, + }; + if (args.outputs) { + _.each(args.outputs, function(o) { + o.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 {Callback} cb + * @returns {Callback} cb - Return error or the address + */ +API.prototype.createAddress = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + + var self = this; + + self._doPostRequest('/v1/addresses/', {}, function(err, address) { + if (err) return cb(err); + + if (!Verifier.checkAddress(self.credentials, address)) { + return cb(new ServerCompromisedError('Server sent fake address')); + } + + return cb(null, address); + }); +}; + +/** + * Get your main addresses + * + * @param {Object} opts + * @param {Boolean} opts.doNotVerify + * @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; + + self._doGetRequest('/v1/addresses/', 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 ServerCompromisedError('Server sent fake address')); + } + return cb(null, addresses); + }); +}; + +/** + * Update wallet balance + * + * @param {Callback} cb + */ +API.prototype.getBalance = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + this._doGetRequest('/v1/balance/', 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 ServerCompromisedError('Server sent fake transaction proposal')); + + var result; + if (opts.forAirGapped) { + result = { + txps: JSON.parse(JSON.stringify(txps)), + encryptedPkr: WalletUtils.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 ServerCompromisedError('Server sent fake transaction proposal')); + + var signatures = txp.signatures || WalletUtils.signTxp(txp, self.credentials.xPrivKey); + + 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 Error('You do not have the required keys to sign transactions'); + + if (self.isPrivKeyEncrypted()) + return cb(new Error('Private Key is encrypted, cannot sign')); + + var publicKeyRing; + try { + publicKeyRing = JSON.parse(WalletUtils.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.addPublicKeyRing(publicKeyRing); + + if (!Verifier.checkTxProposalBody(self.credentials, txp)) + throw new Error('Fake transaction proposal'); + + return WalletUtils.signTxp(txp, self.credentials.xPrivKey); +}; + + +/** + * 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); + }); +}; + + +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 self = this; + + self.getPayPro(txp, function(err, paypro) { + + if (paypro) { + + var t = WalletUtils.buildTx(txp); + self.createAddress(function(err, addr) { + if (err) return cb(err); + + PayPro.send({ + http: self.payProHttp, + url: txp.payProUrl, + amountSat: txp.amount, + refundAddr: addr.address, + merchant_data: paypro.merchant_data, + rawTx: t.uncheckedSerialize(), + }, function(err, ack, memo) { + if (err) return cb(err); + self._doBroadcast(txp, function(err, txp) { + return cb(err, txp, memo); + }); + }); + }); + } else { + self._doBroadcast(txp, cb); + } + }); +}; + +/** + * Remove a transaction proposal + * + * @param {Object} txp + * @param {Callback} cb + * @return {Callback} cb - Return error or empty + */ +API.prototype.removeTxProposal = function(txp, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + + var self = this; + + var url = '/v1/txproposals/' + txp.id; + self._doDeleteRequest(url, function(err) { + return cb(err); + }); +}; + +/** + * Get transaction history + * + * @param {Object} opts + * @param {Number} opts.skip (defaults to 0) + * @param {Number} opts.limit + * @param {Callback} cb + * @return {Callback} cb - Return error or array of transactions + */ +API.prototype.getTxHistory = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + + var self = this; + var args = []; + if (opts) { + if (opts.skip) args.push('skip=' + opts.skip); + if (opts.limit) args.push('limit=' + opts.limit); + } + var qs = ''; + if (args.length > 0) { + qs = '?' + args.join('&'); + } + + var url = '/v1/txhistory/' + qs; + self._doGetRequest(url, function(err, txs) { + if (err) return cb(err); + self._processTxps(txs); + return cb(null, txs); + }); +}; + +/** + * getTx + * + * @param {String} TransactionId + * @return {Callback} cb - Return error or transaction + */ +API.prototype.getTx = function(id, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + + var self = this; + var url = '/v1/txproposals/' + id; + this._doGetRequest(url, function(err, tx) { + if (err) return cb(err); + + self._processTxps([tx]); + return cb(null, tx); + }); +}; + + +/** + * Start an address scanning process. + * When finished, the scanning process will send a notification 'ScanFinished' to all copayers. + * + * @param {Object} opts + * @param {Boolean} opts.includeCopayerBranches (defaults to false) + * @param {Callback} cb + */ +API.prototype.startScan = function(opts, cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + + var self = this; + + var args = { + includeCopayerBranches: opts.includeCopayerBranches, + }; + + self._doPostRequest('/v1/addresses/scan', args, function(err) { + return cb(err); + }); +}; + +/* + * + * Compatibility Functions + * + */ + +API.prototype._oldCopayDecrypt = function(username, password, blob) { + var SEP1 = '@#$'; + var SEP2 = '%^#@'; + + var decrypted; + try { + var passphrase = username + SEP1 + password; + decrypted = sjcl.decrypt(passphrase, blob); + } catch (e) { + passphrase = username + SEP2 + password; + try { + decrypted = sjcl.decrypt(passphrase, blob); + } catch (e) { + log.debug(e); + }; + } + + if (!decrypted) + return null; + + var ret; + try { + ret = JSON.parse(decrypted); + } catch (e) {}; + return ret; +}; + + +API.prototype.getWalletIdsFromOldCopay = function(username, password, blob) { + var p = this._oldCopayDecrypt(username, password, blob); + if (!p) return null; + var ids = p.walletIds.concat(_.keys(p.focusedTimestamps)); + return _.uniq(ids); +}; + +API.prototype._walletPrivKeyFromOldCopayWallet = function(w) { + // IN BWS, the master Pub Keys are not sent to the server, + // so it is safe to use them as seed for wallet's shared secret. + var seed = w.publicKeyRing.copayersExtPubKeys.sort().join(''); + var seedBuf = new Buffer(seed); + var privKey = new Bitcore.PrivateKey.fromBuffer(Bitcore.crypto.Hash.sha256(seedBuf)); + return privKey.toString(); +}; + +/** + * createWalletFromOldCopay + * + * @param username + * @param password + * @param blob + * @param cb + * @return {undefined} + */ +API.prototype.createWalletFromOldCopay = function(username, password, blob, cb) { + var self = this; + var w = this._oldCopayDecrypt(username, password, blob); + if (!w) return cb('Could not decrypt'); + + if (w.publicKeyRing.copayersExtPubKeys.length != w.opts.totalCopayers) + return cb('Wallet is incomplete, cannot be imported'); + + var m = w.opts.requiredCopayers; + var n = w.opts.totalCopayers; + var walletId = w.opts.id; + var walletName = w.opts.name; + var network = w.opts.networkName; + this.credentials = Credentials.fromOldCopayWallet(w); + var walletPrivKey = this._walletPrivKeyFromOldCopayWallet(w); + var copayerName = this.credentials.copayerName; + + // First: Try to get the wallet with the imported credentials... + this.getStatus(function(err) { + // No error? -> Wallet is ready. + if (!err) { + log.debug('Wallet is already imported'); + self.credentials.addWalletInfo(walletId, walletName, m, n, + walletPrivKey, copayerName); + return cb(); + }; + + self.createWallet(walletName, copayerName, m, n, { + network: network, + id: walletId, + walletPrivKey: walletPrivKey, + }, function(err, secret) { + if (err && err.code == 'WEXISTS') { + + self.credentials.addWalletInfo(walletId, walletName, m, n, + walletPrivKey, copayerName); + + return self._replaceTemporaryRequestKey(function(err) { + if (err) return cb(err); + self.openWallet(function(err) { + return cb(err, true); + }); + }); + } + if (err) return cb(err); + + var i = 1; + async.eachSeries(self.credentials.publicKeyRing, function(item, next) { + if (item.xPubKey == self.credentials.xPubKey) + return next(); + self._doJoinWallet(walletId, walletPrivKey, item.xPubKey, item.requestPubKey, item.copayerName, { + isTemporaryRequestKey: true + }, next); + }, cb); + }); + }); +}; + +/* +Replace temporary request key + */ +API.prototype._replaceTemporaryRequestKey = function(cb) { + $.checkState(this.credentials && this.credentials.isComplete()); + + var args = { + name: this.credentials.copayerName, + xPubKey: this.credentials.xPubKey, + requestPubKey: this.credentials.requestPubKey, + isTemporaryRequestKey: false, + }; + + var hash = WalletUtils.getCopayerHash(args.name, args.xPubKey, args.requestPubKey); + args.copayerSignature = WalletUtils.signMessage(hash, this.credentials.walletPrivKey); + + // Use tmp request key to create the request. + var path0 = WalletUtils.PATHS.BASE_ADDRESS_DERIVATION; + var requestDerivationBase = (new Bitcore.HDPrivateKey(this.credentials.xPrivKey)) + .derive(path0); + + var path1 = WalletUtils.PATHS.TMP_REQUEST_KEY; + var requestDerivation = requestDerivationBase.derive(path1); + args._requestPrivKey = requestDerivation.privateKey.toString(); + + + this._doPutRequest('/v1/copayers/', args, function(err, wallet) { + if (err) return cb(err); + return cb(null, wallet); + }); +}; + + +module.exports = API; + +}).call(this,require('_process'),require("buffer").Buffer) +},{"../package.json":448,"./clienterror":3,"./credentials":4,"./log":6,"./paypro":7,"./servercompromisederror":8,"./verifier":9,"_process":283,"async":10,"bitcore-wallet-utils":34,"browser-request":116,"buffer":133,"events":274,"lodash":306,"preconditions":307,"request":312,"sjcl":397,"socket.io-client":398,"url":301,"util":303}],3:[function(require,module,exports){ +function ClientError(code, message) { + this.code = code; + this.message = message; +}; + +ClientError.prototype.toString = function() { + return ''; +}; + +module.exports = ClientError; + +},{}],4:[function(require,module,exports){ +'use strict'; + +var $ = require('preconditions').singleton(); +var _ = require('lodash'); +var WalletUtils = require('bitcore-wallet-utils'); +var Bitcore = WalletUtils.Bitcore; +var sjcl = require('sjcl'); + +var FIELDS = [ + 'network', + 'xPrivKey', + 'xPrivKeyEncrypted', + 'xPubKey', + 'requestPrivKey', + 'requestPubKey', + 'copayerId', + 'publicKeyRing', + 'walletId', + 'walletName', + 'm', + 'n', + 'walletPrivKey', + 'personalEncryptingKey', + 'sharedEncryptingKey', + 'copayerName', + 'externalSource', + 'externalIndex' +]; + +var EXPORTABLE_FIELDS = [ + 'xPrivKey', + 'xPrivKeyEncrypted', + 'requestPrivKey', + 'xPubKey', + 'm', + 'n', + 'publicKeyRing', + 'sharedEncryptingKey', + 'externalSource', + 'externalIndex' +]; + +function Credentials() { + this.version = '1.0.0'; +}; + +Credentials.create = function(network) { + var x = new Credentials(); + + x.network = network; + x.xPrivKey = (new Bitcore.HDPrivateKey(network)).toString(); + x._expand(); + return x; +}; + +Credentials.fromExtendedPrivateKey = function(xPrivKey) { + var x = new Credentials(); + x.xPrivKey = xPrivKey; + x._expand(); + return x; +}; + +Credentials.fromExternalWalletPublicKey = function(xPubKey, source, index) { + var x = new Credentials(); + x.xPubKey = xPubKey; + x.externalSource = source; + x.externalIndex = index; + x._expand(); + return x; +}; + +Credentials.prototype._expand = function() { + $.checkState(this.xPrivKey || this.xPubKey); + + if (this.xPrivKey) { + var xPrivKey = new Bitcore.HDPrivateKey.fromString(this.xPrivKey); + + var addressDerivation = xPrivKey.derive(WalletUtils.PATHS.BASE_ADDRESS_DERIVATION); + this.xPubKey = (new Bitcore.HDPublicKey(addressDerivation)).toString(); + + var requestDerivation = xPrivKey.derive(WalletUtils.PATHS.REQUEST_KEY); + this.requestPrivKey = requestDerivation.privateKey.toString(); + this.requestPubKey = requestDerivation.publicKey.toString(); + } + var network = WalletUtils.getNetworkFromXPubKey(this.xPubKey); + if (this.hasExternalSource()) { + var xPrivKey = new Bitcore.PrivateKey(network); + this.requestPrivKey = xPrivKey.toString(); + this.requestPubKey = xPrivKey.toPublicKey().toString(); + } + if (this.network) { + $.checkState(this.network == network); + } else { + this.network = network; + } + + this.personalEncryptingKey = WalletUtils.privateKeyToAESKey(this.requestPrivKey); + this.copayerId = WalletUtils.xPubToCopayerId(this.xPubKey); +}; + +Credentials.fromObj = function(obj) { + var x = new Credentials(); + + _.each(FIELDS, function(k) { + x[k] = obj[k]; + }); + + $.checkState(x.xPrivKey || x.xPubKey || x.xPrivKeyEncrypted, "invalid input"); + return x; +}; + +Credentials.prototype.toObj = function() { + var self = this; + + var x = {}; + _.each(FIELDS, function(k) { + x[k] = self[k]; + }); + return x; +}; + +Credentials.prototype.addWalletInfo = function(walletId, walletName, m, n, walletPrivKey, copayerName) { + this.walletId = walletId; + this.walletName = walletName; + this.m = m; + this.n = n; + this.walletPrivKey = walletPrivKey; + if (walletPrivKey) + this.sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); + + if (copayerName) + this.copayerName = copayerName; + + if (n == 1) { + this.addPublicKeyRing([{ + xPubKey: this.xPubKey, + requestPubKey: this.requestPubKey, + }]); + } +}; + +Credentials.prototype.hasWalletInfo = function() { + return !!this.walletId; +}; + +Credentials.prototype.isPrivKeyEncrypted = function() { + return (!!this.xPrivKeyEncrypted) && !this.xPrivKey; +}; + +Credentials.prototype.hasPrivKeyEncrypted = function() { + return (!!this.xPrivKeyEncrypted); +}; + +Credentials.prototype.setPrivateKeyEncryption = function(password, opts) { + if (this.xPrivKeyEncrypted) + throw new Error('Encrypted Privkey Already exists'); + + if (!this.xPrivKey) + throw new Error('No private key to encrypt'); + + + this.xPrivKeyEncrypted = sjcl.encrypt(password, this.xPrivKey, opts); + if (!this.xPrivKeyEncrypted) + throw new Error('Could not encrypt'); +}; + + +Credentials.prototype.disablePrivateKeyEncryption = function() { + if (!this.xPrivKeyEncrypted) + throw new Error('Private Key is not encrypted'); + + if (!this.xPrivKey) + throw new Error('Wallet is locked, cannot disable encryption'); + + this.xPrivKeyEncrypted = null; +}; + + +Credentials.prototype.lock = function() { + if (!this.xPrivKeyEncrypted) + throw new Error('Could not lock, no encrypted private key'); + + delete this.xPrivKey; +}; + +Credentials.prototype.unlock = function(password) { + if (this.xPrivKeyEncrypted) { + this.xPrivKey = sjcl.decrypt(password, this.xPrivKeyEncrypted); + } +}; + +Credentials.prototype.addPublicKeyRing = function(publicKeyRing) { + this.publicKeyRing = _.clone(publicKeyRing); +}; + +Credentials.prototype.updatePublicKeyRing = function(publicKeyRing) { + _.each(this.publicKeyRing, function(x) { + if (x.isTemporaryRequestKey) { + var y = _.find(publicKeyRing, { + xPubKey: x.xPubKey + }); + if (y && !y.isTemporaryRequestKey) { + x.requestPubKey = y.requestPubKey; + x.isTemporaryRequestKey = y.isTemporaryRequestKey; + } + } + }); +}; + +Credentials.prototype.canSign = function() { + return (!!this.xPrivKey || !!this.xPrivKeyEncrypted); +}; + +Credentials.prototype.isComplete = function() { + if (!this.m || !this.n) return false; + if (!this.publicKeyRing || this.publicKeyRing.length != this.n) return false; + return true; +}; + +Credentials.prototype.hasExternalSource = function() { + return (typeof this.externalSource == "string"); +}; + +Credentials.prototype.getExternalSourceName = function() { + return this.externalSource; +}; + +Credentials.prototype.getExternalIndex = function() { + return this.externalIndex; +}; + +Credentials.prototype.hasTemporaryRequestKeys = function() { + if (!this.isComplete()) return null; + return _.any(this.publicKeyRing, function(item) { + return item.isTemporaryRequestKey; + }); +}; + +Credentials.prototype.exportCompressed = function() { + var self = this; + var values = _.map(EXPORTABLE_FIELDS, function(field) { + if ((field == 'xPubKey' || field == 'requestPrivKey') && self.canSign()) return ''; + if (field == 'requestPrivKey') { + return Bitcore.PrivateKey.fromString(self.requestPrivKey).toWIF(); + } + if (field == 'publicKeyRing') { + return _.reject(self.publicKeyRing, { + xPubKey: self.xPubKey + }); + } + return self[field]; + }); + values.unshift(self.version); + + return JSON.stringify(values); +}; + +Credentials.importCompressed = function(compressed, password) { + var list; + try { + list = JSON.parse(compressed); + } catch (ex) { + throw new Error('Invalid compressed format'); + } + + var x = new Credentials(); + + // Remove version + var version = list[0]; + list = _.rest(list); + + _.each(EXPORTABLE_FIELDS, function(field, i) { + x[field] = list[i]; + }); + + if (password) + x.unlock(password); + x._expand(); + if (password) + x.lock(password); + + x.network = WalletUtils.getNetworkFromXPubKey(x.xPubKey); + x.publicKeyRing.push({ + xPubKey: x.xPubKey, + requestPubKey: x.requestPubKey, + }); + return x; +}; + +Credentials.fromOldCopayWallet = function(w){ + var credentials = Credentials.fromExtendedPrivateKey(w.privateKey.extendedPrivateKeyString); + + var pkr = _.map(w.publicKeyRing.copayersExtPubKeys, function(xPubStr) { + + var isMe = xPubStr === credentials.xPubKey; + var requestDerivation; + + if (isMe) { + var path = WalletUtils.PATHS.REQUEST_KEY; + requestDerivation = (new Bitcore.HDPrivateKey(credentials.xPrivKey)) + .derive(path).hdPublicKey; + } else { + var path = WalletUtils.PATHS.TMP_REQUEST_KEY; + requestDerivation = (new Bitcore.HDPublicKey(xPubStr)).derive(path); + } + + // Grab Copayer Name + var hd = new Bitcore.HDPublicKey(xPubStr).derive('m/2147483646/0/0'); + var pubKey = hd.publicKey.toString('hex'); + var copayerName = w.publicKeyRing.nicknameFor[pubKey]; + if (isMe) { + credentials.copayerName = copayerName; + } + + return { + xPubKey: xPubStr, + requestPubKey: requestDerivation.publicKey.toString(), + isTemporaryRequestKey: !isMe, + copayerName: copayerName, + }; + }); + credentials.addPublicKeyRing(pkr); + return credentials; +}; + +module.exports = Credentials; + +},{"bitcore-wallet-utils":34,"lodash":306,"preconditions":307,"sjcl":397}],5:[function(require,module,exports){ +/** + * The official client library for bitcore-wallet-service. + * @module Client + */ + +/** + * Client API. + * @alias module:Client.API + */ +var client = module.exports = require('./api'); + +/** + * Verifier module. + * @alias module:Client.Verifier + */ +client.Verifier = require('./verifier'); +client.Utils = require('bitcore-wallet-utils'); +client.sjcl = require('sjcl'); + +// Expose bitcore +client.Bitcore = require('bitcore-wallet-utils').Bitcore; + +},{"./api":2,"./verifier":9,"bitcore-wallet-utils":34,"sjcl":397}],6:[function(require,module,exports){ +var _ = require('lodash'); +/** + * @desc + * A simple logger that wraps the console.log methods when available. + * + * Usage: + *
+ *   log = new Logger('copay');
+ *   log.setLevel('info');
+ *   log.debug('Message!'); // won't show
+ *   log.setLevel('debug');
+ *   log.debug('Message!', 1); // will show '[debug] copay: Message!, 1'
+ * 
+ * + * @param {string} name - a name for the logger. This will show up on every log call + * @constructor + */ +var Logger = function(name) { + this.name = name || 'log'; + this.level = 2; +}; + +Logger.prototype.getLevels = function() { + return levels; +}; + + +var levels = { + 'debug': 0, + 'info': 1, + 'log': 2, + 'warn': 3, + 'error': 4, + 'fatal': 5 +}; + +_.each(levels, function(level, levelName) { + Logger.prototype[levelName] = function() { + if (level >= levels[this.level]) { + + if (Error.stackTraceLimit && this.level == 'debug') { + var old = Error.stackTraceLimit; + Error.stackTraceLimit = 2; + var stack; + + // this hack is to be compatible with IE11 + try { + anerror(); + } catch (e) { + stack = e.stack; + } + var lines = stack.split('\n'); + var caller = lines[2]; + caller = ':' + caller.substr(6); + Error.stackTraceLimit = old; + } + + var str = '[' + levelName + (caller || '') + '] ' + arguments[0], + extraArgs, + extraArgs = [].slice.call(arguments, 1); + if (console[levelName]) { + extraArgs.unshift(str); + console[levelName].apply(console, extraArgs); + } else { + if (extraArgs.length) { + str += JSON.stringify(extraArgs); + } + console.log(str); + } + } + }; +}); + +/** + * @desc + * Sets the level of a logger. A level can be any bewteen: 'debug', 'info', 'log', + * 'warn', 'error', and 'fatal'. That order matters: if a logger's level is set to + * 'warn', calling level.debug won't have any effect. + * + * @param {number} level - the name of the logging level + */ +Logger.prototype.setLevel = function(level) { + this.level = level; +}; + +/** + * @class Logger + * @method debug + * @desc Log messages at the debug level. + * @param {*} args - the arguments to be logged. + */ +/** + * @class Logger + * @method info + * @desc Log messages at the info level. + * @param {*} args - the arguments to be logged. + */ +/** + * @class Logger + * @method log + * @desc Log messages at an intermediary level called 'log'. + * @param {*} args - the arguments to be logged. + */ +/** + * @class Logger + * @method warn + * @desc Log messages at the warn level. + * @param {*} args - the arguments to be logged. + */ +/** + * @class Logger + * @method error + * @desc Log messages at the error level. + * @param {*} args - the arguments to be logged. + */ +/** + * @class Logger + * @method fatal + * @desc Log messages at the fatal level. + * @param {*} args - the arguments to be logged. + */ + +var logger = new Logger('copay'); +var error = new Error(); +logger.setLevel('info'); +module.exports = logger; + +},{"lodash":306}],7:[function(require,module,exports){ +(function (process,Buffer){ +var $ = require('preconditions').singleton(); + +var WalletUtils = require('bitcore-wallet-utils'); +var Bitcore = WalletUtils.Bitcore; +var BitcorePayPro = require('bitcore-payment-protocol'); +var PayPro = {}; + +PayPro._nodeRequest = function(opts, cb) { + opts.agent = false; + var http = opts.httpNode || (opts.proto === 'http' ? require("http") : require("https")); + + var fn = opts.method == 'POST' ? 'post' : 'get'; + + http[fn](opts, function(res) { + if (res.statusCode != 200) + return cb('HTTP Request Error'); + + var data = []; // List of Buffer objects + res.on("data", function(chunk) { + data.push(chunk); // Append Buffer object + }); + res.on("end", function() { + data = Buffer.concat(data); // Make one large Buffer of it + return cb(null, data); + }); + }); +}; + +PayPro._browserRequest = function(opts, cb) { + var method = (opts.method || 'GET').toUpperCase(); + var url = opts.url; + var req = opts; + + req.headers = req.headers || {}; + req.body = req.body || req.data || ''; + + var xhr = opts.xhr || new XMLHttpRequest(); + xhr.open(method, url, true); + + Object.keys(req.headers).forEach(function(key) { + var val = req.headers[key]; + if (key === 'Content-Length') return; + if (key === 'Content-Transfer-Encoding') return; + xhr.setRequestHeader(key, val); + }); + xhr.responseType = 'arraybuffer'; + + xhr.onload = function(event) { + var response = xhr.response; + return cb(null, new Uint8Array(response)); + }; + + xhr.onerror = function(event) { + var status; + if (xhr.status === 0 || !xhr.statusText) { + status = 'HTTP Request Error'; + } else { + status = xhr.statusText; + } + return cb(status); + }; + + if (req.body) { + xhr.send(req.body); + } else { + xhr.send(null); + } +}; + +var getHttp = function(opts) { + var match = opts.url.match(/^((http[s]?):\/)?\/?([^:\/\s]+)((\/\w+)*\/)([\w\-\.]+[^#?\s]+)(.*)?(#[\w\-]+)?$/); + + opts.proto = RegExp.$2; + opts.host = RegExp.$3; + opts.path = RegExp.$4 + RegExp.$6; + if (opts.http) return opts.http; + + var env = opts.env; + if (!env) + env = (process && !process.browser) ? 'node' : 'browser'; + + return (env == "node") ? PayPro._nodeRequest : http = PayPro._browserRequest;; +}; + +PayPro.get = function(opts, cb) { + $.checkArgument(opts && opts.url); + + var http = getHttp(opts); + opts.headers = opts.headers || { + 'Accept': BitcorePayPro.PAYMENT_REQUEST_CONTENT_TYPE, + 'Content-Type': 'application/octet-stream', + }; + + http(opts, function(err, dataBuffer) { + if (err) return cb(err); + var request; + try { + var body = BitcorePayPro.PaymentRequest.decode(dataBuffer); + request = (new BitcorePayPro()).makePaymentRequest(body); + } catch (e) { + return cb('Could not parse payment protocol:' + e) + } + + var signature = request.get('signature'); + var serializedDetails = request.get('serialized_payment_details'); + + // Verify the signature + var verified = request.verify(true); + + // Get the payment details + var decodedDetails = BitcorePayPro.PaymentDetails.decode(serializedDetails); + var pd = new BitcorePayPro(); + pd = pd.makePaymentDetails(decodedDetails); + + var outputs = pd.get('outputs'); + if (outputs.length > 1) + return cb(new Error('Payment Protocol Error: Requests with more that one output are not supported')) + + var output = outputs[0]; + + var amount = output.get('amount'); + amount = amount.low + amount.high * 0x100000000; + + + var network = pd.get('network') == 'test' ? 'testnet' : 'livenet'; + + // We love payment protocol + var offset = output.get('script').offset; + var limit = output.get('script').limit; + + // NOTE: For some reason output.script.buffer + // is only an ArrayBuffer + var buffer = new Buffer(new Uint8Array(output.get('script').buffer)); + var scriptBuf = buffer.slice(offset, limit); + var addr = new Bitcore.Address.fromScript(new Bitcore.Script(scriptBuf), network); + + var md = pd.get('merchant_data'); + + if (md) { + md = md.toString(); + } + + return cb(null, { + verified: verified.verified, + verifyData: { + caName: verified.caName, + selfSigned: verified.selfSigned, + }, + expires: pd.get('expires'), + memo: pd.get('memo'), + time: pd.get('time'), + merchant_data: md, + toAddress: addr.toString(), + amount: amount, + network: network, + domain: opts.host, + url: opts.url, + }); + }); +}; + + +PayPro._getPayProRefundOutputs = function(addrStr, amount) { + amount = amount.toString(10); + + var output = new BitcorePayPro.Output(); + var addr = new Bitcore.Address(addrStr); + var hash = addr.toObject().hash; + + var s = new Bitcore.Script(); + s.add(Bitcore.Opcode.OP_HASH160) + .add(new Buffer(hash, 'hex')) + .add(Bitcore.Opcode.OP_EQUAL); + + // console.log('PayPro refund address set to:', addrStr,s); + output.set('script', s.toBuffer()); + output.set('amount', amount); + return [output]; +}; + + +PayPro._createPayment = function(merchant_data, rawTx, refundAddr, amountSat) { + var pay = new BitcorePayPro(); + pay = pay.makePayment(); + + if (merchant_data) { + merchant_data = new Buffer(merchant_data); + pay.set('merchant_data', merchant_data); + } + + var txBuf = new Buffer(rawTx, 'hex'); + pay.set('transactions', [txBuf]); + + var refund_outputs = this._getPayProRefundOutputs(refundAddr, amountSat); + if (refund_outputs) + pay.set('refund_to', refund_outputs); + + // Unused for now + // options.memo = ''; + // pay.set('memo', options.memo); + + pay = pay.serialize(); + var buf = new ArrayBuffer(pay.length); + var view = new Uint8Array(buf); + for (var i = 0; i < pay.length; i++) { + view[i] = pay[i]; + } + + return view; +}; + +PayPro.send = function(opts, cb) { + $.checkArgument(opts.merchant_data) + .checkArgument(opts.url) + .checkArgument(opts.rawTx) + .checkArgument(opts.refundAddr) + .checkArgument(opts.amountSat); + + var payment = PayPro._createPayment(opts.merchant_data, opts.rawTx, opts.refundAddr, opts.amountSat); + + var http = getHttp(opts); + opts.method = 'POST'; + opts.headers = opts.headers || { + 'Accept': BitcorePayPro.PAYMENT_ACK_CONTENT_TYPE, + 'Content-Type': BitcorePayPro.PAYMENT_CONTENT_TYPE, + // 'Content-Type': 'application/octet-stream', + }; + opts.body = payment; + + http(opts, function(err, rawData) { + if (err) return cb(err); + var memo; + if (rawData) { + try { + var data = BitcorePayPro.PaymentACK.decode(rawData); + var pp = new BitcorePayPro(); + var ack = pp.makePaymentACK(data); + memo = ack.get('memo'); + } catch (e) {}; + } + return cb(null, rawData, memo); + }); +}; + +module.exports = PayPro; + +}).call(this,require('_process'),require("buffer").Buffer) +},{"_process":283,"bitcore-payment-protocol":11,"bitcore-wallet-utils":34,"buffer":133,"http":275,"https":279,"preconditions":307}],8:[function(require,module,exports){ +function ServerCompromisedError(message) { + this.code = 'SERVERCOMPROMISED'; + this.message = message; +}; + +module.exports = ServerCompromisedError; + +},{}],9:[function(require,module,exports){ +/** @namespace Verifier */ + +var $ = require('preconditions').singleton(); +var _ = require('lodash'); + +var WalletUtils = require('bitcore-wallet-utils'); +var Bitcore = WalletUtils.Bitcore; + +var log = require('./log'); + +/** + * @desc Verifier constructor. Checks data given by the server + * + * @constructor + */ +function Verifier(opts) {}; + +/** + * Check address + * + * @param {Function} credentials + * @param {String} address + * @returns {Boolean} true or false + */ +Verifier.checkAddress = function(credentials, address) { + $.checkState(credentials.isComplete()); + var local = WalletUtils.deriveAddress(credentials.publicKeyRing, address.path, credentials.m, credentials.network); + return (local.address == address.address && + _.difference(local.publicKeys, address.publicKeys).length === 0); +}; + +/** + * Check copayers + * + * @param {Function} credentials + * @param {Array} copayers + * @returns {Boolean} true or false + */ +Verifier.checkCopayers = function(credentials, copayers) { + $.checkState(credentials.walletPrivKey); + var walletPubKey = Bitcore.PrivateKey.fromString(credentials.walletPrivKey).toPublicKey().toString(); + + if (copayers.length != credentials.n) { + log.error('Missing public keys in server response'); + return false; + } + + // Repeated xpub kes? + var uniq = []; + var error; + _.each(copayers, function(copayer) { + if (error) return; + + if (uniq[copayers.xPubKey]++) { + log.error('Repeated public keys in server response'); + error = true; + } + + // Not signed pub keys + if (!copayer.name || !copayer.xPubKey || !copayer.requestPubKey || !copayer.signature) { + log.error('Missing copayer fields in server response'); + error = true; + } else { + var hash = WalletUtils.getCopayerHash(copayer.name, copayer.xPubKey, copayer.requestPubKey); + if (!WalletUtils.verifyMessage(hash, copayer.signature, walletPubKey)) { + log.error('Invalid signatures in server response'); + error = true; + } + } + }); + + if (error) return false; + + if (!_.contains(_.pluck(copayers, 'xPubKey'), credentials.xPubKey)) { + log.error('Server response does not contains our public keys') + return false; + } + return true; +}; + +Verifier.checkTxProposalBody = function(credentials, txp) { + $.checkArgument(txp.creatorId); + $.checkState(credentials.isComplete()); + + var creatorKeys = _.find(credentials.publicKeyRing, function(item) { + if (WalletUtils.xPubToCopayerId(item.xPubKey) === txp.creatorId) return true; + }); + + if (!creatorKeys) return false; + + var creatorSigningPubKey = creatorKeys.requestPubKey; + var hash; + if (txp.outputs) { + var outputs = _.map(txp.outputs, function(o) { + return { + toAddress: o.toAddress, + amount: o.amount, + message: o.encryptedMessage || o.message || null + }; + }); + var proposalHeader = { + outputs: outputs, + message: txp.encryptedMessage || txp.message || null, + payProUrl: txp.payProUrl || undefined + }; + hash = WalletUtils.getProposalHash(proposalHeader); + } else { + hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.encryptedMessage || txp.message || null, txp.payProUrl); + } + log.debug('Regenerating & verifying tx proposal hash -> Hash: ', hash, ' Signature: ', txp.proposalSignature); + + if (!WalletUtils.verifyMessage(hash, txp.proposalSignature, creatorSigningPubKey)) + return false; + + if (!Verifier.checkAddress(credentials, txp.changeAddress)) + return false; + + return true; +}; + + + +/** + * Check transaction proposal + * + * @param {Function} credentials + * @param {Object} txp + * @param {Object} Optional: paypro + * @param {Boolean} isLegit + */ +Verifier.checkTxProposal = function(credentials, txp, opts) { + opts = opts || {}; + + if (!this.checkTxProposalBody(credentials, txp)) + return false; + + if (opts.paypro) { + if (txp.toAddress != opts.paypro.toAddress || txp.amount != opts.paypro.amount) + return false; + } + + return true; +}; + +module.exports = Verifier; + +},{"./log":6,"bitcore-wallet-utils":34,"lodash":306,"preconditions":307}],10:[function(require,module,exports){ +(function (process){ +/*! + * async + * https://github.com/caolan/async + * + * Copyright 2010-2014 Caolan McMahon + * Released under the MIT license + */ +/*jshint onevar: false, indent:4 */ +/*global setImmediate: false, setTimeout: false, console: false */ +(function () { + + var async = {}; + + // global on the server, window in the browser + var root, previous_async; + + root = this; + if (root != null) { + previous_async = root.async; + } + + async.noConflict = function () { + root.async = previous_async; + return async; + }; + + function only_once(fn) { + var called = false; + return function() { + if (called) throw new Error("Callback was already called."); + called = true; + fn.apply(root, arguments); + } + } + + //// cross-browser compatiblity functions //// + + var _toString = Object.prototype.toString; + + var _isArray = Array.isArray || function (obj) { + return _toString.call(obj) === '[object Array]'; + }; + + var _each = function (arr, iterator) { + if (arr.forEach) { + return arr.forEach(iterator); + } + for (var i = 0; i < arr.length; i += 1) { + iterator(arr[i], i, arr); + } + }; + + var _map = function (arr, iterator) { + if (arr.map) { + return arr.map(iterator); + } + var results = []; + _each(arr, function (x, i, a) { + results.push(iterator(x, i, a)); + }); + return results; + }; + + var _reduce = function (arr, iterator, memo) { + if (arr.reduce) { + return arr.reduce(iterator, memo); + } + _each(arr, function (x, i, a) { + memo = iterator(memo, x, i, a); + }); + return memo; + }; + + var _keys = function (obj) { + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + keys.push(k); + } + } + return keys; + }; + + //// exported async module functions //// + + //// nextTick implementation with browser-compatible fallback //// + if (typeof process === 'undefined' || !(process.nextTick)) { + if (typeof setImmediate === 'function') { + async.nextTick = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + async.setImmediate = async.nextTick; + } + else { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + async.setImmediate = async.nextTick; + } + } + else { + async.nextTick = process.nextTick; + if (typeof setImmediate !== 'undefined') { + async.setImmediate = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + } + else { + async.setImmediate = async.nextTick; + } + } + + async.each = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + _each(arr, function (x) { + iterator(x, only_once(done) ); + }); + function done(err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(); + } + } + } + }; + async.forEach = async.each; + + async.eachSeries = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + async.forEachSeries = async.eachSeries; + + async.eachLimit = function (arr, limit, iterator, callback) { + var fn = _eachLimit(limit); + fn.apply(null, [arr, iterator, callback]); + }; + async.forEachLimit = async.eachLimit; + + var _eachLimit = function (limit) { + + return function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed >= arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + started += 1; + running += 1; + iterator(arr[started - 1], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed >= arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + } + })(); + }; + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.each].concat(args)); + }; + }; + var doParallelLimit = function(limit, fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [_eachLimit(limit)].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.eachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + if (!callback) { + eachfn(arr, function (x, callback) { + iterator(x.value, function (err) { + callback(err); + }); + }); + } else { + var results = []; + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + async.mapLimit = function (arr, limit, iterator, callback) { + return _mapLimit(limit)(arr, iterator, callback); + }; + + var _mapLimit = function(limit) { + return doParallelLimit(limit, _asyncMap); + }; + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.eachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + var remainingTasks = keys.length + if (!remainingTasks) { + return callback(); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + remainingTasks-- + _each(listeners.slice(0), function (fn) { + fn(); + }); + }; + + addListener(function () { + if (!remainingTasks) { + var theCallback = callback; + // prevent final callback from calling itself if it errors + callback = function () {}; + + theCallback(null, results); + } + }); + + _each(keys, function (k) { + var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]]; + var taskCallback = function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + if (err) { + var safeResults = {}; + _each(_keys(results), function(rkey) { + safeResults[rkey] = results[rkey]; + }); + safeResults[k] = args; + callback(err, safeResults); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + results[k] = args; + async.setImmediate(taskComplete); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true) && !results.hasOwnProperty(k); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.retry = function(times, task, callback) { + var DEFAULT_TIMES = 5; + var attempts = []; + // Use defaults if times not passed + if (typeof times === 'function') { + callback = task; + task = times; + times = DEFAULT_TIMES; + } + // Make sure times is a number + times = parseInt(times, 10) || DEFAULT_TIMES; + var wrappedTask = function(wrappedCallback, wrappedResults) { + var retryAttempt = function(task, finalAttempt) { + return function(seriesCallback) { + task(function(err, result){ + seriesCallback(!err || finalAttempt, {err: err, result: result}); + }, wrappedResults); + }; + }; + while (times) { + attempts.push(retryAttempt(task, !(times-=1))); + } + async.series(attempts, function(done, data){ + data = data[data.length - 1]; + (wrappedCallback || callback)(data.err, data.result); + }); + } + // If a callback is passed, run this as a controll flow + return callback ? wrappedTask() : wrappedTask + }; + + async.waterfall = function (tasks, callback) { + callback = callback || function () {}; + if (!_isArray(tasks)) { + var err = new Error('First argument to waterfall must be an array of functions'); + return callback(err); + } + if (!tasks.length) { + return callback(); + } + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback.apply(null, arguments); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.setImmediate(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + var _parallel = function(eachfn, tasks, callback) { + callback = callback || function () {}; + if (_isArray(tasks)) { + eachfn.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + eachfn.each(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.parallel = function (tasks, callback) { + _parallel({ map: async.map, each: async.each }, tasks, callback); + }; + + async.parallelLimit = function(tasks, limit, callback) { + _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (_isArray(tasks)) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.eachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doWhilst = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + var args = Array.prototype.slice.call(arguments, 1); + if (test.apply(null, args)) { + async.doWhilst(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doUntil = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + var args = Array.prototype.slice.call(arguments, 1); + if (!test.apply(null, args)) { + async.doUntil(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.queue = function (worker, concurrency) { + if (concurrency === undefined) { + concurrency = 1; + } + function _insert(q, data, pos, callback) { + if (!q.started){ + q.started = true; + } + if (!_isArray(data)) { + data = [data]; + } + if(data.length == 0) { + // call drain immediately if there are no tasks + return async.setImmediate(function() { + if (q.drain) { + q.drain(); + } + }); + } + _each(data, function(task) { + var item = { + data: task, + callback: typeof callback === 'function' ? callback : null + }; + + if (pos) { + q.tasks.unshift(item); + } else { + q.tasks.push(item); + } + + if (q.saturated && q.tasks.length === q.concurrency) { + q.saturated(); + } + async.setImmediate(q.process); + }); + } + + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + started: false, + paused: false, + push: function (data, callback) { + _insert(q, data, false, callback); + }, + kill: function () { + q.drain = null; + q.tasks = []; + }, + unshift: function (data, callback) { + _insert(q, data, true, callback); + }, + process: function () { + if (!q.paused && workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if (q.empty && q.tasks.length === 0) { + q.empty(); + } + workers += 1; + var next = function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if (q.drain && q.tasks.length + workers === 0) { + q.drain(); + } + q.process(); + }; + var cb = only_once(next); + worker(task.data, cb); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + }, + idle: function() { + return q.tasks.length + workers === 0; + }, + pause: function () { + if (q.paused === true) { return; } + q.paused = true; + q.process(); + }, + resume: function () { + if (q.paused === false) { return; } + q.paused = false; + q.process(); + } + }; + return q; + }; + + async.priorityQueue = function (worker, concurrency) { + + function _compareTasks(a, b){ + return a.priority - b.priority; + }; + + function _binarySearch(sequence, item, compare) { + var beg = -1, + end = sequence.length - 1; + while (beg < end) { + var mid = beg + ((end - beg + 1) >>> 1); + if (compare(item, sequence[mid]) >= 0) { + beg = mid; + } else { + end = mid - 1; + } + } + return beg; + } + + function _insert(q, data, priority, callback) { + if (!q.started){ + q.started = true; + } + if (!_isArray(data)) { + data = [data]; + } + if(data.length == 0) { + // call drain immediately if there are no tasks + return async.setImmediate(function() { + if (q.drain) { + q.drain(); + } + }); + } + _each(data, function(task) { + var item = { + data: task, + priority: priority, + callback: typeof callback === 'function' ? callback : null + }; + + q.tasks.splice(_binarySearch(q.tasks, item, _compareTasks) + 1, 0, item); + + if (q.saturated && q.tasks.length === q.concurrency) { + q.saturated(); + } + async.setImmediate(q.process); + }); + } + + // Start with a normal queue + var q = async.queue(worker, concurrency); + + // Override push to accept second parameter representing priority + q.push = function (data, priority, callback) { + _insert(q, data, priority, callback); + }; + + // Remove unshift function + delete q.unshift; + + return q; + }; + + async.cargo = function (worker, payload) { + var working = false, + tasks = []; + + var cargo = { + tasks: tasks, + payload: payload, + saturated: null, + empty: null, + drain: null, + drained: true, + push: function (data, callback) { + if (!_isArray(data)) { + data = [data]; + } + _each(data, function(task) { + tasks.push({ + data: task, + callback: typeof callback === 'function' ? callback : null + }); + cargo.drained = false; + if (cargo.saturated && tasks.length === payload) { + cargo.saturated(); + } + }); + async.setImmediate(cargo.process); + }, + process: function process() { + if (working) return; + if (tasks.length === 0) { + if(cargo.drain && !cargo.drained) cargo.drain(); + cargo.drained = true; + return; + } + + var ts = typeof payload === 'number' + ? tasks.splice(0, payload) + : tasks.splice(0, tasks.length); + + var ds = _map(ts, function (task) { + return task.data; + }); + + if(cargo.empty) cargo.empty(); + working = true; + worker(ds, function () { + working = false; + + var args = arguments; + _each(ts, function (data) { + if (data.callback) { + data.callback.apply(null, args); + } + }); + + process(); + }); + }, + length: function () { + return tasks.length; + }, + running: function () { + return working; + } + }; + return cargo; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _each(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + async.nextTick(function () { + callback.apply(null, memo[key]); + }); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.memo = memo; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + }; + }; + + async.times = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.map(counter, iterator, callback); + }; + + async.timesSeries = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.mapSeries(counter, iterator, callback); + }; + + async.seq = function (/* functions... */) { + var fns = arguments; + return function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + async.reduce(fns, args, function (newargs, fn, cb) { + fn.apply(that, newargs.concat([function () { + var err = arguments[0]; + var nextargs = Array.prototype.slice.call(arguments, 1); + cb(err, nextargs); + }])) + }, + function (err, results) { + callback.apply(that, [err].concat(results)); + }); + }; + }; + + async.compose = function (/* functions... */) { + return async.seq.apply(null, Array.prototype.reverse.call(arguments)); + }; + + var _applyEach = function (eachfn, fns /*args...*/) { + var go = function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + return eachfn(fns, function (fn, cb) { + fn.apply(that, args.concat([cb])); + }, + callback); + }; + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + return go.apply(this, args); + } + else { + return go; + } + }; + async.applyEach = doParallel(_applyEach); + async.applyEachSeries = doSeries(_applyEach); + + async.forever = function (fn, callback) { + function next(err) { + if (err) { + if (callback) { + return callback(err); + } + throw err; + } + fn(next); + } + next(); + }; + + // Node.js + if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + // AMD / RequireJS + else if (typeof define !== 'undefined' && define.amd) { + define([], function () { + return async; + }); + } + // included directly via