From 32f281fb821d19241485c92601faa35f38d91812 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 21 Nov 2014 11:06:47 -0300 Subject: [PATCH 01/23] add paypro checks --- js/models/TxProposal.js | 53 ++++++++- js/models/Wallet.js | 235 +++++++------------------------------- test/PayPro.js | 126 -------------------- test/TxProposal.js | 128 +++++++++++++++++---- test/mocks/FakeBuilder.js | 30 +++-- 5 files changed, 221 insertions(+), 351 deletions(-) diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index fba658556..9e6842818 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -41,6 +41,38 @@ function TxProposal(opts) { this._sync(); } +TxProposal.prototype._checkPayPro = function() { + if (!this.merchant) return; + +console.log('[TxProposal.js.46]', + this.paymentProtocolURL , this.merchant.request_url); + + if (this.paymentProtocolURL !== this.merchant.request_url) + throw new Error('PayPro: Mismatch on Payment URLs'); + + if (!this.merchant.outs || this.merchant.outs.length !== 1) + throw new Error('PayPro: Unsopported number of outputs'); + + if (this.merchant.expires < (this.getSent() || Date.now()/1000.) ) + throw new Error('PayPro: Request expired'); + + if (!this.merchant.total || !this.merchant.outs[0].amountSatStr || !this.merchant.outs[0].address) + throw new Error('PayPro: Missing amount'); + + if (this.builder.vanilla.outs.length != 1) + throw new Error('PayPro: Wrong outs in Tx'); + + + var ppOut = this.merchant.outs[0]; + var txOut = this.builder.vanilla.outs[0]; + + if (ppOut.address !== txOut.address) + throw new Error('PayPro: Wrong out address in Tx'); + + if (ppOut.amountSatStr !== txOut.amountSatStr) + throw new Error('PayPro: Wrong amount in Tx'); + +}; TxProposal.prototype._check = function() { @@ -61,6 +93,23 @@ TxProposal.prototype._check = function() { if (hashType && hashType !== Transaction.SIGHASH_ALL) throw new Error('Invalid tx proposal: bad signatures'); }); + this._checkPayPro(); +}; + + +TxProposal.prototype.trimForStorage = function() { + // TODO (remove builder / builderObj. utxos, etc) + // + return this; +}; + +TxProposal.prototype.addMerchantData = function(merchantData) { + var m = _.clone(merchantData); + + // remove unneeded data + m.raw = m.pr.pki_data = m.pr.signature = undefined; + this.merchant = m; + this._checkPayPro(); }; TxProposal.prototype.rejectCount = function() { @@ -101,7 +150,6 @@ TxProposal.prototype._sync = function() { return this; } - TxProposal.prototype.getId = function() { preconditions.checkState(this.builder); return this.builder.build().getNormalizedHash().toString('hex'); @@ -248,11 +296,14 @@ TxProposal.prototype.setRejected = function(copayerId) { if (!this.rejectedBy[copayerId]) this.rejectedBy[copayerId] = Date.now(); + + return this; }; TxProposal.prototype.setSent = function(sentTxid) { this.sentTxid = sentTxid; this.sentTs = Date.now(); + return this; }; TxProposal.prototype.getSent = function() { diff --git a/js/models/Wallet.js b/js/models/Wallet.js index fdc60b08d..4e1aee4c2 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -474,8 +474,14 @@ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { log.info('Received a Payment Protocol TX Proposal'); self.fetchPaymentTx(txp.paymentProtocolURL, function(err, merchantData) { if (err) return cb(err); - txp.merchant = merchantData; - return cb(); + + try { + txp.addMerchantData(merchantData); + } catch (e) { + log.error(e); + err = 'BADPAYPRO: ' + e.toString(); + } + return cb(err); }); }; @@ -1435,14 +1441,7 @@ Wallet.prototype.sign = function(ntxid) { var myId = this.getMyCopayerId(); var txp = this.txProposals.get(ntxid); - // If this is a payment protocol request, - // ensure it hasn't been tampered with. - if (!this.verifyPaymentRequest(ntxid)) { - throw new Error('Bad payment request'); - } - var before = txp.countSignatures(); - var keys = this.privateKey.getForPaths(txp.inputChainPaths); txp.builder.sign(keys); @@ -1491,7 +1490,8 @@ Wallet.prototype.sendTx = function(ntxid, cb) { if (txid) { log.debug('Wallet:' + self.getName() + ' Broadcasted TX. BITCOIND txid:', txid); - self.txProposals.get(ntxid).setSent(txid); + var txp = self.txProposals.get(ntxid); + txp.setSent(txid); self.sendTxProposal(ntxid); self.emitAndKeepAlive('txProposalsUpdated'); return cb(txid); @@ -1676,11 +1676,9 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { untrusted: !trust.caTrusted, selfSigned: trust.selfSigned }, + expires: expires, request_url: options.uri, total: bignum('0', 10).toString(10), - // Expose so other copayers can verify signature - // and identity, not to mention data. - raw: pr.serialize().toString('hex') }; return this.getUnspent(function(err, safeUnspent, unspent) { @@ -1694,9 +1692,7 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { } catch (e) { var msg = e.message || ''; if (msg.indexOf('not enough unspent tx outputs to fulfill')) { - var sat = /(\d+)/.exec(msg)[1]; e = new Error('No unspent outputs available.'); - e.amount = sat; return cb(e); } } @@ -1815,21 +1811,21 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { } var postInfo = { - method: 'POST', - url: txp.merchant.pr.pd.payment_url, - headers: { - // BIP-71 - 'Accept': PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE - // XHR does not allow these: - // 'Content-Length': (pay.byteLength || pay.length) + '', - // 'Content-Transfer-Encoding': 'binary' - }, - // Technically how this should be done via XHR (used to - // be the ArrayBuffer, now you send the View instead). - data: view, - responseType: 'arraybuffer' - }; + method: 'POST', + url: txp.merchant.pr.pd.payment_url, + headers: { + // BIP-71 + 'Accept': PayPro.PAYMENT_ACK_CONTENT_TYPE, + 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE + // XHR does not allow these: + // 'Content-Length': (pay.byteLength || pay.length) + '', + // 'Content-Transfer-Encoding': 'binary' + }, + // Technically how this should be done via XHR (used to + // be the ArrayBuffer, now you send the View instead). + data: view, + responseType: 'arraybuffer' + }; return Wallet.request(postInfo) .success(function(data, status, headers, config) { @@ -1940,7 +1936,7 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) merchantData.total = bignum(merchantData.total, 10); - var outs = []; + var outs = {}; merchantData.pr.pd.outputs.forEach(function(output) { var amount = output.amount; @@ -1964,13 +1960,11 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) var network = merchantData.pr.pd.network === 'main' ? 'livenet' : 'testnet'; var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), network); - outs.push({ - address: addr[0].toString(), - amountSatStr: bignum.fromBuffer(v, { - endian: 'big', - size: 1 - }).toString(10) - }); + 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', @@ -1978,11 +1972,18 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) })); }); + if (Object.keys(outs) > 1) + throw new Error('PayPro: Unsupported outputs'); + + merchantData.outs = outs; merchantData.total = merchantData.total.toString(10); var b = new Builder(opts) .setUnspent(unspent) - .setOutputs(outs); + .setOutputs({ + address: _.keys(outs)[0], + amountSatStr: _.values(outs)[0].toString(10), + }); merchantData.pr.pd.outputs.forEach(function(output, i) { var script = { @@ -2030,6 +2031,7 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) var meSeen = {}; if (priv) meSeen[myId] = now; + console.log('[Wallet.js.2043]', options, merchantData); //TODO var ntxid = this.txProposals.add(new TxProposal({ inputChainPaths: inputChainPaths, signedBy: me, @@ -2045,159 +2047,6 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) return ntxid; }; -/** - * @desc Verifies a PaymentRequest sent by another peer - * This essentially ensures that a copayer hasn't tampered with a - * PaymentRequest message from a payment server. It verifies the signature - * based on the cert, and checks to ensure the desired outputs are the same as - * the ones on the tx proposal. - * @TODO: Document better - */ -Wallet.prototype.verifyPaymentRequest = function(ntxid) { - if (!ntxid) return false; - - var txp = _.isObject(ntxid) ? ntxid : this.txProposals.get(ntxid); - - // If we're not a payment protocol proposal, ignore. - if (!txp.merchant) return true; - - // The copayer didn't send us the raw payment request, unverifiable. - if (!txp.merchant.raw) return false; - - // var tx = txp.builder.tx; - var tx = txp.builder.build(); - - var data = new Buffer(txp.merchant.raw, 'hex'); - data = PayPro.PaymentRequest.decode(data); - var pr = new PayPro(); - pr = pr.makePaymentRequest(data); - - // Verify the signature so we know this is the real request. - var trust = pr.verify(true); - if (!trust.verified) { - // Signature does not match cert. It may have - // been modified by an untrustworthy person. - // We should not sign this transaction proposal! - return false; - } - - var details = pr.get('serialized_payment_details'); - details = PayPro.PaymentDetails.decode(details); - var pd = new PayPro(); - pd = pd.makePaymentDetails(details); - - var outputs = pd.get('outputs'); - - if (tx.outs.length < outputs.length) { - // Outputs do not and cannot match. - return false; - } - - // Figure out whether the user is supposed - // to decide the value of the outputs. - var undecided = false; - var total = bignum('0', 10); - for (var i = 0; i < outputs.length; i++) { - var output = outputs[i]; - var amount = output.get('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; - total = total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - } - if (+total.toString(10) === 0) { - undecided = true; - } - - for (var i = 0; i < outputs.length; i++) { - var output = outputs[i]; - - 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)) - }; - - // Expected value - // little endian (keep this LE to compare with tx output value) - var ev = new Buffer(8); - ev[0] = (amount.low >> 0) & 0xff; - ev[1] = (amount.low >> 8) & 0xff; - ev[2] = (amount.low >> 16) & 0xff; - ev[3] = (amount.low >> 24) & 0xff; - ev[4] = (amount.high >> 0) & 0xff; - ev[5] = (amount.high >> 8) & 0xff; - ev[6] = (amount.high >> 16) & 0xff; - ev[7] = (amount.high >> 24) & 0xff; - - // Expected script - var es = script.buffer.slice(script.offset, script.limit); - - // Actual value - var av = tx.outs[i].v; - - // Actual script - var as = tx.outs[i].s; - - // XXX allow changing of script as long as address is same - // var as = es; - - // XXX allow changing of script as long as address is same - // var network = pd.get('network') === 'main' ? 'livenet' : 'testnet'; - // var es = bitcore.Address.fromScriptPubKey(new bitcore.Script(es), network)[0]; - // var as = bitcore.Address.fromScriptPubKey(new bitcore.Script(tx.outs[i].s), network)[0]; - - if (undecided) { - av = ev = new Buffer([0]); - } - - // Make sure the tx's output script and values match the payment request's. - if (av.toString('hex') !== ev.toString('hex') || as.toString('hex') !== es.toString('hex')) { - // Verifiable outputs do not match outputs of merchant - // data. We should not sign this transaction proposal! - return false; - } - - // Checking the merchant data itself isn't technically - // necessary as long as we check the transaction, but - // we can do it for good measure. - var ro = txp.merchant.pr.pd.outputs[i]; - - // Actual value - // little endian (keep this LE to compare with the ev above) - var av = new Buffer(8); - av[0] = (ro.amount.low >> 0) & 0xff; - av[1] = (ro.amount.low >> 8) & 0xff; - av[2] = (ro.amount.low >> 16) & 0xff; - av[3] = (ro.amount.low >> 24) & 0xff; - av[4] = (ro.amount.high >> 0) & 0xff; - av[5] = (ro.amount.high >> 8) & 0xff; - av[6] = (ro.amount.high >> 16) & 0xff; - av[7] = (ro.amount.high >> 24) & 0xff; - - // Actual script - var as = new Buffer(ro.script.buffer, 'hex') - .slice(ro.script.offset, ro.script.limit); - - if (av.toString('hex') !== ev.toString('hex') || as.toString('hex') !== es.toString('hex')) { - return false; - } - } - - return true; -}; - /** * @desc Mark that a user has seen a given TxProposal * @return {boolean} true if the internal state has changed @@ -2243,7 +2092,7 @@ Wallet.prototype.subscribeToAddresses = function() { var addrInfo = this.publicKeyRing.getAddressesInfo(); this.blockchain.subscribe(_.pluck(addrInfo, 'addressStr')); - log.debug('Subscribed to ' + addrInfo.length + ' addresses'); + log.debug('Subscribed to ' + addrInfo.length + ' addresses'); }; /** diff --git a/test/PayPro.js b/test/PayPro.js index c8a19bb7d..5ce1d9164 100644 --- a/test/PayPro.js +++ b/test/PayPro.js @@ -788,132 +788,6 @@ describe('PayPro (in Wallet) model', function() { }); }); - it('#try to sign a tampered payment request (raw)', 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); - should.exist(ntxid); - should.exist(merchantData); - - // Tamper with payment request in its raw form: - var data = new Buffer(merchantData.raw, 'hex'); - data = PayPro.PaymentRequest.decode(data); - var pr = new PayPro(); - pr = pr.makePaymentRequest(data); - var details = pr.get('serialized_payment_details'); - details = PayPro.PaymentDetails.decode(details); - var pd = new PayPro(); - pd = pd.makePaymentDetails(details); - var outputs = pd.get('outputs'); - outputs[outputs.length - 1].set('amount', 1000000000); - pd.set('outputs', outputs); - pr.set('serialized_payment_details', pd.serialize()); - merchantData.raw = pr.serialize().toString('hex'); - - var myId = w.getMyCopayerId(); - var txp = w.txProposals.get(ntxid); - should.exist(txp); - should.exist(txp.signedBy[myId]); - should.not.exist(txp.rejectedBy[myId]); - - w.verifyPaymentRequest(ntxid).should.equal(false); - - return done(); - }); - }); - - it('#try to sign a tampered payment request (abstract)', 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); - should.exist(ntxid); - should.exist(merchantData); - - // Tamper with payment request in its abstract form: - var outputs = merchantData.pr.pd.outputs; - var output = outputs[outputs.length - 1]; - var amount = output.amount; - amount.low = 2; - - var myId = w.getMyCopayerId(); - var txp = w.txProposals.get(ntxid); - should.exist(txp); - should.exist(txp.signedBy[myId]); - should.not.exist(txp.rejectedBy[myId]); - - w.verifyPaymentRequest(ntxid).should.equal(false); - - return done(); - }); - }); - - it('#try to sign a tampered txp tx (abstract)', 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); - should.exist(ntxid); - should.exist(merchantData); - - // Tamper with payment request in its abstract form: - var txp = w.txProposals.get(ntxid); - var tx = txp.builder.tx || txp.builder.build(); - tx.outs[0].v = new Buffer([2, 0, 0, 0, 0, 0, 0, 0]); - - var myId = w.getMyCopayerId(); - var txp = w.txProposals.get(ntxid); - should.exist(txp); - should.exist(txp.signedBy[myId]); - should.not.exist(txp.rejectedBy[myId]); - - w.verifyPaymentRequest(ntxid).should.equal(false); - - return done(); - }); - }); - - it('#sign an untampered payment request', 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); - should.exist(ntxid); - should.exist(merchantData); - - var myId = w.getMyCopayerId(); - var txp = w.txProposals.get(ntxid); - should.exist(txp); - should.exist(txp.signedBy[myId]); - should.not.exist(txp.rejectedBy[myId]); - - w.verifyPaymentRequest(ntxid).should.equal(true); - - return done(); - }); - }); - it('#close payment server', function(done) { server.close(function() { return done(); diff --git a/test/TxProposal.js b/test/TxProposal.js index c74f0ffcd..e2a106df3 100644 --- a/test/TxProposal.js +++ b/test/TxProposal.js @@ -11,12 +11,12 @@ var networks = bitcore.networks; var FakeBuilder = requireMock('FakeBuilder'); var TxProposal = copay.TxProposal; -var dummyProposal = new TxProposal({ - creator: 1, +var dummyProposal = function() { return new TxProposal({ + creator: 'creator', createdTs: 1, builder: new FakeBuilder(), inputChainPaths: ['m/1'], -}); +})}; var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"]; @@ -208,8 +208,8 @@ describe('TxProposal', function() { }); it('#_verifyScriptSig, two signatures', function() { // Data taken from bitcore's TransactionBuilder test - var txp = dummyProposal; - var tx = dummyProposal.builder.build(); + var txp = dummyProposal(); + var tx = dummyProposal().builder.build(); var ret = TxProposal._verifySignatures(pubkeys, validScriptSig, tx.hashForSignature()); ret.should.deep.equal(['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3']); }); @@ -223,13 +223,13 @@ describe('TxProposal', function() { Buffer.isBuffer(info.script.getBuffer()).should.equal(true); }); it('#_updateSignedBy', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp._inputSigners.should.deep.equal([ ['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3'] ]); }); describe('#_check', function() { - var txp = dummyProposal; + var txp = dummyProposal(); var backup = txp.builder.tx.ins; it('OK', function() { @@ -272,8 +272,96 @@ describe('TxProposal', function() { txp.builder.tx.ins[0].s = backup; }); }); + + describe('#_checkPayPro', function() { + var txp, md; + beforeEach(function() { + txp = dummyProposal(); + txp.paymentProtocolURL = '123'; + md = { + request_url: '123', + pr: { + pd: { + expires: 123, + memo: 'memo', + + }, + }, + total: '1230', + outs: [{ + address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', + amountSatStr: "123" + }], + expires: 92345678900, + }; + }); + + it('OK no merchant data', function() { + txp._checkPayPro(); + }); + it('OK merchant data', function() { + txp.addMerchantData(md); + }); + it('NOK URL', function() { + txp.paymentProtocolURL = '1234'; + (function() { + txp.addMerchantData(md); + }).should.throw('Mismatch'); + }); + it('NOK OUTS', function() { + md.outs = []; + (function() { + txp.addMerchantData(md); + }).should.throw('outputs'); + }); + it('NOK OUTS (case 2)', function() { + md.outs = [{}, {}]; + (function() { + txp.addMerchantData(md); + }).should.throw('outputs'); + }); + it('NOK OUTS (case 3)', function() { + md.outs = [{}, {}]; + (function() { + txp.addMerchantData(md); + }).should.throw('outputs'); + }); + it('NOK Amount', function() { + md.total = undefined; + (function() { + txp.addMerchantData(md); + }).should.throw('amount'); + }); + it('NOK Outs case 4', function() { + md.outs[0].address = 'aaa'; + (function() { + txp.addMerchantData(md); + }).should.throw('address'); + }); + it('NOK Outs case 5', function() { + md.outs[0].amountSatStr = '432'; + (function() { + txp.addMerchantData(md); + }).should.throw('amount'); + }); + + it('NOK Expired', function() { + md.expires = 1; + (function() { + txp.addMerchantData(md); + }).should.throw('expired'); + }); + + it('OK Expired but sent', function() { + md.expires = 2; + txp.sentTs = 1; + txp.addMerchantData(md); + }); + + }); + describe('#merge', function() { - var txp = dummyProposal; + var txp = dummyProposal(); var backup = txp.builder.tx.ins; it('with self', function() { var hasChanged = txp.merge(txp); @@ -307,7 +395,7 @@ describe('TxProposal', function() { }); describe('#setCopayers', function() { it("should fails if Tx has no creator", function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.signedBy = { 'hugo': 1 }; @@ -319,7 +407,7 @@ describe('TxProposal', function() { }).should.throw('no creator'); }); it("should fails if Tx is not signed by creator", function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.creator = 'creator'; txp.signedBy = { 'hugo': 1 @@ -336,7 +424,7 @@ describe('TxProposal', function() { it("should fails if Tx has unmapped signatures", function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.creator = 'creator'; txp.signedBy = { creator: 1 @@ -353,7 +441,7 @@ describe('TxProposal', function() { // This was disabled. Unnecessary to check this. it.skip("should be signed by sender", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp._inputSigners = [ ['pk1', 'pk0'] @@ -372,7 +460,7 @@ describe('TxProposal', function() { it("should set signedBy (trivial case)", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp._inputSigners = [ ['pk1', 'pk0'] @@ -390,7 +478,7 @@ describe('TxProposal', function() { txp.signedBy['creator'].should.gte(ts); }); it("should assign creator", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp._inputSigners = [ ['pk0'] @@ -409,7 +497,7 @@ describe('TxProposal', function() { txp.seenBy['creator'].should.equal(txp.createdTs); }) it("New tx should have only 1 signature", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp.signedBy = {}; delete txp['creator']; @@ -431,7 +519,7 @@ describe('TxProposal', function() { }) it("if signed, should not change ts", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp._inputSigners = [ ['pk0', 'pk1'] @@ -456,25 +544,25 @@ describe('TxProposal', function() { describe('micelaneous functions', function() { it('should report rejectCount', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.rejectCount().should.equal(0); txp.setRejected(['juan']) txp.rejectCount().should.equal(1); }); it('should report isPending 1', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.rejectedBy = []; txp.sentTxid = 1; txp.isPending(3).should.equal(false); }); it('should report isPending 2', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.rejectedBy = []; txp.sentTxid = null; txp.isPending(3).should.equal(true); }); it('should report isPending 3', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.rejectedBy = [1, 2, 3, 4]; txp.sentTxid = null; txp.isPending(3).should.equal(false); diff --git a/test/mocks/FakeBuilder.js b/test/mocks/FakeBuilder.js index 9b3aea95b..ae2d0affe 100644 --- a/test/mocks/FakeBuilder.js +++ b/test/mocks/FakeBuilder.js @@ -2,10 +2,12 @@ var bitcore = bitcore || require('bitcore'); var Script = bitcore.Script; -var VALID_SCRIPTSIG_BUF = new Buffer('0048304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101473044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae','hex'); +var VALID_SCRIPTSIG_BUF = new Buffer('0048304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101473044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae', 'hex'); function Tx() { - this.ins = [{s: VALID_SCRIPTSIG_BUF }]; + this.ins = [{ + s: VALID_SCRIPTSIG_BUF + }]; }; Tx.prototype.getHashType = function() { @@ -23,26 +25,32 @@ function FakeBuilder() { this.test = 1; this.tx = new Tx(); this.signhash = 1; - this.inputMap = [{ address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', + this.inputMap = [{ + address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', scriptPubKey: new Script(new Buffer('a914dc0623476aefb049066b09b0147a022e6eb8429187', 'hex')), scriptType: 4, - i: 0 }]; + i: 0 + }]; - this.vanilla = { - scriptSig: [VALID_SCRIPTSIG_BUF], - } + this.vanilla = { + scriptSig: [VALID_SCRIPTSIG_BUF], + outs: [{ + address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', + amountSatStr: '123', + }] + + } } -FakeBuilder.prototype.merge = function() { -}; +FakeBuilder.prototype.merge = function() {}; -FakeBuilder.prototype.build = function() { +FakeBuilder.prototype.build = function() { return this.tx; }; -FakeBuilder.prototype.toObj = function() { +FakeBuilder.prototype.toObj = function() { return this; }; FakeBuilder.VALID_SCRIPTSIG_BUF = VALID_SCRIPTSIG_BUF; From ae941392367b9353215f9353627df43cfb4fda5c Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 22 Nov 2014 12:38:53 -0300 Subject: [PATCH 02/23] test passing with new TxProposal checks --- js/models/TxProposal.js | 9 +++------ js/models/Wallet.js | 31 ++++++++++++++++--------------- js/plugins/InsightStorage.js | 2 ++ test/PayPro.js | 6 +----- test/mocks/FakeBuilder.js | 4 ++-- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index 9e6842818..1bfbf6c23 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -44,9 +44,6 @@ function TxProposal(opts) { TxProposal.prototype._checkPayPro = function() { if (!this.merchant) return; -console.log('[TxProposal.js.46]', - this.paymentProtocolURL , this.merchant.request_url); - if (this.paymentProtocolURL !== this.merchant.request_url) throw new Error('PayPro: Mismatch on Payment URLs'); @@ -59,12 +56,12 @@ console.log('[TxProposal.js.46]', if (!this.merchant.total || !this.merchant.outs[0].amountSatStr || !this.merchant.outs[0].address) throw new Error('PayPro: Missing amount'); - if (this.builder.vanilla.outs.length != 1) + var outs = JSON.parse(this.builder.vanilla.outs); + if (_.size(outs) != 1) throw new Error('PayPro: Wrong outs in Tx'); - var ppOut = this.merchant.outs[0]; - var txOut = this.builder.vanilla.outs[0]; + var txOut = outs[0]; if (ppOut.address !== txOut.address) throw new Error('PayPro: Wrong out address in Tx'); diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 4e1aee4c2..908dd0259 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -1328,11 +1328,15 @@ Wallet.prototype.getTxProposals = function() { var copayers = this.getRegisteredCopayerIds(); for (var ntxid in this.txProposals.txps) { var txp = this.txProposals.getTxProposal(ntxid, copayers); + txp.signedByUs = txp.signedBy[this.getMyCopayerId()] ? true : false; txp.rejectedByUs = txp.rejectedBy[this.getMyCopayerId()] ? true : false; txp.finallyRejected = this.totalCopayers - txp.rejectCount < this.requiredCopayers; txp.isPending = !txp.finallyRejected && !txp.sentTxid; + // si no gastada + // y si no esta expirada; + if (!txp.readonly || txp.finallyRejected || txp.sentTs) { ret.push(txp); } @@ -1682,21 +1686,17 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { }; 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) { - var msg = e.message || ''; - if (msg.indexOf('not enough unspent tx outputs to fulfill')) { - e = new Error('No unspent outputs available.'); - return cb(e); - } - } - return cb(null, merchantData, pr); + } catch (e) { err = e;} + return cb(err, merchantData, pr); } var ntxid = self.createPaymentTxSync(options, merchantData, safeUnspent); @@ -1972,18 +1972,20 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) })); }); - if (Object.keys(outs) > 1) + // TODO, for now we only support PayPro with 1 output. + if (_.size(outs) !== 1) throw new Error('PayPro: Unsupported outputs'); - merchantData.outs = outs; + 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({ - address: _.keys(outs)[0], - amountSatStr: _.values(outs)[0].toString(10), - }); + .setOutputs(merchantData.outs); merchantData.pr.pd.outputs.forEach(function(output, i) { var script = { @@ -2031,7 +2033,6 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) var meSeen = {}; if (priv) meSeen[myId] = now; - console.log('[Wallet.js.2043]', options, merchantData); //TODO var ntxid = this.txProposals.add(new TxProposal({ inputChainPaths: inputChainPaths, signedBy: me, diff --git a/js/plugins/InsightStorage.js b/js/plugins/InsightStorage.js index c65ccbfe2..25a125be8 100644 --- a/js/plugins/InsightStorage.js +++ b/js/plugins/InsightStorage.js @@ -156,6 +156,8 @@ InsightStorage.prototype.setItem = function(name, value, callback) { var passphrase = this.getPassphrase(); var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64'); var registerUrl = this.storeUrl + '/save'; + + log.debug('setItem ' + name + ' size:'+ (value.length/1024).toFixed(1) + 'kb' ); this.request.post({ url: registerUrl, headers: { diff --git a/test/PayPro.js b/test/PayPro.js index 5ce1d9164..21e272842 100644 --- a/test/PayPro.js +++ b/test/PayPro.js @@ -137,11 +137,7 @@ describe('PayPro (in Wallet) model', 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 = function(cb) { - return setTimeout(function() { - return cb(null, unspentTest, unspentTest); - }, 1); - }; + w.getUnspent = sinon.stub().yields(null, unspentTest, unspentTest); return w; }; diff --git a/test/mocks/FakeBuilder.js b/test/mocks/FakeBuilder.js index ae2d0affe..f066ceec2 100644 --- a/test/mocks/FakeBuilder.js +++ b/test/mocks/FakeBuilder.js @@ -34,10 +34,10 @@ function FakeBuilder() { this.vanilla = { scriptSig: [VALID_SCRIPTSIG_BUF], - outs: [{ + outs: JSON.stringify([{ address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', amountSatStr: '123', - }] + }]), } } From efb5b28c50cda1a37c78bf9a30aa270802d3ce5c Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 22 Nov 2014 19:17:35 -0300 Subject: [PATCH 03/23] refactor PayPro at Wallet. Fetch working --- README.md | 12 + js/controllers/send.js | 130 ++++------ js/models/Wallet.js | 569 ++++++++++++++--------------------------- 3 files changed, 246 insertions(+), 465 deletions(-) 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; -}; /** From e9005c2ca0cb72f4b90bc27cb16a764298b84eb6 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 22 Nov 2014 20:00:18 -0300 Subject: [PATCH 04/23] wallet test passing --- js/models/TxProposal.js | 13 ++++++- js/models/Wallet.js | 54 +++++++++----------------- test/Wallet.js | 86 ++++++++++++++++++++++++++--------------- 3 files changed, 85 insertions(+), 68 deletions(-) diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index 1bfbf6c23..bb36052c3 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -38,6 +38,17 @@ function TxProposal(opts) { this.readonly = opts.readonly || null; this.merchant = opts.merchant || null; this.paymentProtocolURL = opts.paymentProtocolURL || null; + + if (opts.creator) { + var now = Date.now(); + var me = {}; + me[opts.creator] = now; + + this.signedBy = this.seenBy = me; + this.creator = opts.creator; + this.createdTs = now; + } + this._sync(); } @@ -50,7 +61,7 @@ TxProposal.prototype._checkPayPro = function() { if (!this.merchant.outs || this.merchant.outs.length !== 1) throw new Error('PayPro: Unsopported number of outputs'); - if (this.merchant.expires < (this.getSent() || Date.now()/1000.) ) + if (this.merchant.expires < (this.getSent() || Date.now() / 1000.)) throw new Error('PayPro: Request expired'); if (!this.merchant.total || !this.merchant.outs[0].amountSatStr || !this.merchant.outs[0].address) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 1d382b068..5ce44a90a 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -1541,7 +1541,9 @@ Wallet.prototype.fetchPaymentRequest = function(options, cb) { var merchantData, err; try { merchantData = self.parsePaymentRequest(options, pr); - } catch (e) { err = e}; + } catch (e) { + err = e + }; log.debug('PayPro request data', merchantData); return cb(err, merchantData); @@ -1831,7 +1833,7 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { ack = paypro.makePaymentACK(data); return self.receivePaymentRequestACK(ntxid, tx, txp, ack, cb); }) - .error(function(data, status ) { + .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) { @@ -2136,7 +2138,7 @@ Wallet.prototype.createTx = function(opts, cb) { var self = this; var toAddress = opts.toAddress; var amountSat = opts.amountSat; - preconditions.checkArgument(!opts.comment || opts.comment.length <= 100); + var comment = opts.comment; var url = opts.url; if (url && !opts.merchantData) { @@ -2148,7 +2150,7 @@ Wallet.prototype.createTx = function(opts, cb) { if (err) return cb(err); opts.merchantData = merchantData; opts.amountSat = merchantData.outs[0].address; - opts.toAddress = merchantData.outs[0].amount; + opts.toAddress = merchantData.outs[0].amount; self.createTx(opts, cb); }); }; @@ -2158,19 +2160,21 @@ Wallet.prototype.createTx = function(opts, cb) { this.getUnspent(function(err, safeUnspent) { if (err) return cb(new Error('Could not get list of UTXOs')); - var ntxid; + var ntxid, txp; try { - 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); + txp = self.createTxProposal(toAddress, amountSat, comment, safeUnspent, opts.builderOpts); } catch (e) { + log.error(e); return cb(e); } + if (opts.merchantData) + txp.addMerchantData(opts.merchantData); + + var ntxid = self.txProposals.add(txp); + log.debug('TXP Added: ', ntxid); + + if (!ntxid) { return cb(new Error('Error creating the transaction')); } @@ -2194,10 +2198,11 @@ var sanitize = function(address) { * @desc Create a transaction proposal * @TODO: Document more */ -Wallet.prototype.createTxProposal = function(toAddress, amountSat, utxos, builderOpts) { +Wallet.prototype.createTxProposal = function(toAddress, amountSat, comment, utxos, builderOpts) { preconditions.checkArgument(toAddress); preconditions.checkArgument(amountSat); preconditions.checkArgument(_.isArray(utxos)); + preconditions.checkArgument(!comment || comment.length <= 100, 'Comment too long'); builderOpts = builderOpts || {}; var pkr = this.publicKeyRing; @@ -2257,32 +2262,11 @@ Wallet.prototype.createTxProposal = function(toAddress, amountSat, utxos, builde inputChainPaths: inputChainPaths, comment: comment, builder: b, + creator: this.getMyCopayerId(), }); }; -/* 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; - - // Add metadata to TxP - txp.signedBy = txp.seenBy = me; - txp.creator = myId; - txp.createdTs = now; - - var ntxid = this.txProposals.add(txp); - return ntxid; -}; - /** * @desc Updates all the indexes for the current publicKeyRing. This scans * the blockchain looking for transactions on derived addresses. diff --git a/test/Wallet.js b/test/Wallet.js index 63665173c..aa034fa20 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -222,7 +222,7 @@ describe('Wallet model', function() { unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true); var f = function() { - var ntxid = w.createTxSync( + var ntxid = w.createTxProposal( '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', '123456789', null, @@ -233,18 +233,18 @@ describe('Wallet model', function() { }); + it('#create, check builder opts', 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); - var ntxid = w.createTxSync( + var txp = w.createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', null, unspentTest ); - var t = w.txProposals; - var opts = JSON.parse(t.txps[ntxid].builder.vanilla.opts); + var opts = JSON.parse(txp.builder.vanilla.opts); opts.signhash.should.equal(1); (opts.lockTime === null).should.be.true; should.not.exist(opts.fee); @@ -257,15 +257,13 @@ describe('Wallet model', function() { unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - var ntxid = w.createTxSync( + var txp = w.createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', null, unspentTest ); - var t = w.txProposals; - var txp = t.txps[ntxid]; Object.keys(txp._inputSigners).length.should.equal(1); var tx = txp.builder.build(); should.exist(tx); @@ -282,15 +280,13 @@ describe('Wallet model', function() { unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - var ntxid = w.createTxSync( + var txp = w.createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', comment, unspentTest ); - var t = w.txProposals; - var txp = t.txps[ntxid]; var tx = txp.builder.build(); should.exist(tx); txp.comment.should.equal(comment); @@ -304,16 +300,14 @@ describe('Wallet model', function() { unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - var badCreate = function() { - w.createTxSync( + (function() { + w.createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', comment, unspentTest ); - } - - chai.expect(badCreate).to.throw(Error); + }).should.throw('Comment'); }); it('#addressIsOwn', function() { @@ -336,21 +330,19 @@ describe('Wallet model', function() { for (var index = 0; index < 3; index++) { unspentTest[0].address = w.publicKeyRing.getAddress(index, isChange, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(index, isChange, w.publicKey); - w.createTxSync( + var txp = w.createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', null, unspentTest ); - var t = w.txProposals; - var k = Object.keys(t.txps)[0]; - var tx = t.txps[k].builder.build(); + var tx = txp.builder.build(); should.exist(tx); tx.isComplete().should.equal(false); tx.countInputMissingSignatures(0).should.equal(2); - (t.txps[k].signedBy[w.privateKey.getId()] - ts > 0).should.equal(true); - (t.txps[k].seenBy[w.privateKey.getId()] - ts > 0).should.equal(true); + (txp.signedBy[w.privateKey.getId()] - ts > 0).should.equal(true); + (txp.seenBy[w.privateKey.getId()] - ts > 0).should.equal(true); } } }); @@ -774,7 +766,10 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { ntxid.length.should.equal(64); done(); }); @@ -788,7 +783,10 @@ describe('Wallet model', function() { var w = createW2([k2]); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { w.on('txProposalsUpdated', function() { w.getTxProposals()[0].signedByUs.should.equal(true); w.getTxProposals()[0].rejectedByUs.should.equal(false); @@ -802,7 +800,10 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { (function() { w.reject(ntxid); }).should.throw('reject a signed'); @@ -814,7 +815,10 @@ describe('Wallet model', function() { var oldK = w.privateKey; var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { var s = sinon.stub(w, 'getMyCopayerId').returns('213'); Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(0); w.reject(ntxid); @@ -828,7 +832,10 @@ describe('Wallet model', function() { var w = createW2(null, 1); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { w.sendTx(ntxid, function(txid) { txid.length.should.equal(64); done(); @@ -839,7 +846,10 @@ describe('Wallet model', function() { var w = createW2(null, 1); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { var txp = w.txProposals.get(ntxid); // Assign fake builder txp.builder = new Builder(); @@ -858,7 +868,10 @@ describe('Wallet model', function() { var w = createW2(null, 1); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { sinon.stub(w.blockchain, 'broadcast').yields({ statusCode: 303 }); @@ -872,7 +885,10 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { w.sendTxProposal.bind(w).should.throw('Illegal Argument.'); (function() { w.sendTxProposal(ntxid); @@ -885,7 +901,10 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { w.sendAllTxProposals.bind(w).should.not.throw(); (function() { w.sendAllTxProposals(); @@ -900,7 +919,10 @@ describe('Wallet model', function() { var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); sinon.stub(w, 'getUnspent').yields('error', null); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { + w.createTx({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { chai.expect(err.message).to.equal('Could not get list of UTXOs'); done(); }); @@ -2049,7 +2071,7 @@ describe('Wallet model', function() { }]; w.blockchain.getTransactions = sinon.stub().yields(null, { - items: txs.slice(2,3), + items: txs.slice(2, 3), totalItems: txs.length, }); w.getAddressesInfo = sinon.stub().returns([{ From ae0dd4090311ca3c52e08af870bce8ac64aaae52 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 23 Nov 2014 00:29:50 -0300 Subject: [PATCH 05/23] refactor PayPro tests --- js/controllers/send.js | 2 +- js/models/TxProposal.js | 7 +- js/models/Wallet.js | 52 ++- js/util/HTTP.js | 75 ++++ test/PayPro.js | 793 --------------------------------- test/Wallet.js | 122 ++++- test/mocks/FakePayProServer.js | 425 +++++------------- 7 files changed, 345 insertions(+), 1131 deletions(-) create mode 100644 js/util/HTTP.js delete mode 100644 test/PayPro.js 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; + }, +}; From 49cee79a6c2c0e5240b9702219bbcfcab19c733f Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 23 Nov 2014 00:32:33 -0300 Subject: [PATCH 06/23] paypro fetching working on creator and receiver --- js/models/Wallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 24fd2f6ce..963a0f8b8 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -474,7 +474,7 @@ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { return cb(); log.info('Received a Payment Protocol TX Proposal'); - self.fetchPaymentRequest(txp.paymentProtocolURL, function(err, merchantData) { + self.fetchPaymentRequest({url:txp.paymentProtocolURL}, function(err, merchantData) { if (err) return cb(err); // This will verify current TXP data vs. merchantData (e.g., out addresses) From 3310bb6677e14d68965840b9925199099d7a4ee2 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 23 Nov 2014 10:45:11 -0300 Subject: [PATCH 07/23] refactor address book (remove signatures) --- README.md | 1 + js/models/Wallet.js | 330 ++++++++++++++++---------------------------- test/Wallet.js | 55 +------- 3 files changed, 121 insertions(+), 265 deletions(-) diff --git a/README.md b/README.md index 934e8c515..f58f03b9a 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ 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. + * Memos from the custormer to the server are not supported (i.e. there is no place to write messages to the server in the current UX) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 963a0f8b8..5a3835a5e 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -474,7 +474,9 @@ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { return cb(); log.info('Received a Payment Protocol TX Proposal'); - self.fetchPaymentRequest({url:txp.paymentProtocolURL}, function(err, merchantData) { + self.fetchPaymentRequest({ + url: txp.paymentProtocolURL + }, function(err, merchantData) { if (err) return cb(err); // This will verify current TXP data vs. merchantData (e.g., out addresses) @@ -602,8 +604,6 @@ Wallet.prototype._onSeen = function(senderId, data) { * @desc * Handle a ADDRESSBOOK message received * - * {@see Wallet#verifyAddressbookEntry} - * * @param {string} senderId * @param {Object} data * @param {Object} data.addressBook @@ -613,17 +613,23 @@ Wallet.prototype._onSeen = function(senderId, data) { Wallet.prototype._onAddressBook = function(senderId, data) { preconditions.checkState(data.addressBook); log.debug('Wallet:' + this.id + ' RECV ADDRESSBOOK:', data); + var rcv = data.addressBook; - var hasChange; - for (var key in rcv) { - if (!this.addressBook[key]) { - var isVerified = this.verifyAddressbookEntry(rcv[key], senderId, key); - if (isVerified) { - this.addressBook[key] = rcv[key]; - hasChange = true; - } + console.log('[Wallet.js.618:rcv:]', rcv); //TODO + + var hasChange, self = this; + _.each(rcv, function(value, key) { + if (!self.addressBook[key] && Address.validate(key)) { + + self.addressBook[key] = _.pick(value, ['createdTs', 'label']); + + // Force author to senderId. + self.addressBook[key].copayerId = senderId; + + hasChange = true; } - } + }); + if (hasChange) { this.emitAndKeepAlive('addressBookUpdated'); } @@ -1272,15 +1278,31 @@ Wallet.prototype.sendIndexes = function(recipients) { }; /** + * sendAddressBook * @desc Send our addressBook to other recipients + * * @param {string[]} recipients - the pubkeys of the recipients + * @param onlyKey + * @return {undefined} */ -Wallet.prototype.sendAddressBook = function(recipients) { - if (!Object.keys(this.addressBook).length) return; - log.debug('Wallet:' + this.id + ' ### SENDING addressBook TO:', recipients || 'All', this.addressBook); +Wallet.prototype.sendAddressBook = function(recipients, onlyKey) { + var toSend = [], + myId = this.getMyCopayerId(); + + if (onlyKey) { + toSend = [this.addressBook[onlyKey]]; + } else { + toSend = _.find(this.addressBook, function(key, value) { + return value.copayerId = myId; + }); + } + if (_.isEmpty(toSend)) return; + + log.debug('Wallet:' + this.id + ' ### SENDING addressBook TO:', recipients || 'All', toSend); + this.send(recipients, { type: 'addressbook', - addressBook: this.addressBook, + addressBook: toSend, walletId: this.id, }); }; @@ -1473,24 +1495,25 @@ Wallet.prototype.sign = function(ntxid) { * @param {broadcastCallback} cb */ Wallet.prototype.sendTx = function(ntxid, cb) { - var txp = this.txProposals.get(ntxid); + var self = this; + var txp = this.txProposals.get(ntxid); var tx = txp.builder.build(); if (!tx.isComplete()) throw new Error('Tx is not complete. Can not broadcast'); - log.debug('Wallet:' + this.id + ' Broadcasting Transaction'); + log.info('Wallet:' + this.id + ' Broadcasting Transaction ntxid:' + ntxid); + + var serializedTx = tx.serialize(); + if (txp.merchant) { - return this.sendPaymentTx(ntxid, cb); + this.sendPaymentTx(ntxid, serializedTx); } - var scriptSig = tx.ins[0].getScript(); - var size = scriptSig.serialize().length; - var txHex = tx.serialize().toString('hex'); - log.debug('Wallet:' + this.id + ' Raw transaction: ', txHex); + var txHex = serializedTx.toString('hex'); + log.debug('\tRaw transaction: ', txHex); - var self = this; this.blockchain.broadcast(txHex, function(err, txid) { if (err) log.error('Error sending TX:', err); @@ -1633,7 +1656,7 @@ Wallet.prototype._addOutputsToMerchantData = function(merchantData) { * @param {Object} options * @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 + * @param {PayProRequest} rawData */ Wallet.prototype.parsePaymentRequest = function(options, rawData) { var self = this; @@ -1711,33 +1734,16 @@ Wallet.prototype.parsePaymentRequest = function(options, rawData) { /** * @desc Send a payment transaction to a server, complying with BIP70 * - * @TODO: Get this out of here. - * * @param {string} ntxid - the transaction proposal id - * @param {Object} options - * @param {string} options.refund_to - * @param {string} options.memo - * @param {string} options.comment - * @param {Function} cb + * @param {Function} txHex + * + * emits paymentACK(server's memo) */ -Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { +Wallet.prototype.sendPaymentTx = function(ntxid, txHex) { var self = this; - if (!cb) { - cb = options; - options = {}; - } - - var txp = this.txProposals.get(ntxid); - if (!txp) return; - - var tx = txp.builder.build(); - if (!tx.isComplete()) return; - log.debug('Sending Transaction'); - var refund_outputs = []; - - options.refund_to = options.refund_to || this.publicKeyRing.getPubKeys(0, false, this.getMyCopayerId())[0]; + 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) { @@ -1786,15 +1792,14 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { merchant_data = new Buffer(merchant_data, 'hex'); pay.set('merchant_data', merchant_data); } - pay.set('transactions', [tx.serialize()]); + pay.set('transactions', [serializedTx]); pay.set('refund_to', refund_outputs); - options.memo = options.memo || options.comment || 'Hi server, I would like to give you some money.'; - - pay.set('memo', options.memo); + // 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); @@ -1824,81 +1829,16 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { .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); + var ack = paypro.makePaymentACK(data); + var memo = ack.get('memo'); + log.debug('Payment Acknowledged!: %s', memo); + self.emitAndKeepAlive('paymentACK', memo); }) .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) { - return cb(txid, txp.merchant); - }); + log.error('Sending payment notification: XHR status: ' + status); }); }; -/** - * @desc Handles a PaymentRequestACK from the server - */ -Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) { - var self = this; - - var payment = ack.get('payment'); - var memo = ack.get('memo'); - - log.debug('Our payment was acknowledged!'); - log.debug('Message from Merchant: %s', memo); - - payment = PayPro.Payment.decode(payment); - var pay = new PayPro(); - payment = pay.makePayment(payment); - - txp.merchant.ack = { - memo: memo - }; - - if (payment.message.transactions && payment.message.transactions.length) { - tx = payment.message.transactions[0]; - if (!tx) { - log.debug('Sending to server was not met with a returned tx.'); - return this._processTxProposalSeen(ntxid, function(err, txid) { - log.debug('[Wallet.js.1613:txid:%s]', txid); - return cb(txid, txp.merchant); - }); - } - 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; - } - } else { - log.debug('WARNING: This server does not comply by standards.'); - log.debug('It is not returning a copy of the transaction.'); - } - - var txid = tx.calcHash().toString('hex'); - var txHex = tx.serialize().toString('hex'); - log.debug('Raw transaction: ', txHex); - - // XXX This fixes the invalid signature error: - // we might as well broadcast it ourselves anyway. - this.blockchain.broadcast(txHex, function(err, txid) { - log.debug('BITCOIND txid:', txid); - if (txid) { - self.txProposals.get(ntxid).setSent(txid); - self.sendTxProposal(ntxid); - self.emitAndKeepAlive('txProposalsUpdated'); - return cb(txid, txp.merchant); - } else { - log.debug('PayPro Sent failed. Checking if the TX was sent already'); - self._processTxProposalSent(ntxid, function(err, txid) { - return cb(txid, txp.merchant); - }); - } - }); -}; - /** * @desc Mark that a user has seen a given TxProposal * @return {boolean} true if the internal state has changed @@ -2043,7 +1983,7 @@ Wallet.prototype.maxRejectCount = function() { * @param {Object[]} safeUnspendList * @param {Object[]} unspentList */ -/** +/* @ TODO add cached? * @desc Get a list of unspent transaction outputs * @param {getUnspentCallback} cb */ @@ -2069,6 +2009,7 @@ Wallet.prototype.getUnspent = function(cb) { }); }; +// TODO. not used. Wallet.prototype.removeTxWithSpentInputs = function(cb) { var self = this; @@ -2121,8 +2062,7 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { }; /** - * @desc Create a transaction proposal - * @TODO: Document more + * @desc Create a transaction proposal, and run many sanity checks */ Wallet.prototype.createTx = function(opts, cb) { preconditions.checkArgument(_.isObject(opts)); @@ -2183,66 +2123,90 @@ Wallet.prototype.createTx = function(opts, cb) { }); }; -// TODO (eordano): Move this to bitcore -var sanitize = function(address) { - if (/^bitcoin:/g.test(address)) { +/** + * _newAddress + * Returns an Address object from an address string or a BIP21 URL.* + * @param address + * @return { bitcore.Address } + */ + +Wallet._newAddress = function(address) { + if (/ ^ bitcoin: /g.test(address)) { return new BIP21(address).address; } return new Address(address); }; -/** - * @desc Create a transaction proposal - * @TODO: Document more + +Wallet.prototype._getBuilder = function(opts) { + opts = opts || {}; + + if (!opts.remainderOut) { + 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]; + } + + return new Builder(opts); +}; + + +/* + * createTxProposal + * Creates a transaction proposal and run many sanity checks + * + * @param toAddress + * @param amountSat + * @param comment (optional) + * @param utxos + * @param builderOpts bitcore.TransactionBuilder options(like spendUnconfirmed) + * @return {TxProposal} The newly created transaction proposal.* + * Throws errors on unexpected inputs. */ + Wallet.prototype.createTxProposal = function(toAddress, amountSat, comment, utxos, builderOpts) { preconditions.checkArgument(toAddress); preconditions.checkArgument(amountSat); preconditions.checkArgument(_.isArray(utxos)); preconditions.checkArgument(!comment || comment.length <= 100, 'Comment too long'); - builderOpts = builderOpts || {}; var pkr = this.publicKeyRing; var priv = this.privateKey; - var addr = sanitize(toAddress); + var addr = Wallet._newAddress(toAddress); + preconditions.checkState(addr && addr.data && addr.isValid(), 'Bad address:' + addr.toString()); preconditions.checkArgument(addr.network().name === this.getNetworkName(), 'networkname mismatch'); preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete'); preconditions.checkState(priv, 'no private key'); - if (!builderOpts.remainderOut) { - builderOpts.remainderOut = { - address: this._doGenerateAddress(true).toString() - }; - } - if (_.isUndefined(builderOpts.spendUnconfirmed)) { - builderOpts.spendUnconfirmed = this.spendUnconfirmed; - } + var b = this._getBuilder(builderOpts); - for (var k in Wallet.builderOpts) { - builderOpts[k] = Wallet.builderOpts[k]; - } - - var b = new Builder(builderOpts) - .setUnspent(utxos) + b.setUnspent(utxos) .setOutputs([{ address: addr.data, amountSatStr: amountSat, }]); - log.debug('Creating TX: Builder ready'); var selectedUtxos = b.getSelectedUnspent(); if (selectedUtxos.length > TX_MAX_INS) - throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.length + ' inputs. Aborting'); + 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)); + b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); var keys = priv.getForPaths(inputChainPaths); b.sign(keys); @@ -2253,7 +2217,8 @@ Wallet.prototype.createTxProposal = function(toAddress, amountSat, comment, utxo throw new Error('Could not sign generated tx'); var txSize = tx.getSize(); - if (txSize / 1024 > TX_MAX_SIZE_KB) + if (txSize / + 1024 > TX_MAX_SIZE_KB) throw new Error('BIG: Resulting TX is too big ' + txSize + ' bytes. Aborting'); return new TxProposal({ @@ -2429,44 +2394,17 @@ Wallet.prototype.setAddressBook = function(key, label) { this._checkAddressBook(key); var copayerId = this.getMyCopayerId(); var ts = Date.now(); - var payload = { - address: key, - label: label, - copayerId: copayerId, - createdTs: ts - }; var newEntry = { hidden: false, createdTs: ts, copayerId: copayerId, label: label, - signature: this.signJson(payload) }; this.addressBook[key] = newEntry; - this.sendAddressBook(); + this.sendAddressBook(null, key); this.emitAndKeepAlive('addressBookUpdated'); }; -/** - * @desc Verifies that an addressbook entry is correctly signed by a copayer - * - * @param {Object} rcvEntry - the entry in the address book - * @param {string} senderId - the pubkey of a copayer - * @param {string} key - the base58 encoded address - * @return {boolean} true if the signature matches - */ -Wallet.prototype.verifyAddressbookEntry = function(rcvEntry, senderId, key) { - if (!key) throw new Error('Keys are required'); - var signature = rcvEntry.signature; - var payload = { - address: key, - label: rcvEntry.label, - copayerId: rcvEntry.copayerId, - createdTs: rcvEntry.createdTs - }; - return this.verifySignedJson(senderId, payload, signature); -}; - /** * @desc Hides or unhides an address book entry * @param {string} key - the address in the addressbook @@ -2501,40 +2439,6 @@ Wallet.prototype.isReady = function() { return this.publicKeyRing.isComplete(); }; -/** - * @desc Sign a JSON - * - * @TODO: THIS WON'T WORK ALLWAYS! JSON.stringify doesn't warants an order - * @param {Object} payload - the payload to verify - * @return {string} base64 encoded string - */ -Wallet.prototype.signJson = function(payload) { - var key = new bitcore.Key(); - key.private = new Buffer(this.getMyCopayerIdPriv(), 'hex'); - key.regenerateSync(); - var sign = bitcore.Message.sign(JSON.stringify(payload), key); - return sign.toString('hex'); -} - -/** - * @desc Verify that a JSON object is correctly signed - * - * @TODO: THIS WON'T WORK ALLWAYS! JSON.stringify doesn't warants an order - * - * @param {string} senderId - a sender's public key, hex encoded - * @param {Object} payload - the object to verify - * @param {string} signature - a sender's public key, hex encoded - * @return {boolean} - */ -Wallet.prototype.verifySignedJson = function(senderId, payload, signature) { - var pubkey = new Buffer(senderId, 'hex'); - var sign = new Buffer(signature, 'hex'); - var v = bitcore.Message.verifyWithPubKey(pubkey, JSON.stringify(payload), sign); - return v; -} - - - /** * @desc Return a list of past transactions * diff --git a/test/Wallet.js b/test/Wallet.js index 2430383d3..17580551f 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -1351,7 +1351,7 @@ describe('Wallet model', function() { createdTs: 1404769393509, hidden: false, label: "adsf", - signature: "3046022100d4cdefef66ab8cea26031d5df03a38fc9ec9b09b0fb31d3a26b6e204918e9e78022100ecdbbd889ec99ea1bfd471253487af07a7fa7c0ac6012ca56e10e66f335e4586" + dummy: 'foo', } }, walletId: "11d23e638ed84c06", @@ -1363,58 +1363,9 @@ describe('Wallet model', function() { Object.keys(w.addressBook).length.should.equal(2); w._onAddressBook(senderId, data, true); Object.keys(w.addressBook).length.should.equal(3); + should.exist(w.addressBook['3Ae1ieAYNXznm7NkowoFTu5MkzgrTfDz8Z'].createdTs); + should.not.exist(w.addressBook['3Ae1ieAYNXznm7NkowoFTu5MkzgrTfDz8Z'].dummy); }); - - it('should return signed object', function() { - var w = createW(); - var payload = { - address: 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx', - label: 'Faucet', - copayerId: '026a55261b7c898fff760ebe14fd22a71892295f3b49e0ca66727bc0a0d7f94d03', - createdTs: 1403102115 - }; - should.exist(w.signJson(payload)); - }); - - it('should verify signed object', function() { - var w = createW(); - - var payload = { - address: "3Ae1ieAYNXznm7NkowoFTu5MkzgrTfDz8Z", - label: "adsf", - copayerId: "03baa45498fee1045fa8f91a2913f638dc3979b455498924d3cf1a11303c679cdb", - createdTs: 1404769393509 - } - - var signature = "3046022100d4cdefef66ab8cea26031d5df03a38fc9ec9b09b0fb31d3a26b6e204918e9e78022100ecdbbd889ec99ea1bfd471253487af07a7fa7c0ac6012ca56e10e66f335e4586"; - - var pubKey = "03baa45498fee1045fa8f91a2913f638dc3979b455498924d3cf1a11303c679cdb"; - - w.verifySignedJson(pubKey, payload, signature).should.equal(true); - payload.label = 'Another'; - w.verifySignedJson(pubKey, payload, signature).should.equal(false); - }); - - it('should verify signed addressbook entry', function() { - var w = createW(); - var key = "3Ae1ieAYNXznm7NkowoFTu5MkzgrTfDz8Z"; - var pubKey = "03baa45498fee1045fa8f91a2913f638dc3979b455498924d3cf1a11303c679cdb"; - w.addressBook[key] = { - copayerId: pubKey, - createdTs: 1404769393509, - hidden: false, - label: "adsf", - signature: "3046022100d4cdefef66ab8cea26031d5df03a38fc9ec9b09b0fb31d3a26b6e204918e9e78022100ecdbbd889ec99ea1bfd471253487af07a7fa7c0ac6012ca56e10e66f335e4586" - }; - - w.verifyAddressbookEntry(w.addressBook[key], pubKey, key).should.equal(true); - w.addressBook[key].label = 'Another'; - w.verifyAddressbookEntry(w.addressBook[key], pubKey, key).should.equal(false); - (function() { - w.verifyAddressbookEntry(); - }).should.throw(); - }); - }); it('#getNetworkName', function() { From a7de2ababd5097463490460a2fb2e4946c912486 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 23 Nov 2014 15:52:39 -0300 Subject: [PATCH 08/23] rm networking on nonShared wallets --- js/controllers/send.js | 94 +++++++++---------- js/models/Async.js | 9 +- js/models/Insight.js | 4 +- js/models/TxProposal.js | 18 ++++ js/models/TxProposals.js | 18 ++++ js/models/Wallet.js | 189 ++++++++++++++++++++------------------- 6 files changed, 179 insertions(+), 153 deletions(-) diff --git a/js/controllers/send.js b/js/controllers/send.js index efceab7fb..adc22d0f1 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -24,6 +24,7 @@ angular.module('copayApp.controllers').controller('SendController', $scope.isRateAvailable = false; $scope.rateService = rateService; + $scope.showScanner = false; rateService.whenAvailable(function() { @@ -105,33 +106,19 @@ angular.module('copayApp.controllers').controller('SendController', var msg = err.toString(); if (msg.match('BIG')) - msg = 'The transaction have too many inputs. Try creating many transactions for smaller amounts.' + msg = 'The transaction have too many inputs. Try creating many transactions for smaller amounts' if (msg.match('totalNeededAmount')) - msg = 'Not enough funds.' + msg = 'Not enough funds' + + var message = 'The transaction' + (w.isShared() ? ' proposal' : '') + + ' could not be created: ' + msg; - 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'; @@ -160,16 +147,21 @@ angular.module('copayApp.controllers').controller('SendController', merchant: $rootScope.merchant.request_url }; } - // reset fields - $scope.address = $scope.amount = $scope.commentText = null; - form.address.$pristine = form.amount.$pristine = true; - - w.createTx({ - toAddress: address, - amountSat: amount, - comment: commentText, + w.spend({ + toAddress: address, + amountSat: amount, + comment: commentText, url: (payInfo && payInfo.merchant) ? payInfo.merchant : null, - }, $scope._afterSend); + }, function(err, txid, status) { + // reset fields + $scope.address = $scope.amount = $scope.commentText = null; + form.address.$pristine = form.amount.$pristine = true; + $rootScope.pendingPayment = null; + if (err) return $scope._showError(err); + + $scope.notifyStatus(status); + $scope.loadTxs(); + }); }; // QR code Scanner @@ -267,6 +259,7 @@ angular.module('copayApp.controllers').controller('SendController', $scope.openScanner = function() { if (window.cordova) return $scope.scannerIntent(); +console.log('[send.js.260] OPENN'); //TODO $scope.showScanner = true; // Wait a moment until the canvas shows @@ -371,22 +364,24 @@ angular.module('copayApp.controllers').controller('SendController', $scope.amount = $rootScope.topAmount; }; + $scope.notifyStatus = function(status) { + if (status == copay.Wallet.TX_BROADCASTED) + notification.success('Success', 'Transaction broadcasted!'); + else if (status == copay.Wallet.TX_PROPOSAL_SENT) + notification.success('Success', 'Transaction proposal created'); + else if (status == copay.Wallet.TX_SIGNED) + notification.success('Success', 'Transaction proposal was signed'); + else + notification.error('Error', 'Unknown error occured'); + }; + $scope.send = function(ntxid, cb) { $scope.error = $scope.success = null; $scope.loading = true; $rootScope.txAlertCount = 0; - w.sendTx(ntxid, function(txid, merchantData) { - if (!txid) { - notification.error('Error', 'There was an error sending the transaction'); - } else { - if (!merchantData) { - notification.success('Success', 'Transaction broadcasted!'); - } else { - notification.success('Success', 'Payment sent. ' + merchantData.ack.memo); - } - } - + w.broadcastTx(ntxid, function(err, txid, status) { + $scope.notifyStatus(status); if (cb) return cb(); else $scope.loadTxs(); }); @@ -396,20 +391,10 @@ angular.module('copayApp.controllers').controller('SendController', $scope.loading = true; $scope.error = $scope.success = null; - try { - w.sign(ntxid); - } catch (e) { - notification.error('Error', 'There was an error signing the transaction'); + w.signAndSend(ntxid, function(err, id, status) { + $scope.notifyStatus(status); $scope.loadTxs(); - return; - } - - var p = w.txProposals.getTxProposal(ntxid); - if (p.builder.isFullySigned()) { - $scope.send(ntxid); - } else { - $scope.loadTxs(); - } + }); }; $scope.reject = function(ntxid) { @@ -417,7 +402,6 @@ angular.module('copayApp.controllers').controller('SendController', $rootScope.txAlertCount = 0; w.reject(ntxid); notification.warning('Transaction rejected', 'You rejected the transaction successfully'); - $scope.loading = false; $scope.loadTxs(); }; @@ -510,7 +494,9 @@ angular.module('copayApp.controllers').controller('SendController', }, 10 * 1000); // Payment Protocol URI (BIP-72) - $scope.wallet.fetchPaymentRequest({url: uri.merchant}, function(err, merchantData) { + $scope.wallet.fetchPaymentRequest({ + url: uri.merchant + }, function(err, merchantData) { if (!timeout) return; clearTimeout(timeout); diff --git a/js/models/Async.js b/js/models/Async.js index 0e9d0c522..361162283 100644 --- a/js/models/Async.js +++ b/js/models/Async.js @@ -207,7 +207,9 @@ Network.prototype._onMessage = function(enc) { log.debug('Ignoring trailing message. Ts:', enc.ts); return; } - log.debug('Async: receiving ' + JSON.stringify(payload)); + + log.info('Network message: ', payload.type); + log.debug('Message payload:', payload); var self = this; switch (payload.type) { @@ -239,6 +241,7 @@ Network.prototype._onMessage = function(enc) { Network.prototype._setupConnectionHandlers = function(opts, cb) { preconditions.checkState(this.socket); + log.debug('setting up connection', opts); var self = this; self.socket.on('connect_error', function(m) { @@ -260,7 +263,7 @@ Network.prototype._setupConnectionHandlers = function(opts, cb) { if (fromTs) { self.ignoreMessageFromTs = fromTs; } - log.info('Async: synchronizing from: ',fromTs); + log.info('Async: syncing from: ', fromTs); self.socket.emit('sync', fromTs); self.started = true; }); @@ -398,8 +401,6 @@ Network.prototype.send = function(dest, payload, cb) { if (to == this.copayerId) continue; - log.debug('SEND to: ' + to, this.copayerId, JSON.stringify(payload)); - var message = this.encode(to, payload); this.socket.emit('message', message); } diff --git a/js/models/Insight.js b/js/models/Insight.js index 2f3e8f90c..a040c7fec 100644 --- a/js/models/Insight.js +++ b/js/models/Insight.js @@ -224,9 +224,7 @@ Insight.prototype.subscribe = function(addresses) { s.emit('subscribe', address); s.on(address, handler); - } else { - log.debug('Already subcribed to: ', address); - } + } }); }; diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index 934319171..09a32812c 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -82,6 +82,24 @@ TxProposal.prototype._checkPayPro = function() { }; + +TxProposal.prototype.isFullySigned = function() { + return txp.builder && txp.builder.isFullySigned(); +}; + +TxProposal.prototype.sign = function(keys, signerId) { + var before = txp.countSignatures(); + txp.builder.sign(keys); + + var signaturesAdded = txp.countSignatures() > before; + if (signaturesAdded){ + txp.signedBy[signerId] = Date.now(); + } + return signturesAdded; +}; + + + TxProposal.prototype._check = function() { if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) { diff --git a/js/models/TxProposals.js b/js/models/TxProposals.js index d800ddacf..df359f57a 100644 --- a/js/models/TxProposals.js +++ b/js/models/TxProposals.js @@ -205,4 +205,22 @@ TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { return ret; }; +/** + * purge + * + * @param deleteAll + * @return {undefined} + */ +TxProposals.prototype.purge = function(deleteAll, maxRejectCount) { + var m = _.size(this.txps); + + if (deleteAll) { + this.deleteAll(); + } else { + this.deletePending(maxRejectCount); + } + var n = _.size(this.txps); + return m - n; +}; + module.exports = TxProposals; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 5a3835a5e..eaf3f84f2 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -33,6 +33,8 @@ var copayConfig = require('../../config'); var TX_MAX_SIZE_KB = 50; var TX_MAX_INS = 70; + + /** * @desc * Wallet manages a private key for Copay, network, storage of the wallet for @@ -119,6 +121,11 @@ function Wallet(opts) { inherits(Wallet, events.EventEmitter); + +Wallet.TX_BROADCASTED = 'txBroadcasted'; +Wallet.TX_PROPOSAL_SENT = 'txProposalSent'; +Wallet.TX_SIGNED = 'txSigned'; + Wallet.prototype.emitAndKeepAlive = function(args) { var args = Array.prototype.slice.call(arguments); log.debug('Wallet Emitting:', args); @@ -260,13 +267,10 @@ Wallet.prototype._newAddresses = function(dontUpdateUx) { * Processes the data using {@link HDParams#fromList} and merges it with the * {@link Wallet#publicKeyRing}. * - * Triggers a {@link Wallet#store} if the internal state has changed. - * * @param {string} senderId - the sender id * @param {Object} data - the data recived, {@see HDParams#fromList} */ Wallet.prototype._onIndexes = function(senderId, data) { - log.debug('Wallet:' + this.id + ' RECV INDEXES:', data); var inIndexes = HDParams.fromList(data.indexes); var hasChanged = this.publicKeyRing.mergeIndexes(inIndexes); if (hasChanged) { @@ -448,7 +452,8 @@ Wallet.prototype._processTxProposalSeen = function(ntxid) { }; -Wallet.prototype._processTxProposalSent = function(ntxid, cb) { + +Wallet.prototype._checkIfTxProposalIsSent = function(ntxid, cb) { var self = this; var txp = this.txProposals.get(ntxid); @@ -459,7 +464,7 @@ Wallet.prototype._processTxProposalSent = function(ntxid, cb) { } } self.emitAndKeepAlive('txProposalsUpdated'); - if (cb) return cb(null, txid); + if (cb) return cb(null, txid, txid ? Wallet.TX_BROADCASTED : null); }); }; @@ -501,9 +506,8 @@ Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) { var tx = mergeInfo.txp.builder.build(); if (tx.isComplete()) - self._processTxProposalSent(mergeInfo.ntxid); - else if (mergeInfo.hasChanged) { - self.sendTxProposal(mergeInfo.ntxid); + self._checkIfTxProposalIsSent(mergeInfo.ntxid); + else { self.emitAndKeepAlive('txProposalsUpdated'); } return cb(); @@ -521,9 +525,9 @@ Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) { */ Wallet.prototype._onTxProposal = function(senderId, data) { var self = this; - log.debug('Wallet:' + this.id + ' RECV TXPROPOSAL: ', data); var m; +console.log('[Wallet.js.533]a', this.txProposals.txps); //TODO try { m = this.txProposals.merge(data.txProposal, Wallet.builderOpts); var keyMap = this._getKeyMap(m.txp); @@ -534,8 +538,9 @@ Wallet.prototype._onTxProposal = function(senderId, data) { this.txProposals.deleteOne(m.ntxid); m = null; } +console.log('[Wallet.js.533]a', this.txProposals.txps); //TODO - self._processIncomingTxProposal(m, function(err) { + this._processIncomingTxProposal(m, function(err) { if (err) { log.error('Corrupt TX proposal received from:', senderId, err.toString()); if (m && m.ntxid) @@ -588,8 +593,6 @@ Wallet.prototype._onReject = function(senderId, data) { */ Wallet.prototype._onSeen = function(senderId, data) { preconditions.checkState(data.ntxid); - log.debug('Wallet:' + this.id + ' RECV SEEN:', data); - var txp = this.txProposals.get(data.ntxid); txp.setSeen(senderId); this.emitAndKeepAlive('txProposalEvent', { @@ -611,14 +614,12 @@ Wallet.prototype._onSeen = function(senderId, data) { * @emits txProposalEvent */ Wallet.prototype._onAddressBook = function(senderId, data) { - preconditions.checkState(data.addressBook); - log.debug('Wallet:' + this.id + ' RECV ADDRESSBOOK:', data); + if (!data.addressBook || !_.isArray(data.addressBook)) + return; - var rcv = data.addressBook; - console.log('[Wallet.js.618:rcv:]', rcv); //TODO + var self = this, hasChange; - var hasChange, self = this; - _.each(rcv, function(value, key) { + _.each(data.addressBook, function(value, key) { if (!self.addressBook[key] && Address.validate(key)) { self.addressBook[key] = _.pick(value, ['createdTs', 'label']); @@ -660,8 +661,6 @@ Wallet.prototype.updateSyncedTimestamp = function(ts) { * Triggers a call to {@link Wallet#sendWalletReady} */ Wallet.prototype._onNoMessages = function() { - if (!this.isShared()) return; - log.debug('Wallet:' + this.id + ' No messages at the server. Requesting peer sync from: ' + (this.syncedTimestamp + 1)); this.sendWalletReady(null, parseInt((this.syncedTimestamp + 1) / 1000000)); }; @@ -676,13 +675,14 @@ Wallet.prototype._onNoMessages = function() { * @emits corrupt */ Wallet.prototype._onData = function(senderId, data, ts) { +console.log('[Wallet.js.533]0', this.txProposals.txps); //TODO preconditions.checkArgument(senderId); preconditions.checkArgument(data); preconditions.checkArgument(data.type); preconditions.checkArgument(ts); preconditions.checkArgument(_.isNumber(ts)); - log.debug('Wallet:' + this.id + ' RECV', senderId, data); + log.debug('Wallet:' + this.getName() + ' RECV:', data.type, data, senderId); this.updateSyncedTimestamp(ts); @@ -917,8 +917,13 @@ Wallet.prototype._setBlockchainListeners = function() { */ Wallet.prototype.netStart = function() { var self = this; - var net = this.network; + if (!this.isShared()) { + self.emitAndKeepAlive('ready'); + return; + } + + var net = this.network; net.removeAllListeners(); net.on('connect', self._onConnect.bind(self)); net.on('data', self._onData.bind(self)); @@ -953,7 +958,7 @@ Wallet.prototype.netStart = function() { }; - log.debug('Wallet:' + self.id + ' Starting networking: ' + startOpts.copayerId); + log.debug('Wallet:' + self.id + ' Starting network.'); net.start(startOpts, function() { self._setBlockchainListeners(); self.emitAndKeepAlive('ready', net.getPeer()); @@ -1154,11 +1159,16 @@ Wallet.fromObj = function(o, readOpts) { /** - * @desc Send a message to other peers + * @desc sendToPeers a message to other peers * @param {string[]} recipients - the pubkey of the recipients of the message * @param {Object} obj - the data to be sent to them */ -Wallet.prototype.send = function(recipients, obj) { +Wallet.prototype._sendToPeers = function(recipients, obj) { + if (!this.isShared()) return; + + log.info('Wallet:' + this.getName() + ' ### SENDING ' + obj.type); + log.debug('Sending obj', obj); + this.network.send(recipients, obj); }; @@ -1181,8 +1191,7 @@ Wallet.prototype.sendAllTxProposals = function(recipients, sinceTs) { */ Wallet.prototype.sendTxProposal = function(ntxid, recipients) { preconditions.checkArgument(ntxid); - log.debug('Wallet:' + this.id + ' ### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals); - this.send(recipients, { + this._sendToPeers(recipients, { type: 'txProposal', txProposal: this.txProposals.get(ntxid).toObjTrim(), walletId: this.id, @@ -1195,8 +1204,7 @@ Wallet.prototype.sendTxProposal = function(ntxid, recipients) { */ Wallet.prototype.sendSeen = function(ntxid) { preconditions.checkArgument(ntxid); - log.debug('Wallet:' + this.id + ' ### SENDING seen: ' + ntxid + ' TO: All'); - this.send(null, { + this._sendToPeers(null, { type: 'seen', ntxid: ntxid, walletId: this.id, @@ -1209,8 +1217,8 @@ Wallet.prototype.sendSeen = function(ntxid) { */ Wallet.prototype.sendReject = function(ntxid) { preconditions.checkArgument(ntxid); - log.debug('Wallet:' + this.id + ' ### SENDING reject: ' + ntxid + ' TO: All'); - this.send(null, { + + this._sendToPeers(null, { type: 'reject', ntxid: ntxid, walletId: this.id, @@ -1222,9 +1230,7 @@ Wallet.prototype.sendReject = function(ntxid) { * @param {string[]} [recipients] - the pubkeys of the recipients */ Wallet.prototype.sendWalletReady = function(recipients, sinceTs) { - log.debug('Wallet:' + this.id + ' ### SENDING WalletReady TO:', recipients || 'All'); - - this.send(recipients, { + this._sendToPeers(recipients, { type: 'walletReady', walletId: this.id, sinceTs: sinceTs, @@ -1239,7 +1245,7 @@ Wallet.prototype.sendWalletReady = function(recipients, sinceTs) { Wallet.prototype.sendWalletId = function(recipients) { log.debug('Wallet:' + this.id + ' ### SENDING walletId TO:', recipients || 'All', this.id); - this.send(recipients, { + this._sendToPeers(recipients, { type: 'walletId', walletId: this.id, opts: this._optsToObj(), @@ -1252,12 +1258,11 @@ Wallet.prototype.sendWalletId = function(recipients) { * @param {string[]} [recipients] - the pubkeys of the recipients */ Wallet.prototype.sendPublicKeyRing = function(recipients) { - log.debug('Wallet:' + this.id + ' ### SENDING publicKeyRing TO:', recipients || 'All', this.publicKeyRing.toObj()); - var publicKeyRing = this.publicKeyRing.toObj(); + var publicKeyRingObj = this.publicKeyRing.toObj(); - this.send(recipients, { + this._sendToPeers(recipients, { type: 'publicKeyRing', - publicKeyRing: publicKeyRing, + publicKeyRing: publicKeyRingObj, walletId: this.id, }); }; @@ -1268,9 +1273,7 @@ Wallet.prototype.sendPublicKeyRing = function(recipients) { */ Wallet.prototype.sendIndexes = function(recipients) { var indexes = HDParams.serialize(this.publicKeyRing.indexes); - log.debug('Wallet:' + this.id + ' ### INDEXES TO:', recipients || 'All', indexes); - - this.send(recipients, { + this._sendToPeers(recipients, { type: 'indexes', indexes: indexes, walletId: this.id, @@ -1289,18 +1292,17 @@ Wallet.prototype.sendAddressBook = function(recipients, onlyKey) { var toSend = [], myId = this.getMyCopayerId(); - if (onlyKey) { - toSend = [this.addressBook[onlyKey]]; + if (onlyKey && this.addressBook[onlyKey]) { + toSend = {}; + toSend[onlyKey] = this.addressBook[onlyKey]; } else { - toSend = _.find(this.addressBook, function(key, value) { - return value.copayerId = myId; + toSend = _.filter(this.addressBook, function(entry) { + return entry.copayerId === myId; }); } if (_.isEmpty(toSend)) return; - log.debug('Wallet:' + this.id + ' ### SENDING addressBook TO:', recipients || 'All', toSend); - - this.send(recipients, { + this._sendToPeers(recipients, { type: 'addressbook', addressBook: toSend, walletId: this.id, @@ -1432,17 +1434,11 @@ Wallet.prototype.getPendingTxProposals = function() { * @return {number} the number of deleted proposals */ Wallet.prototype.purgeTxProposals = function(deleteAll) { - var m = this.txProposals.length(); - - if (deleteAll) { - this.txProposals.deleteAll(); - } else { - this.txProposals.deletePending(this.maxRejectCount()); + var deleted = this.txProposals.purge(deleteAll, this.maxRejectCount()); + if (deleted) { + this.emitAndKeepAlive('txProposalsUpdated'); } - this.emitAndKeepAlive('txProposalsUpdated'); - - var n = this.txProposals.length(); - return m - n; + return deleted; }; /** @@ -1467,34 +1463,40 @@ Wallet.prototype.reject = function(ntxid) { Wallet.prototype.sign = function(ntxid) { preconditions.checkState(!_.isUndefined(this.getMyCopayerId())); - var myId = this.getMyCopayerId(); var txp = this.txProposals.get(ntxid); - - var before = txp.countSignatures(); var keys = this.privateKey.getForPaths(txp.inputChainPaths); - txp.builder.sign(keys); - if (txp.countSignatures() <= before) { + var signaturesAdded = txp.sign(keys, this.getMyCopayerId()); + if (!signturesAdded) return false; - } - txp.signedBy[myId] = Date.now(); - this.sendTxProposal(ntxid); this.emitAndKeepAlive('txProposalsUpdated'); - return true; }; -/** - * @callback broadcastCallback - * @param {string} txid - the transaction id on the blockchain - */ + +Wallet.prototype.signAndSend = function(ntxid, cb) { + if (this.sign(ntxid)) { + var txp = w.txProposals.get(ntxid); + if (txp.isFullySigned()) { + return this.broadcastTx(ntxid, cb); + } else { + this.sendTxProposal(ntxid); + return cb(null, ntxid, Wallet.TX_SIGNED ); + } + } else { + return cb(new Error('Could not sign the proposal')); + } +}; + /** * @desc Broadcasts a transaction to the blockchain * @param {string} ntxid - the transaction proposal id * @param {broadcastCallback} cb + * @callback broadcastCallback + * @param {string} txid - the transaction id on the blockchain */ -Wallet.prototype.sendTx = function(ntxid, cb) { +Wallet.prototype.broadcastTx = function(ntxid, cb) { var self = this; var txp = this.txProposals.get(ntxid); @@ -1506,7 +1508,6 @@ Wallet.prototype.sendTx = function(ntxid, cb) { var serializedTx = tx.serialize(); - if (txp.merchant) { this.sendPaymentTx(ntxid, serializedTx); } @@ -1519,17 +1520,15 @@ Wallet.prototype.sendTx = function(ntxid, cb) { log.error('Error sending TX:', err); if (txid) { - log.debug('Wallet:' + self.getName() + ' Broadcasted TX. BITCOIND txid:', 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'); - return cb(txid); + return cb(null, txid, Wallet.TX_BROADCASTED); } else { log.info('Wallet:' + self.getName() + '. Sent failed. Checking if the TX was sent already'); - self._processTxProposalSent(ntxid, function(err, txid) { - return cb(txid); - }); + self._checkIfTxProposalIsSent(ntxid, cb); } }); }; @@ -2062,9 +2061,11 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { }; /** - * @desc Create a transaction proposal, and run many sanity checks + * @desc Spends coins from the wallet + * Create a Transaction Proposal and broadcast it or send it + * to copayers */ -Wallet.prototype.createTx = function(opts, cb) { +Wallet.prototype.spend = function(opts, cb) { preconditions.checkArgument(_.isObject(opts)); log.debug('create Options', opts); @@ -2098,7 +2099,8 @@ Wallet.prototype.createTx = function(opts, cb) { var ntxid, txp; try { - txp = self.createTxProposal(toAddress, amountSat, comment, safeUnspent, opts.builderOpts); + txp = self._createTxProposal(toAddress, + amountSat, comment, safeUnspent, opts.builderOpts); } catch (e) { log.error(e); return cb(e); @@ -2109,17 +2111,21 @@ Wallet.prototype.createTx = function(opts, cb) { } var ntxid = self.txProposals.add(txp); - log.debug('TXP Added: ', ntxid); - - if (!ntxid) { return cb(new Error('Error creating the transaction')); } + log.debug('TXP Added: ', ntxid); + self.sendIndexes(); - self.sendTxProposal(ntxid); - self.emitAndKeepAlive('txProposalsUpdated'); - return cb(null, ntxid); + // Needs only one signature? Broadcast it! + if (!self.requiresMultipleSignatures()) { + self.broadcastTx(ntxid, cb); + } else { + self.sendTxProposal(ntxid); + self.emitAndKeepAlive('txProposalsUpdated'); + return cb(null, ntxid, Wallet.TX_PROPOSAL_SENT); + } }); }; @@ -2159,7 +2165,7 @@ Wallet.prototype._getBuilder = function(opts) { /* - * createTxProposal + * _createTxProposal * Creates a transaction proposal and run many sanity checks * * @param toAddress @@ -2171,7 +2177,7 @@ Wallet.prototype._getBuilder = function(opts) { * Throws errors on unexpected inputs. */ -Wallet.prototype.createTxProposal = function(toAddress, amountSat, comment, utxos, builderOpts) { +Wallet.prototype._createTxProposal = function(toAddress, amountSat, comment, utxos, builderOpts) { preconditions.checkArgument(toAddress); preconditions.checkArgument(amountSat); preconditions.checkArgument(_.isArray(utxos)); @@ -2234,7 +2240,6 @@ Wallet.prototype.createTxProposal = function(toAddress, amountSat, comment, utxo * @desc Updates all the indexes for the current publicKeyRing. This scans * the blockchain looking for transactions on derived addresses. * - * Triggers a wallet {@link Wallet#store} call * @param {Function} callback - called when all indexes have been updated. Receives an error, if any, as first argument */ Wallet.prototype.updateIndexes = function(callback) { @@ -2380,7 +2385,7 @@ Wallet.prototype.getNetwork = function() { */ Wallet.prototype._checkAddressBook = function(key) { if (this.addressBook[key] && this.addressBook[key].copayerId != -1) { - throw new Error('This address already exists in your Address Book: ' + address); + throw new Error('This address already exists in your Address Book'); } }; From f3f9ca3e33cec9e8bb4cea7c2dbee85d7b8f7721 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 23 Nov 2014 16:31:08 -0300 Subject: [PATCH 09/23] addressbook \& tx working again --- js/controllers/send.js | 2 -- js/models/Async.js | 3 --- js/models/TxProposal.js | 15 ++++++++------- js/models/TxProposals.js | 10 ---------- js/models/Wallet.js | 11 ++++++----- 5 files changed, 14 insertions(+), 27 deletions(-) diff --git a/js/controllers/send.js b/js/controllers/send.js index adc22d0f1..a46fec5d1 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -258,8 +258,6 @@ angular.module('copayApp.controllers').controller('SendController', $scope.openScanner = function() { if (window.cordova) return $scope.scannerIntent(); - -console.log('[send.js.260] OPENN'); //TODO $scope.showScanner = true; // Wait a moment until the canvas shows diff --git a/js/models/Async.js b/js/models/Async.js index 361162283..522cf2ffe 100644 --- a/js/models/Async.js +++ b/js/models/Async.js @@ -208,9 +208,6 @@ Network.prototype._onMessage = function(enc) { return; } - log.info('Network message: ', payload.type); - log.debug('Message payload:', payload); - var self = this; switch (payload.type) { case 'hello': diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index 09a32812c..a65bed269 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -44,7 +44,8 @@ function TxProposal(opts) { var me = {}; me[opts.creator] = now; - this.signedBy = this.seenBy = me; + this.signedBy = me; + this.signedBy = _.clone(me); this.creator = opts.creator; this.createdTs = now; } @@ -84,18 +85,18 @@ TxProposal.prototype._checkPayPro = function() { TxProposal.prototype.isFullySigned = function() { - return txp.builder && txp.builder.isFullySigned(); + return this.builder && this.builder.isFullySigned(); }; TxProposal.prototype.sign = function(keys, signerId) { - var before = txp.countSignatures(); - txp.builder.sign(keys); + var before = this.countSignatures(); + this.builder.sign(keys); - var signaturesAdded = txp.countSignatures() > before; + var signaturesAdded = this.countSignatures() > before; if (signaturesAdded){ - txp.signedBy[signerId] = Date.now(); + this.signedBy[signerId] = Date.now(); } - return signturesAdded; + return signaturesAdded; }; diff --git a/js/models/TxProposals.js b/js/models/TxProposals.js index df359f57a..2fb992acc 100644 --- a/js/models/TxProposals.js +++ b/js/models/TxProposals.js @@ -178,16 +178,6 @@ TxProposals.prototype.getTxProposal = function(ntxid, copayers) { }; -TxProposals.prototype.reject = function(ntxid, copayerId) { - var txp = this.get(ntxid); - txp.setRejected(copayerId); -}; - -TxProposals.prototype.seen = function(ntxid, copayerId) { - var txp = this.get(ntxid); - txp.setSeen(copayerId); -}; - //returns the unspent txid-vout used in PENDING Txs TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { var ret = {}; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index eaf3f84f2..8369e53b8 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -614,11 +614,10 @@ Wallet.prototype._onSeen = function(senderId, data) { * @emits txProposalEvent */ Wallet.prototype._onAddressBook = function(senderId, data) { - if (!data.addressBook || !_.isArray(data.addressBook)) + if (!data.addressBook || !_.isObject(data.addressBook)) return; var self = this, hasChange; - _.each(data.addressBook, function(value, key) { if (!self.addressBook[key] && Address.validate(key)) { @@ -630,6 +629,7 @@ Wallet.prototype._onAddressBook = function(senderId, data) { hasChange = true; } }); +console.log('[Wallet.js.635:hasChange:]',hasChange); //TODO if (hasChange) { this.emitAndKeepAlive('addressBookUpdated'); @@ -1447,7 +1447,8 @@ Wallet.prototype.purgeTxProposals = function(deleteAll) { * @emits txProposalsUpdated */ Wallet.prototype.reject = function(ntxid) { - var txp = this.txProposals.reject(ntxid, this.getMyCopayerId()); + var txp = this.txProposals.get(ntxid); + txp.setRejected(this.getMyCopayerId()); this.sendReject(ntxid); this.emitAndKeepAlive('txProposalsUpdated'); }; @@ -1467,7 +1468,7 @@ Wallet.prototype.sign = function(ntxid) { var keys = this.privateKey.getForPaths(txp.inputChainPaths); var signaturesAdded = txp.sign(keys, this.getMyCopayerId()); - if (!signturesAdded) + if (!signaturesAdded) return false; this.emitAndKeepAlive('txProposalsUpdated'); @@ -1477,7 +1478,7 @@ Wallet.prototype.sign = function(ntxid) { Wallet.prototype.signAndSend = function(ntxid, cb) { if (this.sign(ntxid)) { - var txp = w.txProposals.get(ntxid); + var txp = this.txProposals.get(ntxid); if (txp.isFullySigned()) { return this.broadcastTx(ntxid, cb); } else { From 9b327cd45813b2b385041d5a1dffbb1c0baa7a9d Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 23 Nov 2014 17:41:34 -0300 Subject: [PATCH 10/23] tests again! --- js/models/TxProposal.js | 36 ++++++++++--------- js/models/Wallet.js | 33 +++++++---------- test/TxProposal.js | 2 +- test/Wallet.js | 75 +++++++++++++++++++++------------------ test/mocks/FakeBuilder.js | 7 +++- 5 files changed, 79 insertions(+), 74 deletions(-) diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index a65bed269..7c986cd0a 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -11,6 +11,7 @@ var Key = bitcore.Key; var buffertools = bitcore.buffertools; var preconditions = require('preconditions').instance(); +var TX_MAX_SIZE_KB = 50; var VERSION = 1; var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment', 'paymentProtocolURL']; @@ -44,10 +45,14 @@ function TxProposal(opts) { var me = {}; me[opts.creator] = now; - this.signedBy = me; - this.signedBy = _.clone(me); + this.seenBy = me; + this.signedBy = {}; this.creator = opts.creator; this.createdTs = now; + if (opts.signWith) { + if (!this.sign(opts.signWith, opts.creator)) + throw new Error('Could not sign generated tx'); + } } this._sync(); @@ -99,8 +104,6 @@ TxProposal.prototype.sign = function(keys, signerId) { return signaturesAdded; }; - - TxProposal.prototype._check = function() { if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) { @@ -108,6 +111,11 @@ TxProposal.prototype._check = function() { } var tx = this.builder.build(); + + var txSize = tx.getSize(); + if (txSize / 1024 > TX_MAX_SIZE_KB) + throw new Error('BIG: Invalid TX proposal. Too big: ' + txSize + ' bytes'); + if (!tx.ins.length) throw new Error('Invalid tx proposal: no ins'); @@ -301,17 +309,6 @@ TxProposal._infoFromRedeemScript = function(s) { }; }; -TxProposal.prototype.mergeBuilder = function(incoming) { - var b0 = this.builder; - var b1 = incoming.builder; - - var before = JSON.stringify(b0.toObj()); - b0.merge(b1); - var after = JSON.stringify(b0.toObj()); - return after !== before; -}; - - TxProposal.prototype.getSeen = function(copayerId) { return this.seenBy[copayerId]; }; @@ -421,9 +418,14 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { // merge will not merge any metadata. TxProposal.prototype.merge = function(incoming) { - var hasChanged = this.mergeBuilder(incoming); + // Note that all inputs must have the same number of signatures, so checking + // one (0) is OK. + var before = this._inputSigners[0].length; + this.builder.merge(incoming.builder); this._sync(); - return hasChanged; + + var after = this._inputSigners[0].length; + return after !== before; }; //This should be on bitcore / Transaction diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 8369e53b8..361407b69 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -31,7 +31,6 @@ var Async = require('./Async'); var Insight = module.exports.Insight = require('./Insight'); var copayConfig = require('../../config'); -var TX_MAX_SIZE_KB = 50; var TX_MAX_INS = 70; @@ -527,7 +526,6 @@ Wallet.prototype._onTxProposal = function(senderId, data) { var self = this; var m; -console.log('[Wallet.js.533]a', this.txProposals.txps); //TODO try { m = this.txProposals.merge(data.txProposal, Wallet.builderOpts); var keyMap = this._getKeyMap(m.txp); @@ -538,7 +536,6 @@ console.log('[Wallet.js.533]a', this.txProposals.txps); //TODO this.txProposals.deleteOne(m.ntxid); m = null; } -console.log('[Wallet.js.533]a', this.txProposals.txps); //TODO this._processIncomingTxProposal(m, function(err) { if (err) { @@ -546,6 +543,9 @@ console.log('[Wallet.js.533]a', this.txProposals.txps); //TODO if (m && m.ntxid) self.txProposals.deleteOne(m.ntxid); m = null; + } else { + if (m && m.hasChanged) + self.sendTxProposal(m.ntxid); } self._processProposalEvents(senderId, m); }); @@ -1166,7 +1166,7 @@ Wallet.fromObj = function(o, readOpts) { Wallet.prototype._sendToPeers = function(recipients, obj) { if (!this.isShared()) return; - log.info('Wallet:' + this.getName() + ' ### SENDING ' + obj.type); + log.info('Wallet:' + this.getName() + ' ### Sending ' + obj.type); log.debug('Sending obj', obj); this.network.send(recipients, obj); @@ -2086,7 +2086,7 @@ Wallet.prototype.spend = function(opts, cb) { opts.merchantData = merchantData; opts.toAddress = merchantData.outs[0].address; opts.amountSat = parseInt(merchantData.outs[0].amountSatStr); - return self.createTx(opts, cb); + return self.spend(opts, cb); }); }; preconditions.checkArgument(amountSat, 'no amount'); @@ -2095,7 +2095,7 @@ Wallet.prototype.spend = function(opts, cb) { this.getUnspent(function(err, safeUnspent) { if (err) { log.info(err); - return cb(new Error('CreateTx: Could not get list of UTXOs')); + return cb(new Error('Spend: Could not get list of UTXOs')); } var ntxid, txp; @@ -2212,28 +2212,21 @@ Wallet.prototype._createTxProposal = function(toAddress, amountSat, comment, utx var inputChainPaths = selectedUtxos.map(function(utxo) { return pkr.pathForAddress(utxo.address); }); - b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - var keys = priv.getForPaths(inputChainPaths); - b.sign(keys); - 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'); - - return new TxProposal({ + var myId = this.getMyCopayerId(); + var keys = priv.getForPaths(inputChainPaths); + return new TxProposal({ inputChainPaths: inputChainPaths, comment: comment, builder: b, - creator: this.getMyCopayerId(), + creator: myId, + signWith: keys, }); + + return txp; }; diff --git a/test/TxProposal.js b/test/TxProposal.js index e2a106df3..881677f34 100644 --- a/test/TxProposal.js +++ b/test/TxProposal.js @@ -371,7 +371,7 @@ describe('TxProposal', function() { it('with less signatures', function() { var backup = txp.builder.vanilla.scriptSig[0]; txp.builder.merge = function() { - // 2 signatures. + // Only one signatures. this.vanilla.scriptSig = ['0048304502207d8e832bd576c93300e53ab6cbd68641961bec60690c358fd42d8e42b7d7d687022100a1daa89923efdb4c9b615d065058d9e1644f67000694a7d0806759afa7bef19b014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; this.tx.ins[0].s = new Buffer(this.vanilla.scriptSig[0], 'hex'); }; diff --git a/test/Wallet.js b/test/Wallet.js index 17580551f..d3738e0c5 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -234,7 +234,7 @@ describe('Wallet model', function() { unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true); var f = function() { - var ntxid = w.createTxProposal( + var ntxid = w._createTxProposal( '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', '123456789', null, @@ -250,7 +250,7 @@ describe('Wallet model', 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); - var txp = w.createTxProposal( + var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', null, @@ -269,7 +269,7 @@ describe('Wallet model', function() { unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - var txp = w.createTxProposal( + var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', null, @@ -282,6 +282,7 @@ describe('Wallet model', function() { chai.expect(txp.comment).to.be.null; tx.isComplete().should.equal(false); Object.keys(txp.seenBy).length.should.equal(1); + Object.keys(txp.signedBy).length.should.equal(1); }); it('#create with comment', function() { @@ -292,7 +293,7 @@ describe('Wallet model', function() { unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - var txp = w.createTxProposal( + var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', comment, @@ -313,7 +314,7 @@ describe('Wallet model', function() { unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); (function() { - w.createTxProposal( + w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', comment, @@ -342,7 +343,7 @@ describe('Wallet model', function() { for (var index = 0; index < 3; index++) { unspentTest[0].address = w.publicKeyRing.getAddress(index, isChange, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(index, isChange, w.publicKey); - var txp = w.createTxProposal( + var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', null, @@ -778,7 +779,7 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { @@ -795,7 +796,7 @@ describe('Wallet model', function() { var w = createW2([k2]); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { @@ -812,7 +813,7 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { @@ -827,7 +828,7 @@ describe('Wallet model', function() { var oldK = w.privateKey; var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { @@ -844,12 +845,14 @@ describe('Wallet model', function() { var w = createW2(null, 1); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { - w.sendTx(ntxid, function(txid) { + w.broadcastTx(ntxid, function(err, txid, status) { + should.not.exist(err); txid.length.should.equal(64); + status.should.equal(Wallet.TX_BROADCASTED); done(); }); }); @@ -858,7 +861,7 @@ describe('Wallet model', function() { var w = createW2(null, 1); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { @@ -871,7 +874,7 @@ describe('Wallet model', function() { } }); (function() { - w.sendTx(ntxid); + w.broadcastTx(ntxid); }).should.throw('Tx is not complete. Can not broadcast'); done(); }); @@ -880,7 +883,7 @@ describe('Wallet model', function() { var w = createW2(null, 1); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { @@ -888,7 +891,7 @@ describe('Wallet model', function() { statusCode: 303 }); var spyCheckSentTx = sinon.spy(w, '_checkSentTx'); - w.sendTx(ntxid, function() {}); + w.broadcastTx(ntxid, function() {}); chai.expect(spyCheckSentTx.calledOnce).to.be.true; done(); }); @@ -897,7 +900,7 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { @@ -913,7 +916,7 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { @@ -994,12 +997,12 @@ describe('Wallet model', function() { }); }); - describe('#createTx', function() { + describe('#spend', function() { it('should fail if insight server is down', function(done) { var w = cachedCreateW2(); var utxo = createUTXO(w); sinon.stub(w, 'getUnspent').yields('error', null); - w.createTx({ + w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { @@ -1014,7 +1017,7 @@ describe('Wallet model', function() { var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); sinon.stub(w, 'fetchPaymentRequest').yields('error'); - w.createTx({ + w.spend({ url: 'test', }, function(err, ntxid) { should.exist(err); @@ -1038,7 +1041,7 @@ describe('Wallet model', function() { }, total: '123400', }); - w.createTx({ + w.spend({ url: 'test', }, function(err, ntxid) { should.not.exist(err); @@ -1150,12 +1153,12 @@ describe('Wallet model', function() { - describe('#send', function() { + describe('#_sendToPeers', function() { it('should call this.network.send', function() { var w = cachedCreateW2(); var save = w.network.send; w.network.send = sinon.spy(); - w.send(); + w._sendToPeers(null, {type:'hola'}); w.network.send.calledOnce.should.equal(true); w.network.send = save; }); @@ -1607,7 +1610,7 @@ describe('Wallet model', function() { spy.called.should.be.true; }); - it('should handle new', function(done) { + it('should handle new 1', function(done) { var data = { txProposal: { dummy: 1, @@ -1643,11 +1646,13 @@ describe('Wallet model', function() { }); w._onTxProposal('senderID', data); + spy1.called.should.be.true; spy2.called.should.be.true; txp.setSeen.calledOnce.should.be.true; - w.sendSeen.calledOnce.should.be.true; - w.sendTxProposal.calledOnce.should.be.true; + w.sendSeen.calledOnce.should.equal(true); + w.sendTxProposal.calledOnce.should.equal(true); + }); it('should handle signed', function(done) { @@ -1715,14 +1720,14 @@ describe('Wallet model', function() { ntxid: 1, txp: txp, new: false, - hasChanged: true, + hasChanged: false, }); w._checkSentTx = sinon.stub().yields('123'); w._onTxProposal('senderID', data); - txp.setSent.calledOnce.should.be.true; - txp.setSent.calledWith('123').should.be.true; - w.sendTxProposal.called.should.be.false; + txp.setSent.calledOnce.should.equal(true); + txp.setSent.calledWith('123').should.equal(true); + w.sendTxProposal.called.should.equal(false); done(); }); @@ -1749,14 +1754,14 @@ describe('Wallet model', function() { ntxid: 1, txp: txp, new: false, - hasChanged: true, + hasChanged: false, }); w._checkSentTx = sinon.stub().yields(false); w._onTxProposal('senderID', data); - txp.setSent.called.should.be.false; - txp.setSent.calledWith(1).should.be.false; - w.sendTxProposal.called.should.be.false; + txp.setSent.called.should.equal(false); + txp.setSent.calledWith(1).should.equal(false); + w.sendTxProposal.called.should.equal(false); done(); }); diff --git a/test/mocks/FakeBuilder.js b/test/mocks/FakeBuilder.js index f066ceec2..63d247d06 100644 --- a/test/mocks/FakeBuilder.js +++ b/test/mocks/FakeBuilder.js @@ -10,6 +10,11 @@ function Tx() { }]; }; + +Tx.prototype.getSize = function() { + return 1; +}; + Tx.prototype.getHashType = function() { return 1; }; @@ -38,7 +43,7 @@ function FakeBuilder() { address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', amountSatStr: '123', }]), - + } } From 6462968d5e3b44ff2e0c00e747647f3e99d71d4d Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 23 Nov 2014 19:19:05 -0300 Subject: [PATCH 11/23] add UX to payment acknoledged --- js/models/Identity.js | 3 + js/models/TxProposal.js | 3 +- js/models/Wallet.js | 147 ++++++++++++++++----------------- js/services/controllerUtils.js | 4 + 4 files changed, 80 insertions(+), 77 deletions(-) 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); From abd19e5a969e8046d030d913880aa43a1eade5fe Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 24 Nov 2014 11:46:51 -0300 Subject: [PATCH 12/23] updates from eordano comments. Better jsdocs some more tests. Still WIP --- copay.js | 3 - js/models/Async.js | 2 +- js/models/Wallet.js | 214 +++++++++++++++++++++++------------- test/Wallet.js | 259 +++++++++++++++++++++++++------------------- util/build.js | 3 - 5 files changed, 292 insertions(+), 189 deletions(-) diff --git a/copay.js b/copay.js index bdb5c759b..79a70f8b0 100644 --- a/copay.js +++ b/copay.js @@ -20,6 +20,3 @@ module.exports.Compatibility = require('./js/models/Compatibility'); module.exports.PluginManager = require('./js/models/PluginManager'); module.exports.version = require('./version').version; module.exports.commitHash = require('./version').commitHash; - -// test hack :s, will fix -module.exports.FakePayProServer = require('./test/mocks/FakePayProServer'); diff --git a/js/models/Async.js b/js/models/Async.js index 522cf2ffe..b7b9bca33 100644 --- a/js/models/Async.js +++ b/js/models/Async.js @@ -260,7 +260,7 @@ Network.prototype._setupConnectionHandlers = function(opts, cb) { if (fromTs) { self.ignoreMessageFromTs = fromTs; } - log.info('Async: syncing from: ', fromTs); + log.info('Async: synchronizing from: ', fromTs); self.socket.emit('sync', fromTs); self.started = true; }); diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 09c41b749..99cdb825f 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -36,8 +36,7 @@ var TX_MAX_INS = 70; /** * @desc - * Wallet manages a private key for Copay, network, storage of the wallet for - * persistance, and blockchain information. + * Wallet manages a private key for Copay, network and blockchain information. * * @TODO: Split this leviathan. * @@ -133,13 +132,13 @@ Wallet.prototype.emitAndKeepAlive = function(args) { }; /** - * @TODO: Document this. Its usage is kind of weird + * @desc Fixed & Forced TransactionBuilder options, for genereration transactions. * * @static - * @property lockTime - * @property signhash - * @property fee - * @property feeSat + * @property lockTime null + * @property signhash SIGHASH + * @property fee null (automatic) + * @property feeSat null */ Wallet.builderOpts = { lockTime: null, @@ -468,6 +467,15 @@ Wallet.prototype._checkIfTxProposalIsSent = function(ntxid, cb) { }; +/** + * _processTxProposalPayPro + * + * @desc Process and incoming PayPro TX Proposal. Fetchs the payment request + * from the merchant. + * + * @param mergeInfo Proposals merge information, as returned by TxProposals.merge + * @return {fetchPaymentRequestCallback} + */ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { var self = this; var txp = mergeInfo.txp; @@ -494,6 +502,14 @@ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { }); }; +/** + * _processIncomingTxProposal + * + * @desc Process an incoming transaction proposal. Runs safety and sanity checks on it. + * + * @param mergeInfo Proposals merge information, as returned by TxProposals.merge + * @return {errCallback} + */ Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) { if (!mergeInfo) return cb(); var self = this; @@ -617,7 +633,8 @@ Wallet.prototype._onAddressBook = function(senderId, data) { if (!data.addressBook || !_.isObject(data.addressBook)) return; - var self = this, hasChange; + var self = this, + hasChange; _.each(data.addressBook, function(value, key) { if (!self.addressBook[key] && Address.validate(key)) { @@ -629,7 +646,6 @@ Wallet.prototype._onAddressBook = function(senderId, data) { hasChange = true; } }); -console.log('[Wallet.js.635:hasChange:]',hasChange); //TODO if (hasChange) { this.emitAndKeepAlive('addressBookUpdated'); @@ -675,7 +691,6 @@ Wallet.prototype._onNoMessages = function() { * @emits corrupt */ Wallet.prototype._onData = function(senderId, data, ts) { -console.log('[Wallet.js.533]0', this.txProposals.txps); //TODO preconditions.checkArgument(senderId); preconditions.checkArgument(data); preconditions.checkArgument(data.type); @@ -1159,13 +1174,13 @@ Wallet.fromObj = function(o, readOpts) { /** - * @desc sendToPeers a message to other peers - * @param {string[]} recipients - the pubkey of the recipients of the message - * @param {Object} obj - the data to be sent to them + * @desc sends a message to peers + * @param {string[]} recipients - the pubkey of the recipients of the message. Null for sending to all peers. + * @param {Object} obj - the data to be sent to them. + * @param {String} obj.type - Type of the message to be send */ Wallet.prototype._sendToPeers = function(recipients, obj) { if (!this.isShared()) return; - log.info('Wallet:' + this.getName() + ' ### Sending ' + obj.type); log.debug('Sending obj', obj); @@ -1173,7 +1188,7 @@ Wallet.prototype._sendToPeers = function(recipients, obj) { }; /** - * @desc Send the set of TxProposals to some peers + * @desc Send the set of TxProposals to peers * @param {string[]} recipients - the pubkeys of the recipients */ Wallet.prototype.sendAllTxProposals = function(recipients, sinceTs) { @@ -1243,8 +1258,6 @@ Wallet.prototype.sendWalletReady = function(recipients, sinceTs) { * @param {string[]} [recipients] - the pubkeys of the recipients */ Wallet.prototype.sendWalletId = function(recipients) { - log.debug('Wallet:' + this.id + ' ### SENDING walletId TO:', recipients || 'All', this.id); - this._sendToPeers(recipients, { type: 'walletId', walletId: this.id, @@ -1454,12 +1467,25 @@ Wallet.prototype.reject = function(ntxid) { }; /** - * @desc Sign a proposal + * @callback signCallback + * @param {Error} error if any + * @param {number} Transaction ID or Transaction Proposal ID + * @param {status} Wallet.TX_* Status: + * + * TX_BROADCASTED + * TX_SIGNED + * TX_PROPOSAL_SENT + */ + + +/** + * @desc Signs a proposal * @param {string} ntxid the id of the transaction proposal to sign * @emits txProposalsUpdated * @throws {Error} Could not sign proposal * @throws {Error} Bad payment request * @return {boolean} true if signing actually incremented the number of signatures + * @emits txProposalsUpdated */ Wallet.prototype.sign = function(ntxid) { preconditions.checkState(!_.isUndefined(this.getMyCopayerId())); @@ -1476,6 +1502,17 @@ Wallet.prototype.sign = function(ntxid) { }; +/** + * + * @desc signs and send or broadcast a transaction. + * In m-n wallets, + * if m==1 it will broadcast it to the Bitcoin Network + * if n>1 it will send the proposal to the peers + * + * @param ntxid Transaction Proposal Id + * @param {signCallback} cb + * @throws {Error} Could not sign proposal + */ Wallet.prototype.signAndSend = function(ntxid, cb) { if (this.sign(ntxid)) { var txp = this.txProposals.get(ntxid); @@ -1483,7 +1520,7 @@ Wallet.prototype.signAndSend = function(ntxid, cb) { return this.broadcastTx(ntxid, cb); } else { this.sendTxProposal(ntxid); - return cb(null, ntxid, Wallet.TX_SIGNED ); + return cb(null, ntxid, Wallet.TX_SIGNED); } } else { return cb(new Error('Could not sign the proposal')); @@ -1493,9 +1530,8 @@ Wallet.prototype.signAndSend = function(ntxid, cb) { /** * @desc Broadcasts a transaction to the blockchain * @param {string} ntxid - the transaction proposal id - * @param {broadcastCallback} cb - * @callback broadcastCallback * @param {string} txid - the transaction id on the blockchain + * @param {signCallback} cb */ Wallet.prototype.broadcastTx = function(ntxid, cb) { var self = this; @@ -1505,32 +1541,36 @@ Wallet.prototype.broadcastTx = function(ntxid, cb) { if (!tx.isComplete()) throw new Error('Tx is not complete. Can not broadcast'); - - var serializedTx = tx.serialize(); - log.info('Wallet:' + this.id + ' Broadcasting Transaction ntxid:' + ntxid); - var txHex = serializedTx.toString('hex'); + var txHex = tx.serialize().toString('hex'); log.debug('\tRaw transaction: ', txHex); this.blockchain.broadcast(txHex, function(err, txid) { - if (err) - log.error('Error sending TX:', err); + if (err) { + log.error('Error sending TX:' + err); + return cb(err);; + } if (txid) { log.debug('Wallet:' + self.getName() + ' broadcasted a TX. BITCOIND txid:', txid); 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); + var data = this.createPayProPayment(txp); + self.sendPayProPayment(txp, data, function(err, data) { + if (err) return cb(err); + self.onPayProPaymentAck(ntxid, data); + }); } + self.sendTxProposal(ntxid); + self.emitAndKeepAlive('txProposalsUpdated'); return cb(null, txid, Wallet.TX_BROADCASTED); + } else { log.info('Wallet:' + self.getName() + '. Sent failed. Checking if the TX was sent already'); self._checkIfTxProposalIsSent(ntxid, cb); @@ -1538,11 +1578,18 @@ Wallet.prototype.broadcastTx = function(ntxid, cb) { }); }; + /** - * @desc Create a Payment Protocol transaction + * @callback {fetchPaymentRequestCallback} + * @param {string=} err - an error, if any + * @param {Object} merchantData - object representing the payment request. Add described on BIP70 merchant_data + */ + +/** + * @desc Creates a Payment Protocol 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 + * @return {fetchPaymentRequestCallback} cb */ Wallet.prototype.fetchPaymentRequest = function(options, cb) { preconditions.checkArgument(_.isObject(options)); @@ -1578,24 +1625,18 @@ Wallet.prototype.fetchPaymentRequest = function(options, 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; - }); - * - */ +/** + * _addOutputsToMerchantData + * + * @desc parses merchant_data internal output representation and stores + * the result in merchant_data.outs = [{address: xx, amountSatStr: xx}], + * to be compatible with TransactionBuilder. + *` + * @param merchantData BIP70 merchant_data (from the payment request) + * @throws {Error} PayPro: Unsupported inputs + * @return {undefined} + */ Wallet.prototype._addOutputsToMerchantData = function(merchantData) { var total = bignum(0); @@ -1737,7 +1778,7 @@ Wallet.prototype.parsePaymentRequest = function(options, rawData) { /** * _getPayProRefundOutputs - * Create refund address for PayPro. + * Create refund outputs for a PayPro Payment Message * Uses current transaction's change address. * * @param txp @@ -1750,13 +1791,23 @@ Wallet.prototype._getPayProRefundOutputs = function(txp) { var output = new PayPro.Output(); var script = pkr.getScriptPubKeyHex(index.changeIndex, true, this.pubkey); - output.set('script',new Buffer(script, 'hex')); + output.set('script', new Buffer(script, 'hex')); output.set('amount', amount); return [output]; }; -Wallet.prototype._createPaymentTx = function(txp, txHex) { +/** + * + * @desc Creates a Payment Protocol Payment message for the given TX Proposal + * @param txp Transaction Proposal + * @param txHex + * @return {undefined} + */ +Wallet.prototype.createPayProPayment = function(txp) { + + var tx = txp.builder.build(); + var txBuf = tx.serialize(); var refund_outputs = this._getPayProRefundOutputs(txp); @@ -1770,7 +1821,7 @@ Wallet.prototype._createPaymentTx = function(txp, txHex) { pay.set('merchant_data', merchant_data); } - pay.set('transactions', [txHex]); + pay.set('transactions', [txBuf]); pay.set('refund_to', refund_outputs); // Unused for now @@ -1787,19 +1838,40 @@ Wallet.prototype._createPaymentTx = function(txp, txHex) { return view; }; + /** - * @desc Send a payment transaction to a server, complying with BIP70 + * onPayProPaymentAck * - * @param {string} ntxid - the transaction proposal id - * @param {Function} txHex + * @desc parse and process a Payment Protocol Payment Ack. Updates + * given TX Proposal with merchant's memo and send it to copayers * - * emits paymentACK(server's memo) + * @param ntxid ID of the Transaction Proposal + * @param rawData of the Payment Ack + * @emits paymentACK - (merchants's memo) */ -Wallet.prototype.sendPaymentTx = function(ntxid, txHex) { +Wallet.prototype.onPayProPaymentAck = function(ntxid, rawData) { + var data = PayPro.PaymentACK.decode(rawData); + var paypro = new PayPro(); + 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); +}; + + +/** + * @desc Send a payment transaction to a merchant, complying with BIP70 + * on Acknoledge, updates the TX Proposal with server's memo and send it + * to peers + * + * @param {string} ntxid - the transaction proposal ID for with the + * + */ +Wallet.prototype.sendPayProPayment = function(txp, data, cb) { var self = this; - var txp = this.txProposals.get(ntxid); - var data = this._createPaymentTx(txp, txHex); - + log.debug('Sending Payment Message to merchant server'); var postInfo = { method: 'POST', @@ -1818,19 +1890,13 @@ Wallet.prototype.sendPaymentTx = function(ntxid, txHex) { responseType: 'arraybuffer' }; - return this.httpUtil.request(postInfo) + this.httpUtil.request(postInfo) .success(function(rawData) { - var data = PayPro.PaymentACK.decode(rawData); - var paypro = new PayPro(); - 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); + return cb(null, rawData); }) .error(function(data, status) { log.error('Sending payment notification: XHR status: ' + status); + return cb(new Error(status)); }); }; @@ -1899,7 +1965,7 @@ Wallet.prototype.addressIsOwn = function(addrStr) { /** * Estimate a tx fee in satoshis given its input count - * only for spending all wallet funds + * (only used when spending all wallet funds) */ Wallet.estimatedFee = function(unspentCount) { preconditions.checkArgument(_.isNumber(unspentCount)); @@ -2071,6 +2137,7 @@ Wallet.prototype.spend = function(opts, cb) { var comment = opts.comment; var url = opts.url; + // PayPro? Fetch payment data and recurse if (url && !opts.merchantData) { return self.fetchPaymentRequest({ url: url, @@ -2083,7 +2150,8 @@ Wallet.prototype.spend = function(opts, cb) { opts.amountSat = parseInt(merchantData.outs[0].amountSatStr); return self.spend(opts, cb); }); - }; + } + preconditions.checkArgument(amountSat, 'no amount'); preconditions.checkArgument(toAddress, 'no address'); @@ -2213,7 +2281,7 @@ Wallet.prototype._createTxProposal = function(toAddress, amountSat, comment, utx var tx = b.build(); var myId = this.getMyCopayerId(); var keys = priv.getForPaths(inputChainPaths); - return new TxProposal({ + return new TxProposal({ inputChainPaths: inputChainPaths, comment: comment, builder: b, diff --git a/test/Wallet.js b/test/Wallet.js index d3738e0c5..a1ce3642d 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -767,7 +767,10 @@ describe('Wallet model', function() { 'confirmations': 10, 'confirmationsFromCache': false }]; + sinon.stub(w, 'sendIndexes'); var addr = w.generateAddress().toString(); + w.sendIndexes.restore(); + utxo[0].address = addr; utxo[0].scriptPubKey = (new bitcore.Address(addr)).getScriptPubKey().serialize().toString('hex'); return utxo; @@ -775,124 +778,154 @@ describe('Wallet model', function() { var toAddress = 'mjfAe7YrzFujFf8ub5aUrCaN5GfSABdqjh'; var amountSatStr = '10000'; - it('should create transaction', function(done) { - var w = cachedCreateW2(); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, ntxid) { - ntxid.length.should.equal(64); - done(); - }); - }); - - it('should create & sign transaction from received funds', function(done) { - var k2 = new PrivateKey({ - networkName: walletConfig.networkName - }); - - var w = createW2([k2]); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, ntxid) { - w.on('txProposalsUpdated', function() { - w.getTxProposals()[0].signedByUs.should.equal(true); - w.getTxProposals()[0].rejectedByUs.should.equal(false); + describe('#spend', function() { + it('should create transaction', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + ntxid.length.should.equal(64); done(); }); - w.privateKey = k2; - w.sign(ntxid).should.equal.true; }); - }); - it('should fail to reject a signed transaction', function() { - var w = cachedCreateW2(); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, ntxid) { - (function() { + + it('should create & sign transaction from received funds', function(done) { + var k2 = new PrivateKey({ + networkName: walletConfig.networkName + }); + + var w = createW2([k2]); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + w.on('txProposalsUpdated', function() { + w.getTxProposals()[0].signedByUs.should.equal(true); + w.getTxProposals()[0].rejectedByUs.should.equal(false); + done(); + }); + w.privateKey = k2; + w.sign(ntxid).should.equal.true; + }); + }); + it('should fail to reject a signed transaction', function() { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + (function() { + w.reject(ntxid); + }).should.throw('reject a signed'); + }); + }); + + it('should create & reject transaction', function(done) { + var w = cachedCreateW2(); + var oldK = w.privateKey; + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + var s = sinon.stub(w, 'getMyCopayerId').returns('213'); + Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(0); w.reject(ntxid); - }).should.throw('reject a signed'); + Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(1); + w.txProposals.get(ntxid).rejectedBy['213'].should.gt(1); + s.restore(); + done(); + }); }); - }); + it('should fail to send incomplete transaction', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); - it('should create & reject transaction', function(done) { - var w = cachedCreateW2(); - var oldK = w.privateKey; - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, ntxid) { - var s = sinon.stub(w, 'getMyCopayerId').returns('213'); - Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(0); - w.reject(ntxid); - Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(1); - w.txProposals.get(ntxid).rejectedBy['213'].should.gt(1); - s.restore(); - done(); + // TODO in this test, txp should be created with createTxProposal + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + var txp = w.txProposals.get(ntxid); + // Assign fake builder + txp.builder = new Builder(); + sinon.stub(txp.builder, 'build').returns({ + isComplete: function() { + return false; + } + }); + (function() { + w.broadcastTx(ntxid); + }).should.throw('Tx is not complete. Can not broadcast'); + done(); + }); }); - }); - it('should create & sign & send a transaction', function(done) { - var w = createW2(null, 1); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, ntxid) { - w.broadcastTx(ntxid, function(err, txid, status) { + it('should send a TX proposal to peers if incomplete', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + + sinon.spy(w, 'sendIndexes'); + sinon.spy(w, 'sendTxProposal'); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, id, status) { should.not.exist(err); - txid.length.should.equal(64); + should.exist(id); + status.should.equal(Wallet.TX_PROPOSAL_SENT); + w.sendTxProposal.calledOnce.should.equal(true); + w.sendIndexes.calledOnce.should.equal(true); + done(); + }); + }); + it('should broadcast a TX if complete', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + sinon.spy(w, 'sendIndexes'); + sinon.spy(w, 'sendTxProposal'); + sinon.spy(w, 'broadcastTx'); + sinon.stub(w, 'requiresMultipleSignatures').returns(false); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, id, status) { + should.not.exist(err); + should.exist(id); status.should.equal(Wallet.TX_BROADCASTED); + w.sendTxProposal.calledOnce.should.equal(true); + w.sendIndexes.calledOnce.should.equal(true); + w.broadcastTx.calledOnce.should.equal(true); done(); }); }); }); - it('should fail to send incomplete transaction', function(done) { + + it('should return error if failing to send', function(done) { var w = createW2(null, 1); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); + sinon.stub(w, 'requiresMultipleSignatures').returns(false); + sinon.spy(w, 'sendIndexes'); + sinon.spy(w, 'sendTxProposal'); + sinon.stub(w.blockchain, 'broadcast').yields('error'); w.spend({ toAddress: toAddress, amountSat: amountSatStr, - }, function(err, ntxid) { - var txp = w.txProposals.get(ntxid); - // Assign fake builder - txp.builder = new Builder(); - sinon.stub(txp.builder, 'build').returns({ - isComplete: function() { - return false; - } - }); - (function() { - w.broadcastTx(ntxid); - }).should.throw('Tx is not complete. Can not broadcast'); - done(); - }); - }); - it('should check if transaction already sent when failing to send', function(done) { - var w = createW2(null, 1); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, ntxid) { - sinon.stub(w.blockchain, 'broadcast').yields({ - statusCode: 303 - }); - var spyCheckSentTx = sinon.spy(w, '_checkSentTx'); - w.broadcastTx(ntxid, function() {}); - chai.expect(spyCheckSentTx.calledOnce).to.be.true; + }, function(err, id, status) { + err.should.equal('error'); + w.sendTxProposal.calledOnce.should.equal(false); + w.sendIndexes.calledOnce.should.equal(true); done(); }); }); @@ -940,19 +973,19 @@ describe('Wallet model', function() { url: 'http://xxx', }; - var rawData ='wqer'; + 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); + sinon.stub(w.httpUtil, 'request').returns(s); - w.fetchPaymentRequest(opts, function(err, merchantData){ + w.fetchPaymentRequest(opts, function(err, merchantData) { should.not.exist(err); should.exist(merchantData); - w.parsePaymentRequest.firstCall.args.should.deep.equal([opts,rawData]); + w.parsePaymentRequest.firstCall.args.should.deep.equal([opts, rawData]); done(); }); }); @@ -964,14 +997,14 @@ describe('Wallet model', function() { url: 'http://xxx', }; - var rawData ='wqer'; + 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){ + sinon.stub(w.httpUtil, 'request').returns(s); + w.fetchPaymentRequest(opts, function(err, merchantData) { err.toString().should.contain('status'); done(); }); @@ -983,13 +1016,13 @@ describe('Wallet model', function() { // FakePayProServer.getRequest should be parametrizable describe('#parsePaymentRequest', function() { it('should parse a Payment Request', function() { - var now = Date.now()/1000; + var now = Date.now() / 1000; var w = cachedCreateW2(); var opts = { url: 'http://xxx', }; var data = FakePayProServer.getRequest(); - var md = w.parsePaymentRequest(opts,data); + 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); @@ -1158,7 +1191,9 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var save = w.network.send; w.network.send = sinon.spy(); - w._sendToPeers(null, {type:'hola'}); + w._sendToPeers(null, { + type: 'hola' + }); w.network.send.calledOnce.should.equal(true); w.network.send = save; }); @@ -2389,6 +2424,12 @@ describe('Wallet model', function() { }); }); + describe.skip('#onPayProPaymentAck', function() { + it('should emit', function() { + var w = cachedCreateW2(); + w.onPayProPaymentAck('id', 'data'); + }); + }); describe.skip('#read', function() { var network, blockchain; diff --git a/util/build.js b/util/build.js index 908729562..ace41ebfe 100644 --- a/util/build.js +++ b/util/build.js @@ -131,9 +131,6 @@ var createBundle = function(opts) { b.require('./test/mocks/FakePayProServer', { expose: './mocks/FakePayProServer' }); - b.require('./test/mocks/FakePayProServer', { - expose: '../../mocks/FakePayProServer' - }); b.require('./test/mocks/FakeBuilder', { expose: './mocks/FakeBuilder' }); From 6a540be8fc60cd703ab42fcbffcc9cd471303839 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 09:45:44 -0300 Subject: [PATCH 13/23] updates from eordano comments. Better jsdocs some more tests. Still WIP --- js/models/Wallet.js | 204 ++++++++++------ test/Wallet.js | 495 +++++++++++++++++++++++++------------- test/mocks/FakeBuilder.js | 5 + 3 files changed, 464 insertions(+), 240 deletions(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 99cdb825f..99f9cd80e 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -100,7 +100,7 @@ function Wallet(opts) { this.syncedTimestamp = opts.syncedTimestamp || 0; this.lastMessageFrom = {}; - this.paymentRequests = opts.paymentRequests || {}; + this.paymentRequestsCache = {}; var networkName = Wallet.obtainNetworkName(opts); @@ -353,7 +353,7 @@ Wallet.prototype._processProposalEvents = function(senderId, m) { type: 'new', cId: senderId } - } else if (m.newCopayer.length) { + } else if (m.newCopayer && m.newCopayer.length) { ev = { type: 'signed', cId: m.newCopayer[0] @@ -417,31 +417,36 @@ Wallet.prototype._getKeyMap = function(txp) { /** * @callback transactionCallback - * @param {false|Transaction} returnValue + * @param {error} error + * @param {number} transaction ID (if sent) */ /** * @desc * Asyncchronously check with the blockchain if a given transaction was sent. * - * @param {string} ntxid - the transaction + * @param {string} ntxid - the transaction proposal * @param {transactionCallback} cb */ -Wallet.prototype._checkSentTx = function(ntxid, cb) { +Wallet.prototype._checkIfTxIsSent = function(ntxid, cb) { var txp = this.txProposals.get(ntxid); var tx = txp.builder.build(); - var txHex = tx.serialize().toString('hex'); //Use calcHash NOT getHash which could be cached. var txid = bitcore.util.formatHashFull(tx.calcHash()); this.blockchain.getTransaction(txid, function(err, tx) { - if (err) return cb(false); - return cb(txid); + return cb(err, !err ? txid : null); }); }; -Wallet.prototype._processTxProposalSeen = function(ntxid) { +/** + * + * @desc Set Incomming Transaction Proposal seen status + * and send `seen` messages to peers if aplicable. + * @param ntxid + */ +Wallet.prototype._setTxProposalSeen = function(ntxid) { var txp = this.txProposals.get(ntxid); if (!txp.getSeen(this.getMyCopayerId())) { txp.setSeen(this.getMyCopayerId()); @@ -451,18 +456,27 @@ Wallet.prototype._processTxProposalSeen = function(ntxid) { -Wallet.prototype._checkIfTxProposalIsSent = function(ntxid, cb) { +/** + * @desc updates Tx Proposal Sent status by checking the blockchain + * + * @param ntxid + * @param {transactionCallback} cb + */ +Wallet.prototype._updateTxProposalSent = function(ntxid, cb) { var self = this; var txp = this.txProposals.get(ntxid); - this._checkSentTx(ntxid, function(txid) { + this._checkIfTxIsSent(ntxid, function(err, txid) { + if (err) return cb(err); + if (txid) { if (!txp.getSent()) { txp.setSent(txid); } + self.emitAndKeepAlive('txProposalsUpdated'); } - self.emitAndKeepAlive('txProposalsUpdated'); - if (cb) return cb(null, txid, txid ? Wallet.TX_BROADCASTED : null); + if (cb) + return cb(null, txid, txid ? Wallet.TX_BROADCASTED : null); }); }; @@ -517,11 +531,11 @@ Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) { self._processTxProposalPayPro(mergeInfo, function(err) { if (err) return cb(err); - self._processTxProposalSeen(mergeInfo.ntxid); + self._setTxProposalSeen(mergeInfo.ntxid); var tx = mergeInfo.txp.builder.build(); if (tx.isComplete()) - self._checkIfTxProposalIsSent(mergeInfo.ntxid); + self._updateTxProposalSent(mergeInfo.ntxid); else { self.emitAndKeepAlive('txProposalsUpdated'); } @@ -554,6 +568,7 @@ Wallet.prototype._onTxProposal = function(senderId, data) { } this._processIncomingTxProposal(m, function(err) { + if (err) { log.error('Corrupt TX proposal received from:', senderId, err.toString()); if (m && m.ntxid) @@ -563,6 +578,7 @@ Wallet.prototype._onTxProposal = function(senderId, data) { if (m && m.hasChanged) self.sendTxProposal(m.ntxid); } + self._processProposalEvents(senderId, m); }); }; @@ -1479,7 +1495,7 @@ Wallet.prototype.reject = function(ntxid) { /** - * @desc Signs a proposal + * @desc Signs a transaction proposal * @param {string} ntxid the id of the transaction proposal to sign * @emits txProposalsUpdated * @throws {Error} Could not sign proposal @@ -1527,8 +1543,49 @@ Wallet.prototype.signAndSend = function(ntxid, cb) { } }; + /** - * @desc Broadcasts a transaction to the blockchain + * @desc Broadcast a tx proposal. In case of failure, check if the resulting + * transactions is already on the blockchain. + * + * @param ntxid + * @param cb + * @return {undefined} + */ +Wallet.prototype._doBroadcastTx = function(ntxid, cb) { + var self = this; + var txp = this.txProposals.get(ntxid); + var tx = txp.builder.build(); + + if (!tx.isComplete()) + throw new Error('Tx is not complete. Can not broadcast'); + + var txHex = tx.serialize().toString('hex'); + + log.info('Wallet:' + this.id + ' Broadcasting Transaction ntxid:' + ntxid); + log.debug('\tRaw transaction: ', txHex); + + this.blockchain.broadcast(txHex, function(err, txid) { + if (err || !txid) { + + log.info('Wallet:' + self.getName() + '. Sent failed:' + + err + '. Checking if the TX was sent already'); + + self._checkIfTxIsSent(ntxid, function(err, txid) { + return cb(err, txid); + }); + } else { + log.info('Wallet:' + self.getName() + ' broadcasted a TX. BITCOIND txid:', txid); + return cb(null, txid); + } + }); +}; + +/** + * @desc Broadcasts a transaction to the blockchain, updates tx transactions + * sent status. If the tx proposal is a payment protocol request,it will also + * send the payment message to the server,and process the response. + * * @param {string} ntxid - the transaction proposal id * @param {string} txid - the transaction id on the blockchain * @param {signCallback} cb @@ -1536,49 +1593,30 @@ Wallet.prototype.signAndSend = function(ntxid, cb) { Wallet.prototype.broadcastTx = function(ntxid, cb) { var self = this; - var txp = this.txProposals.get(ntxid); - var tx = txp.builder.build(); - if (!tx.isComplete()) - throw new Error('Tx is not complete. Can not broadcast'); + self._doBroadcastTx(ntxid, function(err, txid) { + if (err) return cb(err); + preconditions.checkState(txid); - log.info('Wallet:' + this.id + ' Broadcasting Transaction ntxid:' + ntxid); + var txp = self.txProposals.get(ntxid); + txp.setSent(txid); - var txHex = tx.serialize().toString('hex'); - log.debug('\tRaw transaction: ', txHex); - this.blockchain.broadcast(txHex, function(err, txid) { - if (err) { - log.error('Error sending TX:' + err); - return cb(err);; + // 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) { + var data = self.createPayProPayment(txp); + self.sendPayProPayment(txp, data, function(err, data) { + if (err) return cb(err); + self.onPayProPaymentAck(ntxid, data); + }); } - if (txid) { - log.debug('Wallet:' + self.getName() + ' broadcasted a TX. BITCOIND txid:', txid); - - txp.setSent(txid); - - // 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) { - var data = this.createPayProPayment(txp); - self.sendPayProPayment(txp, data, function(err, data) { - if (err) return cb(err); - self.onPayProPaymentAck(ntxid, data); - }); - } - - self.sendTxProposal(ntxid); - self.emitAndKeepAlive('txProposalsUpdated'); - return cb(null, txid, Wallet.TX_BROADCASTED); - - } else { - log.info('Wallet:' + self.getName() + '. Sent failed. Checking if the TX was sent already'); - self._checkIfTxProposalIsSent(ntxid, cb); - } + self.sendTxProposal(ntxid); + self.emitAndKeepAlive('txProposalsUpdated'); + return cb(null, txid, Wallet.TX_BROADCASTED); }); }; - /** * @callback {fetchPaymentRequestCallback} * @param {string=} err - an error, if any @@ -1595,34 +1633,38 @@ 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; - this.httpUtil.request({ - method: 'GET', - url: options.url, - headers: { - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE - }, - responseType: 'arraybuffer' + if (self.paymentRequestsCache[options.url]) + return cb(null, self.paymentRequestsCache[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') + + var merchantData, err; + try { + merchantData = self.parsePaymentRequest(options, rawData); + } catch (e) { + err = e + }; + + log.debug('PayPro request data', merchantData); + + self.paymentRequestsCache[options.url] = merchantData; + return cb(err, merchantData); }) - .success(function(rawData) { - log.info('PayPro Request done successfully. Parsing response') - - var merchantData, err; - try { - merchantData = self.parsePaymentRequest(options, rawData); - } catch (e) { - err = e - }; - - log.debug('PayPro request data', merchantData); - return cb(err, merchantData); - }) - .error(function(data, status) { - log.debug('Server did not return PaymentRequest.\nXHR status: ' + status); - return cb(new Error('Status: ' + status)); - }); + .error(function(data, status) { + log.debug('Server did not return PaymentRequest.\nXHR status: ' + status); + return cb(new Error('Status: ' + status)); + }); }; @@ -2123,9 +2165,17 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { }; /** + * spend + * * @desc Spends coins from the wallet * Create a Transaction Proposal and broadcast it or send it * to copayers + * @param {object} opts + * @param {string} opts.toAddress address to send coins + * @param {number} opts.amountSat amount in satoshis + * @param {string} opts.comment optional transaction proposal private comment (for copayers) + * @param {string} opts.url optional (payment protocol URL). If this is given, toAddress will be ignored, and amount could be ignored or not, depending on the payment protocol request. + * @param {signCallback} cb */ Wallet.prototype.spend = function(opts, cb) { preconditions.checkArgument(_.isObject(opts)); diff --git a/test/Wallet.js b/test/Wallet.js index a1ce3642d..2a599daa3 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -1,5 +1,4 @@ 'use strict'; - var Wallet = copay.Wallet; var PrivateKey = copay.PrivateKey; var Network = requireMock('FakeNetwork'); @@ -845,30 +844,6 @@ describe('Wallet model', function() { done(); }); }); - it('should fail to send incomplete transaction', function(done) { - var w = createW2(null, 1); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - - // TODO in this test, txp should be created with createTxProposal - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, ntxid) { - var txp = w.txProposals.get(ntxid); - // Assign fake builder - txp.builder = new Builder(); - sinon.stub(txp.builder, 'build').returns({ - isComplete: function() { - return false; - } - }); - (function() { - w.broadcastTx(ntxid); - }).should.throw('Tx is not complete. Can not broadcast'); - done(); - }); - }); it('should send a TX proposal to peers if incomplete', function(done) { var w = createW2(null, 1); var utxo = createUTXO(w); @@ -909,55 +884,112 @@ describe('Wallet model', function() { done(); }); }); - }); - it('should return error if failing to send', function(done) { - var w = createW2(null, 1); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - sinon.stub(w, 'requiresMultipleSignatures').returns(false); - sinon.spy(w, 'sendIndexes'); - sinon.spy(w, 'sendTxProposal'); - sinon.stub(w.blockchain, 'broadcast').yields('error'); - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, id, status) { - err.should.equal('error'); - w.sendTxProposal.calledOnce.should.equal(false); - w.sendIndexes.calledOnce.should.equal(true); + it('should return error if failing to send', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + sinon.stub(w, 'requiresMultipleSignatures').returns(false); + sinon.spy(w, 'sendIndexes'); + sinon.spy(w, 'sendTxProposal'); + sinon.stub(w, '_doBroadcastTx').yields('error'); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, id, status) { + err.should.equal('error'); + w.sendTxProposal.calledOnce.should.equal(false); + w.sendIndexes.calledOnce.should.equal(true); + done(); + }); + }); + it('should send TxProposal', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + w.sendTxProposal.bind(w).should.throw('Illegal Argument.'); + (function() { + w.sendTxProposal(ntxid); + }).should.not.throw(); + done(); + }); + }); + + it('should send all TxProposal', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + w.sendAllTxProposals.bind(w).should.not.throw(); + (function() { + w.sendAllTxProposals(); + }).should.not.throw(); + done(); + }); + }); + + + }); + describe.only('#broadcastTx', function() { + it('should fail to send incomplete transaction', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + var txp = w._createTxProposal(toAddress, amountSatStr + 0, 'hola', utxo); + var ntxid = w.txProposals.add(txp); + + // Assign fake builder + txp.builder = new Builder(); + sinon.stub(txp.builder, 'build').returns({ + isComplete: function() { + return false; + } + }); + (function() { + w.broadcastTx(ntxid); + }).should.throw('Tx is not complete. Can not broadcast'); done(); }); - }); - it('should send TxProposal', function(done) { - var w = cachedCreateW2(); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, ntxid) { - w.sendTxProposal.bind(w).should.throw('Illegal Argument.'); - (function() { - w.sendTxProposal(ntxid); - }).should.not.throw(); - done(); - }); - }); - it('should send all TxProposal', function(done) { - var w = cachedCreateW2(); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.spend({ - toAddress: toAddress, - amountSat: amountSatStr, - }, function(err, ntxid) { - w.sendAllTxProposals.bind(w).should.not.throw(); - (function() { - w.sendAllTxProposals(); - }).should.not.throw(); - done(); + it('should broadcast a Tx', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + var txp = w._createTxProposal(toAddress, amountSatStr + 0, 'hola', utxo); + var ntxid = w.txProposals.add(txp); + + sinon.stub(w, '_doBroadcastTx').yields(null, 1234); + w.broadcastTx(ntxid, function(err, txid, status) { + should.not.exist(err); + txid.should.equal(1234); + status.should.equal(Wallet.TX_BROADCASTED); + done(); + }); + }); + + it('should call CreatePayPayPayment on a PayPro payment', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + var txp = w._createTxProposal(toAddress, amountSatStr + 0, 'hola', utxo); + txp.paymentProtocolURL = 'url'; + txp.merchant = + var ntxid = w.txProposals.add(txp); + + sinon.stub(w.blockchain, 'broadcast').yields(null, 1234); + sinon.stub(w.httpUtil, 'request').returns(w.httpUtil).yields('data'); + sinon.stub(w.httpUtil, 'success').returns(w.httpUtil).yields(null, 'data'); + sinon.stub(w, 'onPayProPaymentAck').yields('data'); + w.broadcastTx(ntxid, function(err, txid, status) { + should.not.exist(err); + txid.should.equal(1234); + status.should.equal(Wallet.TX_BROADCASTED); + done(); + }); }); }); @@ -1615,43 +1647,82 @@ describe('Wallet model', function() { }); }); - - describe('_onTxProposal', function() { - var w; + var w, data, txp; + + beforeEach(function() { + w = cachedCreateW(); + data = { + txProposal: { + dummy: 1, + }, + }; + sinon.stub(w.txProposals, 'deleteOne'); + }); + + + it('should handle corrupt tx', function(done) { + w.txProposals.merge = sinon.stub().throws(new Error('test error')); + + w.on('txProposalEvent', function(e) { + e.type.should.equal('corrupt'); + w.txProposals.deleteOne.calledOnce.should.equal(false); + done(); + }); + w._onTxProposal('senderID', data); + }); + + it('should call _processIncomingTxProposal', function(done) { + var args = { + xxx: 'yyy', + new: true, + txp: { + setCopayers: sinon.stub(), + }, + }; + sinon.stub(w.txProposals, 'merge').returns(args); + sinon.stub(w, '_processIncomingTxProposal').yields(null); + sinon.stub(w, '_getKeyMap').returns(null); + + w.on('txProposalEvent', function(e) { + e.type.should.equal('new'); + w._processIncomingTxProposal.getCall(0).args[0].should.deep.equal(args); + done(); + }); + w._onTxProposal('senderID', data); + }); + + it('should handle corrupt tx, case2', function(done) { + sinon.stub(w.txProposals, 'merge').returns({ + ntxid: '1' + }); + sinon.stub(w, '_getKeyMap').throws(new Error('test error')); + + w.on('txProposalEvent', function(e) { + e.type.should.equal('corrupt'); + w.txProposals.deleteOne.calledWith('1').should.equal(true); + w._getKeyMap.restore(); + done(); + }); + w._onTxProposal('senderID', data); + }); + }); + + + describe.skip('_onTxProposal', function() { + var w, data, txp; + beforeEach(function() { w = cachedCreateW(); w._getKeyMap = sinon.stub(); w.sendSeen = sinon.spy(); w.sendTxProposal = sinon.spy(); - }); - - it('should handle corrupt tx', function(done) { - var data = { + data = { txProposal: { dummy: 1, }, }; - w.txProposals.merge = sinon.stub().throws(new Error('test error')); - - var spy = sinon.spy(); - w.on('txProposalEvent', spy); - w.on('txProposalEvent', function(e) { - e.type.should.equal('corrupt'); - done(); - }); - - w._onTxProposal('senderID', data); - spy.called.should.be.true; - }); - - it('should handle new 1', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; - var txp = { + txp = { getSeen: sinon.stub().returns(false), setSeen: sinon.spy(), setCopayers: sinon.spy(), @@ -1671,23 +1742,24 @@ describe('Wallet model', function() { hasChanged: true, }); + }); + it('should handle new 1', function(done) { + var spy1 = sinon.spy(); var spy2 = sinon.spy(); w.on('txProposalEvent', spy1); w.on('txProposalsUpdated', spy2); w.on('txProposalEvent', function(e) { e.type.should.equal('new'); + spy1.called.should.be.true; + spy2.called.should.be.true; + txp.setSeen.calledOnce.should.be.true; + w.sendSeen.calledOnce.should.equal(true); + w.sendTxProposal.calledOnce.should.equal(true); done(); }); w._onTxProposal('senderID', data); - - spy1.called.should.be.true; - spy2.called.should.be.true; - txp.setSeen.calledOnce.should.be.true; - w.sendSeen.calledOnce.should.equal(true); - w.sendTxProposal.calledOnce.should.equal(true); - }); it('should handle signed', function(done) { @@ -1721,69 +1793,20 @@ describe('Wallet model', function() { w.on('txProposalsUpdated', spy2); w.on('txProposalEvent', function(e) { e.type.should.equal('signed'); + spy1.called.should.be.true; + spy2.called.should.be.true; + txp.setSeen.calledOnce.should.be.false; + w.sendSeen.calledOnce.should.be.false; + w.sendTxProposal.calledOnce.should.be.true; + done(); }); w._onTxProposal('senderID', data); - spy1.called.should.be.true; - spy2.called.should.be.true; - txp.setSeen.calledOnce.should.be.false; - w.sendSeen.calledOnce.should.be.false; - w.sendTxProposal.calledOnce.should.be.true; }); - it('should mark as broadcast when complete', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; - var txp = { - getSeen: sinon.stub().returns(true), - setCopayers: sinon.stub().returns(['new copayer']), - getSent: sinon.stub().returns(false), - setSent: sinon.spy(), - builder: { - build: sinon.stub().returns({ - isComplete: sinon.stub().returns(true), - }), - }, - }; - - w.txProposals.get = sinon.stub().returns(txp); - w.txProposals.merge = sinon.stub().returns({ - ntxid: 1, - txp: txp, - new: false, - hasChanged: false, - }); - w._checkSentTx = sinon.stub().yields('123'); - - w._onTxProposal('senderID', data); - txp.setSent.calledOnce.should.equal(true); - txp.setSent.calledWith('123').should.equal(true); - w.sendTxProposal.called.should.equal(false); - done(); - }); it('should only mark as broadcast if found in the blockchain', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; - var txp = { - getSeen: sinon.stub().returns(true), - setCopayers: sinon.stub().returns(['new copayer']), - getSent: sinon.stub().returns(false), - setSent: sinon.spy(), - builder: { - build: sinon.stub().returns({ - isComplete: sinon.stub().returns(true), - }), - }, - }; - w.txProposals.get = sinon.stub().returns(txp); w.txProposals.merge = sinon.stub().returns({ ntxid: 1, @@ -1792,12 +1815,14 @@ describe('Wallet model', function() { hasChanged: false, }); w._checkSentTx = sinon.stub().yields(false); + w.on('txProposalEvent', function(e) { + txp.setSent.called.should.equal(false); + txp.setSent.calledWith(1).should.equal(false); + w.sendTxProposal.called.should.equal(false); + done(); + }); w._onTxProposal('senderID', data); - txp.setSent.called.should.equal(false); - txp.setSent.calledWith(1).should.equal(false); - w.sendTxProposal.called.should.equal(false); - done(); }); it('should not overwrite sent info', function(done) { @@ -2531,3 +2556,147 @@ describe('Wallet model', function() { var o = '{"opts":{"id":"dbfe10c3fae71cea", "spendUnconfirmed":1,"requiredCopayers":3,"totalCopayers":5,"version":"0.0.5","networkName":"testnet"},"networkNonce":"0000000000000001","networkNonces":[],"publicKeyRing":{"walletId":"dbfe10c3fae71cea","networkName":"testnet","requiredCopayers":3,"totalCopayers":5,"indexes":[{"copayerIndex":2,"changeIndex":0,"receiveIndex":0}],"copayersExtPubKeys":["tpubD6NzVbkrYhZ4YGK8ZhZ8WVeBXNAAoTYjjpw9twCPiNGrGQYFktP3iVQkKmZNiFnUcAFMJRxJVJF6Nq9MDv2kiRceExJaHFbxUCGUiRhmy97","tpubD6NzVbkrYhZ4YKGDJkzWdQsQV3AcFemaQKiwNhV4RL8FHnBFvinidGdQtP8RKj3h34E65RkdtxjrggZYqsEwJ8RhhN2zz9VrjLnrnwbXYNc","tpubD6NzVbkrYhZ4YkDiewjb32Pp3Sz9WK2jpp37KnL7RCrHAyPpnLfgdfRnTdpn6DTWmPS7niywfgWiT42aJb1J6CjWVNmkgsMCxuw7j9DaGKB","tpubD6NzVbkrYhZ4XEtUAz4UUTWbprewbLTaMhR8NUvSJUEAh4Sidxr6rRPFdqqVRR73btKf13wUjds2i8vVCNo8sbKrAnyoTr3o5Y6QSbboQjk","tpubD6NzVbkrYhZ4Yj9AAt6xUVuGPVd8jXCrEE6V2wp7U3PFh8jYYvVad31b4VUXEYXzSnkco4fktu8r4icBsB2t3pCR3WnhVLedY2hxGcPFLKD"],"nicknameFor":{}},"txProposals":{"txps":[],"walletId":"dbfe10c3fae71cea","networkName":"testnet"},"privateKey":{"extendedPrivateKeyString":"tprv8ZgxMBicQKsPeoHLg3tY75z4xLeEe8MqAXLNcRA6J6UTRvHV8VZTXznt9eoTmSk1fwSrwZtMhY3XkNsceJ14h6sCXHSWinRqMSSbY8tfhHi","networkName":"testnet"},"addressBook":{},"settings":{"unitName":"BTC","unitToSatoshi":100000000,"unitDecimals":8,"alternativeName":"Argentine Peso","alternativeIsoCode":"ARS"}}'; }); + + +var PP.merchant_data = { + request_url: 'url', + pr: { + pd: { + payment_url: 'url' + } + } +}; + + +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==' +}; + +x509.priv = new Buffer(x509.priv, 'base64'); +x509.pub = new Buffer(x509.pub, 'base64'); +x509.der = new Buffer(x509.der, 'base64'); +x509.pem = new Buffer(x509.pem, 'base64'); + +PP.outs = [{ + address: 'mkYn9qmYwMZfovTb6cd7yCGeNozqUyyhK7', + amountSatStr: '3000' +}]; +PP.getRequest = function() { + + 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); + + // 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); + }); + + /** + * 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.'); + pd.set('payment_url', 'https://pay_url'); + 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); + + return pr.serialize(); +}; +PP.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; +}; diff --git a/test/mocks/FakeBuilder.js b/test/mocks/FakeBuilder.js index 63d247d06..8d81531ff 100644 --- a/test/mocks/FakeBuilder.js +++ b/test/mocks/FakeBuilder.js @@ -11,6 +11,11 @@ function Tx() { }; +Tx.prototype.serialize = function() { + return new Buffer('1234','hex'); +}; + + Tx.prototype.getSize = function() { return 1; }; From 3706b00724cfc839928d0726b9d8316bcf852b87 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 10:59:49 -0300 Subject: [PATCH 14/23] better blackbox wallet tests --- test/Wallet.js | 66 ++++++++++------ test/mocks/FakePayProServer.js | 140 --------------------------------- util/build.js | 3 - 3 files changed, 43 insertions(+), 166 deletions(-) delete mode 100644 test/mocks/FakePayProServer.js diff --git a/test/Wallet.js b/test/Wallet.js index 2a599daa3..9592d1ddc 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -4,10 +4,10 @@ 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; +var PayPro = bitcore.PayPro; function assertObjectEqual(a, b) { @@ -937,7 +937,7 @@ describe('Wallet model', function() { }); - describe.only('#broadcastTx', function() { + describe('#broadcastTx', function() { it('should fail to send incomplete transaction', function(done) { var w = createW2(null, 1); var utxo = createUTXO(w); @@ -957,13 +957,13 @@ describe('Wallet model', function() { done(); }); - it('should broadcast a Tx', function(done) { + it('should broadcast a TX', function(done) { var w = createW2(null, 1); var utxo = createUTXO(w); - var txp = w._createTxProposal(toAddress, amountSatStr + 0, 'hola', utxo); + var txp = w._createTxProposal(PP.outs[0].address, PP.outs[0].amountSatStr, 'hola', utxo); var ntxid = w.txProposals.add(txp); + sinon.stub(w.blockchain, 'broadcast').yields(null, 1234); - sinon.stub(w, '_doBroadcastTx').yields(null, 1234); w.broadcastTx(ntxid, function(err, txid, status) { should.not.exist(err); txid.should.equal(1234); @@ -971,23 +971,35 @@ describe('Wallet model', function() { done(); }); }); + - it('should call CreatePayPayPayment on a PayPro payment', function(done) { + it('should send Payment Messages on a PayPro payment', function(done) { var w = createW2(null, 1); var utxo = createUTXO(w); - var txp = w._createTxProposal(toAddress, amountSatStr + 0, 'hola', utxo); - txp.paymentProtocolURL = 'url'; - txp.merchant = + var txp = w._createTxProposal(PP.outs[0].address, PP.outs[0].amountSatStr, 'hola', utxo); + txp.paymentProtocolURL = PP.merchant_data.request_url; + txp.addMerchantData(PP.merchant_data); var ntxid = w.txProposals.add(txp); + var success = sinon.stub().yields('paymentACK123').returns({ + error: sinon.stub(), + }); sinon.stub(w.blockchain, 'broadcast').yields(null, 1234); - sinon.stub(w.httpUtil, 'request').returns(w.httpUtil).yields('data'); - sinon.stub(w.httpUtil, 'success').returns(w.httpUtil).yields(null, 'data'); - sinon.stub(w, 'onPayProPaymentAck').yields('data'); + sinon.stub(w.httpUtil, 'request').returns({ + success: success, + }); + sinon.stub(w, 'onPayProPaymentAck'); + + w.broadcastTx(ntxid, function(err, txid, status) { should.not.exist(err); txid.should.equal(1234); status.should.equal(Wallet.TX_BROADCASTED); + w.httpUtil.request.calledOnce.should.equal(true); + w.httpUtil.request.getCall(0).args[0].url.should.equal('url123'); + success.calledOnce.should.equal(true); + w.onPayProPaymentAck.calledOnce.should.equal(true); + w.onPayProPaymentAck.getCall(0).args[1].should.equal('paymentACK123'); done(); }); }); @@ -1045,7 +1057,7 @@ describe('Wallet model', function() { }); // TODO parsePaymentRequest should have more tests, - // FakePayProServer.getRequest should be parametrizable + // PP.getRequest should be parametrizable describe('#parsePaymentRequest', function() { it('should parse a Payment Request', function() { var now = Date.now() / 1000; @@ -1053,9 +1065,9 @@ describe('Wallet model', function() { var opts = { url: 'http://xxx', }; - var data = FakePayProServer.getRequest(); + var data = PP.getRequest(); var md = w.parsePaymentRequest(opts, data); - md.outs.should.deep.equal(FakePayProServer.outs); + md.outs.should.deep.equal(PP.outs); md.request_url.should.equal(opts.url); md.pr.untrusted.should.equal(true); md.expires.should.be.above(now); @@ -2558,14 +2570,6 @@ describe('Wallet model', function() { }); -var PP.merchant_data = { - request_url: 'url', - pr: { - pd: { - payment_url: 'url' - } - } -}; var x509 = { @@ -2580,10 +2584,26 @@ x509.pub = new Buffer(x509.pub, 'base64'); x509.der = new Buffer(x509.der, 'base64'); x509.pem = new Buffer(x509.pem, 'base64'); +var PP = {}; + PP.outs = [{ address: 'mkYn9qmYwMZfovTb6cd7yCGeNozqUyyhK7', amountSatStr: '3000' }]; + +PP.merchant_data = { + request_url: 'url123', + outs: PP.outs, + total: PP.outs[0].amountSatStr, + pr: { + pd: { + payment_url: 'url123' + } + } +}; + + + PP.getRequest = function() { var uid = 0; diff --git a/test/mocks/FakePayProServer.js b/test/mocks/FakePayProServer.js deleted file mode 100644 index a2ec56942..000000000 --- a/test/mocks/FakePayProServer.js +++ /dev/null @@ -1,140 +0,0 @@ -'use strict'; - -var bitcore = bitcore || require('bitcore'); -var Buffer = bitcore.Buffer; -var PayPro = bitcore.PayPro; - -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==' -}; - -x509.priv = new Buffer(x509.priv, 'base64'); -x509.pub = new Buffer(x509.pub, 'base64'); -x509.der = new Buffer(x509.der, 'base64'); -x509.pem = new Buffer(x509.pem, 'base64'); - -module.exports = { - outs: [{ - address: 'mkYn9qmYwMZfovTb6cd7yCGeNozqUyyhK7', - amountSatStr: '3000' - }], - getRequest: function() { - - 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); - - // 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); - }); - - /** - * 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.'); - pd.set('payment_url', 'https://pay_url'); - 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); - - 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; - }, -}; diff --git a/util/build.js b/util/build.js index ace41ebfe..4b4b1f74d 100644 --- a/util/build.js +++ b/util/build.js @@ -128,9 +128,6 @@ var createBundle = function(opts) { b.require('./test/mocks/FakeNetwork', { expose: './mocks/FakeNetwork' }); - b.require('./test/mocks/FakePayProServer', { - expose: './mocks/FakePayProServer' - }); b.require('./test/mocks/FakeBuilder', { expose: './mocks/FakeBuilder' }); From a759d801b43df9b3d3f9bf9d173de4973f54f051 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 13:21:00 -0300 Subject: [PATCH 15/23] fix karma --- js/models/Wallet.js | 5 +- karma.conf.js | 1 - test/TxProposal.js | 24 ++++--- test/unit/controllers/controllersSpec.js | 89 ++++++++++-------------- 4 files changed, 53 insertions(+), 66 deletions(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 99f9cd80e..b54be9e19 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2168,8 +2168,8 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { * spend * * @desc Spends coins from the wallet - * Create a Transaction Proposal and broadcast it or send it - * to copayers + * Create a Transaction Proposal and send it + * to copayers (broadcast it in a 1-x wallet) * @param {object} opts * @param {string} opts.toAddress address to send coins * @param {number} opts.amountSat amount in satoshis @@ -2231,6 +2231,7 @@ Wallet.prototype.spend = function(opts, cb) { log.debug('TXP Added: ', ntxid); +console.log('[Wallet.js.2233]'); //TODO self.sendIndexes(); // Needs only one signature? Broadcast it! if (!self.requiresMultipleSignatures()) { diff --git a/karma.conf.js b/karma.conf.js index da06fc59c..84647dae2 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -56,7 +56,6 @@ module.exports = function(config) { 'js/init.js', 'test/mocks/FakeBlockchainSocket.js', - 'test/mocks/FakePayProServer.js', 'test/mocha.conf.js', diff --git a/test/TxProposal.js b/test/TxProposal.js index 881677f34..c1b69a5ac 100644 --- a/test/TxProposal.js +++ b/test/TxProposal.js @@ -10,17 +10,23 @@ var util = bitcore.util; var networks = bitcore.networks; var FakeBuilder = requireMock('FakeBuilder'); var TxProposal = copay.TxProposal; - -var dummyProposal = function() { return new TxProposal({ - creator: 'creator', - createdTs: 1, - builder: new FakeBuilder(), - inputChainPaths: ['m/1'], -})}; +var Buffer = bitcore.Buffer; var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"]; describe('TxProposal', function() { + + function dummyProposal() { + return new TxProposal({ + creator: 'creator', + createdTs: 1, + builder: new FakeBuilder(), + inputChainPaths: ['m/1'], + }) + }; + + + describe('new', function() { it('should fail to create an instance with wrong arguments', function() { @@ -351,13 +357,13 @@ describe('TxProposal', function() { txp.addMerchantData(md); }).should.throw('expired'); }); - + it('OK Expired but sent', function() { md.expires = 2; txp.sentTs = 1; txp.addMerchantData(md); }); - + }); describe('#merge', function() { diff --git a/test/unit/controllers/controllersSpec.js b/test/unit/controllers/controllersSpec.js index 3dc682211..1343b5ef1 100644 --- a/test/unit/controllers/controllersSpec.js +++ b/test/unit/controllers/controllersSpec.js @@ -12,12 +12,16 @@ saveAs = function(blob, filename) { }; }; -var startServer = require('../../mocks/FakePayProServer'); - describe("Unit: Controllers", function() { config.plugins.LocalStorage = true; config.plugins.GoogleDrive = null; + var anAddr = 'mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'; + var anAmount = 1000; + var aComment = 'hola'; + + + var invalidForm = { $invalid: true }; @@ -69,24 +73,30 @@ describe("Unit: Controllers", function() { w.getTransactionHistory = sinon.stub().yields(null); w.getNetworkName = sinon.stub().returns('testnet'); - w.createTx = sinon.stub().yields(null); - w.sendTx = sinon.stub().yields(null); + w.spend = sinon.stub().yields(null); + w.sendTxProposal = sinon.stub(); + w.broadcastTx = sinon.stub().yields(null); w.requiresMultipleSignatures = sinon.stub().returns(true); w.getTxProposals = sinon.stub().returns([1, 2, 3]); w.getPendingTxProposals = sinon.stub().returns({ - txs : [{ isPending : true }], + txs: [{ + isPending: true + }], pendingForUs: 1 }); w.getId = sinon.stub().returns(1234); - w.on = sinon.stub().yields({'e': 'errmsg', 'loading': false}); + w.on = sinon.stub().yields({ + 'e': 'errmsg', + 'loading': false + }); w.getBalance = sinon.stub().returns(10000); w.publicKeyRing = sinon.stub().yields(null); w.publicKeyRing.nicknameForCopayer = sinon.stub().returns('nickcopayer'); w.updateFocusedTimestamp = sinon.stub().returns(1415804323); - w.getAddressesInfo = sinon.stub().returns([ - { addressStr: "2MxvwvfshZxw4SkkaJZ8NDKLyepa9HLMKtu", - isChange: false } - ]); + w.getAddressesInfo = sinon.stub().returns([{ + addressStr: "2MxvwvfshZxw4SkkaJZ8NDKLyepa9HLMKtu", + isChange: false + }]); var iden = {}; iden.deleteWallet = sinon.stub().yields(null); @@ -240,31 +250,35 @@ describe("Unit: Controllers", function() { }); it('should create a transaction proposal with given values', function() { - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); - sendForm.amount.$setViewValue(1000); + sendForm.address.$setViewValue(anAddr); + sendForm.amount.$setViewValue(anAmount); + sendForm.comment.$setViewValue(aComment); scope.loadTxs = sinon.spy(); var w = scope.wallet; scope.submitForm(sendForm); - sinon.assert.callCount(w.createTx, 1); - sinon.assert.callCount(w.sendTx, 0); + sinon.assert.callCount(w.spend, 1); + sinon.assert.callCount(w.broadcastTx, 0); sinon.assert.callCount(scope.loadTxs, 1); - w.createTx.getCall(0).args[0].should.equal('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); - w.createTx.getCall(0).args[1].should.equal(1000 * scope.wallet.settings.unitToSatoshi); - (typeof w.createTx.getCall(0).args[2]).should.equal('undefined'); + var spendArgs = w.spend.getCall(0).args[0]; + spendArgs.toAddress.should.equal(anAddr); + spendArgs.amountSat.should.equal(anAmount * scope.wallet.settings.unitToSatoshi); + spendArgs.comment.should.equal(aComment); }); it('should handle big values in 100 BTC', function() { var old = scope.wallet.settings.unitToSatoshi; scope.wallet.settings.unitToSatoshi = 100000000;; - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); + sendForm.address.$setViewValue(anAddr); sendForm.amount.$setViewValue(100); + sendForm.address.$setViewValue(anAddr); + scope.loadTxs = sinon.spy(); scope.submitForm(sendForm); var w = scope.wallet; - w.createTx.getCall(0).args[1].should.equal(100 * scope.wallet.settings.unitToSatoshi); + w.spend.getCall(0).args[0].amountSat.should.equal(100 * scope.wallet.settings.unitToSatoshi); scope.wallet.settings.unitToSatoshi = old; }); @@ -276,11 +290,11 @@ describe("Unit: Controllers", function() { var old = $rootScope.wallet.settings.unitToSatoshi; $rootScope.wallet.settings.unitToSatoshi = 100000000;; - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); + sendForm.address.$setViewValue(anAddr); sendForm.amount.$setViewValue(5000); scope.submitForm(sendForm); - w.createTx.getCall(0).args[1].should.equal(5000 * $rootScope.wallet.settings.unitToSatoshi); + w.spend.getCall(0).args[0].amountSat.should.equal(5000 * $rootScope.wallet.settings.unitToSatoshi); $rootScope.wallet.settings.unitToSatoshi = old; })); @@ -300,39 +314,6 @@ describe("Unit: Controllers", function() { done(); }); }); - - it('should create and send a transaction proposal', function() { - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); - sendForm.amount.$setViewValue(1000); - scope.loadTxs = sinon.spy(); - - var w = scope.wallet; - w.requiresMultipleSignatures = sinon.stub().returns(false); - w.totalCopayers = w.requiredCopayers = 1; - - - scope.submitForm(sendForm); - sinon.assert.callCount(w.createTx, 1); - sinon.assert.callCount(w.sendTx, 1); - sinon.assert.callCount(scope.loadTxs, 1); - }); - - it('should not send txp when there is an error at creation', function() { - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); - sendForm.amount.$setViewValue(1000); - scope.wallet.totalCopayers = scope.wallet.requiredCopayers = 1; - scope.loadTxs = sinon.spy(); - var w = scope.wallet; - w.createTx.yields('error'); - w.isShared = sinon.stub().returns(false); - - - scope.submitForm(sendForm); - - sinon.assert.callCount(w.createTx, 1); - sinon.assert.callCount(w.sendTx, 0); - sinon.assert.callCount(scope.loadTxs, 1); - }); }); describe("Unit: Version Controller", function() { From 5681d8e87d02b37570d7ed2693a3c5d022c8481b Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 13:29:03 -0300 Subject: [PATCH 16/23] refactor test to use scripts --- test/Wallet.js | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/test/Wallet.js b/test/Wallet.js index 9592d1ddc..0d336cabc 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -8,6 +8,7 @@ var TransactionBuilder = bitcore.TransactionBuilder; var Transaction = bitcore.Transaction; var Address = bitcore.Address; var PayPro = bitcore.PayPro; +var Buffer = bitcore.Buffer; function assertObjectEqual(a, b) { @@ -2618,34 +2619,8 @@ PP.getRequest = function() { // 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 - ])); + var addr = new bitcore.Address(PP.outs[0].address); + po.set('script', addr.getScriptPubKey().getBuffer()); outputs.push(po.message); }); From 7a862e3d9b551a1c884962b954f72d251206065a Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 13:50:19 -0300 Subject: [PATCH 17/23] code formatting --- test/Wallet.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Wallet.js b/test/Wallet.js index 0d336cabc..53ab57fb5 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -2574,10 +2574,10 @@ describe('Wallet model', function() { 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: 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBeFRKdUsyYUdMbjFkWEpLRGg0TXdQTFVrbDNISTVwR25HNWFjNGwvMGlobXE4Y3dDCitGVlBnWk1TNTlheWtpc0IrekM3dnR2a0prL2J2K0JTT1g3b3hkSXN1TDNkS1FGcHVYWFZmcmRiOTV3WW40TSsKL25qRWhYTWxoVk1IL09DaUFnOUpLaFRLV0w2R1JXWkFBaEE3bEJSaGdTTkRUaVRDNTFDYmlLN3hBNnBONCt0UQpIeG9tSlBYclpSa2JCMmtsT2ZXd2J2OTNZM0oxS0ZEK2kwUE1RSEx3N3JoRXVteEM5MytISFVWWVZIN0gxVFBaCkgxYmRVSkowMmdRZXlsSnNzWUNKeWRaUHpOVC96dXRzL0tKV2RSdjVseHdHOXU5dE1OTWdoSmJtQWFNa01HaSsKbzdQTkV5UDNxSEZyWXBZaHM1cHFMSE1STkI3OFFNOUllTmpMRndJREFRQUJBb0lCQVFERVJyalBiQUdjbmwxaAorZGIrOTczNGZ0aElBUkpWSko1dTRFK1JKcThSRWhGTEVLUFlKNW0yUC94dVZBMXpYV2xnYXhaRUZ6d1VRaUpZCjdsOEpLVjlwSHhReVlaQ1M4dndYZzhpWGtzdndQaWRvQmN1YW4vd0RWQ1FCZXk2VkxjVXpSYUd1Ui9sTHNYK1YKN2Z0QjBvUnFsSXFrYmNQZE1NdnFUeG93UnVoUG11Q3JWVGpPNHBiTnFuU09OUExPaUovRkFYYjJwZnpGZnBCUgpHeCtFTW16d2UrSEZuSkJHRGhIWjk5bm4vVEJmYUp6TlZDcURZLzNid3o1WDdIUU5ZN1QrSnlUVUZzZVE5NHhzCnpya2lidGRmVGNUanB1K1VoWm80c1p6Q3IrZkhHWm9FOUdEUHF0ZDRnQ3ByazRFS0pzbXFCRVN4QlhTRGhZZ04KOXBVRDM4c1pBb0dCQU9yZkRqdDZaL0ZDamFuVThXek5GaWYrOVQxQTJ4b013RDVWU2xNdVJyWW1HbGZyMEM5TQpmMUVvZ2l2dVRrYnA3cmtnZFRhWVRTYndmTnFaQkt4Y3R5YzdCaGRwWnhERVdKa2Z5cThxVngvem1Cek1JK1ZzCjJLYi9hcHZXcmJlb3NET0NyeUg1YzhKc1VUOXhUWDNYYnhFanlPSlFCU1lHRE1qUHlKNkU5czZMQW9HQkFOYnYKd2d0S2Nra0tLbDJhNXZzaGR2RENnNnFLL1FnT20vNktUSlVKRVNqaHoydFIrZlBWUjcwVEg5UmhoVFJscERXQgpCd3oyU2NCc1RRNDIvTGsxRnkyMFQvck12S3VmSEw1VE1BNGZ6NWRxMUxIbmN6ejZVazVnWEtBT09rUjlVdVhpClR0eTNoREcyQkM4Nk1LTVJ4SjUxRWJxam94d0VSMTAwU2FuTVBmTWxBb0dBSUhLY1pyOHNhUHBHMC9XbFBPREEKZE5vV1MxWVFidkxnQkR5SVBpR2doejJRV2lFcjY3em53ZkNVdXpqNiszVUtFKzFXQkNyYVRjemZrdHVjOTZyLwphcDRPNDJFZWFnU1dNT0ZoZ1AyYWQ4R1JmRGovcEl4N0NlY3pkVUFkVThnc1A1R0lYR3M0QU40eUEwL0Y0dUxHCloxbklRT3ZKS2syZnFvWjZNdHd2dEswQ2dZRUFnSjdGTGVDRTkzUmYyZGdDZFRHWGJZZlpKc3M1bEFLNkV0NUwKNmJ1ZFN5dWw1Z0VPWkgyekNsQlJjZFJSMUFNbSt1V1ZoSW8xcERLckFlQ2g1MnIvemRmakxLQXNIejkrQWQ3aQpHUEdzVmw0Vm5jaDFTMzQ0bHJKUGUzQklLZ2djL1hncDNTYnNzcHJMY2orT0wyZElrOUpXbzZ1Y3hmMUJmMkwwCjJlbGhBUWtDZ1lCWHN5elZWL1pKcVhOcFdDZzU1TDNVRm9UTHlLU3FsVktNM1dpRzVCS240QWF6VkNITCtHUVUKeHd4U2dSOWZRNEludStyUHJOM0lteWswbEtQR0Y5U3pDUlJUaUpGUjcyc05xbE82bDBWOENXUkFQVFBKY2dxVgoxVThOSEs4YjNaaUlvR0orbXNOenBkeHJqNjJIM0E2K1krQXNOWTRTbVVUWEg5eWpnK251a2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=', + pub: 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF4VEp1SzJhR0xuMWRYSktEaDRNdwpQTFVrbDNISTVwR25HNWFjNGwvMGlobXE4Y3dDK0ZWUGdaTVM1OWF5a2lzQit6Qzd2dHZrSmsvYnYrQlNPWDdvCnhkSXN1TDNkS1FGcHVYWFZmcmRiOTV3WW40TSsvbmpFaFhNbGhWTUgvT0NpQWc5SktoVEtXTDZHUldaQUFoQTcKbEJSaGdTTkRUaVRDNTFDYmlLN3hBNnBONCt0UUh4b21KUFhyWlJrYkIya2xPZld3YnY5M1kzSjFLRkQraTBQTQpRSEx3N3JoRXVteEM5MytISFVWWVZIN0gxVFBaSDFiZFVKSjAyZ1FleWxKc3NZQ0p5ZFpQek5UL3p1dHMvS0pXCmRSdjVseHdHOXU5dE1OTWdoSmJtQWFNa01HaStvN1BORXlQM3FIRnJZcFloczVwcUxITVJOQjc4UU05SWVOakwKRndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==', + der: 'MIIDBjCCAe4CCQDI2qWdA3/VpDANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE0MDcxNjAxMzM1MVoXDTE1MDcxNjAxMzM1MVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMUybitmhi59XVySg4eDMDy1JJdxyOaRpxuWnOJf9IoZqvHMAvhVT4GTEufWspIrAfswu77b5CZP27/gUjl+6MXSLLi93SkBabl11X63W/ecGJ+DPv54xIVzJYVTB/zgogIPSSoUyli+hkVmQAIQO5QUYYEjQ04kwudQm4iu8QOqTePrUB8aJiT162UZGwdpJTn1sG7/d2NydShQ/otDzEBy8O64RLpsQvd/hx1FWFR+x9Uz2R9W3VCSdNoEHspSbLGAicnWT8zU/87rbPyiVnUb+ZccBvbvbTDTIISW5gGjJDBovqOzzRMj96hxa2KWIbOaaixzETQe/EDPSHjYyxcCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAL6AMMfC3TlRcmsIgHxjVD4XYtISlldnrn2X9zvFbJKCpNy8XQQosQxrhyfzPHQKjlS2L/KCGMnjx9QkYD2Hlp1MJ1uVv9888th/gcZOv3Or3hQyi5K1Sh5xCG+69lUOqUEGu9B4irsqoFomQVbQolSy+t4apdJi7kuEDwFDk4gZiVEfsuX+naN5a6pCnWnhX1Vf4fKwfkLobKKXm2zQVsjxlwBAqOEmJGDLoRMXH56qJnEZ/dqsczaJOHQSi9mFEHL0r5rsEDTT5AVxdnBfNnyGaCH7/zANEko+FGBj1JdJaJgFTXdbxDoyoPTPD+LJqSK5XYToo46y/T0u9CLveNA==', + pem: 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCakNDQWU0Q0NRREkycVdkQTMvVnBEQU5CZ2txaGtpRzl3MEJBUVVGQURCRk1Rc3dDUVlEVlFRR0V3SkIKVlRFVE1CRUdBMVVFQ0F3S1UyOXRaUzFUZEdGMFpURWhNQjhHQTFVRUNnd1lTVzUwWlhKdVpYUWdWMmxrWjJsMApjeUJRZEhrZ1RIUmtNQjRYRFRFME1EY3hOakF4TXpNMU1Wb1hEVEUxTURjeE5qQXhNek0xTVZvd1JURUxNQWtHCkExVUVCaE1DUVZVeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjAKSUZkcFpHZHBkSE1nVUhSNUlFeDBaRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQgpBTVV5Yml0bWhpNTlYVnlTZzRlRE1EeTFKSmR4eU9hUnB4dVduT0pmOUlvWnF2SE1BdmhWVDRHVEV1ZldzcElyCkFmc3d1NzdiNUNaUDI3L2dVamwrNk1YU0xMaTkzU2tCYWJsMTFYNjNXL2VjR0orRFB2NTR4SVZ6SllWVEIvemcKb2dJUFNTb1V5bGkraGtWbVFBSVFPNVFVWVlFalEwNGt3dWRRbTRpdThRT3FUZVByVUI4YUppVDE2MlVaR3dkcApKVG4xc0c3L2QyTnlkU2hRL290RHpFQnk4TzY0Ukxwc1F2ZC9oeDFGV0ZSK3g5VXoyUjlXM1ZDU2ROb0VIc3BTCmJMR0FpY25XVDh6VS84N3JiUHlpVm5VYitaY2NCdmJ2YlREVElJU1c1Z0dqSkRCb3ZxT3p6Uk1qOTZoeGEyS1cKSWJPYWFpeHpFVFFlL0VEUFNIall5eGNDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFMNkFNTWZDMwpUbFJjbXNJZ0h4alZENFhZdElTbGxkbnJuMlg5enZGYkpLQ3BOeThYUVFvc1F4cmh5ZnpQSFFLamxTMkwvS0NHCk1uang5UWtZRDJIbHAxTUoxdVZ2OTg4OHRoL2djWk92M09yM2hReWk1SzFTaDV4Q0crNjlsVU9xVUVHdTlCNGkKcnNxb0ZvbVFWYlFvbFN5K3Q0YXBkSmk3a3VFRHdGRGs0Z1ppVkVmc3VYK25hTjVhNnBDblduaFgxVmY0Zkt3ZgprTG9iS0tYbTJ6UVZzanhsd0JBcU9FbUpHRExvUk1YSDU2cUpuRVovZHFzY3phSk9IUVNpOW1GRUhMMHI1cnNFCkRUVDVBVnhkbkJmTm55R2FDSDcvekFORWtvK0ZHQmoxSmRKYUpnRlRYZGJ4RG95b1BUUEQrTEpxU0s1WFlUb28KNDZ5L1QwdTlDTHZlTkE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==' }; x509.priv = new Buffer(x509.priv, 'base64'); From 10938b43d81939859ff03e66c3abafe8359b6e1a Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 13:58:10 -0300 Subject: [PATCH 18/23] fixes on jsdoc --- js/models/TxProposal.js | 3 +++ js/models/TxProposals.js | 1 - js/models/Wallet.js | 7 +++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index bccb7f618..32afe115b 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -419,6 +419,9 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { // merge will not merge any metadata. TxProposal.prototype.merge = function(incoming) { + preconditions.checkArgument(_.isFunction(incoming._sync)); + incoming._sync(); + // Note that all inputs must have the same number of signatures, so checking // one (0) is OK. var before = this._inputSigners[0].length; diff --git a/js/models/TxProposals.js b/js/models/TxProposals.js index 2fb992acc..a1b890f7d 100644 --- a/js/models/TxProposals.js +++ b/js/models/TxProposals.js @@ -98,7 +98,6 @@ TxProposals.prototype.toObj = function() { TxProposals.prototype.merge = function(inObj, builderOpts) { var incomingTx = TxProposal.fromUntrustedObj(inObj, builderOpts); - incomingTx._sync(); var myTxps = this.txps; var ntxid = incomingTx.getId(); diff --git a/js/models/Wallet.js b/js/models/Wallet.js index b54be9e19..046123e1a 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2081,15 +2081,14 @@ Wallet.prototype.maxRejectCount = function() { /** * @callback getUnspentCallback - * @TODO: Document this better + * @desc Get a list of unspent transaction outputs * @param {string} error * @param {Object[]} safeUnspendList * @param {Object[]} unspentList - */ -/* @ TODO add cached? - * @desc Get a list of unspent transaction outputs * @param {getUnspentCallback} cb */ + +// TODO: Can we add cache to getUnspent? Wallet.prototype.getUnspent = function(cb) { var self = this; this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) { From 4e54fe6cf7c3cac21c8dbe8a553401804b75498e Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 15:09:51 -0300 Subject: [PATCH 19/23] fix pending notification --- js/controllers/history.js | 2 ++ js/models/Wallet.js | 8 +++-- js/services/controllerUtils.js | 59 +++++++++++++--------------------- test/Wallet.js | 5 +++ views/history.html | 4 ++- 5 files changed, 39 insertions(+), 39 deletions(-) diff --git a/js/controllers/history.js b/js/controllers/history.js index 770bae7c5..3f47fb95c 100644 --- a/js/controllers/history.js +++ b/js/controllers/history.js @@ -144,6 +144,8 @@ angular.module('copayApp.controllers').controller('HistoryController', _.each(items, function(r) { r.ts = r.minedTs || r.sentTs; }); + + $scope.blockchain_txs = w.cached_txs = items; $scope.nbPages = res.nbPages; $scope.totalItems = res.nbItems; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 046123e1a..fa14dad38 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -1897,9 +1897,11 @@ Wallet.prototype.onPayProPaymentAck = function(ntxid, rawData) { var ack = paypro.makePaymentACK(data); var memo = ack.get('memo'); log.debug('Payment Acknowledged!: %s', memo); + + var txp = this.txProposals.get(ntxid); txp.paymentAckMemo = memo; - self.sendTxProposal(ntxid); - self.emitAndKeepAlive('paymentACK', memo); + this.sendTxProposal(ntxid); + this.emitAndKeepAlive('paymentACK', memo); }; @@ -2571,6 +2573,8 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { var proposals = self.getTxProposals(); var satToUnit = 1 / self.settings.unitToSatoshi; + + function extractInsOuts(tx) { // Inputs var inputs = _.map(tx.vin, function(item) { diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index dd1d07959..34f9937be 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -46,7 +46,7 @@ angular.module('copayApp.services') }; root.onError = function(scope) { - if (scope) { + if (scope) { scope.loading = false; } } @@ -64,12 +64,6 @@ angular.module('copayApp.services') }; - root.updateTxsAndBalance = function(w) { - root.updateTxs(w); - root.updateBalance(w, function() { - $rootScope.$digest(); - }); - }; root.installWalletHandlers = function($scope, w) { @@ -129,11 +123,13 @@ angular.module('copayApp.services') } }); w.on('newAddresses', function() { - root.updateTxsAndBalance(w); + root.updateBalance(w); }); w.on('txProposalsUpdated', function() { - root.updateTxsAndBalance(w); + if (root.isFocusedWallet(wid)) { + root.updateTxs(); + } }); w.on('paymentACK', function(memo) { @@ -142,26 +138,29 @@ angular.module('copayApp.services') w.on('txProposalEvent', function(e) { - root.updateTxsAndBalance(w); + if (root.isFocusedWallet(wid)) { + root.updateTxs(); + } + // TODO: add wallet name notification var user = w.publicKeyRing.nicknameForCopayer(e.cId); var name = w.getName(); switch (e.type) { case 'new': - notification.info('['+ name +'] New Transaction', + notification.info('[' + name + '] New Transaction', $filter('translate')('You received a transaction proposal from') + ' ' + user); break; case 'signed': - notification.info('['+ name +'] Transaction Signed', + notification.info('[' + name + '] Transaction Signed', $filter('translate')('A transaction was signed by') + ' ' + user); break; case 'rejected': - notification.info('['+ name +'] Transaction Rejected', + notification.info('[' + name + '] Transaction Rejected', $filter('translate')('A transaction was rejected by') + ' ' + user); break; case 'corrupt': - notification.error('['+ name +'] Transaction Error', - $filter('translate')('Received corrupt transaction from') + ' ' + user); + notification.error('[' + name + '] Transaction Error', + $filter('translate')('Received corrupt transaction from') + ' ' + user); break; } $rootScope.$digest(); @@ -185,20 +184,12 @@ angular.module('copayApp.services') notification.enableHtml5Mode(); // for chrome: if support, enable it uriHandler.register(); $rootScope.unitName = config.unitName; - $rootScope.txAlertCount = 0; + $rootScope.pendingTxCount = 0; $rootScope.initialConnection = true; $rootScope.reconnecting = false; $rootScope.isCollapsed = true; $rootScope.iden = iden; - - // TODO - // $rootScope.$watch('txAlertCount', function(txAlertCount) { - // if (txAlertCount && txAlertCount > 0) { - // - // notification.info('New Transaction', ($rootScope.txAlertCount == 1) ? 'You have a pending transaction proposal' : $filter('translate')('You have') + ' ' + $rootScope.txAlertCount + ' ' + $filter('translate')('pending transaction proposals'), txAlertCount); - // } - // }); }; @@ -279,7 +270,7 @@ angular.module('copayApp.services') r.lockedBalanceBTC = (balanceSat - safeBalanceSat) / COIN; - if (r.safeUnspentCount){ + if (r.safeUnspentCount) { var estimatedFee = copay.Wallet.estimatedFee(r.safeUnspentCount); r.topAmount = (((r.availableBalance * w.settings.unitToSatoshi).toFixed(0) - estimatedFee) / w.settings.unitToSatoshi); } @@ -323,8 +314,6 @@ angular.module('copayApp.services') w.balanceInfo = {}; var scope = root.isFocusedWallet(w.id) && !refreshAll ? $rootScope : w.balanceInfo; - root.updateAddressList(); - var wid = w.getId(); if (_balanceCache[wid]) { @@ -349,7 +338,7 @@ angular.module('copayApp.services') }); }; - root.computeAlternativeAmount = function(w, tx, cb) { + root.setAlternativeAmount = function(w, tx, cb) { rateService.whenAvailable(function() { _.each(tx.outs, function(out) { var valueSat = out.value * w.settings.unitToSatoshi; @@ -360,12 +349,13 @@ angular.module('copayApp.services') }); }; - root.updateTxs = function(w) { - w = w || $rootScope.wallet; - if (!w) return root.onErrorDigest(); + root.updateTxs = function() { + var w = $rootScope.wallet; + if (!w) return; + var res = w.getPendingTxProposals(); _.each(res.txs, function(tx) { - root.computeAlternativeAmount(w, tx); + root.setAlternativeAmount(w, tx); if (tx.merchant) { var url = tx.merchant.request_url; var domain = /^(?:https?)?:\/\/([^\/:]+).*$/.exec(url)[1]; @@ -373,14 +363,11 @@ angular.module('copayApp.services') } }); $rootScope.txps = res.txs; - if ($rootScope.pendingTxCount < res.pendingForUs) { - $rootScope.txAlertCount = res.pendingForUs; - } $rootScope.pendingTxCount = res.pendingForUs; }; root.deleteWallet = function($scope, w, cb) { - if (!w) return root.onErrorDigest(); + if (!w) return root.onErrorDigest(); var name = w.getName(); $rootScope.iden.deleteWallet(w.id, function() { notification.info(name + ' deleted', $filter('translate')('This wallet was deleted')); diff --git a/test/Wallet.js b/test/Wallet.js index 53ab57fb5..215989817 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -2462,10 +2462,15 @@ describe('Wallet model', function() { }); }); + // TODO describe.skip('#onPayProPaymentAck', function() { it('should emit', function() { var w = cachedCreateW2(); + sinon.stub(w,'emitAndKeepAlive'); w.onPayProPaymentAck('id', 'data'); + + w.calledOnce.should.equal(true); + w.getCall(0).args.should.deep.equal(['paymentACK', 'data']); }); }); diff --git a/views/history.html b/views/history.html index 9336d8639..12f548780 100644 --- a/views/history.html +++ b/views/history.html @@ -71,9 +71,11 @@

- {{btx.merchant.pr.pd.memo}} + {{btx.merchant.pr.pd.memo}}

+ {{btx.paymentAckMemo}} [{{btx.merchant.domain}}] +

From 0e6044ae6f3da3a0c4d46226b9754cf1ec5be9b3 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 15:12:31 -0300 Subject: [PATCH 20/23] add paymentAckMemo to history --- js/models/Wallet.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index fa14dad38..a3753431a 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2669,12 +2669,14 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { }); if (proposal) { + // TODO refactor tx.comment = proposal.comment; tx.sentTs = proposal.sentTs; tx.merchant = proposal.merchant; tx.peerActions = proposal.peerActions; tx.finallyRejected = proposal.finallyRejected; tx.merchant = proposal.merchant; + tx.paymentAckMemo = proposal.paymentAckMemo; tx.peerActions = proposal.peerActions; tx.finallyRejected = proposal.finallyRejected; From 32396c1ad44200a036f859159477df18e5a015de Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 15:20:44 -0300 Subject: [PATCH 21/23] index proposals in history for performace --- js/models/Wallet.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index a3753431a..60a8b12c9 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2573,6 +2573,7 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { var proposals = self.getTxProposals(); var satToUnit = 1 / self.settings.unitToSatoshi; + var indexedProposals = _.indexBy(proposal,'sentTxid'); function extractInsOuts(tx) { @@ -2664,10 +2665,7 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { tx.amount = tx.amountSat * satToUnit; tx.minedTs = !_.isNaN(tx.time) ? tx.time * 1000 : undefined; - var proposal = _.findWhere(proposals, { - sentTxid: tx.txid - }); - + var proposal = indexedProposals[tx.txid]; if (proposal) { // TODO refactor tx.comment = proposal.comment; From fb891d3f89a1160d3cf850315f70c91a5cb35d65 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 16:41:56 -0300 Subject: [PATCH 22/23] on reject / seen checks --- js/models/Identity.js | 3 +- js/models/Wallet.js | 160 ++++++++++++++++++++++++------------------ test/Wallet.js | 20 ++---- 3 files changed, 98 insertions(+), 85 deletions(-) diff --git a/js/models/Identity.js b/js/models/Identity.js index 54a8913b7..fbb5c8783 100644 --- a/js/models/Identity.js +++ b/js/models/Identity.js @@ -197,7 +197,8 @@ Identity.prototype.storeWallet = function(wallet, cb) { this.storage.setItem(key, val, function(err) { if (err) { - log.debug('Wallet:' + wallet.getName() + ' couldnt be stored:', err); + log.error('Wallet:' + wallet.getName() + ' couldnt be stored:', err); + log.error('Wallet:' + wallet.getName() + ' Size:', JSON.stringify(wallet.sizes())); } if (cb) return cb(err); diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 60a8b12c9..19c5be1ec 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -1,6 +1,5 @@ 'use strict'; -var EventEmitter = require('events').EventEmitter; var _ = require('lodash'); var preconditions = require('preconditions').singleton(); var inherits = require('inherits'); @@ -358,12 +357,9 @@ Wallet.prototype._processProposalEvents = function(senderId, m) { type: 'signed', cId: m.newCopayer[0] }; + } else { + log.error('unknown tx proposal event:', m) } - } else { - ev = { - type: 'corrupt', - cId: senderId - }; } if (ev) this.emitAndKeepAlive('txProposalEvent', ev); @@ -557,30 +553,33 @@ Wallet.prototype._onTxProposal = function(senderId, data) { var m; try { - m = this.txProposals.merge(data.txProposal, Wallet.builderOpts); - var keyMap = this._getKeyMap(m.txp); + m = self.txProposals.merge(data.txProposal, Wallet.builderOpts); + var keyMap = self._getKeyMap(m.txp); m.newCopayer = m.txp.setCopayers(senderId, keyMap); } catch (e) { log.error('Corrupt TX proposal received from:', senderId, e.toString()); if (m && m.ntxid) - this.txProposals.deleteOne(m.ntxid); + self.txProposals.deleteOne(m.ntxid); m = null; } - this._processIncomingTxProposal(m, function(err) { + if (m) { - if (err) { - log.error('Corrupt TX proposal received from:', senderId, err.toString()); - if (m && m.ntxid) - self.txProposals.deleteOne(m.ntxid); - m = null; - } else { - if (m && m.hasChanged) - self.sendTxProposal(m.ntxid); - } + self._processIncomingTxProposal(m, function(err) { - self._processProposalEvents(senderId, m); - }); + if (err) { + log.error('Corrupt TX proposal received from:', senderId, err.toString()); + if (m && m.ntxid) + self.txProposals.deleteOne(m.ntxid); + m = null; + } else { + if (m && m.hasChanged) + self.sendTxProposal(m.ntxid); + } + + self._processProposalEvents(senderId, m); + }); + } }; /** @@ -597,20 +596,21 @@ Wallet.prototype._onReject = function(senderId, data) { preconditions.checkState(data.ntxid); log.debug('Wallet:' + this.id + ' RECV REJECT:', data); - var txp = this.txProposals.get(data.ntxid); + try { + var txp = this.txProposals.get(data.ntxid); + } catch (e) {}; - if (!txp) - throw new Error('Received Reject for an unknown TX from:' + senderId); + if (txp) { + if (txp.signedBy[senderId]) + throw new Error('Received Reject for an already signed TX from:' + senderId); - if (txp.signedBy[senderId]) - throw new Error('Received Reject for an already signed TX from:' + senderId); - - txp.setRejected(senderId); - this.emitAndKeepAlive('txProposalEvent', { - type: 'rejected', - cId: senderId, - txId: data.ntxid - }); + txp.setRejected(senderId); + this.emitAndKeepAlive('txProposalEvent', { + type: 'rejected', + cId: senderId, + txId: data.ntxid + }); + } }; /** @@ -625,14 +625,17 @@ Wallet.prototype._onReject = function(senderId, data) { */ Wallet.prototype._onSeen = function(senderId, data) { preconditions.checkState(data.ntxid); - var txp = this.txProposals.get(data.ntxid); - txp.setSeen(senderId); - this.emitAndKeepAlive('txProposalEvent', { - type: 'seen', - cId: senderId, - txId: data.ntxid - }); - + try { + var txp = this.txProposals.get(data.ntxid); + } catch (e) {}; + if (txp) { + txp.setSeen(senderId); + this.emitAndKeepAlive('txProposalEvent', { + type: 'seen', + cId: senderId, + txId: data.ntxid + }); + } }; /** @@ -1083,6 +1086,23 @@ Wallet.prototype.toObj = function() { return walletObj; }; +/** + * @desc: returns the sizes, by component, of a wallet, in bytes. + * + * @return {object} sizes by component name and 'total' for the total wallet size. + */ +Wallet.prototype.sizes = function() { + var obj = this.toObj(); + var sizes = {}, + total = 0; + _.each(obj, function(val, key) { + var s = JSON.stringify(val).length; + sizes[key] = s; + total += s; + }); + sizes.total = total; + return sizes; +}; Wallet.fromUntrustedObj = function(obj, readOpts) { obj = _.clone(obj); @@ -1638,33 +1658,33 @@ Wallet.prototype.fetchPaymentRequest = function(options, cb) { if (self.paymentRequestsCache[options.url]) return cb(null, self.paymentRequestsCache[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') - - var merchantData, err; - try { - merchantData = self.parsePaymentRequest(options, rawData); - } catch (e) { - err = e - }; - - log.debug('PayPro request data', merchantData); - - self.paymentRequestsCache[options.url] = merchantData; - return cb(err, merchantData); + this.httpUtil.request({ + method: 'GET', + url: options.url, + headers: { + 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + }, + responseType: 'arraybuffer' }) - .error(function(data, status) { - log.debug('Server did not return PaymentRequest.\nXHR status: ' + status); - return cb(new Error('Status: ' + status)); - }); + .success(function(rawData) { + log.info('PayPro Request done successfully. Parsing response') + + var merchantData, err; + try { + merchantData = self.parsePaymentRequest(options, rawData); + } catch (e) { + err = e + }; + + log.debug('PayPro request data', merchantData); + + self.paymentRequestsCache[options.url] = merchantData; + return cb(err, merchantData); + }) + .error(function(data, status) { + log.debug('Server did not return PaymentRequest.\nXHR status: ' + status); + return cb(new Error('Status: ' + status)); + }); }; @@ -2232,7 +2252,7 @@ Wallet.prototype.spend = function(opts, cb) { log.debug('TXP Added: ', ntxid); -console.log('[Wallet.js.2233]'); //TODO + console.log('[Wallet.js.2233]'); //TODO self.sendIndexes(); // Needs only one signature? Broadcast it! if (!self.requiresMultipleSignatures()) { @@ -2573,8 +2593,8 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { var proposals = self.getTxProposals(); var satToUnit = 1 / self.settings.unitToSatoshi; - var indexedProposals = _.indexBy(proposal,'sentTxid'); - + var indexedProposals = _.indexBy(proposals, 'sentTxid'); + function extractInsOuts(tx) { // Inputs diff --git a/test/Wallet.js b/test/Wallet.js index 215989817..f534ccfc0 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -1674,15 +1674,12 @@ describe('Wallet model', function() { }); - it('should handle corrupt tx', function(done) { + it('should handle corrupt tx', function() { w.txProposals.merge = sinon.stub().throws(new Error('test error')); - w.on('txProposalEvent', function(e) { - e.type.should.equal('corrupt'); - w.txProposals.deleteOne.calledOnce.should.equal(false); - done(); - }); + sinon.stub(w, 'on'); w._onTxProposal('senderID', data); + w.on.called.should.equal(false); }); it('should call _processIncomingTxProposal', function(done) { @@ -1705,19 +1702,14 @@ describe('Wallet model', function() { w._onTxProposal('senderID', data); }); - it('should handle corrupt tx, case2', function(done) { + it('should handle corrupt tx, case2', function() { sinon.stub(w.txProposals, 'merge').returns({ ntxid: '1' }); + sinon.stub(w, 'on'); sinon.stub(w, '_getKeyMap').throws(new Error('test error')); - - w.on('txProposalEvent', function(e) { - e.type.should.equal('corrupt'); - w.txProposals.deleteOne.calledWith('1').should.equal(true); - w._getKeyMap.restore(); - done(); - }); w._onTxProposal('senderID', data); + w.on.called.should.equal(false); }); }); From 78933f882111912a5da628b242b223b12a417080 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 25 Nov 2014 17:44:51 -0300 Subject: [PATCH 23/23] lodash on loop --- js/models/Wallet.js | 22 +++++++++++----------- test/Wallet.js | 28 ++++++++++++++-------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 19c5be1ec..66ceb9c80 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -598,7 +598,9 @@ Wallet.prototype._onReject = function(senderId, data) { try { var txp = this.txProposals.get(data.ntxid); - } catch (e) {}; + } catch (e) { + log.info(e); + }; if (txp) { if (txp.signedBy[senderId]) @@ -1401,22 +1403,20 @@ Wallet.prototype.generateAddress = function(isChange, cb) { */ Wallet.prototype.getTxProposals = function() { var ret = []; - var copayers = this.getRegisteredCopayerIds(); - for (var ntxid in this.txProposals.txps) { - var txp = this.txProposals.getTxProposal(ntxid, copayers); + var self = this; + var copayers = self.getRegisteredCopayerIds(); + var myId = self.getMyCopayerId(); - txp.signedByUs = txp.signedBy[this.getMyCopayerId()] ? true : false; - txp.rejectedByUs = txp.rejectedBy[this.getMyCopayerId()] ? true : false; - txp.finallyRejected = this.totalCopayers - txp.rejectCount < this.requiredCopayers; + _.each(self.txProposals.txps, function(txp, ntxid){ + txp.signedByUs = txp.signedBy[myId] ? true : false; + txp.rejectedByUs = txp.rejectedBy[self.getMyCopayerId()] ? true : false; + txp.finallyRejected = self.totalCopayers - txp.rejectCount < self.requiredCopayers; txp.isPending = !txp.finallyRejected && !txp.sentTxid; - // si no gastada - // y si no esta expirada; - if (!txp.readonly || txp.finallyRejected || txp.sentTs) { ret.push(txp); } - } + }); return ret; }; diff --git a/test/Wallet.js b/test/Wallet.js index f534ccfc0..71dd56044 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -972,7 +972,7 @@ describe('Wallet model', function() { done(); }); }); - + it('should send Payment Messages on a PayPro payment', function(done) { var w = createW2(null, 1); @@ -1895,13 +1895,13 @@ describe('Wallet model', function() { describe('_onReject', function() { - it('should fails if unknown tx', function() { + it('should do nothing on unknown tx', function() { var w = cachedCreateW(); - (function() { - w._onReject(1, { - ntxid: 1 - }, 1); - }).should.throw('Unknown TXP'); + var spy1 = sinon.spy(w, 'emitAndKeepAlive'); + w._onReject(1, { + ntxid: 1 + }, 1); + spy1.called.should.equal(false); }); it('should fail to reject a signed tx', function() { var w = cachedCreateW(); @@ -1949,13 +1949,13 @@ describe('Wallet model', function() { describe('_onSeen', function() { - it('should fails if unknown tx', function() { + it('should do nothing on unknown tx', function() { var w = cachedCreateW(); - (function() { - w._onReject(1, { - ntxid: 1 - }, 1); - }).should.throw('Unknown TXP'); + var spy1 = sinon.spy(w, 'emitAndKeepAlive'); + w._onReject(1, { + ntxid: 1 + }, 1); + spy1.called.should.equal(false); }); it('should set seen a tx', function() { var w = cachedCreateW(); @@ -2458,7 +2458,7 @@ describe('Wallet model', function() { describe.skip('#onPayProPaymentAck', function() { it('should emit', function() { var w = cachedCreateW2(); - sinon.stub(w,'emitAndKeepAlive'); + sinon.stub(w, 'emitAndKeepAlive'); w.onPayProPaymentAck('id', 'data'); w.calledOnce.should.equal(true);