diff --git a/lib/server.js b/lib/server.js index d5a3578..af836a4 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1929,6 +1929,7 @@ WalletService.prototype._canCreateTx = function(cb) { }; WalletService.prototype._validateOutputs = function(opts, wallet, cb) { + var A = Bitcore_[wallet.coin].Address; var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore_[wallet.coin].Transaction.DUST_AMOUNT); if (_.isEmpty(opts.outputs)) return new ClientError('No outputs were specified'); @@ -1943,7 +1944,7 @@ WalletService.prototype._validateOutputs = function(opts, wallet, cb) { var toAddress = {}; try { - toAddress = new Bitcore.Address(output.toAddress); + toAddress = new A(output.toAddress); } catch (ex) { return Errors.INVALID_ADDRESS; } diff --git a/test/integration/helpers.js b/test/integration/helpers.js index bb8008e..d7b57de 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -277,6 +277,8 @@ helpers.stubUtxos = function(server, wallet, amounts, opts, cb) { if (!helpers._utxos) helpers._utxos = {}; + var S = Bitcore_[wallet.coin].Script; + async.waterfall([ function(next) { @@ -298,10 +300,10 @@ helpers.stubUtxos = function(server, wallet, amounts, opts, cb) { var scriptPubKey; switch (wallet.addressType) { case Constants.SCRIPT_TYPES.P2SH: - scriptPubKey = Bitcore.Script.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut(); + scriptPubKey = S.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut(); break; case Constants.SCRIPT_TYPES.P2PKH: - scriptPubKey = Bitcore.Script.buildPublicKeyHashOut(address.address); + scriptPubKey = S.buildPublicKeyHashOut(address.address); break; } should.exist(scriptPubKey); diff --git a/test/integration/server.js b/test/integration/server.js index 92ea2c4..17278ae 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -11,6 +11,12 @@ log.debug = log.verbose; log.level = 'info'; var Bitcore = require('bitcore-lib'); +var Bitcore_ = { + btc: Bitcore, + bch: require('bitcore-lib-cash') +}; + + var Common = require('../../lib/common'); var Utils = Common.Utils; @@ -2518,275 +2524,260 @@ describe('Wallet service', function() { }); }); - describe('#createTx', function() { - describe('Tx proposal creation & publishing', function() { - var server, wallet; - beforeEach(function(done) { - helpers.createAndJoinWallet(1, 1, function(s, w) { - server = s; - wallet = w; - done(); - }); + var addrMap = { + btc: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + bch: 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X', + } + + _.each(['bch', 'btc'], function(coin) { + + describe('#createTx ' + coin, function() { + var addressStr; + before(function() { + addressStr = addrMap[coin]; }); - it('should create a tx', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, - }], - message: 'some message', - customData: 'some custom data', - feePerKb: 123e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.walletM.should.equal(1); - tx.walletN.should.equal(1); - tx.requiredRejections.should.equal(1); - tx.requiredSignatures.should.equal(1); - tx.isAccepted().should.equal.false; - tx.isRejected().should.equal.false; - tx.isPending().should.equal.true; - tx.isTemporary().should.equal.true; - tx.amount.should.equal(helpers.toSatoshi(0.8)); - tx.feePerKb.should.equal(123e2); - should.not.exist(tx.feeLevel); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.should.be.empty; - done(); - }); - }); - }); - }); - describe('Validations', function() { - it('should fail to create a tx without outputs', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = { - outputs: [], - feePerKb: 123e2, - }; - server.createTx(txOpts, function(err, tx) { - should.exist(err); - should.not.exist(tx); - err.message.should.equal('No outputs were specified'); - done(); - }); - }); - }); - it('should fail to create tx for invalid address', function(done) { - helpers.stubUtxos(server, wallet, 1, function() { - var txOpts = { - outputs: [{ - toAddress: 'invalid address', - amount: 0.5e8 - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.exist(err); - should.not.exist(tx); - // may fail due to Non-base58 character, or Checksum mismatch, or other - done(); - }); - }); - }); - it('should fail to create tx for address of different network', function(done) { - helpers.stubUtxos(server, wallet, 1, function() { - var txOpts = { - outputs: [{ - toAddress: 'myE38JHdxmQcTJGP1ZiX4BiGhDxMJDvLJD', - amount: 0.5e8 - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(tx); - should.exist(err); - err.code.should.equal('INCORRECT_ADDRESS_NETWORK'); - err.message.should.equal('Incorrect address network'); - done(); - }); - }); - }); - it('should fail to create tx for invalid amount', function(done) { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(tx); - should.exist(err); - err.message.should.equal('Invalid amount'); + + describe('Tx proposal creation & publishing ' + coin, function() { + var server, wallet; + beforeEach(function(done) { + helpers.createAndJoinWallet(1, 1, { + coin: coin, + }, function(s, w) { + server = s; + wallet = w; done(); }); }); - it('should fail to specify both feeLevel & feePerKb', function(done) { - helpers.stubUtxos(server, wallet, 2, function() { + + + it('should create a tx', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, + toAddress: addressStr, + amount: 0.8 * 1e8, }], - feeLevel: 'normal', + message: 'some message', + customData: 'some custom data', feePerKb: 123e2, }; - server.createTx(txOpts, function(err, txp) { - should.exist(err); - should.not.exist(txp); - err.toString().should.contain('Only one of feeLevel/feePerKb'); - done(); - }); - }); - }); - it('should be able to create tx with inputs argument', function(done) { - helpers.stubUtxos(server, wallet, [1, 3, 2], function(utxos) { - server.getUtxos({}, function(err, utxos) { + server.createTx(txOpts, function(err, tx) { should.not.exist(err); - var inputs = [utxos[0], utxos[2]]; - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 2.5e8, - }], - feePerKb: 100e2, - inputs: inputs, - }; - server.createTx(txOpts, function(err, tx) { + should.exist(tx); + tx.walletM.should.equal(1); + tx.walletN.should.equal(1); + tx.requiredRejections.should.equal(1); + tx.requiredSignatures.should.equal(1); + tx.isAccepted().should.equal.false; + tx.isRejected().should.equal.false; + tx.isPending().should.equal.true; + tx.isTemporary().should.equal.true; + tx.amount.should.equal(helpers.toSatoshi(0.8)); + tx.feePerKb.should.equal(123e2); + should.not.exist(tx.feeLevel); + server.getPendingTxs({}, function(err, txs) { should.not.exist(err); - should.exist(tx); - tx.inputs.length.should.equal(2); - var txids = _.pluck(tx.inputs, 'txid'); - txids.should.contain(utxos[0].txid); - txids.should.contain(utxos[2].txid); + txs.should.be.empty; done(); }); }); }); }); - it('should be able to specify change address', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function(utxos) { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8e8, - }], - feePerKb: 100e2, - changeAddress: utxos[0].address, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - var t = tx.getBitcoreTx(); - t.getChangeOutput().script.toAddress().toString().should.equal(txOpts.changeAddress); - done(); - }); - }); - }); - it('should be able to specify inputs & absolute fee', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function(utxos) { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8e8, - }], - inputs: utxos, - fee: 1000e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.amount.should.equal(helpers.toSatoshi(0.8)); - should.not.exist(tx.feePerKb); - tx.fee.should.equal(1000e2); - var t = tx.getBitcoreTx(); - t.getFee().should.equal(1000e2); - t.getChangeOutput().satoshis.should.equal(3e8 - 0.8e8 - 1000e2); - done(); - }); - }); - }); - }); - describe('Foreign ID', function() { - it('should create a tx with foreign ID', function(done) { - helpers.stubUtxos(server, wallet, 2, function() { + describe('Validations', function() { + it('should fail to create a tx without outputs', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [], + feePerKb: 123e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + should.not.exist(tx); + err.message.should.equal('No outputs were specified'); + done(); + }); + }); + }); + it('should fail to create tx for invalid address', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var txOpts = { + outputs: [{ + toAddress: 'invalid address', + amount: 0.5e8 + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + should.not.exist(tx); + // may fail due to Non-base58 character, or Checksum mismatch, or other + done(); + }); + }); + }); + it('should fail to create tx for address of different network', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var txOpts = { + outputs: [{ + toAddress: 'myE38JHdxmQcTJGP1ZiX4BiGhDxMJDvLJD', + amount: 0.5e8 + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(tx); + should.exist(err); + err.code.should.equal('INCORRECT_ADDRESS_NETWORK'); + err.message.should.equal('Incorrect address network'); + done(); + }); + }); + }); + it('should fail to create tx for invalid amount', function(done) { var txOpts = { - txProposalId: '123', outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, + toAddress: addressStr, + amount: 0, }], feePerKb: 100e2, }; server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.id.should.equal('123'); + should.not.exist(tx); + should.exist(err); + err.message.should.equal('Invalid amount'); done(); }); }); - }); - it('should return already created tx if same foreign ID is specified and tx still unpublished', function(done) { - helpers.stubUtxos(server, wallet, 2, function() { - var txOpts = { - txProposalId: '123', - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.id.should.equal('123'); - server.createTx(txOpts, function(err, tx) { + it('should fail to specify both feeLevel & feePerKb', function(done) { + helpers.stubUtxos(server, wallet, 2, function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 1e8, + }], + feeLevel: 'normal', + feePerKb: 123e2, + }; + server.createTx(txOpts, function(err, txp) { + should.exist(err); + should.not.exist(txp); + err.toString().should.contain('Only one of feeLevel/feePerKb'); + done(); + }); + }); + }); + it('should be able to create tx with inputs argument', function(done) { + helpers.stubUtxos(server, wallet, [1, 3, 2], function(utxos) { + server.getUtxos({}, function(err, utxos) { should.not.exist(err); - should.exist(tx); - tx.id.should.equal('123'); - server.storage.fetchTxs(wallet.id, {}, function(err, txs) { + var inputs = [utxos[0], utxos[2]]; + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 2.5e8, + }], + feePerKb: 100e2, + inputs: inputs, + }; + server.createTx(txOpts, function(err, tx) { should.not.exist(err); - should.exist(txs); - txs.length.should.equal(1); + should.exist(tx); + tx.inputs.length.should.equal(2); + var txids = _.pluck(tx.inputs, 'txid'); + txids.should.contain(utxos[0].txid); + txids.should.contain(utxos[2].txid); done(); }); }); }); }); - }); - it('should return already published tx if same foreign ID is specified and tx already published', function(done) { - helpers.stubUtxos(server, wallet, [2, 2, 2], function() { - var txOpts = { - txProposalId: '123', - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.id.should.equal('123'); - var publishOpts = helpers.getProposalSignatureOpts(tx, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, function(err, tx) { + it('should be able to specify change address', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function(utxos) { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8e8, + }], + feePerKb: 100e2, + changeAddress: utxos[0].address, + }; + server.createTx(txOpts, function(err, tx) { should.not.exist(err); should.exist(tx); + var t = tx.getBitcoreTx(); + t.getChangeOutput().script.toAddress().toString().should.equal(txOpts.changeAddress); + done(); + }); + }); + }); + it('should be able to specify inputs & absolute fee', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function(utxos) { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8e8, + }], + inputs: utxos, + fee: 1000e2, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + tx.amount.should.equal(helpers.toSatoshi(0.8)); + should.not.exist(tx.feePerKb); + tx.fee.should.equal(1000e2); + var t = tx.getBitcoreTx(); + t.getFee().should.equal(1000e2); + t.getChangeOutput().satoshis.should.equal(3e8 - 0.8e8 - 1000e2); + done(); + }); + }); + }); + }); + + describe('Foreign ID', function() { + it('should create a tx with foreign ID', function(done) { + helpers.stubUtxos(server, wallet, 2, function() { + var txOpts = { + txProposalId: '123', + outputs: [{ + toAddress: addressStr, + amount: 1e8, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + tx.id.should.equal('123'); + done(); + }); + }); + }); + it('should return already created tx if same foreign ID is specified and tx still unpublished', function(done) { + helpers.stubUtxos(server, wallet, 2, function() { + var txOpts = { + txProposalId: '123', + outputs: [{ + toAddress: addressStr, + amount: 1e8, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + tx.id.should.equal('123'); server.createTx(txOpts, function(err, tx) { should.not.exist(err); should.exist(tx); tx.id.should.equal('123'); - tx.status.should.equal('pending'); server.storage.fetchTxs(wallet.id, {}, function(err, txs) { should.not.exist(err); + should.exist(txs); txs.length.should.equal(1); done(); }); @@ -2794,93 +2785,582 @@ describe('Wallet service', function() { }); }); }); + it('should return already published tx if same foreign ID is specified and tx already published', function(done) { + helpers.stubUtxos(server, wallet, [2, 2, 2], function() { + var txOpts = { + txProposalId: '123', + outputs: [{ + toAddress: addressStr, + amount: 1e8, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + tx.id.should.equal('123'); + var publishOpts = helpers.getProposalSignatureOpts(tx, TestData.copayers[0].privKey_1H_0); + server.publishTx(publishOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + tx.id.should.equal('123'); + tx.status.should.equal('pending'); + server.storage.fetchTxs(wallet.id, {}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(1); + done(); + }); + }); + }); + }); + }); + }); }); - }); - describe('Publishing', function() { - it('should be able to publish a temporary tx proposal', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, - }], - feePerKb: 100e2, - message: 'some message', - customData: 'some custom data', - }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, function(err) { + describe('Publishing', function() { + it('should be able to publish a temporary tx proposal', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8 * 1e8, + }], + feePerKb: 100e2, + message: 'some message', + customData: 'some custom data', + }; + server.createTx(txOpts, function(err, txp) { should.not.exist(err); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(1); - should.exist(txs[0].proposalSignature); - done(); - }); - }); - }); - }); - }); - it('should not be able to publish a temporary tx proposal created in a dry run', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, - }], - feePerKb: 100e2, - dryRun: true, - }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, function(err) { - should.exist(err); - err.code.should.equal('TX_NOT_FOUND'); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(0); - done(); - }); - }); - }); - }); - }); - it('should delay NewTxProposal notification until published', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, - }], - feePerKb: 100e2, - message: 'some message', - }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - server.getNotifications({}, function(err, notifications) { - should.not.exist(err); - _.pluck(notifications, 'type').should.not.contain('NewTxProposal'); + should.exist(txp); var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0); server.publishTx(publishOpts, function(err) { should.not.exist(err); - server.getNotifications({}, function(err, notifications) { + server.getPendingTxs({}, function(err, txs) { should.not.exist(err); + txs.length.should.equal(1); + should.exist(txs[0].proposalSignature); + done(); + }); + }); + }); + }); + }); + it('should not be able to publish a temporary tx proposal created in a dry run', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8 * 1e8, + }], + feePerKb: 100e2, + dryRun: true, + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0); + server.publishTx(publishOpts, function(err) { + should.exist(err); + err.code.should.equal('TX_NOT_FOUND'); + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(0); + done(); + }); + }); + }); + }); + }); + it('should delay NewTxProposal notification until published', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8 * 1e8, + }], + feePerKb: 100e2, + message: 'some message', + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + server.getNotifications({}, function(err, notifications) { + should.not.exist(err); + _.pluck(notifications, 'type').should.not.contain('NewTxProposal'); + var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0); + server.publishTx(publishOpts, function(err) { + should.not.exist(err); + server.getNotifications({}, function(err, notifications) { + should.not.exist(err); - var n = _.find(notifications, { - 'type': 'NewTxProposal' + var n = _.find(notifications, { + 'type': 'NewTxProposal' + }); + should.exist(n); + should.exist(n.data.txProposalId); + should.exist(n.data.message); + should.exist(n.data.creatorId); + n.data.creatorId.should.equal(server.copayerId); + done(); }); - should.exist(n); - should.exist(n.data.txProposalId); - should.exist(n.data.message); - should.exist(n.data.creatorId); - n.data.creatorId.should.equal(server.copayerId); + }); + }); + }); + }); + }); + it('should fail to publish non-existent tx proposal', function(done) { + server.publishTx({ + txProposalId: 'wrong-id', + proposalSignature: 'dummy', + }, function(err) { + should.exist(err); + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.should.be.empty; + done(); + }); + }); + }); + it('should fail to publish tx proposal with wrong signature', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8 * 1e8, + }], + feePerKb: 100e2, + message: 'some message', + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + server.publishTx({ + txProposalId: txp.id, + proposalSignature: 'dummy' + }, function(err) { + should.exist(err); + err.message.should.contain('Invalid proposal signature'); + done(); + }); + }); + }); + }); + it('should fail to publish tx proposal not signed by the creator', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8 * 1e8, + }], + feePerKb: 100e2, + message: 'some message', + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + + var publishOpts = { + txProposalId: txp.id, + proposalSignature: helpers.signMessage(txp.getRawTx(), TestData.copayers[1].privKey_1H_0), + } + + server.publishTx(publishOpts, function(err) { + should.exist(err); + err.message.should.contain('Invalid proposal signature'); + done(); + }); + }); + }); + }); + it('should fail to publish a temporary tx proposal if utxos are locked by other pending proposals', function(done) { + var txp1, txp2; + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8 * 1e8, + }], + message: 'some message', + feePerKb: 100e2, + }; + + async.waterfall([ + + function(next) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + next(); + }); + }, + function(next) { + server.createTx(txOpts, next); + }, + function(txp, next) { + txp1 = txp; + server.createTx(txOpts, next); + }, + function(txp, next) { + txp2 = txp; + should.exist(txp1); + should.exist(txp2); + var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0); + server.publishTx(publishOpts, next); + }, + function(txp, next) { + var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0); + server.publishTx(publishOpts, function(err) { + should.exist(err); + err.code.should.equal('UNAVAILABLE_UTXOS'); + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(1); + next(); + }); + }, + function(next) { + // A new tx proposal should use the next available UTXO + server.createTx(txOpts, next); + }, + function(txp3, next) { + should.exist(txp3); + var publishOpts = helpers.getProposalSignatureOpts(txp3, TestData.copayers[0].privKey_1H_0); + server.publishTx(publishOpts, next); + }, + function(txp, next) { + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(2); + next(); + }); + }, + ], function(err) { + should.not.exist(err); + done(); + }); + }); + it('should fail to publish a temporary tx proposal if utxos are already spent', function(done) { + var txp1, txp2; + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8 * 1e8, + }], + message: 'some message', + feePerKb: 100e2, + }; + + async.waterfall([ + + function(next) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + next(); + }); + }, + function(next) { + server.createTx(txOpts, next); + }, + function(txp, next) { + txp1 = txp; + server.createTx(txOpts, next); + }, + function(txp, next) { + txp2 = txp; + should.exist(txp1); + should.exist(txp2); + var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0); + server.publishTx(publishOpts, next); + }, + function(txp, next) { + // Sign & Broadcast txp1 + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H); + server.signTx({ + txProposalId: txp.id, + signatures: signatures, + }, function(err, txp) { + should.not.exist(err); + + helpers.stubBroadcast(); + server.broadcastTx({ + txProposalId: txp.id + }, function(err, txp) { + should.not.exist(err); + should.exist(txp.txid); + txp.status.should.equal('broadcasted'); + next(); + }); + }); + }, + function(next) { + var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0); + server.publishTx(publishOpts, function(err) { + should.exist(err); + err.code.should.equal('UNAVAILABLE_UTXOS'); + next(); + }); + }, + ], function(err) { + should.not.exist(err); + done(); + }); + }); + }); + + describe('Fee levels', function() { + it('should create a tx specifying feeLevel', function(done) { + helpers.stubFeeLevels({ + 1: 400e2, + 2: 200e2, + 6: 180e2, + 24: 90e2, + }); + helpers.stubUtxos(server, wallet, 2, function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 1e8, + }], + feeLevel: 'economy', + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + txp.feePerKb.should.equal(180e2); + txp.feeLevel.should.equal('economy'); + done(); + }); + }); + }); + it('should fail if the specified fee level does not exist', function(done) { + helpers.stubUtxos(server, wallet, 2, function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 1e8, + }], + feeLevel: 'madeUpLevel', + }; + server.createTx(txOpts, function(err, txp) { + should.exist(err); + should.not.exist(txp); + err.toString().should.contain('Invalid fee level'); + done(); + }); + }); + }); + it('should assume "normal" fee level if no feeLevel and no feePerKb/fee is specified', function(done) { + helpers.stubFeeLevels({ + 1: 400e2, + 2: 200e2, + 6: 180e2, + 24: 90e2, + }); + helpers.stubUtxos(server, wallet, 2, function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 1e8, + }], + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + txp.feePerKb.should.equal(200e2); + txp.feeLevel.should.equal('normal'); + done(); + }); + }); + }); + }); + it('should generate new change address for each created tx', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8e8, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx1) { + should.not.exist(err); + should.exist(tx1); + server.createTx(txOpts, function(err, tx2) { + should.not.exist(err); + should.exist(tx2); + tx1.changeAddress.address.should.not.equal(tx2.changeAddress.address); + done(); + }); + }); + }); + }); + it('should support creating a tx with no change address', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var max = 3e8 - 7000; // Fees for this tx at 100bits/kB = 7000 sat + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: max, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + var t = txp.getBitcoreTx().toObject(); + t.outputs.length.should.equal(1); + t.outputs[0].satoshis.should.equal(max); + done(); + }); + }); + }); + it('should fail gracefully if unable to reach the blockchain', function(done) { + blockchainExplorer.getUtxos = sinon.stub().callsArgWith(1, 'dummy error'); + server.createAddress({}, function(err, address) { + should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 1e8 + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.toString().should.equal('dummy error'); + done(); + }); + }); + }); + it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var bitcoreStub = sinon.stub(Bitcore_[coin], 'Transaction'); + bitcoreStub.throws({ + name: 'dummy', + message: 'dummy exception' + }); + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.5e8, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.message.should.equal('dummy exception'); + bitcoreStub.restore(); + done(); + }); + }); + }); + it('should fail to create a tx exceeding max size in kb', function(done) { + var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB; + Defaults.MAX_TX_SIZE_IN_KB = 1; + helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 8e8, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('TX_MAX_SIZE_EXCEEDED'); + Defaults.MAX_TX_SIZE_IN_KB = _oldDefault; + done(); + }); + }); + }); + it('should fail with different error for insufficient funds and locked funds', function(done) { + helpers.stubUtxos(server, wallet, [1, 1], function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 1.1e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.totalAmount.should.equal(2e8); + balance.lockedAmount.should.equal(2e8); + txOpts.outputs[0].amount = 0.8e8; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('LOCKED_FUNDS'); + err.message.should.equal('Funds are locked by pending transaction proposals'); + done(); + }); + }); + }); + }); + }); + it('should fail to create tx for dust amount in outputs', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 20e2, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('DUST_AMOUNT'); + err.message.should.equal('Amount below dust threshold'); + done(); + }); + }); + }); + it('should create tx with 0 change output', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var fee = 4100; // The exact fee of the resulting tx + var amount = 1e8 - fee; + + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: amount, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + var bitcoreTx = tx.getBitcoreTx(); + bitcoreTx.outputs.length.should.equal(1); + bitcoreTx.outputs[0].satoshis.should.equal(tx.amount); + done(); + }); + }); + }); + it('should create tx when there is a pending tx and enough UTXOs', function(done) { + helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 1.5e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + should.exist(tx); + txOpts.outputs[0].amount = 0.8e8; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + should.exist(tx); + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(2); + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.totalAmount.should.equal(3.6e8); + balance.lockedAmount.should.equal(3.6e8); done(); }); }); @@ -2888,603 +3368,147 @@ describe('Wallet service', function() { }); }); }); - it('should fail to publish non-existent tx proposal', function(done) { - server.publishTx({ - txProposalId: 'wrong-id', - proposalSignature: 'dummy', - }, function(err) { - should.exist(err); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.should.be.empty; - done(); - }); - }); - }); - it('should fail to publish tx proposal with wrong signature', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { + it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) { + helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() { var txOpts = { outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, + toAddress: addressStr, + amount: 1.5e8, }], feePerKb: 100e2, - message: 'some message', }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - server.publishTx({ - txProposalId: txp.id, - proposalSignature: 'dummy' - }, function(err) { - should.exist(err); - err.message.should.contain('Invalid proposal signature'); - done(); - }); - }); - }); - }); - it('should fail to publish tx proposal not signed by the creator', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, - }], - feePerKb: 100e2, - message: 'some message', - }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - - var publishOpts = { - txProposalId: txp.id, - proposalSignature: helpers.signMessage(txp.getRawTx(), TestData.copayers[1].privKey_1H_0), - } - - server.publishTx(publishOpts, function(err) { - should.exist(err); - err.message.should.contain('Invalid proposal signature'); - done(); - }); - }); - }); - }); - it('should fail to publish a temporary tx proposal if utxos are locked by other pending proposals', function(done) { - var txp1, txp2; - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, - }], - message: 'some message', - feePerKb: 100e2, - }; - - async.waterfall([ - - function(next) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - next(); - }); - }, - function(next) { - server.createTx(txOpts, next); - }, - function(txp, next) { - txp1 = txp; - server.createTx(txOpts, next); - }, - function(txp, next) { - txp2 = txp; - should.exist(txp1); - should.exist(txp2); - var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, next); - }, - function(txp, next) { - var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, function(err) { - should.exist(err); - err.code.should.equal('UNAVAILABLE_UTXOS'); - next(); - }); - }, - function(next) { - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(1); - next(); - }); - }, - function(next) { - // A new tx proposal should use the next available UTXO - server.createTx(txOpts, next); - }, - function(txp3, next) { - should.exist(txp3); - var publishOpts = helpers.getProposalSignatureOpts(txp3, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, next); - }, - function(txp, next) { - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(2); - next(); - }); - }, - ], function(err) { - should.not.exist(err); - done(); - }); - }); - it('should fail to publish a temporary tx proposal if utxos are already spent', function(done) { - var txp1, txp2; - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, - }], - message: 'some message', - feePerKb: 100e2, - }; - - async.waterfall([ - - function(next) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - next(); - }); - }, - function(next) { - server.createTx(txOpts, next); - }, - function(txp, next) { - txp1 = txp; - server.createTx(txOpts, next); - }, - function(txp, next) { - txp2 = txp; - should.exist(txp1); - should.exist(txp2); - var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, next); - }, - function(txp, next) { - // Sign & Broadcast txp1 - var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H); - server.signTx({ - txProposalId: txp.id, - signatures: signatures, - }, function(err, txp) { - should.not.exist(err); - - helpers.stubBroadcast(); - server.broadcastTx({ - txProposalId: txp.id - }, function(err, txp) { - should.not.exist(err); - should.exist(txp.txid); - txp.status.should.equal('broadcasted'); - next(); - }); - }); - }, - function(next) { - var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, function(err) { - should.exist(err); - err.code.should.equal('UNAVAILABLE_UTXOS'); - next(); - }); - }, - ], function(err) { - should.not.exist(err); - done(); - }); - }); - }); - - describe('Fee levels', function() { - it('should create a tx specifying feeLevel', function(done) { - helpers.stubFeeLevels({ - 1: 400e2, - 2: 200e2, - 6: 180e2, - 24: 90e2, - }); - helpers.stubUtxos(server, wallet, 2, function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, - }], - feeLevel: 'economy', - }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - txp.feePerKb.should.equal(180e2); - txp.feeLevel.should.equal('economy'); - done(); - }); - }); - }); - it('should fail if the specified fee level does not exist', function(done) { - helpers.stubUtxos(server, wallet, 2, function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, - }], - feeLevel: 'madeUpLevel', - }; - server.createTx(txOpts, function(err, txp) { - should.exist(err); - should.not.exist(txp); - err.toString().should.contain('Invalid fee level'); - done(); - }); - }); - }); - it('should assume "normal" fee level if no feeLevel and no feePerKb/fee is specified', function(done) { - helpers.stubFeeLevels({ - 1: 400e2, - 2: 200e2, - 6: 180e2, - 24: 90e2, - }); - helpers.stubUtxos(server, wallet, 2, function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, - }], - }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - txp.feePerKb.should.equal(200e2); - txp.feeLevel.should.equal('normal'); - done(); - }); - }); - }); - }); - it('should generate new change address for each created tx', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8e8, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx1) { - should.not.exist(err); - should.exist(tx1); - server.createTx(txOpts, function(err, tx2) { - should.not.exist(err); - should.exist(tx2); - tx1.changeAddress.address.should.not.equal(tx2.changeAddress.address); - done(); - }); - }); - }); - }); - it('should support creating a tx with no change address', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var max = 3e8 - 7000; // Fees for this tx at 100bits/kB = 7000 sat - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: max, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - var t = txp.getBitcoreTx().toObject(); - t.outputs.length.should.equal(1); - t.outputs[0].satoshis.should.equal(max); - done(); - }); - }); - }); - it('should fail gracefully if unable to reach the blockchain', function(done) { - blockchainExplorer.getUtxos = sinon.stub().callsArgWith(1, 'dummy error'); - server.createAddress({}, function(err, address) { - should.not.exist(err); - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8 - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.exist(err); - err.toString().should.equal('dummy error'); - done(); - }); - }); - }); - it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) { - helpers.stubUtxos(server, wallet, 1, function() { - var bitcoreStub = sinon.stub(Bitcore, 'Transaction'); - bitcoreStub.throws({ - name: 'dummy', - message: 'dummy exception' - }); - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.5e8, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.exist(err); - err.message.should.equal('dummy exception'); - bitcoreStub.restore(); - done(); - }); - }); - }); - it('should fail to create a tx exceeding max size in kb', function(done) { - var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB; - Defaults.MAX_TX_SIZE_IN_KB = 1; - helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 8e8, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('TX_MAX_SIZE_EXCEEDED'); - Defaults.MAX_TX_SIZE_IN_KB = _oldDefault; - done(); - }); - }); - }); - it('should fail with different error for insufficient funds and locked funds', function(done) { - helpers.stubUtxos(server, wallet, [1, 1], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1.1e8, - }], - feePerKb: 100e2, - }; - helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(2e8); - balance.lockedAmount.should.equal(2e8); - txOpts.outputs[0].amount = 0.8e8; - server.createTx(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('LOCKED_FUNDS'); - err.message.should.equal('Funds are locked by pending transaction proposals'); - done(); - }); - }); - }); - }); - }); - it('should fail to create tx for dust amount in outputs', function(done) { - helpers.stubUtxos(server, wallet, 1, function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 20e2, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('DUST_AMOUNT'); - err.message.should.equal('Amount below dust threshold'); - done(); - }); - }); - }); - it('should create tx with 0 change output', function(done) { - helpers.stubUtxos(server, wallet, 1, function() { - var fee = 4100; // The exact fee of the resulting tx - var amount = 1e8 - fee; - - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: amount, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - var bitcoreTx = tx.getBitcoreTx(); - bitcoreTx.outputs.length.should.equal(1); - bitcoreTx.outputs[0].satoshis.should.equal(tx.amount); - done(); - }); - }); - }); - it('should create tx when there is a pending tx and enough UTXOs', function(done) { - helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1.5e8, - }], - feePerKb: 100e2, - }; - helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { - should.exist(tx); - txOpts.outputs[0].amount = 0.8e8; helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { should.exist(tx); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(2); - server.getBalance({}, function(err, balance) { + txOpts.outputs[0].amount = 1.8e8; + server.createTx(txOpts, function(err, tx) { + err.code.should.equal('LOCKED_FUNDS'); + should.not.exist(tx); + server.getPendingTxs({}, function(err, txs) { should.not.exist(err); - balance.totalAmount.should.equal(3.6e8); - balance.lockedAmount.should.equal(3.6e8); - done(); + txs.length.should.equal(1); + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.totalAmount.should.equal(3.6e8); + var amountInputs = _.sum(txs[0].inputs, 'satoshis'); + balance.lockedAmount.should.equal(amountInputs); + balance.lockedAmount.should.be.below(balance.totalAmount); + balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); + done(); + }); }); }); }); }); }); - }); - it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) { - helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1.5e8, - }], - feePerKb: 100e2, + it('should accept a tx proposal signed with a custom key', function(done) { + var reqPrivKey = new Bitcore.PrivateKey(); + var reqPubKey = reqPrivKey.toPublicKey().toString(); + + var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H; + + var accessOpts = { + copayerId: TestData.copayers[0].id44btc, + requestPubKey: reqPubKey, + signature: helpers.signRequestPubKey(reqPubKey, xPrivKey), }; - helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { - should.exist(tx); - txOpts.outputs[0].amount = 1.8e8; - server.createTx(txOpts, function(err, tx) { - err.code.should.equal('LOCKED_FUNDS'); - should.not.exist(tx); - server.getPendingTxs({}, function(err, txs) { + + server.addAccess(accessOpts, function(err) { + should.not.exist(err); + + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: addressStr, + amount: 0.8 * 1e8, + }], + message: 'some message', + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, txp) { should.not.exist(err); - txs.length.should.equal(1); - server.getBalance({}, function(err, balance) { + should.exist(txp); + + var publishOpts = { + txProposalId: txp.id, + proposalSignature: helpers.signMessage(txp.getRawTx(), reqPrivKey), + } + + server.publishTx(publishOpts, function(err) { should.not.exist(err); - balance.totalAmount.should.equal(3.6e8); - var amountInputs = _.sum(txs[0].inputs, 'satoshis'); - balance.lockedAmount.should.equal(amountInputs); - balance.lockedAmount.should.be.below(balance.totalAmount); - balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); - done(); + server.getTx({ + txProposalId: txp.id + }, function(err, x) { + should.not.exist(err); + x.proposalSignature.should.equal(publishOpts.proposalSignature); + x.proposalSignaturePubKey.should.equal(accessOpts.requestPubKey); + x.proposalSignaturePubKeySig.should.equal(accessOpts.signature); + done(); + }); }); }); }); }); }); - }); - it('should accept a tx proposal signed with a custom key', function(done) { - var reqPrivKey = new Bitcore.PrivateKey(); - var reqPubKey = reqPrivKey.toPublicKey().toString(); - - var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H; - - var accessOpts = { - copayerId: TestData.copayers[0].id44btc, - requestPubKey: reqPubKey, - signature: helpers.signRequestPubKey(reqPubKey, xPrivKey), - }; - - server.addAccess(accessOpts, function(err) { - should.not.exist(err); - + it('should be able to send max funds', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, + toAddress: addressStr, + amount: null, }], - message: 'some message', - feePerKb: 100e2, + feePerKb: 10000, + sendMax: true, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + should.not.exist(tx.changeAddress); + tx.amount.should.equal(3e8 - tx.fee); + + var t = tx.getBitcoreTx(); + t.getFee().should.equal(tx.fee); + should.not.exist(t.getChangeOutput()); + t.toObject().inputs.length.should.equal(tx.inputs.length); + t.toObject().outputs[0].satoshis.should.equal(tx.amount); + done(); + }); + }); + }); + it('should shuffle outputs unless specified', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var txOpts = { + outputs: _.times(30, function(i) { + return { + toAddress: addressStr, + amount: (i + 1) * 100e2, + }; + }), + feePerKb: 123e2, }; server.createTx(txOpts, function(err, txp) { should.not.exist(err); should.exist(txp); + var t = txp.getBitcoreTx(); + var changeOutput = t.getChangeOutput().satoshis; + var outputs = _.without(_.pluck(t.outputs, 'satoshis'), changeOutput); - var publishOpts = { - txProposalId: txp.id, - proposalSignature: helpers.signMessage(txp.getRawTx(), reqPrivKey), - } - - server.publishTx(publishOpts, function(err) { + outputs.should.not.deep.equal(_.pluck(txOpts.outputs, 'amount')); + txOpts.noShuffleOutputs = true; + server.createTx(txOpts, function(err, txp) { should.not.exist(err); - server.getTx({ - txProposalId: txp.id - }, function(err, x) { - should.not.exist(err); - x.proposalSignature.should.equal(publishOpts.proposalSignature); - x.proposalSignaturePubKey.should.equal(accessOpts.requestPubKey); - x.proposalSignaturePubKeySig.should.equal(accessOpts.signature); - done(); - }); + should.exist(txp); + + t = txp.getBitcoreTx(); + changeOutput = t.getChangeOutput().satoshis; + outputs = _.without(_.pluck(t.outputs, 'satoshis'), changeOutput); + + outputs.should.deep.equal(_.pluck(txOpts.outputs, 'amount')); + done(); }); }); }); }); }); - it('should be able to send max funds', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: null, - }], - feePerKb: 10000, - sendMax: true, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - should.not.exist(tx.changeAddress); - tx.amount.should.equal(3e8 - tx.fee); - - var t = tx.getBitcoreTx(); - t.getFee().should.equal(tx.fee); - should.not.exist(t.getChangeOutput()); - t.toObject().inputs.length.should.equal(tx.inputs.length); - t.toObject().outputs[0].satoshis.should.equal(tx.amount); - done(); - }); - }); - }); - it('should shuffle outputs unless specified', function(done) { - helpers.stubUtxos(server, wallet, 1, function() { - var txOpts = { - outputs: _.times(30, function(i) { - return { - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: (i + 1) * 100e2, - }; - }), - feePerKb: 123e2, - }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - var t = txp.getBitcoreTx(); - var changeOutput = t.getChangeOutput().satoshis; - var outputs = _.without(_.pluck(t.outputs, 'satoshis'), changeOutput); - - outputs.should.not.deep.equal(_.pluck(txOpts.outputs, 'amount')); - txOpts.noShuffleOutputs = true; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - - t = txp.getBitcoreTx(); - changeOutput = t.getChangeOutput().satoshis; - outputs = _.without(_.pluck(t.outputs, 'satoshis'), changeOutput); - - outputs.should.deep.equal(_.pluck(txOpts.outputs, 'amount')); - done(); - }); - }); - }); - }); }); describe('Backoff time', function(done) {