From d9c72392bd6abd5347710ccea160c8d54f961db8 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 1 Aug 2014 19:45:24 -0700 Subject: [PATCH] paypro: through a lot of debugging. Payment sending is working. --- js/controllers/send.js | 8 +- js/directives.js | 33 +++++++- js/models/core/Wallet.js | 159 ++++++++++++++++++++++++++++++++++----- 3 files changed, 180 insertions(+), 20 deletions(-) diff --git a/js/controllers/send.js b/js/controllers/send.js index 22a92fb4f..d210d368f 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -111,8 +111,12 @@ angular.module('copayApp.controllers').controller('SendController', $rootScope.pendingPayment = null; } - var uri = address.indexOf('bitcoin:') === 0 - && copay.HDPath.parseBitcoinURI(address); + var uri; + if (address.indexOf('bitcoin:') === 0) { + uri = copay.HDPath.parseBitcoinURI(address); + } else if (address.indexOf('Merchant: ') === 0) { + uri = { merchant: address.split(' ')[1] }; + } if (uri && uri.merchant) { w.createPaymentTx({ diff --git a/js/directives.js b/js/directives.js index 4a76f80a3..7c09521ac 100644 --- a/js/directives.js +++ b/js/directives.js @@ -14,6 +14,8 @@ angular.module('copayApp.directives') var validator = function(value) { var uri = copay.HDPath.parseBitcoinURI(value); + window._rootScope = scope; + // Is this a payment protocol URI (BIP-72)? if (uri && uri.merchant) { scope.wallet.fetchPaymentTx(uri.merchant, function(err, merchantData) { @@ -44,6 +46,9 @@ angular.module('copayApp.directives') // XXX There needs to be a better way to do this: total = +total / config.unitToSatoshi; + var sendForm = angular.element( + document.getElementsByName('sendForm')[0]); + var address = angular.element( document.querySelector('input#address')); @@ -75,7 +80,13 @@ angular.module('copayApp.directives') document.querySelector('[title="Send all funds"]')); sendall.attr('class', sendall.attr('class') + ' hidden'); - address.on('change', function(ev) { + // Reset all the changes from the payment protocol weirdness. + // XXX use ng-change attr instead + //address.attr('ng-change', 'ppChange()'); + //scope.ppChange = scope.ppChange || function() { + //address.on('change', function(ev) { + scope.$on('change', function(ev) { + //scope.$watch('address', function(newValue, oldValue) { var val = address.val(); var uri = copay.HDPath.parseBitcoinURI(val || ''); if (!uri || !uri.merchant) { @@ -85,6 +96,13 @@ angular.module('copayApp.directives') if (amount.attr('disabled') === false) { submit.attr('disabled', true); } + sendto.html(sendto.html().replace(/
Server:.*$/, '')); + if (!/hidden/.test(tamount.attr('class'))) { + tamount.attr(tamount.attr('class') + ' hidden'); + } + if (submit.attr('disabled') === false) { + submit.attr('disabled', true); + } if (/ hidden$/.test(sendall.attr('class'))) { sendall.attr('class', sendall.attr('class').replace(' hidden', '')); @@ -92,9 +110,22 @@ angular.module('copayApp.directives') } // TODO: Check paymentRequest expiration, // delete if beyond expiration date. + //}; + //}); }); + //scope.$apply(); // scope.$digest(); ctrl.$setValidity('validAddress', true); + + scope.sendForm.$valid = true; + scope.sendForm.$invalid = false; + scope.sendForm.$pristine = true; + scope.sendForm.address.$valid = true; + scope.sendForm.address.$invalid = false; + scope.sendForm.address.$pristine = true; + scope.sendForm.amount.$valid = true; + scope.sendForm.amount.$invalid = false; + scope.sendForm.amount.$pristine = true; }); ctrl.$setValidity('validAddress', true); diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 66079d834..c1ceab8c9 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -25,15 +25,9 @@ var TxProposals = require('./TxProposals'); var PrivateKey = require('./PrivateKey'); var copayConfig = require('../../../config'); -if (typeof window !== 'undefined') { - var G = window; -} else { - var G = global; -} - -if (typeof angular !== 'undefined') { - G.$http = G.$http || angular.bootstrap().get('$http'); -} +var G = typeof window !== 'undefined' + ? window + : global; function Wallet(opts) { var self = this; @@ -808,8 +802,9 @@ Wallet.prototype.createPaymentTx = function(options, cb) { headers: { 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': 'application/octet-stream', - 'Content-Length': 0 + 'Content-Type': 'application/octet-stream' + // XHR does not allow these: + // 'Content-Length': 0 }, responseType: 'arraybuffer' }) @@ -978,13 +973,13 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { if (options.refund_to) { // pubkey needs to be ripesha'd - options.refund_to = bitcore.sha256ripe160(options.refund_to); + options.refund_to = bitcore.util.sha256ripe160(options.refund_to); var total = txp.merchant.pr.pd.outputs.reduce(function(total, _, i) { return total.add(bignum.fromBuffer(tx.outs[i].v, { endian: 'little', size: 1 })); - }, bugnum('0', 10)); + }, bignum('0', 10)); var rpo = new PayPro(); rpo = rpo.makeOutput(); // XXX Bad - the amount *has* to be a Number in protobufjs @@ -1021,6 +1016,29 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { pay.set('memo', options.memo); + pay = pay.serialize(); + + this.log(pay); + this.log(pay.toString('hex')); + + // https://www.google.com/search?q=angular+%24http+ArrayBuffer+in+body + // https://github.com/feross/buffer/blob/master/index.js + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays + + // var view = new Uint8Array(new ArrayBuffer(pay.length)); + // Buffer._augment(view); + // pay = pay.copy(view); + + // var view = new Uint8Array(new ArrayBuffer(pay.length)); + // view.set(Array.prototype.slice.call(pay), 0); + // pay = view; + + var buf = new ArrayBuffer(pay.length); + var view = new Uint8Array(buf); + for (var i = 0; i < pay.length; i++) { + view[i] = pay[i]; + } + return $http({ method: 'POST', url: txp.merchant.pr.pd.payment_url, @@ -1028,11 +1046,16 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { // BIP-71 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE, - 'Content-Length': pay.length + '', - 'Content-Transfer-Encoding': 'binary' + 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE + // XHR does not allow these: + // 'Content-Length': (pay.byteLength || pay.length) + '', + // 'Content-Transfer-Encoding': 'binary' }, - body: pay.serialize(), + // data: pay, + // data: pay, + // data: view, + data: buf, // Technically how this should be done. + // requestType: 'arraybuffer', responseType: 'arraybuffer' }) .success(function(data, status, headers, config) { @@ -1060,7 +1083,13 @@ Wallet.prototype.receivePaymentRequestACK = function(tx, txp, ack, cb) { payment = pay.makePayment(payment); var tx = payment.message.transactions[0]; + + if (!tx) { + return cb(); + } + if (tx.buffer) { + tx.buffer = new Buffer(new Uint8Array(tx.buffer)); tx.buffer = tx.buffer.slice(tx.offset, tx.limit); var ptx = new bitcore.Transaction(); ptx.parse(tx.buffer); @@ -1551,4 +1580,100 @@ Wallet.prototype.verifySignedJson = function(senderId, payload, signature) { return v; } +// NOTE: Angular $http module does not send ArrayBuffers correctly, so we're +// not going to use it. We'll have to write our own. Otherwise, we could +// hex-encoded our messages and decode them on the other side, but that +// deviates from BIP-70 slightly. +// if (typeof angular !== 'undefined') { +// G.$http = G.$http || angular.bootstrap().get('$http'); +// } + +G.$http = G.$http || function $http(options, callback) { + if (typeof options === 'string') { + 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 || {}; + + if (typeof XMLHttpRequest !== 'undefined') { + var xhr = new XMLHttpRequest(); + xhr.open(method, uri, true); + + Object.keys(options.headers).forEach(function(key) { + var val = options.headers[key]; + if (key === 'Content-Length') return; + if (key === 'Content-Transfer-Encoding') return; + xhr.setRequestHeader(key, val); + }); + + // For older browsers (binary data): + // xhr.overrideMimeType('text/plain; charset=x-user-defined'); + + // Newer browsers (binary data): + // xhr.responseType = 'arraybuffer'; + + if (options.responseType) { + xhr.responseType = options.responseType; + } + + // xhr.onreadystatechange = function() { + // if (xhr.readyState == 4) { + // ; + // } + // }; + + 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) { + return ret._error(null, new Error(event.message), null, options); + }; + + if (options.data || options.body) { + xhr.send(options.data || options.body); + } else { + xhr.send(null); + } + + return ret; + } + + return ret; +}; + module.exports = require('soop')(Wallet);