diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index c102339..97ae8fb 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -37,6 +37,7 @@ TxProposal.create = function(opts) { x.outputOrder = _.shuffle(_.range(2)); x.fee = null; x.network = Bitcore.Address(x.toAddress).toObject().network; + x.feePerKb = opts.feePerKb; return x; }; @@ -68,6 +69,7 @@ TxProposal.fromObj = function(obj) { x.outputOrder = obj.outputOrder; x.fee = obj.fee; x.network = obj.network; + x.feePerKb = obj.feePerKb; return x; }; diff --git a/lib/server.js b/lib/server.js index bdad5ba..740107d 100644 --- a/lib/server.js +++ b/lib/server.js @@ -731,6 +731,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { return cb(); } } catch (ex) { + log.error('Error building Bitcore transaction', ex); return cb(ex); } } @@ -785,7 +786,8 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { * @param {number} opts.amount - Amount to transfer in satoshi. * @param {string} opts.message - A message to attach to this transaction. * @param {string} opts.proposalSignature - S(toAddress|amount|message|payProUrl). Used by other copayers to verify the proposal. - * @param {string} opts.payProUrl - Options: Paypro URL for peers to verify TX + * @param {string} opts.feePerKb - Optional: Use an alternative fee per KB for this TX + * @param {string} opts.payProUrl - Optional: Paypro URL for peers to verify TX * @returns {TxProposal} Transaction proposal. */ WalletService.prototype.createTx = function(opts, cb) { @@ -794,6 +796,10 @@ WalletService.prototype.createTx = function(opts, cb) { if (!Utils.checkRequired(opts, ['toAddress', 'amount', 'proposalSignature'])) return cb(new ClientError('Required argument missing')); + var feePerKb = opts.feePerKb || 10000; + if (feePerKb < WalletUtils.MIN_FEE_PER_KB || feePerKb > WalletUtils.MAX_FEE_PER_KB) + return cb(new ClientError('Invalid fee per KB value')); + self._runLocked(cb, function(cb) { self.getWallet({}, function(err, wallet) { if (err) return cb(err); @@ -825,7 +831,6 @@ WalletService.prototype.createTx = function(opts, cb) { if (opts.amount < Bitcore.Transaction.DUST_AMOUNT) return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); - var changeAddress = wallet.createAddress(true); var txp = Model.TxProposal.create({ @@ -835,6 +840,7 @@ WalletService.prototype.createTx = function(opts, cb) { amount: opts.amount, message: opts.message, proposalSignature: opts.proposalSignature, + feePerKb: feePerKb, payProUrl: opts.payProUrl, changeAddress: changeAddress, requiredSignatures: wallet.m, diff --git a/package.json b/package.json index ebb42bd..e88810a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bitcore-wallet-service", "description": "A service for Mutisig HD Bitcoin Wallets", "author": "BitPay Inc", - "version": "0.0.33", + "version": "0.0.34", "keywords": [ "bitcoin", "copay", @@ -19,8 +19,9 @@ }, "dependencies": { "async": "^0.9.0", - "bitcore": "^0.11.6", - "bitcore-wallet-utils": "0.0.13", + "bitcore": "git://github.com/bitpay/bitcore.git#a4ac3f50d300b3f89fad02f9e38fc536ac90abdc", + "bitcore-explorers": "^0.10.3", + "bitcore-wallet-utils": "0.0.15", "body-parser": "^1.11.0", "coveralls": "^2.11.2", "email-validator": "^1.0.1", @@ -61,14 +62,11 @@ "test": "./node_modules/.bin/mocha", "coveralls": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, - "contributors": [ - { - "name": "Ivan Socolsky", - "email": "ivan@bitpay.com" - }, - { - "name": "Matias Alejo Garcia", - "email": "ematiu@gmail.com" - } - ] + "contributors": [{ + "name": "Ivan Socolsky", + "email": "ivan@bitpay.com" + }, { + "name": "Matias Alejo Garcia", + "email": "ematiu@gmail.com" + }] } diff --git a/test/integration/server.js b/test/integration/server.js index ee38dc2..ac54ffa 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -184,13 +184,15 @@ helpers.stubAddressActivity = function(activeAddresses) { helpers.clientSign = WalletUtils.signTxp; -helpers.createProposalOpts = function(toAddress, amount, message, signingKey) { +helpers.createProposalOpts = function(toAddress, amount, message, signingKey, feePerKb) { var opts = { toAddress: toAddress, amount: helpers.toSatoshi(amount), message: message, proposalSignature: null, }; + if (feePerKb) opts.feePerKb = feePerKb; + var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message); try { opts.proposalSignature = WalletUtils.signMessage(hash, signingKey); @@ -1322,7 +1324,6 @@ describe('Wallet service', function() { }); }); - it('should fail to create tx with invalid proposal signature', function(done) { helpers.stubUtxos(server, wallet, [100, 200], function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, null, 'dummy'); @@ -1409,8 +1410,8 @@ describe('Wallet service', function() { }); it('should fail to create tx when insufficient funds for fee', function(done) { - helpers.stubUtxos(server, wallet, [100], function() { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 100, null, TestData.copayers[0].privKey_1H_0); + helpers.stubUtxos(server, wallet, 0.048222, function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.048200, null, TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { should.exist(err); err.code.should.equal('INSUFFICIENTFUNDS'); @@ -1420,6 +1421,42 @@ describe('Wallet service', function() { }); }); + it('should scale fees according to tx size', function(done) { + helpers.stubUtxos(server, wallet, [1, 1, 1, 1], function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 3.5, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + tx.getBitcoreTx()._estimateSize().should.be.within(1001, 1999); + tx.fee.should.equal(20000); + done(); + }); + }); + }); + + it('should be possible to use a smaller fee', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.99995, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('INSUFFICIENTFUNDS'); + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.99995, null, TestData.copayers[0].privKey_1H_0, 5000); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + tx.fee.should.equal(5000); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + // Sign it to make sure Bitcore doesn't complain about the fees + server.signTx({ + txProposalId: tx.id, + signatures: signatures, + }, function(err) { + should.not.exist(err); + done(); + }); + }); + }); + }); + }); + it('should fail to create tx for dust amount', function(done) { helpers.stubUtxos(server, wallet, [1], function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.00000001, null, TestData.copayers[0].privKey_1H_0); @@ -1434,7 +1471,7 @@ describe('Wallet service', function() { it('should fail to create tx that would return change for dust amount', function(done) { helpers.stubUtxos(server, wallet, [1], function() { - var fee = Bitcore.Transaction.FEE_PER_KB / 1e8; + var fee = 10000 / 1e8; var change = 0.00000001; var amount = 1 - fee - change; @@ -2446,7 +2483,7 @@ describe('Wallet service', function() { helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; - helpers.stubUtxos(server, wallet, helpers.toSatoshi(_.range(4)), function() { + helpers.stubUtxos(server, wallet, _.range(4), function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.01, null, TestData.copayers[0].privKey_1H_0); async.eachSeries(_.range(3), function(i, next) { server.createTx(txOpts, function(err, tx) {