diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index fe95ff9..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'); @@ -64,7 +65,7 @@ TxProposal.prototype._updateStatus = function() { }; -TxProposal.prototype._getBitcoreTx = function(n) { +TxProposal.prototype._getBitcoreTx = function() { var self = this; var t = new Bitcore.Transaction(); @@ -79,6 +80,16 @@ TxProposal.prototype._getBitcoreTx = function(n) { return t; }; +TxProposal.prototype.getNetworkName = function() { + return Bitcore.Address(this.toAddress).toObject().networkName; +}; + +TxProposal.prototype.getRawTx = function() { + var t = this._getBitcoreTx(); + + return t.serialize(); +}; + TxProposal.prototype.addAction = function(copayerId, type, signatures) { var action = new TxProposalAction({ @@ -120,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/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 bc9224c..375ad7e 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'); @@ -263,9 +264,15 @@ 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) { if (err) return cb(err); @@ -273,8 +280,9 @@ CopayServer.prototype._getUtxos = function(opts, cb) { 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', opts.network); + var bc = self._getBlockExplorer('insight', networkName); bc.getUnspentUtxos(addressStrs, function(err, utxos) { if (err) return cb(err); @@ -452,11 +460,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, cb) { + var raw = txp.getRawTx(); + var bc = this._getBlockExplorer('insight', txp.getNetworkName()); + bc.broadcast(raw, function(err, txid) { + return cb(err, txid); + }) }; /** @@ -486,9 +495,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,10 +511,10 @@ CopayServer.prototype.signTx = function(opts, cb) { if (err) return cb(err); if (txp.status == 'accepted') { - self._broadcastTx(txp.rawTx, function(err, txid) { - if (err) return cb(err); + self._broadcastTx(txp, function(err, txid) { + 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(); + }); + }); + }); + }); + }); });