diff --git a/js/models/Identity.js b/js/models/Identity.js index 2503a9c62..54a8913b7 100644 --- a/js/models/Identity.js +++ b/js/models/Identity.js @@ -402,6 +402,9 @@ Identity.prototype.bindWallet = function(w) { w.on('txProposalsUpdated', function() { Identity.storeWalletDebounced(self, w); }); + w.on('paymentAck', function() { + Identity.storeWalletDebounced(self, w); + }); w.on('newAddresses', function() { Identity.storeWalletDebounced(self, w); }); diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index 7c986cd0a..bccb7f618 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -13,7 +13,7 @@ var preconditions = require('preconditions').instance(); var TX_MAX_SIZE_KB = 50; var VERSION = 1; -var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment', 'paymentProtocolURL']; +var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment', 'paymentProtocolURL', 'paymentAckMemo']; function TxProposal(opts) { @@ -38,6 +38,7 @@ function TxProposal(opts) { this.comment = opts.comment || null; this.readonly = opts.readonly || null; this.merchant = opts.merchant || null; + this.paymentAckMemo = null; this.paymentProtocolURL = opts.paymentProtocolURL || null; if (opts.creator) { diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 361407b69..09c41b749 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -1505,13 +1505,10 @@ Wallet.prototype.broadcastTx = function(ntxid, cb) { if (!tx.isComplete()) throw new Error('Tx is not complete. Can not broadcast'); - log.info('Wallet:' + this.id + ' Broadcasting Transaction ntxid:' + ntxid); var serializedTx = tx.serialize(); - if (txp.merchant) { - this.sendPaymentTx(ntxid, serializedTx); - } + log.info('Wallet:' + this.id + ' Broadcasting Transaction ntxid:' + ntxid); var txHex = serializedTx.toString('hex'); log.debug('\tRaw transaction: ', txHex); @@ -1522,10 +1519,17 @@ Wallet.prototype.broadcastTx = function(ntxid, cb) { if (txid) { log.debug('Wallet:' + self.getName() + ' broadcasted a TX. BITCOIND txid:', txid); - var txp = self.txProposals.get(ntxid); + txp.setSent(txid); self.sendTxProposal(ntxid); self.emitAndKeepAlive('txProposalsUpdated'); + + // PAYPRO: Payment message is optional, only if payment_url is set + // This is async. and will notify and update txp async. + if (txp.merchant && txp.merchant.pr.pd.payment_url) { + self.sendPaymentTx(ntxid, serializedTx); + } + return cb(null, txid, Wallet.TX_BROADCASTED); } else { log.info('Wallet:' + self.getName() + '. Sent failed. Checking if the TX was sent already'); @@ -1731,6 +1735,58 @@ Wallet.prototype.parsePaymentRequest = function(options, rawData) { return merchantData; }; +/** + * _getPayProRefundOutputs + * Create refund address for PayPro. + * Uses current transaction's change address. + * + * @param txp + * @return {undefined} + */ +Wallet.prototype._getPayProRefundOutputs = function(txp) { + var pkr = this.publicKeyRing; + var index = pkr.getHDParams(this.publicKey); + var amount = +txp.merchant.total.toString(10); + + var output = new PayPro.Output(); + var script = pkr.getScriptPubKeyHex(index.changeIndex, true, this.pubkey); + output.set('script',new Buffer(script, 'hex')); + output.set('amount', amount); + return [output]; +}; + + +Wallet.prototype._createPaymentTx = function(txp, txHex) { + + var refund_outputs = this._getPayProRefundOutputs(txp); + + // We send this to the serve after receiving a PaymentRequest + var pay = new PayPro(); + pay = pay.makePayment(); + + var merchant_data = txp.merchant.pr.pd.merchant_data; + if (merchant_data) { + merchant_data = new Buffer(merchant_data, 'hex'); + pay.set('merchant_data', merchant_data); + } + + pay.set('transactions', [txHex]); + 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; +}; + /** * @desc Send a payment transaction to a server, complying with BIP70 * @@ -1741,73 +1797,10 @@ Wallet.prototype.parsePaymentRequest = function(options, rawData) { */ Wallet.prototype.sendPaymentTx = function(ntxid, txHex) { var self = this; - - var refund_outputs = []; - options.refund_to = this.publicKeyRing.getPubKeys(0, false, this.getMyCopayerId())[0]; - - if (options.refund_to) { - var total = txp.merchant.pr.pd.outputs.reduce(function(total, _, i) { - // XXX reverse endianness to work around bignum bug: - var txv = tx.outs[i].v; - var v = new Buffer(8); - for (var j = 0; j < 8; j++) v[j] = txv[7 - j]; - return total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - }, bignum('0', 10)); - - var rpo = new PayPro(); - rpo = rpo.makeOutput(); - - // XXX Bad - the amount *has* to be a Number in protobufjs - // Possibly does not matter - server can ignore the amount anyway. - rpo.set('amount', +total.toString(10)); - - rpo.set('script', - Buffer.concat([ - new Buffer([ - 118, // OP_DUP - 169, // OP_HASH160 - 76, // OP_PUSHDATA1 - 20, // number of bytes - ]), - // needs to be ripesha'd - bitcore.util.sha256ripe160(options.refund_to), - new Buffer([ - 136, // OP_EQUALVERIFY - 172 // OP_CHECKSIG - ]) - ]) - ); - - refund_outputs.push(rpo.message); - } - - // We send this to the serve after receiving a PaymentRequest - var pay = new PayPro(); - pay = pay.makePayment(); - var merchant_data = txp.merchant.pr.pd.merchant_data; - if (merchant_data) { - merchant_data = new Buffer(merchant_data, 'hex'); - pay.set('merchant_data', merchant_data); - } - pay.set('transactions', [serializedTx]); - pay.set('refund_to', refund_outputs); - - // Unused for now - // options.memo = ''; - // pay.set('memo', options.memo); - - pay = pay.serialize(); - log.debug('Sending Payment Message:', pay.toString('hex')); - - var buf = new ArrayBuffer(pay.length); - var view = new Uint8Array(buf); - for (var i = 0; i < pay.length; i++) { - view[i] = pay[i]; - } - + var txp = this.txProposals.get(ntxid); + var data = this._createPaymentTx(txp, txHex); + + log.debug('Sending Payment Message to merchant server'); var postInfo = { method: 'POST', url: txp.merchant.pr.pd.payment_url, @@ -1821,7 +1814,7 @@ Wallet.prototype.sendPaymentTx = function(ntxid, txHex) { }, // Technically how this should be done via XHR (used to // be the ArrayBuffer, now you send the View instead). - data: view, + data: data, responseType: 'arraybuffer' }; @@ -1832,6 +1825,8 @@ Wallet.prototype.sendPaymentTx = function(ntxid, txHex) { var ack = paypro.makePaymentACK(data); var memo = ack.get('memo'); log.debug('Payment Acknowledged!: %s', memo); + txp.paymentAckMemo = memo; + self.sendTxProposal(ntxid); self.emitAndKeepAlive('paymentACK', memo); }) .error(function(data, status) { @@ -2131,13 +2126,13 @@ Wallet.prototype.spend = function(opts, cb) { }; /** - * _newAddress + * _getAddress * Returns an Address object from an address string or a BIP21 URL.* * @param address * @return { bitcore.Address } */ -Wallet._newAddress = function(address) { +Wallet._getAddress = function(address) { if (/ ^ bitcoin: /g.test(address)) { return new BIP21(address).address; } @@ -2186,7 +2181,7 @@ Wallet.prototype._createTxProposal = function(toAddress, amountSat, comment, utx var pkr = this.publicKeyRing; var priv = this.privateKey; - var addr = Wallet._newAddress(toAddress); + var addr = Wallet._getAddress(toAddress); preconditions.checkState(addr && addr.data && addr.isValid(), 'Bad address:' + addr.toString()); diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 5eb2084d0..dd1d07959 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -136,6 +136,10 @@ angular.module('copayApp.services') root.updateTxsAndBalance(w); }); + w.on('paymentACK', function(memo) { + notification.success('Payment Acknowledged', memo); + }); + w.on('txProposalEvent', function(e) { root.updateTxsAndBalance(w);