From dba306045c15b731841ba1aba6c1d27f85a08bfb Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Thu, 5 Feb 2015 17:22:38 -0300 Subject: [PATCH 1/3] broadcastTx --- lib/model/txproposal.js | 9 +++- lib/model/wallet.js | 4 ++ lib/server.js | 95 +++++++++++++++++++++++------------------ 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index fe95ff9..9f04307 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -64,7 +64,7 @@ TxProposal.prototype._updateStatus = function() { }; -TxProposal.prototype._getBitcoreTx = function(n) { +TxProposal.prototype._getBitcoreTx = function() { var self = this; var t = new Bitcore.Transaction(); @@ -80,6 +80,13 @@ TxProposal.prototype._getBitcoreTx = function(n) { }; +TxProposal.prototype.getRawTx = function() { + var t = this._getBitcoreTx(); + + return t.serialize(); +}; + + TxProposal.prototype.addAction = function(copayerId, type, signatures) { var action = new TxProposalAction({ copayerId: copayerId, diff --git a/lib/model/wallet.js b/lib/model/wallet.js index c0320f9..f65978d 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -96,6 +96,10 @@ Wallet.prototype.getCopayer = function(copayerId) { }; +Wallet.prototype.getNetworkName = function() { + return this.isTestnet ? 'testnet' : 'livenet'; +}; + Wallet.prototype._getBitcoreNetwork = function() { return this.isTestnet ? Bitcore.Networks.testnet : Bitcore.Networks.livenet; }; diff --git a/lib/server.js b/lib/server.js index a52793b..8e2914b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -263,52 +263,62 @@ CopayServer.prototype._getBlockExplorer = function(provider, network) { } }; +/** + * _getUtxos + * + * @param opts.walletId + */ CopayServer.prototype._getUtxos = function(opts, cb) { var self = this; - // Get addresses for this wallet - self.storage.fetchAddresses(opts.walletId, function(err, addresses) { + + self.storage.fetchWallet(opts.walletId, function(err, wallet) { if (err) return cb(err); - if (addresses.length == 0) return cb(new ClientError('The wallet has no addresses')); - var addressStrs = _.pluck(addresses, 'address'); - var addressToPath = _.indexBy(addresses, 'address'); // TODO : check performance - - var bc = self._getBlockExplorer('insight', opts.network); - bc.getUnspentUtxos(addressStrs, function(err, utxos) { + // Get addresses for this wallet + self.storage.fetchAddresses(opts.walletId, function(err, addresses) { if (err) return cb(err); + if (addresses.length == 0) return cb(new ClientError('The wallet has no addresses')); - self.getPendingTxs({ - walletId: opts.walletId - }, function(err, txps) { + var addressStrs = _.pluck(addresses, 'address'); + var addressToPath = _.indexBy(addresses, 'address'); // TODO : check performance + + var bc = self._getBlockExplorer('insight', wallet.getNetworkName()); + bc.getUnspentUtxos(addressStrs, function(err, utxos) { if (err) return cb(err); - var inputs = _.chain(txps) - .pluck('inputs') - .flatten() - .map(function(utxo) { - return utxo.txid + '|' + utxo.vout - }) - .value(); + self.getPendingTxs({ + walletId: opts.walletId + }, function(err, txps) { + if (err) return cb(err); - var dictionary = _.reduce(utxos, function(memo, utxo) { - memo[utxo.txid + '|' + utxo.vout] = utxo; - return memo; - }, {}); + var inputs = _.chain(txps) + .pluck('inputs') + .flatten() + .map(function(utxo) { + return utxo.txid + '|' + utxo.vout + }) + .value(); - _.each(inputs, function(input) { - if (dictionary[input]) { - dictionary[input].locked = true; - } + var dictionary = _.reduce(utxos, function(memo, utxo) { + memo[utxo.txid + '|' + utxo.vout] = utxo; + return memo; + }, {}); + + _.each(inputs, function(input) { + if (dictionary[input]) { + dictionary[input].locked = true; + } + }); + + // Needed for the clients to sign UTXOs + _.each(utxos, function(utxo) { + utxo.path = addressToPath[utxo.address].path; + utxo.publicKeys = addressToPath[utxo.address].publicKeys; + }); + + return cb(null, utxos); }); - - // Needed for the clients to sign UTXOs - _.each(utxos, function(utxo) { - utxo.path = addressToPath[utxo.address].path; - utxo.publicKeys = addressToPath[utxo.address].publicKeys; - }); - - return cb(null, utxos); }); }); }); @@ -452,11 +462,12 @@ CopayServer.prototype.getTx = function(opts, cb) { }); }; -CopayServer.prototype._broadcastTx = function(rawTx, cb) { - // TODO: this should attempt to broadcast _all_ accepted and not-yet broadcasted (status=='accepted') txps? - cb = cb || function() {}; - - throw 'not implemented'; +CopayServer.prototype._broadcastTx = function(txp, networkName, cb) { + var raw = txp.getRawTx(); + var bc = self._getBlockExplorer('insight', networkName); + bc.broadcast(raw, function(err, txid) { + return cb(err, txid); + }) }; /** @@ -486,9 +497,9 @@ CopayServer.prototype.signTx = function(opts, cb) { var action = _.find(txp.actions, { copayerId: opts.copayerId }); - if (action) + if (action) return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal')); - if (txp.status != 'pending') + if (txp.status != 'pending') return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending')); var copayer = wallet.getCopayer(opts.copayerId); @@ -502,7 +513,7 @@ CopayServer.prototype.signTx = function(opts, cb) { if (err) return cb(err); if (txp.status == 'accepted') { - self._broadcastTx(txp.rawTx, function(err, txid) { + self._broadcastTx(txp, wallet.getNetworkName(), function(err, txid) { if (err) return cb(err); tx.setBroadcasted(txid); From 1200f2b2f081530da649dadc52d6403f4dd5a02d Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 6 Feb 2015 15:15:54 -0300 Subject: [PATCH 2/3] add tests to broadcast --- lib/model/txproposal.js | 6 +- lib/server.js | 80 +++++++++++----------- test/integration.js | 146 +++++++++++++++++++++++++++++++++++----- 3 files changed, 175 insertions(+), 57 deletions(-) diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index 9f04307..6f13900 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -3,6 +3,7 @@ var _ = require('lodash'); var Guid = require('guid'); var Bitcore = require('bitcore'); +var Address = Bitcore.Address; var TxProposalAction = require('./txproposalaction'); @@ -79,6 +80,9 @@ TxProposal.prototype._getBitcoreTx = function() { return t; }; +TxProposal.prototype.getNetworkName = function() { + return Bitcore.Address(this.toAddress).toObject().networkName; +}; TxProposal.prototype.getRawTx = function() { var t = this._getBitcoreTx(); @@ -127,7 +131,7 @@ TxProposal.prototype.checkSignatures = function(signatures, xpub) { oks++; } catch (e) { // TODO only for debug now - console.log('DEBUG ONLY:',e.message); //TODO + console.log('DEBUG ONLY:', e.message); //TODO }; }); return oks === t.inputs.length; diff --git a/lib/server.js b/lib/server.js index 8e2914b..fb3d228 100644 --- a/lib/server.js +++ b/lib/server.js @@ -11,6 +11,7 @@ var events = require('events'); var Bitcore = require('bitcore'); var PublicKey = Bitcore.PublicKey; var HDPublicKey = Bitcore.HDPublicKey; +var Address = Bitcore.Address; var Explorers = require('bitcore-explorers'); var ClientError = require('./clienterror'); @@ -272,53 +273,50 @@ CopayServer.prototype._getUtxos = function(opts, cb) { var self = this; - self.storage.fetchWallet(opts.walletId, function(err, wallet) { + // Get addresses for this wallet + self.storage.fetchAddresses(opts.walletId, function(err, addresses) { if (err) return cb(err); + if (addresses.length == 0) return cb(new ClientError('The wallet has no addresses')); - // Get addresses for this wallet - self.storage.fetchAddresses(opts.walletId, function(err, addresses) { + var addressStrs = _.pluck(addresses, 'address'); + var addressToPath = _.indexBy(addresses, 'address'); // TODO : check performance + var networkName = Bitcore.Address(addressStrs[0]).toObject().networkName; + + var bc = self._getBlockExplorer('insight', networkName); + bc.getUnspentUtxos(addressStrs, function(err, utxos) { if (err) return cb(err); - if (addresses.length == 0) return cb(new ClientError('The wallet has no addresses')); - var addressStrs = _.pluck(addresses, 'address'); - var addressToPath = _.indexBy(addresses, 'address'); // TODO : check performance - - var bc = self._getBlockExplorer('insight', wallet.getNetworkName()); - bc.getUnspentUtxos(addressStrs, function(err, utxos) { + self.getPendingTxs({ + walletId: opts.walletId + }, function(err, txps) { if (err) return cb(err); - self.getPendingTxs({ - walletId: opts.walletId - }, function(err, txps) { - if (err) return cb(err); + var inputs = _.chain(txps) + .pluck('inputs') + .flatten() + .map(function(utxo) { + return utxo.txid + '|' + utxo.vout + }) + .value(); - var inputs = _.chain(txps) - .pluck('inputs') - .flatten() - .map(function(utxo) { - return utxo.txid + '|' + utxo.vout - }) - .value(); + var dictionary = _.reduce(utxos, function(memo, utxo) { + memo[utxo.txid + '|' + utxo.vout] = utxo; + return memo; + }, {}); - var dictionary = _.reduce(utxos, function(memo, utxo) { - memo[utxo.txid + '|' + utxo.vout] = utxo; - return memo; - }, {}); - - _.each(inputs, function(input) { - if (dictionary[input]) { - dictionary[input].locked = true; - } - }); - - // Needed for the clients to sign UTXOs - _.each(utxos, function(utxo) { - utxo.path = addressToPath[utxo.address].path; - utxo.publicKeys = addressToPath[utxo.address].publicKeys; - }); - - return cb(null, utxos); + _.each(inputs, function(input) { + if (dictionary[input]) { + dictionary[input].locked = true; + } }); + + // Needed for the clients to sign UTXOs + _.each(utxos, function(utxo) { + utxo.path = addressToPath[utxo.address].path; + utxo.publicKeys = addressToPath[utxo.address].publicKeys; + }); + + return cb(null, utxos); }); }); }); @@ -464,7 +462,7 @@ CopayServer.prototype.getTx = function(opts, cb) { CopayServer.prototype._broadcastTx = function(txp, networkName, cb) { var raw = txp.getRawTx(); - var bc = self._getBlockExplorer('insight', networkName); + var bc = this._getBlockExplorer('insight', networkName); bc.broadcast(raw, function(err, txid) { return cb(err, txid); }) @@ -514,9 +512,9 @@ CopayServer.prototype.signTx = function(opts, cb) { if (txp.status == 'accepted') { self._broadcastTx(txp, wallet.getNetworkName(), function(err, txid) { - if (err) return cb(err); + if (err) return cb(err, txp); - tx.setBroadcasted(txid); + txp.setBroadcasted(txid); self.storage.storeTx(opts.walletId, txp, function(err) { if (err) return cb(err); diff --git a/test/integration.js b/test/integration.js index 6036fb6..1d88204 100644 --- a/test/integration.js +++ b/test/integration.js @@ -137,15 +137,27 @@ helpers.createUtxos = function(server, wallet, amounts, cb) { address: addresses[i++].address, }; }); - - var bc = sinon.stub(); - bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); - server._getBlockExplorer = sinon.stub().returns(bc); - - return cb(); + return cb(utxos); }); }; + +helpers.stubBlockExplorer = function(server, utxos, txid) { + + var bc = sinon.stub(); + bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); + + if (txid) { + bc.broadcast = sinon.stub().callsArgWith(1, null, txid); + } else { + bc.broadcast = sinon.stub().callsArgWith(1, 'broadcast error'); + } + + server._getBlockExplorer = sinon.stub().returns(bc); +}; + + + helpers.clientSign = function(tx, xpriv, n) { //Derive proper key to sign, for each input var privs = [], @@ -155,7 +167,7 @@ helpers.clientSign = function(tx, xpriv, n) { _.each(tx.inputs, function(i) { if (!derived[i.path]) { derived[i.path] = xpriv.derive(i.path).privateKey; - } + } privs.push(derived[i.path]); }); @@ -171,9 +183,9 @@ helpers.clientSign = function(tx, xpriv, n) { var signatures = []; _.each(privs, function(p) { - var s = t.getSignatures(p)[0].signature.toDER().toString('hex'); - signatures.push(s); - }); + var s = t.getSignatures(p)[0].signature.toDER().toString('hex'); + signatures.push(s); + }); // return signatures; }; @@ -777,6 +789,8 @@ describe('Copay server', function() { helpers.createUtxos(server, wallet, helpers.toSatoshi([100, 200]), function(utxos) { + helpers.stubBlockExplorer(server, utxos); + var txOpts = { copayerId: '1', walletId: '123', @@ -812,7 +826,8 @@ describe('Copay server', function() { it('should fail to create tx when insufficient funds', function(done) { - helpers.createUtxos(server, wallet, helpers.toSatoshi([100]), function() { + helpers.createUtxos(server, wallet, helpers.toSatoshi([100]), function(utxos) { + helpers.stubBlockExplorer(server, utxos); var txOpts = { copayerId: '1', @@ -848,6 +863,7 @@ describe('Copay server', function() { it('should create tx when there is a pending tx and enough UTXOs', function(done) { helpers.createUtxos(server, wallet, helpers.toSatoshi([10.1, 10.2, 10.3]), function(utxos) { + helpers.stubBlockExplorer(server, utxos); var txOpts = { copayerId: '1', @@ -896,6 +912,7 @@ describe('Copay server', function() { it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) { helpers.createUtxos(server, wallet, helpers.toSatoshi([10.1, 10.2, 10.3]), function(utxos) { + helpers.stubBlockExplorer(server, utxos); var txOpts = { copayerId: '1', walletId: '123', @@ -957,6 +974,7 @@ describe('Copay server', function() { isChange: false, }, function(err, address) { helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(utxos) { + helpers.stubBlockExplorer(server, utxos); var txOpts = { copayerId: '1', walletId: '123', @@ -977,7 +995,7 @@ describe('Copay server', function() { }); }); - it('should sign a TX with multiple inputs, different paths', function(done) { + it('should sign a TX with multiple inputs, different paths', function(done) { server.getPendingTxs({ walletId: '123' }, function(err, txs) { @@ -997,7 +1015,7 @@ describe('Copay server', function() { }); }); - it('should fail if one signature is broken', function(done) { + it('should fail if one signature is broken', function(done) { server.getPendingTxs({ walletId: '123' }, function(err, txs) { @@ -1005,7 +1023,7 @@ describe('Copay server', function() { tx.id.should.equal(txid); var signatures = helpers.clientSign(tx, someXPrivKey[0], wallet.n); - signatures[0]=1; + signatures[0] = 1; server.signTx({ walletId: '123', @@ -1018,7 +1036,7 @@ describe('Copay server', function() { }); }); }); - it('should fail on invalids signature', function(done) { + it('should fail on invalids signature', function(done) { server.getPendingTxs({ walletId: '123' }, function(err, txs) { @@ -1040,4 +1058,102 @@ describe('Copay server', function() { }); + + describe('#signTx and broadcast', function() { + var wallet, utxos; + + beforeEach(function(done) { + server = new CopayServer({ + storage: storage, + }); + helpers.createAndJoinWallet('123', 1, 1, function(err, w) { + wallet = w; + server.createAddress({ + walletId: '123', + isChange: false, + }, function(err, address) { + helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(inutxos) { + utxos = inutxos; + done(); + }); + }); + }); + }); + + it('should sign and broadcast a tx', function(done) { + helpers.stubBlockExplorer(server, utxos, '1122334455'); + var txOpts = { + copayerId: '1', + walletId: '123', + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: helpers.toSatoshi(10), + message: 'some message', + otToken: 'dummy', + requestSignature: 'dummy', + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + txp.should.exist; + var txpid = txp.id; + + server.getPendingTxs({ + walletId: '123' + }, function(err, txps) { + var txp = txps[0]; + txp.id.should.equal(txpid); + var signatures = helpers.clientSign(txp, someXPrivKey[0], wallet.n); + server.signTx({ + walletId: '123', + copayerId: '1', + txProposalId: txpid, + signatures: signatures, + }, function(err, txp) { + should.not.exist(err); + txp.status.should.equal('broadcasted'); + txp.txid.should.equal('1122334455'); + done(); + }); + }); + }); + }); + + + it('should keep tx as *accepted* if unable to broadcast it', function(done) { + + helpers.stubBlockExplorer(server, utxos); + var txOpts = { + copayerId: '1', + walletId: '123', + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: helpers.toSatoshi(10), + message: 'some message', + otToken: 'dummy', + requestSignature: 'dummy', + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + txp.should.exist; + var txpid = txp.id; + + server.getPendingTxs({ + walletId: '123' + }, function(err, txps) { + var txp = txps[0]; + txp.id.should.equal(txpid); + var signatures = helpers.clientSign(txp, someXPrivKey[0], wallet.n); + server.signTx({ + walletId: '123', + copayerId: '1', + txProposalId: txpid, + signatures: signatures, + }, function(err, txp) { + err.should.contain('broadcast'); + txp.status.should.equal('accepted'); + should.not.exist(txp.txid); + done(); + }); + }); + }); + }); + }); }); From baf5da9b4d7c60d37fc82859d891f3552993770c Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 6 Feb 2015 15:51:40 -0300 Subject: [PATCH 3/3] use networkname from txp --- lib/server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/server.js b/lib/server.js index fb3d228..66a5450 100644 --- a/lib/server.js +++ b/lib/server.js @@ -460,9 +460,9 @@ CopayServer.prototype.getTx = function(opts, cb) { }); }; -CopayServer.prototype._broadcastTx = function(txp, networkName, cb) { +CopayServer.prototype._broadcastTx = function(txp, cb) { var raw = txp.getRawTx(); - var bc = this._getBlockExplorer('insight', networkName); + var bc = this._getBlockExplorer('insight', txp.getNetworkName()); bc.broadcast(raw, function(err, txid) { return cb(err, txid); }) @@ -511,7 +511,7 @@ CopayServer.prototype.signTx = function(opts, cb) { if (err) return cb(err); if (txp.status == 'accepted') { - self._broadcastTx(txp, wallet.getNetworkName(), function(err, txid) { + self._broadcastTx(txp, function(err, txid) { if (err) return cb(err, txp); txp.setBroadcasted(txid);