diff --git a/README.md b/README.md index a2eee2ae3..934e8c515 100644 --- a/README.md +++ b/README.md @@ -196,4 +196,16 @@ Copay uses the Javascript library Bitcore for Bitcoin related functions. Bitcore var cmd = `node util/build_bitcore.js` cd node $cmd + +Payment Protocol +---------------- + +Copay support BIP70 (Payment Protocol), with the following current limitations: + + * Only one output is allowed. Payment requests is more that one output are not supported. + * Only standard Pay-to-pubkeyhash and Pay-to-scripthash scripts are supported (on payment requests). Other script types will cause the entire payment request to be rejected. + + + + ``` diff --git a/js/controllers/send.js b/js/controllers/send.js index 88a67ef58..ff0b10d7c 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -100,6 +100,38 @@ angular.module('copayApp.controllers').controller('SendController', if (!window.cordova && !navigator.getUserMedia) $scope.disableScanner = 1; + $scope._showError = function(err) { + copay.logger.error(err); + + var msg = err.toString(); + if (msg.match('BIG')) + msg = 'The transaction have too many inputs. Try creating many transactions for smaller amounts.' + + if (msg.match('totalNeededAmount')) + msg = 'Not enough funds.' + + var message = 'The transaction' + (w.isShared() ? ' proposal' : '') + ' could not be created: ' + msg; + $scope.error = message; + $scope.loading = false; + $scope.loadTxs(); + }; + + $scope._afterSend = function(err, ntxid, merchantData) { + $scope.loading = false; + + if (err) + return $scope._showError(err); + + + if (w.requiresMultipleSignatures()) { + notification.success('Success', 'The transaction proposal created'); + $scope.loadTxs(); + } else { + $scope.send(ntxid); + } + $rootScope.pendingPayment = null; + }; + $scope.submitForm = function(form) { if (form.$invalid) { $scope.error = 'Unable to send transaction proposal'; @@ -112,92 +144,32 @@ angular.module('copayApp.controllers').controller('SendController', var amount = parseInt((form.amount.$modelValue * w.settings.unitToSatoshi).toFixed(0)); var commentText = form.comment.$modelValue; - function done(err, ntxid, merchantData) { - if (err) { - copay.logger.error(err); - var msg = err.toString(); - - if (msg.match('BIG')) - msg = 'The transaction have too many inputs. Try creating many transactions for smaller amounts.' - - if (msg.match('totalNeededAmount')) - msg = 'Not enough funds.' - - var message = 'The transaction' + (w.isShared() ? ' proposal' : '') + ' could not be created: ' + msg; - $scope.error = message; - $scope.loading = false; - $scope.loadTxs(); - return; - } - - // If user is granted the privilege of choosing - // their own amount, add it to the tx. - if (merchantData && +merchantData.total === 0) { - var txp = w.txProposals.get(ntxid); - var tx = txp.builder.tx = txp.builder.tx || txp.builder.build(); - tx.outs[0].v = bitcore.Bignum(amount + '', 10).toBuffer({ - // XXX This may not work in node due - // to the bignum only-big endian bug: - endian: 'little', - size: 1 - }); - } - - if (w.requiresMultipleSignatures()) { - $scope.loading = false; - notification.success('Success', 'The transaction proposal created'); - $scope.loadTxs(); - } else { - w.sendTx(ntxid, function(txid, merchantData) { - if (txid) { - var message = 'Transaction id: ' + txid; - if (merchantData) { - if (merchantData.pr.ca) { - message += ' This payment protocol transaction' + ' has been verified through ' + merchantData.pr.ca + '.'; - } - message += merchantData.pr.pd.memo; - message += ' Merchant: ' + merchantData.pr.pd.payment_url; - } - $scope.success = 'Transaction broadcasted' + message; - } else { - $scope.error = 'There was an error sending the transaction'; - } - $scope.loading = false; - $scope.loadTxs(); - }); - } - $rootScope.pendingPayment = null; - } - - var uri; + var payInfo; if (address.indexOf('bitcoin:') === 0) { - uri = new bitcore.BIP21(address).data; + payInfo = (new bitcore.BIP21(address)).data; } else if (/^https?:\/\//.test(address)) { - uri = { + payInfo = { merchant: address }; } // If we're setting the domain, ignore the change. if ($rootScope.merchant && $rootScope.merchant.domain && address === $rootScope.merchant.domain) { - uri = { + payInfo = { merchant: $rootScope.merchant.request_url }; } - - if (uri && uri.merchant) { - w.createPaymentTx({ - uri: uri.merchant, - memo: commentText - }, done); - } else { - w.createTx(address, amount, commentText, done); - } - // reset fields $scope.address = $scope.amount = $scope.commentText = null; form.address.$pristine = form.amount.$pristine = true; + + w.createTx({ + toAddress: address, + amountSat: amount, + comment: commentText, + url: (payInfo && payInfo.merchant) ? url : null, + }, $scope._afterSend); }; // QR code Scanner @@ -411,13 +383,7 @@ angular.module('copayApp.controllers').controller('SendController', if (!merchantData) { notification.success('Success', 'Transaction broadcasted!'); } else { - var message = 'Transaction ID: ' + txid; - if (merchantData.pr.ca) { - message += ' This payment protocol transaction' + ' has been verified through ' + merchantData.pr.ca + '.'; - } - message += ' Message from server: ' + merchantData.ack.memo; - message += ' For merchant: ' + merchantData.pr.pd.payment_url; - notification.success('Success', 'Transaction sent' + message); + notification.success('Success', 'Payment sent. ' + merchantData.ack.memo); } } @@ -433,16 +399,14 @@ angular.module('copayApp.controllers').controller('SendController', try { w.sign(ntxid); } catch (e) { - notification.error('Error','There was an error signing the transaction'); + notification.error('Error', 'There was an error signing the transaction'); $scope.loadTxs(); return; } var p = w.txProposals.getTxProposal(ntxid); if (p.builder.isFullySigned()) { - $scope.send(ntxid, function() { - $scope.loadTxs(); - }); + $scope.send(ntxid); } else { $scope.loadTxs(); } @@ -546,7 +510,7 @@ angular.module('copayApp.controllers').controller('SendController', }, 10 * 1000); // Payment Protocol URI (BIP-72) - $scope.wallet.fetchPaymentTx(uri.merchant, function(err, merchantData) { + $scope.wallet.fetchPaymentRequest({url: uri.merchant}, function(err, merchantData) { if (!timeout) return; clearTimeout(timeout); diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 908dd0259..1d382b068 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -6,7 +6,6 @@ var preconditions = require('preconditions').singleton(); var inherits = require('inherits'); var events = require('events'); var async = require('async'); -var cryptoUtil = require('../util/crypto'); var bitcore = require('bitcore'); var BIP21 = bitcore.BIP21; @@ -19,8 +18,10 @@ var Base58Check = bitcore.Base58.base58Check; var Address = bitcore.Address; var PayPro = bitcore.PayPro; var Transaction = bitcore.Transaction; -var log = require('../log'); +var log = require('../log'); +var cryptoUtil = require('../util/crypto'); +var httpUtil = require('../util/HTTP'); var HDParams = require('./HDParams'); var PublicKeyRing = require('./PublicKeyRing'); var TxProposal = require('./TxProposal'); @@ -70,6 +71,7 @@ function Wallet(opts) { opts.network = opts.network || Wallet._newAsync(opts.networkOpts[networkName]); opts.blockchain = opts.blockchain || Wallet._newInsight(opts.blockchainOpts[networkName]);; + this.httpUtil = opts.httpUtil || httpUtil; //required params ['network', 'blockchain', @@ -472,9 +474,10 @@ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { return cb(); log.info('Received a Payment Protocol TX Proposal'); - self.fetchPaymentTx(txp.paymentProtocolURL, function(err, merchantData) { + self.fetchPaymentRequest(txp.paymentProtocolURL, function(err, merchantData) { if (err) return cb(err); + // This will verify current TXP data vs. merchantData (e.g., out addresses) try { txp.addMerchantData(merchantData); } catch (e) { @@ -1510,96 +1513,125 @@ Wallet.prototype.sendTx = function(ntxid, cb) { /** * @desc Create a Payment Protocol transaction - * @param {Object|string} options - if it's a string, parse it as the uri - * @param {string} options.uri the url for the transaction + * @param {Object|string} options - if it's a string, parse it as the url + * @param {string} options.url the url for the transaction * @param {Function} cb */ -Wallet.prototype.createPaymentTx = function(options, cb) { +Wallet.prototype.fetchPaymentRequest = function(options, cb) { + preconditions.checkArgument(_.isObject(options)); + preconditions.checkArgument(options.url); + preconditions.checkArgument(options.url.indexOf('http') == 0, 'Bad PayPro URL given:' + options.url); + var self = this; - if (_.isString(options)) { - options = { - uri: options - }; - } - options.uri = options.uri || options.url; + this.httpUtil.request({ + method: 'GET', + url: options.url, + headers: { + 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + }, + responseType: 'arraybuffer' + }) + .success(function(rawData) { + log.info('PayPro Request done successfully. Parsing response') - if (options.uri.indexOf('bitcoin:') === 0) { - options.uri = new bitcore.BIP21(options.uri).data.merchant; - if (!options.uri) { - return cb(new Error('No URI.')); - } - } + var data = PayPro.PaymentRequest.decode(rawData); + var paypro = new PayPro(); + var pr = paypro.makePaymentRequest(data); + var merchantData, err; + try { + merchantData = self.parsePaymentRequest(options, pr); + } catch (e) { err = e}; - var req = this.paymentRequests[options.uri]; - if (req) { - delete this.paymentRequests[options.uri]; - this.receivePaymentRequest(options, req.pr, cb); - return; - } - - return Wallet.request({ - method: 'GET', - url: options.uri, - headers: { - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE - }, - responseType: 'arraybuffer' + log.debug('PayPro request data', merchantData); + return cb(err, merchantData); }) - .success(function(data, status, headers, config) { - data = PayPro.PaymentRequest.decode(data); - var pr = new PayPro(); - pr = pr.makePaymentRequest(data); - return self.receivePaymentRequest(options, pr, cb); - }) - .error(function(data, status, headers, config) { - log.debug('Server did not return PaymentRequest.'); - log.debug('XHR status: ' + status); - if (options.fetch) { - return cb(new Error('Status: ' + status)); - } else { - // Should never happen: - return cb(null, null, null); - } + .error(function(data, status) { + log.debug('Server did not return PaymentRequest.\nXHR status: ' + status); + preconditions.checkState(options.fetch); + return cb(new Error('Status: ' + status)); }); }; -/** - * @desc Creates a Payment TxProposal from a uri - * @param {Object} options - * @param {string=} options.uri - * @param {string=} options.url - * @param {Function} cb +/* + * addOutputsToMerchantData + * + * NOTE: We use to: set the TX scripts with the payment request scripts: + * but this is a hack around transaction builder, so we dont do it anymore. + * See Readme.md. For now we only support p2scripthash or p2pubkeyhash + merchantData.pr.pd.outputs.forEach(function(output, i) { + var script = { + offset: output.script.offset, + limit: output.script.limit, + buffer: new Buffer(output.script.buffer, 'hex') + }; + var s = script.buffer.slice(script.offset, script.limit); + b.tx.outs[i].s = s; + }); + * */ -Wallet.prototype.fetchPaymentTx = function(options, cb) { - var self = this; - options = options || {}; - if (_.isString(options)) { - options = { - uri: options +Wallet.prototype._addOutputsToMerchantData = function(merchantData) { + + var total = bignum(0); + var outs = {}; + + _.each(merchantData.pr.pd.outputs, function(output) { + var amount = output.amount; + + // big endian + var v = new Buffer(8); + v[0] = (amount.high >> 24) & 0xff; + v[1] = (amount.high >> 16) & 0xff; + v[2] = (amount.high >> 8) & 0xff; + v[3] = (amount.high >> 0) & 0xff; + v[4] = (amount.low >> 24) & 0xff; + v[5] = (amount.low >> 16) & 0xff; + v[6] = (amount.low >> 8) & 0xff; + v[7] = (amount.low >> 0) & 0xff; + + var script = { + offset: output.script.offset, + limit: output.script.limit, + buffer: new Buffer(output.script.buffer, 'hex') }; - } - options.uri = options.uri || options.url; - options.fetch = true; + var s = script.buffer.slice(script.offset, script.limit); + var network = merchantData.pr.pd.network === 'main' ? 'livenet' : 'testnet'; + var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), network); - var req = this.paymentRequests[options.uri]; - if (req) { - return cb(null, req.merchantData); - } + var a = addr[0].toString(); + outs[a] = bignum.fromBuffer(v, { + endian: 'big', + size: 1 + }).add(outs[a] || bignum(0)); - return this.createPaymentTx(options, function(err, merchantData, pr) { - if (err) return cb(err); - self.paymentRequests[options.uri] = { - merchantData: merchantData, - pr: pr - }; - return cb(null, merchantData); + total = total.add(bignum.fromBuffer(v, { + endian: 'big', + size: 1 + })); }); + + // for now we only support PayPro with 1 output. + if (_.size(outs) !== 1) + throw new Error('PayPro: Unsupported outputs'); + + var out = _.pairs(outs)[0]; + + merchantData.outs = [{ + address: out[0], + amountSatStr: out[1].toString(10), + }]; + merchantData.total = total.toString(10); + + // If user is granted the privilege of choosing + // their own amount, add it to the tx. + if (merchantData.total === 0 && options.amount) { + merchant.outs[0].amountSatStr = merchantData.total = outions.amount; + } }; /** - * @desc Analyzes a payment request and generates a transaction proposal for it. + * @desc Analyzes a payment request and generate merchantData * @param {Object} options * @param {PayProRequest} pr * @param {string} pr.payment_details_version @@ -1609,9 +1641,8 @@ Wallet.prototype.fetchPaymentTx = function(options, cb) { * @param {string} pr.signature * @param {string} options.memo * @param {string} options.comment - * @param {Function} cb */ -Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { +Wallet.prototype.parsePaymentRequest = function(options, pr) { var self = this; var ver = pr.get('payment_details_version'); @@ -1623,18 +1654,11 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { var certs = PayPro.X509Certificates.decode(pki_data); certs = certs.certificate; - // Fix for older versions of bitcore - if (!PayPro.RootCerts) { - PayPro.RootCerts = { - getTrusted: function() {} - }; - } - // Verify Signature var trust = pr.verify(true); if (!trust.verified) { - return cb(new Error('Server sent a bad signature.')); + throw new Error('Server sent a bad signature.'); } details = PayPro.PaymentDetails.decode(details); @@ -1681,38 +1705,11 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { selfSigned: trust.selfSigned }, expires: expires, - request_url: options.uri, + request_url: options.url, total: bignum('0', 10).toString(10), }; - - return this.getUnspent(function(err, safeUnspent, unspent) { - - if (options.fetch) { - if (!unspent || !unspent.length) { - return cb(new Error('No unspent outputs available.')); - } - - var err; - try { - self.createPaymentTxSync(options, merchantData, safeUnspent); - } catch (e) { err = e;} - return cb(err, merchantData, pr); - } - - var ntxid = self.createPaymentTxSync(options, merchantData, safeUnspent); - if (ntxid) { - self.sendIndexes(); - self.sendTxProposal(ntxid); - self.emit('txProposalsUpdated'); - } else { - return cb(new Error('Error creating the transaction')); - } - - log.debug('You are currently on this BTC network:', network); - log.debug('The server sent you a message:', memo); - - return cb(null, ntxid, merchantData); - }); + this._addOutputsToMerchantData(merchantData, options.amount); + return merchantData; }; /** @@ -1827,14 +1824,14 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { responseType: 'arraybuffer' }; - return Wallet.request(postInfo) - .success(function(data, status, headers, config) { - data = PayPro.PaymentACK.decode(data); - var ack = new PayPro(); - ack = ack.makePaymentACK(data); + return this.httpUtil.request(postInfo) + .success(function(rawData) { + var data = PayPro.PaymentACK.decode(rawData); + var paypro = new PayPro(); + ack = paypro.makePaymentACK(data); return self.receivePaymentRequestACK(ntxid, tx, txp, ack, cb); }) - .error(function(data, status, headers, config) { + .error(function(data, status ) { log.debug('Sending to server was not met with a returned tx.'); log.debug('XHR status: ' + status); self._processTxProposalSent(ntxid, function(err, txid) { @@ -1906,148 +1903,6 @@ Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) { }); }; -/** - * @desc Create a Payment Transaction Sync (see BIP70) - * @TODO: Document better - */ -Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) { - var self = this; - var priv = this.privateKey; - var pkr = this.publicKeyRing; - - preconditions.checkState(pkr.isComplete()); - if (options.memo) { - preconditions.checkArgument(options.memo.length <= 100); - } - - var opts = { - remainderOut: { - address: this._doGenerateAddress(true).toString() - } - }; - - if (_.isUndefined(opts.spendUnconfirmed)) { - opts.spendUnconfirmed = this.spendUnconfirmed; - } - - for (var k in Wallet.builderOpts) { - opts[k] = Wallet.builderOpts[k]; - } - - merchantData.total = bignum(merchantData.total, 10); - - var outs = {}; - merchantData.pr.pd.outputs.forEach(function(output) { - var amount = output.amount; - - // big endian - var v = new Buffer(8); - v[0] = (amount.high >> 24) & 0xff; - v[1] = (amount.high >> 16) & 0xff; - v[2] = (amount.high >> 8) & 0xff; - v[3] = (amount.high >> 0) & 0xff; - v[4] = (amount.low >> 24) & 0xff; - v[5] = (amount.low >> 16) & 0xff; - v[6] = (amount.low >> 8) & 0xff; - v[7] = (amount.low >> 0) & 0xff; - - var script = { - offset: output.script.offset, - limit: output.script.limit, - buffer: new Buffer(output.script.buffer, 'hex') - }; - var s = script.buffer.slice(script.offset, script.limit); - var network = merchantData.pr.pd.network === 'main' ? 'livenet' : 'testnet'; - var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), network); - - var a = addr[0].toString(); - outs[a] = bignum.fromBuffer(v, { - endian: 'big', - size: 1 - }).add(outs[a] || bignum(0)); - - merchantData.total = merchantData.total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - }); - - // TODO, for now we only support PayPro with 1 output. - if (_.size(outs) !== 1) - throw new Error('PayPro: Unsupported outputs'); - - var out = _.pairs(outs)[0]; - merchantData.outs = [{ - address: out[0], - amountSatStr: out[1].toString(10), - }]; - merchantData.total = merchantData.total.toString(10); - - var b = new Builder(opts) - .setUnspent(unspent) - .setOutputs(merchantData.outs); - - merchantData.pr.pd.outputs.forEach(function(output, i) { - var script = { - offset: output.script.offset, - limit: output.script.limit, - buffer: new Buffer(output.script.buffer, 'hex') - }; - var s = script.buffer.slice(script.offset, script.limit); - b.tx.outs[i].s = s; - }); - - var selectedUtxos = b.getSelectedUnspent(); - if (selectedUtxos.length > TX_MAX_INS) - throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.length + ' inputs. Aborting'); - - - var inputChainPaths = selectedUtxos.map(function(utxo) { - return pkr.pathForAddress(utxo.address); - }); - - b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - - var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - - if (options.fetch) return; - - log.debug('Created transaction: %s', b.tx.getStandardizedObject()); - - var myId = this.getMyCopayerId(); - var now = Date.now(); - - var tx = b.build(); - if (!tx.countInputSignatures(0)) - throw new Error('Could not sign generated tx'); - - var txSize = tx.getSize(); - if (txSize / 1024 > TX_MAX_SIZE_KB) - throw new Error('BIG: Resulting TX is too big ' + txSize + ' bytes. Aborting'); - - - - var me = {}; - me[myId] = now; - var meSeen = {}; - if (priv) meSeen[myId] = now; - - var ntxid = this.txProposals.add(new TxProposal({ - inputChainPaths: inputChainPaths, - signedBy: me, - seenBy: meSeen, - creator: myId, - createdTs: now, - builder: b, - comment: options.memo, - merchant: merchantData, - paymentProtocolURL: options.uri, - })); - - return ntxid; -}; - /** * @desc Mark that a user has seen a given TxProposal * @return {boolean} true if the internal state has changed @@ -2273,26 +2128,45 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { * @desc Create a transaction proposal * @TODO: Document more */ -Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) { +Wallet.prototype.createTx = function(opts, cb) { + preconditions.checkArgument(_.isObject(opts)); + preconditions.checkArgument(opts.amountSat); + log.debug(opts); + var self = this; + var toAddress = opts.toAddress; + var amountSat = opts.amountSat; + preconditions.checkArgument(!opts.comment || opts.comment.length <= 100); + var url = opts.url; - if (_.isFunction(opts)) { - cb = opts; - opts = {}; - } - opts = opts || {}; - - if (_.isUndefined(opts.spendUnconfirmed)) { - opts.spendUnconfirmed = this.spendUnconfirmed; - } + if (url && !opts.merchantData) { + w.fetchPaymentRequest({ + url: url, + memo: comment, + amount: amountSat, + }, function(err, merchantData) { + if (err) return cb(err); + opts.merchantData = merchantData; + opts.amountSat = merchantData.outs[0].address; + opts.toAddress = merchantData.outs[0].amount; + self.createTx(opts, cb); + }); + }; + preconditions.checkArgument(amountSat); + preconditions.checkArgument(toAddress); this.getUnspent(function(err, safeUnspent) { if (err) return cb(new Error('Could not get list of UTXOs')); var ntxid; try { - ntxid = self.createTxSync(toAddress, amountSatStr, comment, safeUnspent, opts); - log.debug('TX Created: ntxid', ntxid); + var txp = self.createTxProposal(toAddress, amountSat, safeUnspent, opts.builderOpts); + + if (opts.merchantData) + txp.addMerchantData(opts.merchantData); + + var ntxid = self.addNewTxProposal(txp); + log.debug('TXP Added: ', ntxid); } catch (e) { return cb(e); } @@ -2320,36 +2194,41 @@ var sanitize = function(address) { * @desc Create a transaction proposal * @TODO: Document more */ -Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos, opts) { +Wallet.prototype.createTxProposal = function(toAddress, amountSat, utxos, builderOpts) { + preconditions.checkArgument(toAddress); + preconditions.checkArgument(amountSat); + preconditions.checkArgument(_.isArray(utxos)); + builderOpts = builderOpts || {}; + var pkr = this.publicKeyRing; var priv = this.privateKey; - opts = opts || {}; toAddress = sanitize(toAddress); preconditions.checkArgument(toAddress.network().name === this.getNetworkName(), 'networkname mismatch'); preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete'); preconditions.checkState(priv, 'no private key'); - if (comment) preconditions.checkArgument(comment.length <= 100); - if (!opts.remainderOut) { - opts.remainderOut = { + if (!builderOpts.remainderOut) { + builderOpts.remainderOut = { address: this._doGenerateAddress(true).toString() }; } - - for (var k in Wallet.builderOpts) { - opts[k] = Wallet.builderOpts[k]; + if (_.isUndefined(builderOpts.spendUnconfirmed)) { + builderOpts.spendUnconfirmed = this.spendUnconfirmed; } - var b = new Builder(opts) + for (var k in Wallet.builderOpts) { + builderOpts[k] = Wallet.builderOpts[k]; + } + + var b = new Builder(builderOpts) .setUnspent(utxos) .setOutputs([{ address: toAddress.data, - amountSatStr: amountSatStr, + amountSatStr: amountSat, }]); log.debug('Creating TX: Builder ready'); - var selectedUtxos = b.getSelectedUnspent(); if (selectedUtxos.length > TX_MAX_INS) @@ -2363,9 +2242,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - var myId = this.getMyCopayerId(); - var now = Date.now(); + b.sign(keys); var tx = b.build(); @@ -2373,26 +2250,36 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos throw new Error('Could not sign generated tx'); var txSize = tx.getSize(); - if (txSize / 1024 > TX_MAX_SIZE_KB) throw new Error('BIG: Resulting TX is too big ' + txSize + ' bytes. Aborting'); + return new TxProposal({ + inputChainPaths: inputChainPaths, + comment: comment, + builder: b, + }); +}; + +/* addNewTxProposal + * adds a transaction proposal to the list. Sets current copayer and creation metadata. + * + * @param {txp} Transaction Proposal Object + * @desc returns normalized transaction ID + * @param {ntxid} + */ +Wallet.prototype.addNewTxProposal = function(txp) { + var myId = this.getMyCopayerId(); + var now = Date.now(); var me = {}; me[myId] = now; - var meSeen = {}; - if (priv) meSeen[myId] = now; + // Add metadata to TxP + txp.signedBy = txp.seenBy = me; + txp.creator = myId; + txp.createdTs = now; - var ntxid = this.txProposals.add(new TxProposal({ - inputChainPaths: inputChainPaths, - signedBy: me, - seenBy: meSeen, - creator: myId, - createdTs: now, - builder: b, - comment: comment - })); + var ntxid = this.txProposals.add(txp); return ntxid; }; @@ -2664,88 +2551,6 @@ Wallet.prototype.verifySignedJson = function(senderId, payload, signature) { return v; } -/** - * @desc Create a HTTP request - * @TODO: This shouldn't be a wallet responsibility - */ -Wallet.request = function(options, callback) { - if (_.isString(options)) { - options = { - uri: options - }; - } - - options.method = options.method || 'GET'; - options.headers = options.headers || {}; - - var ret = { - success: function(cb) { - this._success = cb; - return this; - }, - error: function(cb) { - this._error = cb; - return this; - }, - _success: function() {; - }, - _error: function(_, err) { - throw err; - } - }; - - var method = (options.method || 'GET').toUpperCase(); - var uri = options.uri || options.url; - var req = options; - - req.headers = req.headers || {}; - req.body = req.body || req.data || {}; - - var xhr = new XMLHttpRequest(); - xhr.open(method, uri, 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); - }); - - if (req.responseType) { - xhr.responseType = req.responseType; - } - - xhr.onload = function(event) { - var response = xhr.response; - var buf = new Uint8Array(response); - var headers = {}; - (xhr.getAllResponseHeaders() || '').replace( - /(?:\r?\n|^)([^:\r\n]+): *([^\r\n]+)/g, - function($0, $1, $2) { - headers[$1.toLowerCase()] = $2; - } - ); - return ret._success(buf, xhr.status, headers, options); - }; - - xhr.onerror = function(event) { - var status; - if (xhr.status === 0 || !xhr.statusText) { - status = 'HTTP Request Error: This endpoint likely does not support cross-origin requests.'; - } else { - status = xhr.statusText; - } - return ret._error(null, status, null, options); - }; - - if (req.body) { - xhr.send(req.body); - } else { - xhr.send(null); - } - - return ret; -}; /**