diff --git a/js/controllers/send.js b/js/controllers/send.js index ff0b10d7c..efceab7fb 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -168,7 +168,7 @@ angular.module('copayApp.controllers').controller('SendController', toAddress: address, amountSat: amount, comment: commentText, - url: (payInfo && payInfo.merchant) ? url : null, + url: (payInfo && payInfo.merchant) ? payInfo.merchant : null, }, $scope._afterSend); }; diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index bb36052c3..934319171 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -77,7 +77,7 @@ TxProposal.prototype._checkPayPro = function() { if (ppOut.address !== txOut.address) throw new Error('PayPro: Wrong out address in Tx'); - if (ppOut.amountSatStr !== txOut.amountSatStr) + if (ppOut.amountSatStr !== txOut.amountSatStr + '') throw new Error('PayPro: Wrong amount in Tx'); }; @@ -112,8 +112,13 @@ TxProposal.prototype.trimForStorage = function() { }; TxProposal.prototype.addMerchantData = function(merchantData) { + preconditions.checkArgument(merchantData.pr); + preconditions.checkArgument(merchantData.request_url); var m = _.clone(merchantData); + if (!this.paymentProtocolURL) + this.paymentProtocolURL = m.request_url; + // remove unneeded data m.raw = m.pr.pki_data = m.pr.signature = undefined; this.merchant = m; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 5ce44a90a..24fd2f6ce 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -1535,12 +1535,9 @@ Wallet.prototype.fetchPaymentRequest = function(options, cb) { .success(function(rawData) { log.info('PayPro Request done successfully. Parsing response') - var data = PayPro.PaymentRequest.decode(rawData); - var paypro = new PayPro(); - var pr = paypro.makePaymentRequest(data); var merchantData, err; try { - merchantData = self.parsePaymentRequest(options, pr); + merchantData = self.parsePaymentRequest(options, rawData); } catch (e) { err = e }; @@ -1550,7 +1547,6 @@ Wallet.prototype.fetchPaymentRequest = function(options, cb) { }) .error(function(data, status) { log.debug('Server did not return PaymentRequest.\nXHR status: ' + status); - preconditions.checkState(options.fetch); return cb(new Error('Status: ' + status)); }); }; @@ -1635,18 +1631,16 @@ Wallet.prototype._addOutputsToMerchantData = function(merchantData) { /** * @desc Analyzes a payment request and generate merchantData * @param {Object} options - * @param {PayProRequest} pr - * @param {string} pr.payment_details_version - * @param {string} pr.pki_type - * @param {Object} pr.data - * @param {string} pr.serialized_payment_details - * @param {string} pr.signature - * @param {string} options.memo - * @param {string} options.comment + * @param {string} options.url url where the pay request was acquired + * @param {string} options.amount Only used if pay requesst allow user to set the amount + * @param {PayProRequest} rawData */ -Wallet.prototype.parsePaymentRequest = function(options, pr) { +Wallet.prototype.parsePaymentRequest = function(options, rawData) { var self = this; + var data = PayPro.PaymentRequest.decode(rawData); + var paypro = new PayPro(); + var pr = paypro.makePaymentRequest(data); var ver = pr.get('payment_details_version'); var pki_type = pr.get('pki_type'); var pki_data = pr.get('pki_data'); @@ -2132,8 +2126,7 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { */ Wallet.prototype.createTx = function(opts, cb) { preconditions.checkArgument(_.isObject(opts)); - preconditions.checkArgument(opts.amountSat); - log.debug(opts); + log.debug('create Options', opts); var self = this; var toAddress = opts.toAddress; @@ -2142,23 +2135,26 @@ Wallet.prototype.createTx = function(opts, cb) { var url = opts.url; if (url && !opts.merchantData) { - w.fetchPaymentRequest({ + return self.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); + opts.toAddress = merchantData.outs[0].address; + opts.amountSat = parseInt(merchantData.outs[0].amountSatStr); + return self.createTx(opts, cb); }); }; - preconditions.checkArgument(amountSat); - preconditions.checkArgument(toAddress); + preconditions.checkArgument(amountSat, 'no amount'); + preconditions.checkArgument(toAddress, 'no address'); this.getUnspent(function(err, safeUnspent) { - if (err) return cb(new Error('Could not get list of UTXOs')); + if (err) { + log.info(err); + return cb(new Error('CreateTx: Could not get list of UTXOs')); + } var ntxid, txp; try { @@ -2168,8 +2164,9 @@ Wallet.prototype.createTx = function(opts, cb) { return cb(e); } - if (opts.merchantData) + if (opts.merchantData) { txp.addMerchantData(opts.merchantData); + } var ntxid = self.txProposals.add(txp); log.debug('TXP Added: ', ntxid); @@ -2207,9 +2204,10 @@ Wallet.prototype.createTxProposal = function(toAddress, amountSat, comment, utxo var pkr = this.publicKeyRing; var priv = this.privateKey; - toAddress = sanitize(toAddress); + var addr = sanitize(toAddress); + preconditions.checkState(addr && addr.data && addr.isValid(), 'Bad address:' + addr.toString()); - preconditions.checkArgument(toAddress.network().name === this.getNetworkName(), 'networkname mismatch'); + preconditions.checkArgument(addr.network().name === this.getNetworkName(), 'networkname mismatch'); preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete'); preconditions.checkState(priv, 'no private key'); @@ -2229,7 +2227,7 @@ Wallet.prototype.createTxProposal = function(toAddress, amountSat, comment, utxo var b = new Builder(builderOpts) .setUnspent(utxos) .setOutputs([{ - address: toAddress.data, + address: addr.data, amountSatStr: amountSat, }]); diff --git a/js/util/HTTP.js b/js/util/HTTP.js new file mode 100644 index 000000000..00daa6d93 --- /dev/null +++ b/js/util/HTTP.js @@ -0,0 +1,75 @@ +module.exports = { + request: function(options, callback) { + preconditions.checkArgument(_.isObject(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 url = options.url; + var req = options; + + req.headers = req.headers || {}; + req.body = req.body || req.data || {}; + + var 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); + }); + + 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; + }, +}; diff --git a/test/PayPro.js b/test/PayPro.js deleted file mode 100644 index 21e272842..000000000 --- a/test/PayPro.js +++ /dev/null @@ -1,793 +0,0 @@ -'use strict'; - -var Wallet = copay.Wallet; -var PrivateKey = copay.PrivateKey; -var Network = requireMock('FakeNetwork'); -var Blockchain = requireMock('FakeBlockchain'); -var TransactionBuilder = bitcore.TransactionBuilder; -var Transaction = bitcore.Transaction; -var Address = bitcore.Address; -var PayPro = bitcore.PayPro; -var bignum = bitcore.Bignum; -var startServer = copay.FakePayProServer; // TODO should be require('./mocks/FakePayProServer'); - -var server; - -var walletConfig = { - requiredCopayers: 1, - totalCopayers: 1, - spendUnconfirmed: true, - reconnectDelay: 100, - networkName: 'testnet' -}; - -var getNewEpk = function() { - return new PrivateKey({ - networkName: walletConfig.networkName, - }) - .deriveBIP45Branch() - .extendedPublicKeyString(); -}; - -describe('PayPro (in Wallet) model', function() { - - if (!is_browser) { - var createW = function(N, conf) { - var c = JSON.parse(JSON.stringify(conf || walletConfig)); - if (!N) N = c.totalCopayers; - - var mainPrivateKey = new copay.PrivateKey({ - networkName: walletConfig.networkName - }); - var mainCopayerEPK = mainPrivateKey.deriveBIP45Branch().extendedPublicKeyString(); - c.privateKey = mainPrivateKey; - - c.publicKeyRing = new copay.PublicKeyRing({ - networkName: c.networkName, - requiredCopayers: Math.min(N, c.requiredCopayers), - totalCopayers: N, - }); - c.publicKeyRing.addCopayer(mainCopayerEPK); - - c.txProposals = new copay.TxProposals({ - networkName: c.networkName, - }); - - var network = new Network(walletConfig.network); - var blockchain = new Blockchain(walletConfig.blockchain); - c.network = network; - c.blockchain = blockchain; - - c.addressBook = { - '2NFR2kzH9NUdp8vsXTB4wWQtTtzhpKxsyoJ': { - label: 'John', - copayerId: '026a55261b7c898fff760ebe14fd22a71892295f3b49e0ca66727bc0a0d7f94d03', - createdTs: 1403102115, - hidden: false - }, - '2MtP8WyiwG7ZdVWM96CVsk2M1N8zyfiVQsY': { - label: 'Jennifer', - copayerId: '032991f836543a492bd6d0bb112552bfc7c5f3b7d5388fcbcbf2fbb893b44770d7', - createdTs: 1403103115, - hidden: false - } - }; - - c.networkName = walletConfig.networkName; - c.version = '0.0.1'; - - c.network = sinon.stub(); - c.network.setHexNonce = sinon.stub(); - c.network.setHexNonces = sinon.stub(); - c.network.getHexNonce = sinon.stub(); - c.network.getHexNonces = sinon.stub(); - c.network.send = sinon.stub(); - - return new Wallet(c); - } - - var unspentTest = [{ - "address": "dummy", - "scriptPubKey": "dummy", - "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", - "vout": 1, - "amount": 10, - "confirmations": 7 - }]; - - var createW2 = function(privateKeys, N, conf) { - if (!N) N = 3; - var w = createW(N, conf); - should.exist(w); - - var pkr = w.publicKeyRing; - - for (var i = 0; i < N - 1; i++) { - if (privateKeys) { - var k = privateKeys[i]; - pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : getNewEpk()); - } else { - pkr.addCopayer(getNewEpk()); - } - } - - return w; - }; - - var cachedW2 = null; - var cachedW2obj = null; - var cachedCreateW2 = function() { - if (!cachedW2) { - cachedW2 = createW2(); - cachedW2obj = cachedW2.toObj(); - cachedW2obj.opts.reconnectDelay = 100; - } - - Wallet._newAsync = sinon.stub().returns(new Network(walletConfig.network)); - Wallet._newInsight = sinon.stub().returns(new Blockchain(walletConfig.blockchain)); - - var w = Wallet.fromObj(cachedW2obj, { - blockchainOpts: {}, - networkOpts: {}, - }); - return w; - }; - - var createWallet = function() { - var w = cachedCreateW2(); - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - w.getUnspent = sinon.stub().yields(null, unspentTest, unspentTest); - return w; - }; - - it('#start the example server', function(done) { - startServer(function(err, s) { - if (err) return done(err); - server = s; - server.uri = 'https://localhost:8080/-'; - done(); - }); - }); - - var pr; - var ppw; - - ppw = createWallet(); - - it('#retrieve a payment request message via http', function(done) { - var w = ppw; - should.exist(w); - - var req = { - headers: { - 'Host': 'localhost:8080', - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': 'application/octet-stream', - 'Content-Length': '0' - }, - socket: { - remoteAddress: 'localhost', - remotePort: 8080 - }, - body: {} - }; - - Object.keys(req.headers).forEach(function(key) { - req.headers[key.toLowerCase()] = req.headers[key]; - }); - - server.GET['/-/request'](req, function(err, res, body) { - var data = PayPro.PaymentRequest.decode(body); - pr = new PayPro(); - pr = pr.makePaymentRequest(data); - done(); - }); - }); - - it('#send a payment message via http', function(done) { - var w = ppw; - should.exist(w); - - var ver = pr.get('payment_details_version'); - var pki_type = pr.get('pki_type'); - var pki_data = pr.get('pki_data'); - var details = pr.get('serialized_payment_details'); - var sig = pr.get('signature'); - - var certs = PayPro.X509Certificates.decode(pki_data); - certs = certs.certificate; - - var verified = pr.verify(); - - if (!verified) { - return done(new Error('Server sent a bad signature.')); - } - - details = PayPro.PaymentDetails.decode(details); - var pd = new PayPro(); - pd = pd.makePaymentDetails(details); - - var network = pd.get('network'); - var outputs = pd.get('outputs'); - var time = pd.get('time'); - var expires = pd.get('expires'); - var memo = pd.get('memo'); - var payment_url = pd.get('payment_url'); - var merchant_data = pd.get('merchant_data'); - - var priv = w.privateKey; - var pkr = w.publicKeyRing; - - var opts = { - remainderOut: { - address: w._doGenerateAddress(true).toString() - } - }; - - var outs = []; - outputs.forEach(function(output) { - var amount = output.get('amount'); - var script = { - offset: output.get('script').offset, - limit: output.get('script').limit, - buffer: new Buffer(new Uint8Array( - output.get('script').buffer)) - }; - - // 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 s = script.buffer.slice(script.offset, script.limit); - var net = network === 'main' ? 'livenet' : 'testnet'; - var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), net); - - outs.push({ - address: addr[0].toString(), - amountSatStr: bitcore.Bignum.fromBuffer(v, { - // XXX for some reason, endian is ALWAYS 'big' - // in node (in the browser it behaves correctly) - endian: 'big', - size: 1 - }).toString(10) - }); - }); - - var b = new bitcore.TransactionBuilder(opts) - .setUnspent(unspentTest) - .setOutputs(outs); - - outputs.forEach(function(output, i) { - var script = { - offset: output.get('script').offset, - limit: output.get('script').limit, - buffer: new Buffer(new Uint8Array( - output.get('script').buffer)) - }; - var s = script.buffer.slice(script.offset, script.limit); - b.tx.outs[i].s = s; - }); - - var selectedUtxos = b.getSelectedUnspent(); - var inputChainPaths = selectedUtxos.map(function(utxo) { - return pkr.pathForAddress(utxo.address); - }); - - b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - - if (priv) { - var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - } - - var tx = b.build(); - - var refund_outputs = []; - - var refund_to = w.publicKeyRing.getPubKeys(0, false, w.getMyCopayerId())[0]; - - var total = 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 - })); - }, bitcore.Bignum('0', 10)); - - var rpo = new PayPro(); - rpo = rpo.makeOutput(); - - 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(refund_to), - new Buffer([ - 136, // OP_EQUALVERIFY - 172 // OP_CHECKSIG - ]) - ]) - ); - - refund_outputs.push(rpo.message); - - var pay = new PayPro(); - pay = pay.makePayment(); - pay.set('merchant_data', new Buffer([0, 1])); - pay.set('transactions', [tx.serialize()]); - pay.set('refund_to', refund_outputs); - pay.set('memo', 'Hi server, I would like to give you some money.'); - - pay = pay.serialize(); - - var req = { - headers: { - 'Host': 'localhost:8080', - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE, - 'Content-Length': pay.length + '' - }, - socket: { - remoteAddress: 'localhost', - remotePort: 8080 - }, - body: pay, - data: pay - }; - - Object.keys(req.headers).forEach(function(key) { - req.headers[key.toLowerCase()] = req.headers[key]; - }); - - server.POST['/-/pay'](req, function(err, res, body) { - if (err) return done(err); - - var data = PayPro.PaymentACK.decode(body); - var ack = new PayPro(); - ack = ack.makePaymentACK(data); - - var payment = ack.get('payment'); - var memo = ack.get('memo'); - - payment = PayPro.Payment.decode(payment); - var pay = new PayPro(); - payment = pay.makePayment(payment); - - var tx = payment.message.transactions[0]; - - if (!tx) { - return done(new Error('No tx in payment ACK.')); - } - - 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); - tx = ptx; - } - - var ackTotal = 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 - })); - }, bitcore.Bignum('0', 10)); - - ackTotal.toString(10).should.equal(total.toString(10)); - - done(); - }); - }); - - it('#retrieve a payment request message via http', function(done) { - var w = ppw; - should.exist(w); - - var req = { - headers: { - 'Host': 'localhost:8080', - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': 'application/octet-stream', - 'Content-Length': '0' - }, - socket: { - remoteAddress: 'localhost', - remotePort: 8080 - }, - body: {} - }; - - Object.keys(req.headers).forEach(function(key) { - req.headers[key.toLowerCase()] = req.headers[key]; - }); - - server.GET['/-/request'](req, function(err, res, body) { - var data = PayPro.PaymentRequest.decode(body); - pr = new PayPro(); - pr = pr.makePaymentRequest(data); - done(); - }); - }); - - it('#send a payment message via http', function(done) { - var w = ppw; - should.exist(w); - - var ver = pr.get('payment_details_version'); - var pki_type = pr.get('pki_type'); - var pki_data = pr.get('pki_data'); - var details = pr.get('serialized_payment_details'); - var sig = pr.get('signature'); - - var certs = PayPro.X509Certificates.decode(pki_data); - certs = certs.certificate; - - var verified = pr.verify(); - - if (!verified) { - return done(new Error('Server sent a bad signature.')); - } - - details = PayPro.PaymentDetails.decode(details); - var pd = new PayPro(); - pd = pd.makePaymentDetails(details); - - var network = pd.get('network'); - var outputs = pd.get('outputs'); - var time = pd.get('time'); - var expires = pd.get('expires'); - var memo = pd.get('memo'); - var payment_url = pd.get('payment_url'); - var merchant_data = pd.get('merchant_data'); - - var priv = w.privateKey; - var pkr = w.publicKeyRing; - - var opts = { - remainderOut: { - address: w._doGenerateAddress(true).toString() - } - }; - - var outs = []; - outputs.forEach(function(output) { - var amount = output.get('amount'); - var script = { - offset: output.get('script').offset, - limit: output.get('script').limit, - buffer: new Buffer(new Uint8Array( - output.get('script').buffer)) - }; - - // 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 s = script.buffer.slice(script.offset, script.limit); - var net = network === 'main' ? 'livenet' : 'testnet'; - var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), net); - - outs.push({ - address: addr[0].toString(), - amountSatStr: bitcore.Bignum.fromBuffer(v, { - // XXX for some reason, endian is ALWAYS 'big' - // in node (in the browser it behaves correctly) - endian: 'big', - size: 1 - }).toString(10) - }); - }); - - var b = new bitcore.TransactionBuilder(opts) - .setUnspent(unspentTest) - .setOutputs(outs); - - outputs.forEach(function(output, i) { - var script = { - offset: output.get('script').offset, - limit: output.get('script').limit, - buffer: new Buffer(new Uint8Array( - output.get('script').buffer)) - }; - var s = script.buffer.slice(script.offset, script.limit); - b.tx.outs[i].s = s; - }); - - var selectedUtxos = b.getSelectedUnspent(); - var inputChainPaths = selectedUtxos.map(function(utxo) { - return pkr.pathForAddress(utxo.address); - }); - - b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - - if (priv) { - var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - } - - var tx = b.build(); - - var refund_outputs = []; - - var refund_to = w.publicKeyRing.getPubKeys(0, false, w.getMyCopayerId())[0]; - - var total = 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 - })); - }, bitcore.Bignum('0', 10)); - - var rpo = new PayPro(); - rpo = rpo.makeOutput(); - - 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(refund_to), - new Buffer([ - 136, // OP_EQUALVERIFY - 172 // OP_CHECKSIG - ]) - ]) - ); - - refund_outputs.push(rpo.message); - - var pay = new PayPro(); - pay = pay.makePayment(); - pay.set('merchant_data', new Buffer([0, 1])); - pay.set('transactions', [tx.serialize()]); - pay.set('refund_to', refund_outputs); - pay.set('memo', 'Hi server, I would like to give you some money.'); - - pay = pay.serialize(); - - var req = { - headers: { - 'Host': 'localhost:8080', - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE, - 'Content-Length': pay.length + '' - }, - socket: { - remoteAddress: 'localhost', - remotePort: 8080 - }, - body: pay, - data: pay - }; - - Object.keys(req.headers).forEach(function(key) { - req.headers[key.toLowerCase()] = req.headers[key]; - }); - - server.POST['/-/pay'](req, function(err, res, body) { - if (err) return done(err); - - var data = PayPro.PaymentACK.decode(body); - var ack = new PayPro(); - ack = ack.makePaymentACK(data); - - var payment = ack.get('payment'); - var memo = ack.get('memo'); - - payment = PayPro.Payment.decode(payment); - var pay = new PayPro(); - payment = pay.makePayment(payment); - - var tx = payment.message.transactions[0]; - - if (!tx) { - return done(new Error('No tx in payment ACK.')); - } - - 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); - tx = ptx; - } - - var ackTotal = 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 - })); - }, bitcore.Bignum('0', 10)); - - ackTotal.toString(10).should.equal(total.toString(10)); - - should.exist(ack); - memo.should.equal('Thank you for your payment!'); - - done(); - }); - }); - - ppw = createWallet(); - - it('#retrieve a payment request message via model', function(done) { - var w = ppw; - should.exist(w); - // Caches Payment Request but does not add TX proposal - w.fetchPaymentTx({ - uri: 'https://localhost:8080/-/request' - }, function(err, merchantData) { - if (err) return done(err); - merchantData.pr.pd.payment_url.should.equal('https://localhost:8080/-/pay'); - return done(); - }); - }); - - it('#add tx proposal based on payment message via model', function(done) { - var w = ppw; - should.exist(w); - var options = { - uri: 'https://localhost:8080/-/request' - }; - var req = w.paymentRequests[options.uri]; - should.exist(req); - delete w.paymentRequests[options.uri]; - w.receivePaymentRequest(options, req.pr, function(err, ntxid, merchantData) { - should.equal(err, null); - should.exist(ntxid); - should.exist(merchantData); - w._ntxid = ntxid; - merchantData.pr.pd.payment_url.should.equal('https://localhost:8080/-/pay'); - return done(); - }); - }); - - it('#add tx proposal based on payment message via model ', function(done) { - - var w = ppw; - should.exist(w); - - w.sendPaymentTx(w._ntxid, function(txid, merchantData) { - should.exist(txid); - should.exist(merchantData); - should.exist(merchantData.ack); - merchantData.ack.memo.should.equal('Thank you for your payment!'); - return done(); - }); - }); - - it('#send a payment request using payment api', function(done) { - var w = createWallet(); - should.exist(w); - var uri = 'bitcoin:2NBzZdFBoQymDgfzH2Pmnthser1E71MmU47?amount=0.00003&r=' + server.uri + '/request'; - var memo = 'Hello, server. I\'d like to make a payment.'; - w.createPaymentTx({ - uri: uri, - memo: memo - }, function(err, ntxid, merchantData) { - should.equal(err, null); - should.exist(ntxid); - should.exist(merchantData); - if (w.isShared()) { - return done(); - } else { - w.sendPaymentTx(ntxid, { - memo: memo - }, function(txid, merchantData) { - should.exist(txid); - should.exist(merchantData); - return done(); - }); - } - }); - }); - - it('#send a payment request with merchant prefix', function(done) { - var w = createWallet(); - should.exist(w); - var address = 'Merchant: ' + server.uri + '/request\nMemo: foo'; - var commentText = 'Hello, server. I\'d like to make a payment.'; - var uri; - - // Replicates code in controllers/send.js: - if (address.indexOf('bitcoin:') === 0) { - uri = new bitcore.BIP21(address).data; - } else if (address.indexOf('Merchant: ') === 0) { - uri = address.split(/\s+/)[1]; - } - - w.createPaymentTx({ - uri: uri, - memo: commentText - }, function(err, ntxid, merchantData) { - should.equal(err, null); - if (w.isShared()) { - should.exist(ntxid); - should.exist(merchantData); - return done(); - } else { - should.exist(merchantData); - w.sendTx(ntxid, function(txid, merchantData) { - should.exist(txid); - should.exist(merchantData); - return done(); - }); - } - }); - }); - - it('#send a payment request with bitcoin uri', function(done) { - var w = createWallet(); - should.exist(w); - var address = 'bitcoin:2NBzZdFBoQymDgfzH2Pmnthser1E71MmU47?amount=0.00003&r=' + server.uri + '/request'; - var commentText = 'Hello, server. I\'d like to make a payment.'; - w.createPaymentTx({ - uri: address, - memo: commentText - }, function(err, ntxid, merchantData) { - should.equal(err, null); - if (w.isShared()) { - should.exist(ntxid); - should.exist(merchantData); - return done(); - } else { - w.sendTx(ntxid, function(txid, merchantData) { - should.exist(txid); - should.exist(merchantData); - return done(); - }); - } - }); - }); - - it('#close payment server', function(done) { - server.close(function() { - return done(); - }); - }); - } -}); diff --git a/test/Wallet.js b/test/Wallet.js index aa034fa20..2430383d3 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -5,6 +5,7 @@ var PrivateKey = copay.PrivateKey; var Network = requireMock('FakeNetwork'); var Blockchain = requireMock('FakeBlockchain'); var Builder = requireMock('FakeBuilder'); +var FakePayProServer = requireMock('FakePayProServer'); var TransactionBuilder = bitcore.TransactionBuilder; var Transaction = bitcore.Transaction; var Address = bitcore.Address; @@ -58,6 +59,15 @@ var addCopayers = function(w) { describe('Wallet model', function() { + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + it('should fail to create an instance', function() { (function() { new Wallet(walletConfig) @@ -210,6 +220,8 @@ describe('Wallet model', function() { blockchainOpts: {}, networkOpts: {}, }); + if (w.httpUtil.request.restore) + w.httpUtil.request.restore(); return w; }; @@ -913,17 +925,123 @@ describe('Wallet model', function() { }); }); + + describe('#fetchPaymentRequest', function() { + it('should fetch a payment request', function(done) { + var w = cachedCreateW2(); + sinon.stub(w, 'parsePaymentRequest').returns({ + hola: 1 + }); + var opts = { + a: 1, + url: 'http://xxx', + }; + + var rawData ='wqer'; + var e = sinon.stub(); + e.error = sinon.stub(); + + var s = sinon.stub(); + s.success = sinon.stub().yields(rawData).returns(e); + + sinon.stub(w.httpUtil,'request').returns(s); + + w.fetchPaymentRequest(opts, function(err, merchantData){ + should.not.exist(err); + should.exist(merchantData); + w.parsePaymentRequest.firstCall.args.should.deep.equal([opts,rawData]); + done(); + }); + }); + + it('should return error on fetch error', function(done) { + var w = cachedCreateW2(); + var opts = { + a: 1, + url: 'http://xxx', + }; + + var rawData ='wqer'; + var e = sinon.stub(); + e.error = sinon.stub().yields(null, 'status'); + + var s = sinon.stub(); + s.success = sinon.stub().returns(e); + sinon.stub(w.httpUtil,'request').returns(s); + w.fetchPaymentRequest(opts, function(err, merchantData){ + err.toString().should.contain('status'); + done(); + }); + }); + + }); + + // TODO parsePaymentRequest should have more tests, + // FakePayProServer.getRequest should be parametrizable + describe('#parsePaymentRequest', function() { + it('should parse a Payment Request', function() { + var now = Date.now()/1000; + var w = cachedCreateW2(); + var opts = { + url: 'http://xxx', + }; + var data = FakePayProServer.getRequest(); + var md = w.parsePaymentRequest(opts,data); + md.outs.should.deep.equal(FakePayProServer.outs); + md.request_url.should.equal(opts.url); + md.pr.untrusted.should.equal(true); + md.expires.should.be.above(now); + }); + }); + describe('#createTx', function() { it('should fail if insight server is down', function(done) { var w = cachedCreateW2(); var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); sinon.stub(w, 'getUnspent').yields('error', null); w.createTx({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { - chai.expect(err.message).to.equal('Could not get list of UTXOs'); + err.message.should.contain('UTXOs'); + done(); + }); + }); + + + it('should fail with broken PayPro', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + sinon.stub(w, 'fetchPaymentRequest').yields('error'); + w.createTx({ + url: 'test', + }, function(err, ntxid) { + should.exist(err); + done(); + }); + }); + + + it('should create a TX with PayPro', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + sinon.stub(w, 'fetchPaymentRequest').yields(null, { + outs: [{ + address: 'n2Wz7KjyzBJVaNMBN88Lj1YUHMDZSAGeMV', + amountSatStr: '123400', + }], + request_url: 'url', + pr: { + signature: '123', + }, + total: '123400', + }); + w.createTx({ + url: 'test', + }, function(err, ntxid) { + should.not.exist(err); done(); }); }); diff --git a/test/mocks/FakePayProServer.js b/test/mocks/FakePayProServer.js index 3b0972872..a2ec56942 100644 --- a/test/mocks/FakePayProServer.js +++ b/test/mocks/FakePayProServer.js @@ -1,94 +1,14 @@ 'use strict'; -var is_browser = typeof process == 'undefined' - || typeof process.versions === 'undefined'; var bitcore = bitcore || require('bitcore'); var Buffer = bitcore.Buffer; var PayPro = bitcore.PayPro; -if (is_browser) { - var copay = require('copay'); //browser -} else { - var copay = require('../../copay'); //node -} -var Wallet = copay.Wallet; var x509 = { - priv: '' - + 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBeFRKdUsyYUdM' - + 'bjFkWEpLRGg0TXdQTFVrbDNISTVwR25HNWFjNGwvMGlobXE4Y3dDCitGVlBnWk1TNTlheWtpc0Ir' - + 'ekM3dnR2a0prL2J2K0JTT1g3b3hkSXN1TDNkS1FGcHVYWFZmcmRiOTV3WW40TSsKL25qRWhYTWxo' - + 'Vk1IL09DaUFnOUpLaFRLV0w2R1JXWkFBaEE3bEJSaGdTTkRUaVRDNTFDYmlLN3hBNnBONCt0UQpI' - + 'eG9tSlBYclpSa2JCMmtsT2ZXd2J2OTNZM0oxS0ZEK2kwUE1RSEx3N3JoRXVteEM5MytISFVWWVZI' - + 'N0gxVFBaCkgxYmRVSkowMmdRZXlsSnNzWUNKeWRaUHpOVC96dXRzL0tKV2RSdjVseHdHOXU5dE1O' - + 'TWdoSmJtQWFNa01HaSsKbzdQTkV5UDNxSEZyWXBZaHM1cHFMSE1STkI3OFFNOUllTmpMRndJREFR' - + 'QUJBb0lCQVFERVJyalBiQUdjbmwxaAorZGIrOTczNGZ0aElBUkpWSko1dTRFK1JKcThSRWhGTEVL' - + 'UFlKNW0yUC94dVZBMXpYV2xnYXhaRUZ6d1VRaUpZCjdsOEpLVjlwSHhReVlaQ1M4dndYZzhpWGtz' - + 'dndQaWRvQmN1YW4vd0RWQ1FCZXk2VkxjVXpSYUd1Ui9sTHNYK1YKN2Z0QjBvUnFsSXFrYmNQZE1N' - + 'dnFUeG93UnVoUG11Q3JWVGpPNHBiTnFuU09OUExPaUovRkFYYjJwZnpGZnBCUgpHeCtFTW16d2Ur' - + 'SEZuSkJHRGhIWjk5bm4vVEJmYUp6TlZDcURZLzNid3o1WDdIUU5ZN1QrSnlUVUZzZVE5NHhzCnpy' - + 'a2lidGRmVGNUanB1K1VoWm80c1p6Q3IrZkhHWm9FOUdEUHF0ZDRnQ3ByazRFS0pzbXFCRVN4QlhT' - + 'RGhZZ04KOXBVRDM4c1pBb0dCQU9yZkRqdDZaL0ZDamFuVThXek5GaWYrOVQxQTJ4b013RDVWU2xN' - + 'dVJyWW1HbGZyMEM5TQpmMUVvZ2l2dVRrYnA3cmtnZFRhWVRTYndmTnFaQkt4Y3R5YzdCaGRwWnhE' - + 'RVdKa2Z5cThxVngvem1Cek1JK1ZzCjJLYi9hcHZXcmJlb3NET0NyeUg1YzhKc1VUOXhUWDNYYnhF' - + 'anlPSlFCU1lHRE1qUHlKNkU5czZMQW9HQkFOYnYKd2d0S2Nra0tLbDJhNXZzaGR2RENnNnFLL1Fn' - + 'T20vNktUSlVKRVNqaHoydFIrZlBWUjcwVEg5UmhoVFJscERXQgpCd3oyU2NCc1RRNDIvTGsxRnky' - + 'MFQvck12S3VmSEw1VE1BNGZ6NWRxMUxIbmN6ejZVazVnWEtBT09rUjlVdVhpClR0eTNoREcyQkM4' - + 'Nk1LTVJ4SjUxRWJxam94d0VSMTAwU2FuTVBmTWxBb0dBSUhLY1pyOHNhUHBHMC9XbFBPREEKZE5v' - + 'V1MxWVFidkxnQkR5SVBpR2doejJRV2lFcjY3em53ZkNVdXpqNiszVUtFKzFXQkNyYVRjemZrdHVj' - + 'OTZyLwphcDRPNDJFZWFnU1dNT0ZoZ1AyYWQ4R1JmRGovcEl4N0NlY3pkVUFkVThnc1A1R0lYR3M0' - + 'QU40eUEwL0Y0dUxHCloxbklRT3ZKS2syZnFvWjZNdHd2dEswQ2dZRUFnSjdGTGVDRTkzUmYyZGdD' - + 'ZFRHWGJZZlpKc3M1bEFLNkV0NUwKNmJ1ZFN5dWw1Z0VPWkgyekNsQlJjZFJSMUFNbSt1V1ZoSW8x' - + 'cERLckFlQ2g1MnIvemRmakxLQXNIejkrQWQ3aQpHUEdzVmw0Vm5jaDFTMzQ0bHJKUGUzQklLZ2dj' - + 'L1hncDNTYnNzcHJMY2orT0wyZElrOUpXbzZ1Y3hmMUJmMkwwCjJlbGhBUWtDZ1lCWHN5elZWL1pK' - + 'cVhOcFdDZzU1TDNVRm9UTHlLU3FsVktNM1dpRzVCS240QWF6VkNITCtHUVUKeHd4U2dSOWZRNElu' - + 'dStyUHJOM0lteWswbEtQR0Y5U3pDUlJUaUpGUjcyc05xbE82bDBWOENXUkFQVFBKY2dxVgoxVThO' - + 'SEs4YjNaaUlvR0orbXNOenBkeHJqNjJIM0E2K1krQXNOWTRTbVVUWEg5eWpnK251a2c9PQotLS0t' - + 'LUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=', - pub: '' - + 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FR' - + 'OEFNSUlCQ2dLQ0FRRUF4VEp1SzJhR0xuMWRYSktEaDRNdwpQTFVrbDNISTVwR25HNWFjNGwvMGlo' - + 'bXE4Y3dDK0ZWUGdaTVM1OWF5a2lzQit6Qzd2dHZrSmsvYnYrQlNPWDdvCnhkSXN1TDNkS1FGcHVY' - + 'WFZmcmRiOTV3WW40TSsvbmpFaFhNbGhWTUgvT0NpQWc5SktoVEtXTDZHUldaQUFoQTcKbEJSaGdT' - + 'TkRUaVRDNTFDYmlLN3hBNnBONCt0UUh4b21KUFhyWlJrYkIya2xPZld3YnY5M1kzSjFLRkQraTBQ' - + 'TQpRSEx3N3JoRXVteEM5MytISFVWWVZIN0gxVFBaSDFiZFVKSjAyZ1FleWxKc3NZQ0p5ZFpQek5U' - + 'L3p1dHMvS0pXCmRSdjVseHdHOXU5dE1OTWdoSmJtQWFNa01HaStvN1BORXlQM3FIRnJZcFloczVw' - + 'cUxITVJOQjc4UU05SWVOakwKRndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==', - der: '' - + 'MIIDBjCCAe4CCQDI2qWdA3/VpDANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTETMBEGA1UE' - + 'CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE0MDcx' - + 'NjAxMzM1MVoXDTE1MDcxNjAxMzM1MVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3Rh' - + 'dGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD' - + 'ggEPADCCAQoCggEBAMUybitmhi59XVySg4eDMDy1JJdxyOaRpxuWnOJf9IoZqvHMAvhVT4GTEufW' - + 'spIrAfswu77b5CZP27/gUjl+6MXSLLi93SkBabl11X63W/ecGJ+DPv54xIVzJYVTB/zgogIPSSoU' - + 'yli+hkVmQAIQO5QUYYEjQ04kwudQm4iu8QOqTePrUB8aJiT162UZGwdpJTn1sG7/d2NydShQ/otD' - + 'zEBy8O64RLpsQvd/hx1FWFR+x9Uz2R9W3VCSdNoEHspSbLGAicnWT8zU/87rbPyiVnUb+ZccBvbv' - + 'bTDTIISW5gGjJDBovqOzzRMj96hxa2KWIbOaaixzETQe/EDPSHjYyxcCAwEAATANBgkqhkiG9w0B' - + 'AQUFAAOCAQEAL6AMMfC3TlRcmsIgHxjVD4XYtISlldnrn2X9zvFbJKCpNy8XQQosQxrhyfzPHQKj' - + 'lS2L/KCGMnjx9QkYD2Hlp1MJ1uVv9888th/gcZOv3Or3hQyi5K1Sh5xCG+69lUOqUEGu9B4irsqo' - + 'FomQVbQolSy+t4apdJi7kuEDwFDk4gZiVEfsuX+naN5a6pCnWnhX1Vf4fKwfkLobKKXm2zQVsjxl' - + 'wBAqOEmJGDLoRMXH56qJnEZ/dqsczaJOHQSi9mFEHL0r5rsEDTT5AVxdnBfNnyGaCH7/zANEko+F' - + 'GBj1JdJaJgFTXdbxDoyoPTPD+LJqSK5XYToo46y/T0u9CLveNA==', - pem: '' - + 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCakNDQWU0Q0NRREkycVdkQTMvVnBEQU5C' - + 'Z2txaGtpRzl3MEJBUVVGQURCRk1Rc3dDUVlEVlFRR0V3SkIKVlRFVE1CRUdBMVVFQ0F3S1UyOXRa' - + 'UzFUZEdGMFpURWhNQjhHQTFVRUNnd1lTVzUwWlhKdVpYUWdWMmxrWjJsMApjeUJRZEhrZ1RIUmtN' - + 'QjRYRFRFME1EY3hOakF4TXpNMU1Wb1hEVEUxTURjeE5qQXhNek0xTVZvd1JURUxNQWtHCkExVUVC' - + 'aE1DUVZVeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJt' - + 'VjAKSUZkcFpHZHBkSE1nVUhSNUlFeDBaRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFE' - + 'Q0NBUW9DZ2dFQgpBTVV5Yml0bWhpNTlYVnlTZzRlRE1EeTFKSmR4eU9hUnB4dVduT0pmOUlvWnF2' - + 'SE1BdmhWVDRHVEV1ZldzcElyCkFmc3d1NzdiNUNaUDI3L2dVamwrNk1YU0xMaTkzU2tCYWJsMTFY' - + 'NjNXL2VjR0orRFB2NTR4SVZ6SllWVEIvemcKb2dJUFNTb1V5bGkraGtWbVFBSVFPNVFVWVlFalEw' - + 'NGt3dWRRbTRpdThRT3FUZVByVUI4YUppVDE2MlVaR3dkcApKVG4xc0c3L2QyTnlkU2hRL290RHpF' - + 'Qnk4TzY0Ukxwc1F2ZC9oeDFGV0ZSK3g5VXoyUjlXM1ZDU2ROb0VIc3BTCmJMR0FpY25XVDh6VS84' - + 'N3JiUHlpVm5VYitaY2NCdmJ2YlREVElJU1c1Z0dqSkRCb3ZxT3p6Uk1qOTZoeGEyS1cKSWJPYWFp' - + 'eHpFVFFlL0VEUFNIall5eGNDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFMNkFNTWZD' - + 'MwpUbFJjbXNJZ0h4alZENFhZdElTbGxkbnJuMlg5enZGYkpLQ3BOeThYUVFvc1F4cmh5ZnpQSFFL' - + 'amxTMkwvS0NHCk1uang5UWtZRDJIbHAxTUoxdVZ2OTg4OHRoL2djWk92M09yM2hReWk1SzFTaDV4' - + 'Q0crNjlsVU9xVUVHdTlCNGkKcnNxb0ZvbVFWYlFvbFN5K3Q0YXBkSmk3a3VFRHdGRGs0Z1ppVkVm' - + 'c3VYK25hTjVhNnBDblduaFgxVmY0Zkt3ZgprTG9iS0tYbTJ6UVZzanhsd0JBcU9FbUpHRExvUk1Y' - + 'SDU2cUpuRVovZHFzY3phSk9IUVNpOW1GRUhMMHI1cnNFCkRUVDVBVnhkbkJmTm55R2FDSDcvekFO' - + 'RWtvK0ZHQmoxSmRKYUpnRlRYZGJ4RG95b1BUUEQrTEpxU0s1WFlUb28KNDZ5L1QwdTlDTHZlTkE9' - + 'PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==' + priv: '' + 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBeFRKdUsyYUdM' + 'bjFkWEpLRGg0TXdQTFVrbDNISTVwR25HNWFjNGwvMGlobXE4Y3dDCitGVlBnWk1TNTlheWtpc0Ir' + 'ekM3dnR2a0prL2J2K0JTT1g3b3hkSXN1TDNkS1FGcHVYWFZmcmRiOTV3WW40TSsKL25qRWhYTWxo' + 'Vk1IL09DaUFnOUpLaFRLV0w2R1JXWkFBaEE3bEJSaGdTTkRUaVRDNTFDYmlLN3hBNnBONCt0UQpI' + 'eG9tSlBYclpSa2JCMmtsT2ZXd2J2OTNZM0oxS0ZEK2kwUE1RSEx3N3JoRXVteEM5MytISFVWWVZI' + 'N0gxVFBaCkgxYmRVSkowMmdRZXlsSnNzWUNKeWRaUHpOVC96dXRzL0tKV2RSdjVseHdHOXU5dE1O' + 'TWdoSmJtQWFNa01HaSsKbzdQTkV5UDNxSEZyWXBZaHM1cHFMSE1STkI3OFFNOUllTmpMRndJREFR' + 'QUJBb0lCQVFERVJyalBiQUdjbmwxaAorZGIrOTczNGZ0aElBUkpWSko1dTRFK1JKcThSRWhGTEVL' + 'UFlKNW0yUC94dVZBMXpYV2xnYXhaRUZ6d1VRaUpZCjdsOEpLVjlwSHhReVlaQ1M4dndYZzhpWGtz' + 'dndQaWRvQmN1YW4vd0RWQ1FCZXk2VkxjVXpSYUd1Ui9sTHNYK1YKN2Z0QjBvUnFsSXFrYmNQZE1N' + 'dnFUeG93UnVoUG11Q3JWVGpPNHBiTnFuU09OUExPaUovRkFYYjJwZnpGZnBCUgpHeCtFTW16d2Ur' + 'SEZuSkJHRGhIWjk5bm4vVEJmYUp6TlZDcURZLzNid3o1WDdIUU5ZN1QrSnlUVUZzZVE5NHhzCnpy' + 'a2lidGRmVGNUanB1K1VoWm80c1p6Q3IrZkhHWm9FOUdEUHF0ZDRnQ3ByazRFS0pzbXFCRVN4QlhT' + 'RGhZZ04KOXBVRDM4c1pBb0dCQU9yZkRqdDZaL0ZDamFuVThXek5GaWYrOVQxQTJ4b013RDVWU2xN' + 'dVJyWW1HbGZyMEM5TQpmMUVvZ2l2dVRrYnA3cmtnZFRhWVRTYndmTnFaQkt4Y3R5YzdCaGRwWnhE' + 'RVdKa2Z5cThxVngvem1Cek1JK1ZzCjJLYi9hcHZXcmJlb3NET0NyeUg1YzhKc1VUOXhUWDNYYnhF' + 'anlPSlFCU1lHRE1qUHlKNkU5czZMQW9HQkFOYnYKd2d0S2Nra0tLbDJhNXZzaGR2RENnNnFLL1Fn' + 'T20vNktUSlVKRVNqaHoydFIrZlBWUjcwVEg5UmhoVFJscERXQgpCd3oyU2NCc1RRNDIvTGsxRnky' + 'MFQvck12S3VmSEw1VE1BNGZ6NWRxMUxIbmN6ejZVazVnWEtBT09rUjlVdVhpClR0eTNoREcyQkM4' + 'Nk1LTVJ4SjUxRWJxam94d0VSMTAwU2FuTVBmTWxBb0dBSUhLY1pyOHNhUHBHMC9XbFBPREEKZE5v' + 'V1MxWVFidkxnQkR5SVBpR2doejJRV2lFcjY3em53ZkNVdXpqNiszVUtFKzFXQkNyYVRjemZrdHVj' + 'OTZyLwphcDRPNDJFZWFnU1dNT0ZoZ1AyYWQ4R1JmRGovcEl4N0NlY3pkVUFkVThnc1A1R0lYR3M0' + 'QU40eUEwL0Y0dUxHCloxbklRT3ZKS2syZnFvWjZNdHd2dEswQ2dZRUFnSjdGTGVDRTkzUmYyZGdD' + 'ZFRHWGJZZlpKc3M1bEFLNkV0NUwKNmJ1ZFN5dWw1Z0VPWkgyekNsQlJjZFJSMUFNbSt1V1ZoSW8x' + 'cERLckFlQ2g1MnIvemRmakxLQXNIejkrQWQ3aQpHUEdzVmw0Vm5jaDFTMzQ0bHJKUGUzQklLZ2dj' + 'L1hncDNTYnNzcHJMY2orT0wyZElrOUpXbzZ1Y3hmMUJmMkwwCjJlbGhBUWtDZ1lCWHN5elZWL1pK' + 'cVhOcFdDZzU1TDNVRm9UTHlLU3FsVktNM1dpRzVCS240QWF6VkNITCtHUVUKeHd4U2dSOWZRNElu' + 'dStyUHJOM0lteWswbEtQR0Y5U3pDUlJUaUpGUjcyc05xbE82bDBWOENXUkFQVFBKY2dxVgoxVThO' + 'SEs4YjNaaUlvR0orbXNOenBkeHJqNjJIM0E2K1krQXNOWTRTbVVUWEg5eWpnK251a2c9PQotLS0t' + 'LUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=', + pub: '' + 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FR' + 'OEFNSUlCQ2dLQ0FRRUF4VEp1SzJhR0xuMWRYSktEaDRNdwpQTFVrbDNISTVwR25HNWFjNGwvMGlo' + 'bXE4Y3dDK0ZWUGdaTVM1OWF5a2lzQit6Qzd2dHZrSmsvYnYrQlNPWDdvCnhkSXN1TDNkS1FGcHVY' + 'WFZmcmRiOTV3WW40TSsvbmpFaFhNbGhWTUgvT0NpQWc5SktoVEtXTDZHUldaQUFoQTcKbEJSaGdT' + 'TkRUaVRDNTFDYmlLN3hBNnBONCt0UUh4b21KUFhyWlJrYkIya2xPZld3YnY5M1kzSjFLRkQraTBQ' + 'TQpRSEx3N3JoRXVteEM5MytISFVWWVZIN0gxVFBaSDFiZFVKSjAyZ1FleWxKc3NZQ0p5ZFpQek5U' + 'L3p1dHMvS0pXCmRSdjVseHdHOXU5dE1OTWdoSmJtQWFNa01HaStvN1BORXlQM3FIRnJZcFloczVw' + 'cUxITVJOQjc4UU05SWVOakwKRndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==', + der: '' + 'MIIDBjCCAe4CCQDI2qWdA3/VpDANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTETMBEGA1UE' + 'CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE0MDcx' + 'NjAxMzM1MVoXDTE1MDcxNjAxMzM1MVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3Rh' + 'dGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD' + 'ggEPADCCAQoCggEBAMUybitmhi59XVySg4eDMDy1JJdxyOaRpxuWnOJf9IoZqvHMAvhVT4GTEufW' + 'spIrAfswu77b5CZP27/gUjl+6MXSLLi93SkBabl11X63W/ecGJ+DPv54xIVzJYVTB/zgogIPSSoU' + 'yli+hkVmQAIQO5QUYYEjQ04kwudQm4iu8QOqTePrUB8aJiT162UZGwdpJTn1sG7/d2NydShQ/otD' + 'zEBy8O64RLpsQvd/hx1FWFR+x9Uz2R9W3VCSdNoEHspSbLGAicnWT8zU/87rbPyiVnUb+ZccBvbv' + 'bTDTIISW5gGjJDBovqOzzRMj96hxa2KWIbOaaixzETQe/EDPSHjYyxcCAwEAATANBgkqhkiG9w0B' + 'AQUFAAOCAQEAL6AMMfC3TlRcmsIgHxjVD4XYtISlldnrn2X9zvFbJKCpNy8XQQosQxrhyfzPHQKj' + 'lS2L/KCGMnjx9QkYD2Hlp1MJ1uVv9888th/gcZOv3Or3hQyi5K1Sh5xCG+69lUOqUEGu9B4irsqo' + 'FomQVbQolSy+t4apdJi7kuEDwFDk4gZiVEfsuX+naN5a6pCnWnhX1Vf4fKwfkLobKKXm2zQVsjxl' + 'wBAqOEmJGDLoRMXH56qJnEZ/dqsczaJOHQSi9mFEHL0r5rsEDTT5AVxdnBfNnyGaCH7/zANEko+F' + 'GBj1JdJaJgFTXdbxDoyoPTPD+LJqSK5XYToo46y/T0u9CLveNA==', + pem: '' + 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCakNDQWU0Q0NRREkycVdkQTMvVnBEQU5C' + 'Z2txaGtpRzl3MEJBUVVGQURCRk1Rc3dDUVlEVlFRR0V3SkIKVlRFVE1CRUdBMVVFQ0F3S1UyOXRa' + 'UzFUZEdGMFpURWhNQjhHQTFVRUNnd1lTVzUwWlhKdVpYUWdWMmxrWjJsMApjeUJRZEhrZ1RIUmtN' + 'QjRYRFRFME1EY3hOakF4TXpNMU1Wb1hEVEUxTURjeE5qQXhNek0xTVZvd1JURUxNQWtHCkExVUVC' + 'aE1DUVZVeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJt' + 'VjAKSUZkcFpHZHBkSE1nVUhSNUlFeDBaRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFE' + 'Q0NBUW9DZ2dFQgpBTVV5Yml0bWhpNTlYVnlTZzRlRE1EeTFKSmR4eU9hUnB4dVduT0pmOUlvWnF2' + 'SE1BdmhWVDRHVEV1ZldzcElyCkFmc3d1NzdiNUNaUDI3L2dVamwrNk1YU0xMaTkzU2tCYWJsMTFY' + 'NjNXL2VjR0orRFB2NTR4SVZ6SllWVEIvemcKb2dJUFNTb1V5bGkraGtWbVFBSVFPNVFVWVlFalEw' + 'NGt3dWRRbTRpdThRT3FUZVByVUI4YUppVDE2MlVaR3dkcApKVG4xc0c3L2QyTnlkU2hRL290RHpF' + 'Qnk4TzY0Ukxwc1F2ZC9oeDFGV0ZSK3g5VXoyUjlXM1ZDU2ROb0VIc3BTCmJMR0FpY25XVDh6VS84' + 'N3JiUHlpVm5VYitaY2NCdmJ2YlREVElJU1c1Z0dqSkRCb3ZxT3p6Uk1qOTZoeGEyS1cKSWJPYWFp' + 'eHpFVFFlL0VEUFNIall5eGNDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFMNkFNTWZD' + 'MwpUbFJjbXNJZ0h4alZENFhZdElTbGxkbnJuMlg5enZGYkpLQ3BOeThYUVFvc1F4cmh5ZnpQSFFL' + 'amxTMkwvS0NHCk1uang5UWtZRDJIbHAxTUoxdVZ2OTg4OHRoL2djWk92M09yM2hReWk1SzFTaDV4' + 'Q0crNjlsVU9xVUVHdTlCNGkKcnNxb0ZvbVFWYlFvbFN5K3Q0YXBkSmk3a3VFRHdGRGs0Z1ppVkVm' + 'c3VYK25hTjVhNnBDblduaFgxVmY0Zkt3ZgprTG9iS0tYbTJ6UVZzanhsd0JBcU9FbUpHRExvUk1Y' + 'SDU2cUpuRVovZHFzY3phSk9IUVNpOW1GRUhMMHI1cnNFCkRUVDVBVnhkbkJmTm55R2FDSDcvekFO' + 'RWtvK0ZHQmoxSmRKYUpnRlRYZGJ4RG95b1BUUEQrTEpxU0s1WFlUb28KNDZ5L1QwdTlDTHZlTkE9' + 'PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==' }; x509.priv = new Buffer(x509.priv, 'base64'); @@ -96,234 +16,125 @@ x509.pub = new Buffer(x509.pub, 'base64'); x509.der = new Buffer(x509.der, 'base64'); x509.pem = new Buffer(x509.pem, 'base64'); -function startServer(cb) { - if (Wallet.request._server) { - setTimeout(function() { - return cb(null, Wallet.request._server); - }, 1); - return; - } +module.exports = { + outs: [{ + address: 'mkYn9qmYwMZfovTb6cd7yCGeNozqUyyhK7', + amountSatStr: '3000' + }], + getRequest: function() { - var old = Wallet.request; + var uid = 0; - var server = { - GET: { + var outputs = []; - /** - * Receive "I want to pay" - */ + [2000, 1000].forEach(function(value) { + var po = new PayPro(); + po = po.makeOutput(); + // number of satoshis to be paid + po.set('amount', value); - '/-/request': function(req, cb) { - var res = { - statusCode: 200, - headers: {}, - body: {} - }; - - var uid = 0; - - var outputs = []; - - [2000, 1000].forEach(function(value) { - var po = new PayPro(); - po = po.makeOutput(); - // number of satoshis to be paid - po.set('amount', value); - // a TxOut script where the payment should be sent. similar to OP_CHECKSIG - po.set('script', new Buffer([ - 118, // OP_DUP - 169, // OP_HASH160 - 76, // OP_PUSHDATA1 - 20, // number of bytes - 55, - 48, - 254, - 188, - 186, - 4, - 186, - 208, - 205, - 71, - 108, - 251, - 130, - 15, - 156, - 55, - 215, - 70, - 111, - 217, - 136, // OP_EQUALVERIFY - 172 // OP_CHECKSIG - ])); - outputs.push(po.message); - }); - - /** - * Payment Details - */ - - var mdata = new Buffer([0]); - uid++; - if (uid > 0xffff) { - throw new Error('UIDs bigger than 0xffff not supported.'); - } else if (uid > 0xff) { - mdata = new Buffer([(uid >> 8) & 0xff, (uid >> 0) & 0xff]) - } else { - mdata = new Buffer([0, uid]) - } - var now = Date.now() / 1000 | 0; - var pd = new PayPro(); - pd = pd.makePaymentDetails(); - pd.set('network', 'test'); - pd.set('outputs', outputs); - pd.set('time', now); - pd.set('expires', now + 60 * 60 * 24); - pd.set('memo', 'Hello, this is the server, we would like some money.'); - var port = +req.headers.host.split(':')[1] || server.port; - pd.set('payment_url', 'https://localhost:' + port + '/-/pay'); - pd.set('merchant_data', mdata); - - /* - * PaymentRequest - */ - - var cr = new PayPro(); - cr = cr.makeX509Certificates(); - cr.set('certificate', [x509.der]); - - // We send the PaymentRequest to the customer - var pr = new PayPro(); - pr = pr.makePaymentRequest(); - pr.set('payment_details_version', 1); - pr.set('pki_type', 'x509+sha256'); - pr.set('pki_data', cr.serialize()); - pr.set('serialized_payment_details', pd.serialize()); - pr.sign(x509.priv); - - pr = pr.serialize(); - - // BIP-71 - set the content-type - res.headers['Content-Type'] = PayPro.PAYMENT_REQUEST_CONTENT_TYPE; - res.headers['Content-Length'] = pr.length + ''; - res.headers['Content-Transfer-Encoding'] = 'binary'; - - res.body = pr; - - return cb(null, res, res.body); - }, - }, - - /** - * Receive Payment - */ - - POST: { - '/-/pay': function(req, cb) { - var body = req.body; - - var res = { - statusCode: 200, - headers: {}, - body: {} - }; - - body = PayPro.Payment.decode(body); - - var pay = new PayPro(); - pay = pay.makePayment(body); - var merchant_data = pay.get('merchant_data'); - var transactions = pay.get('transactions'); - var refund_to = pay.get('refund_to'); - var memo = pay.get('memo'); - - // We send this to the customer after receiving a Payment - // Then we propogate the transaction through bitcoin network - var ack = new PayPro(); - ack = ack.makePaymentACK(); - ack.set('payment', pay.message); - ack.set('memo', 'Thank you for your payment!'); - - ack = ack.serialize(); - - // BIP-71 - set the content-type - res.headers['Content-Type'] = PayPro.PAYMENT_ACK_CONTENT_TYPE; - res.headers['Content-Length'] = ack.length + ''; - res.headers['Content-Transfer-Encoding'] = 'binary'; - - transactions = transactions.map(function(tx) { - 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); - return ptx; - }); - - res.body = ack; - - return cb(null, res, res.body); - } - }, - listen: function(port, cb) { - if (cb) return cb(); - }, - close: function(cb) { - Wallet.request = old; - return cb(); - } - }; - - Wallet.request = function(options) { - 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 path = uri.replace(/^https?:\/\/[^\/]+/, ''); - var req = options; - req.headers = req.headers || {}; - req.body = req.data || req.body || {}; - req.socket = { - remoteAddress: 'localhost' - }; - req.headers['Host'] = 'localhost:8080'; - Object.keys(req.headers).forEach(function(key) { - req.headers[key] = req.headers[key] + ''; - req.headers[key.toLowerCase()] = req.headers[key] + ''; + // TODO use bitcore / script!! + // a TxOut script where the payment should be sent. similar to OP_CHECKSIG + po.set('script', new Buffer([ + 118, // OP_DUP + 169, // OP_HASH160 + 76, // OP_PUSHDATA1 + 20, // number of bytes + 55, + 48, + 254, + 188, + 186, + 4, + 186, + 208, + 205, + 71, + 108, + 251, + 130, + 15, + 156, + 55, + 215, + 70, + 111, + 217, + 136, // OP_EQUALVERIFY + 172 // OP_CHECKSIG + ])); + outputs.push(po.message); }); - setTimeout(function() { - server[method][path](req, function(err, res, body) { - if (err) return ret._error(null, err, null, options); - Object.keys(res.headers).forEach(function(key) { - res.headers[key] = res.headers[key] + ''; - res.headers[key.toLowerCase()] = res.headers[key] + ''; - }); - return ret._success(body, res.statusCode, res.headers, options); - }); - }, 1); - return ret; - }; - Wallet.request._server = server; + /** + * Payment Details + */ - setTimeout(function() { - return cb(null, server); - }, 1); -} + var mdata = new Buffer([0]); + uid++; + if (uid > 0xffff) { + throw new Error('UIDs bigger than 0xffff not supported.'); + } else if (uid > 0xff) { + mdata = new Buffer([(uid >> 8) & 0xff, (uid >> 0) & 0xff]) + } else { + mdata = new Buffer([0, uid]) + } + var now = Date.now() / 1000 | 0; + var pd = new PayPro(); + pd = pd.makePaymentDetails(); + pd.set('network', 'test'); + pd.set('outputs', outputs); + pd.set('time', now); + pd.set('expires', now + 60 * 60 * 24); + pd.set('memo', 'Hello, this is the server, we would like some money.'); + pd.set('payment_url', 'https://pay_url'); + pd.set('merchant_data', mdata); -module.exports = startServer; + /* + * PaymentRequest + */ + + var cr = new PayPro(); + cr = cr.makeX509Certificates(); + cr.set('certificate', [x509.der]); + + // We send the PaymentRequest to the customer + var pr = new PayPro(); + pr = pr.makePaymentRequest(); + pr.set('payment_details_version', 1); + pr.set('pki_type', 'x509+sha256'); + pr.set('pki_data', cr.serialize()); + pr.set('serialized_payment_details', pd.serialize()); + pr.sign(x509.priv); + + return pr.serialize(); + }, + processPayment: function(payment) { + body = PayPro.Payment.decode(payment); + var pay = new PayPro(); + pay = pay.makePayment(body); + var merchant_data = pay.get('merchant_data'); + var transactions = pay.get('transactions'); + var refund_to = pay.get('refund_to'); + var memo = pay.get('memo'); + + // We send this to the customer after receiving a Payment + // Then we propogate the transaction through bitcoin network + var ack = new PayPro(); + ack = ack.makePaymentACK(); + ack.set('payment', pay.message); + ack.set('memo', 'Thank you for your payment!'); + + ack = ack.serialize(); + + transactions = transactions.map(function(tx) { + 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); + return ptx; + }); + + return ack; + }, +};