Merge pull request #304 from isocolsky/improve_fees

Adaptive fees
This commit is contained in:
Matias Alejo Garcia 2015-07-28 11:11:00 -03:00
commit 33e9c06fab
5 changed files with 81 additions and 41 deletions

View File

@ -64,6 +64,7 @@ TxProposal.create = function(opts) {
x.inputPaths = [];
x.requiredSignatures = opts.requiredSignatures;
x.requiredRejections = opts.requiredRejections;
x.walletN = opts.walletN;
x.status = 'pending';
x.actions = [];
x.fee = null;
@ -100,6 +101,7 @@ TxProposal.fromObj = function(obj) {
x.inputs = obj.inputs;
x.requiredSignatures = obj.requiredSignatures;
x.requiredRejections = obj.requiredRejections;
x.walletN = obj.walletN;
x.status = obj.status;
x.txid = obj.txid;
x.broadcastedOn = obj.broadcastedOn;
@ -169,6 +171,30 @@ TxProposal.prototype.getRawTx = function() {
return t.uncheckedSerialize();
};
TxProposal.prototype.getEstimatedSize = function() {
// Note: found empirically based on all multisig P2SH inputs and within m & n allowed limits.
var safetyMargin = 0.05;
var walletM = this.requiredSignatures;
var overhead = 4 + 4 + 9 + 9;
var inputSize = walletM * 72 + this.walletN * 36 + 44;
var outputSize = 34;
var nbInputs = this.inputs.length;
var nbOutputs = (_.isArray(this.outputs) ? this.outputs.length : 1) + 1;
var size = overhead + inputSize * nbInputs + outputSize * nbOutputs;
return parseInt((size * (1 + safetyMargin)).toFixed(0));
};
TxProposal.prototype.estimateFee = function() {
var size = this.getEstimatedSize();
var fee = this.feePerKb * size / 1000;
// Round up to nearest bit
this.fee = parseInt((Math.ceil(fee / 100) * 100).toFixed(0));
};
/**
* getTotalAmount

View File

@ -692,7 +692,7 @@ WalletService.prototype._totalizeUtxos = function(utxos) {
};
WalletService.prototype._computeKbToSendMax = function(utxos, amount, cb) {
WalletService.prototype._computeBytesToSendMax = function(utxos, cb) {
var self = this;
var unlockedUtxos = _.reject(utxos, 'locked');
@ -701,17 +701,16 @@ WalletService.prototype._computeKbToSendMax = function(utxos, amount, cb) {
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
var t = WalletUtils.newBitcoreTransaction();
try {
_.each(unlockedUtxos, function(i) {
t.from(i, i.publicKeys, wallet.m);
});
t.to(utxos[0].address, amount);
var sizeInKb = Math.ceil(t._estimateSize() / 1000);
return cb(null, sizeInKb);
} catch (ex) {
return cb(ex);
}
var txp = Model.TxProposal.create({
walletId: self.walletId,
requiredSignatures: wallet.m,
walletN: wallet.n,
});
txp.inputs = unlockedUtxos;
var size = txp.getEstimatedSize();
return cb(null, size);
});
};
@ -744,11 +743,11 @@ WalletService.prototype.getBalance = function(opts, cb) {
balance.byAddress = _.values(byAddress);
self._computeKbToSendMax(utxos, balance.availableAmount, function(err, sizeInKb) {
self._computeBytesToSendMax(utxos, function(err, size) {
if (err) {
log.error('Could not compute fees needed to transfer max amount', err);
}
balance.totalKbToSendMax = sizeInKb || 0;
balance.totalBytesToSendMax = size || 0;
return cb(null, balance);
});
});
@ -796,7 +795,7 @@ WalletService.prototype.getFeeLevels = function(opts, cb) {
var samplePoints = _.uniq(_.pluck(levels, 'nbBlocks'));
self._sampleFeeLevels(network, samplePoints, function(err, feeSamples) {
var values = _.map(levels, function(level) {
var result = {
var result = {
level: level.name,
};
if (err || feeSamples[level.nbBlocks] < 0) {
@ -875,9 +874,12 @@ WalletService.prototype._selectTxInputs = function(txp, cb) {
if (total >= txp.getTotalAmount()) {
try {
txp.setInputs(selected);
txp.estimateFee();
bitcoreTx = txp.getBitcoreTx();
bitcoreError = bitcoreTx.getSerializationError({
disableIsFullySigned: true,
disableSmallFees: true,
disableLargeFees: true,
});
if (!bitcoreError) {
txp.fee = bitcoreTx.getFee();
@ -901,7 +903,6 @@ WalletService.prototype._selectTxInputs = function(txp, cb) {
});
};
WalletService.prototype._canCreateTx = function(copayerId, cb) {
var self = this;
self.storage.fetchLastTxs(self.walletId, copayerId, 5 + WalletService.backoffOffset, function(err, txs) {
@ -972,7 +973,7 @@ WalletService.prototype.createTx = function(opts, cb) {
valid: false
})) return;
var feePerKb = opts.feePerKb || 10000;
var feePerKb = opts.feePerKb || WalletUtils.DEFAULT_FEE_PER_KB;
if (feePerKb < WalletUtils.MIN_FEE_PER_KB || feePerKb > WalletUtils.MAX_FEE_PER_KB)
return cb(new ClientError('Invalid fee per KB value'));
@ -1047,6 +1048,7 @@ WalletService.prototype.createTx = function(opts, cb) {
payProUrl: opts.payProUrl,
requiredSignatures: wallet.m,
requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1),
walletN: wallet.n,
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
});

View File

@ -2,7 +2,7 @@
"name": "bitcore-wallet-service",
"description": "A service for Mutisig HD Bitcoin Wallets",
"author": "BitPay Inc",
"version": "0.0.46",
"version": "0.0.47",
"keywords": [
"bitcoin",
"copay",
@ -20,7 +20,7 @@
"dependencies": {
"async": "^0.9.0",
"bitcore": "^0.12.9",
"bitcore-wallet-utils": "^0.0.22",
"bitcore-wallet-utils": "^0.0.23",
"body-parser": "^1.11.0",
"coveralls": "^2.11.2",
"email-validator": "^1.0.1",

View File

@ -1370,7 +1370,7 @@ describe('Wallet service', function() {
balance.totalAmount.should.equal(helpers.toSatoshi(6));
balance.lockedAmount.should.equal(0);
balance.availableAmount.should.equal(helpers.toSatoshi(6));
balance.totalKbToSendMax.should.equal(1);
balance.totalBytesToSendMax.should.equal(578);
balance.totalConfirmedAmount.should.equal(helpers.toSatoshi(4));
balance.lockedConfirmedAmount.should.equal(0);
@ -1396,7 +1396,7 @@ describe('Wallet service', function() {
balance.totalAmount.should.equal(0);
balance.lockedAmount.should.equal(0);
balance.availableAmount.should.equal(0);
balance.totalKbToSendMax.should.equal(0);
balance.totalBytesToSendMax.should.equal(0);
should.exist(balance.byAddress);
balance.byAddress.length.should.equal(0);
done();
@ -1412,7 +1412,7 @@ describe('Wallet service', function() {
balance.totalAmount.should.equal(0);
balance.lockedAmount.should.equal(0);
balance.availableAmount.should.equal(0);
balance.totalKbToSendMax.should.equal(0);
balance.totalBytesToSendMax.should.equal(0);
should.exist(balance.byAddress);
balance.byAddress.length.should.equal(0);
done();
@ -1440,7 +1440,7 @@ describe('Wallet service', function() {
should.exist(balance);
balance.totalAmount.should.equal(helpers.toSatoshi(9));
balance.lockedAmount.should.equal(0);
balance.totalKbToSendMax.should.equal(2);
balance.totalBytesToSendMax.should.equal(1535);
done();
});
});
@ -1516,10 +1516,10 @@ describe('Wallet service', function() {
}));
fees.priority.feePerKB.should.equal(50000);
should.not.exist(fees.priority.nbBlocks);
fees.normal.feePerKB.should.equal(18000);
fees.normal.nbBlocks.should.equal(4);
fees.economy.feePerKB.should.equal(0);
fees.economy.nbBlocks.should.equal(12);
done();
@ -1613,7 +1613,8 @@ describe('Wallet service', function() {
tx.isAccepted().should.equal.false;
tx.isRejected().should.equal.false;
tx.amount.should.equal(helpers.toSatoshi(80));
tx.fee.should.equal(Bitcore.Transaction.FEE_PER_KB);
var estimatedFee = WalletUtils.DEFAULT_FEE_PER_KB * 400 / 1000; // fully signed tx should have about 400 bytes
tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
@ -1836,8 +1837,8 @@ describe('Wallet service', function() {
var txOpts = helpers.createSimpleProposalOpts('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);
var estimatedFee = WalletUtils.DEFAULT_FEE_PER_KB * 1300 / 1000; // fully signed tx should have about 1300 bytes
tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee);
done();
});
});
@ -1845,16 +1846,18 @@ describe('Wallet service', function() {
it('should be possible to use a smaller fee', function(done) {
helpers.stubUtxos(server, wallet, 1, function() {
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.99995, null, TestData.copayers[0].privKey_1H_0);
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.99995, null, TestData.copayers[0].privKey_1H_0, 80000);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INSUFFICIENTFUNDS');
var txOpts = helpers.createSimpleProposalOpts('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);
var estimatedFee = 5000 * 400 / 1000; // fully signed tx should have about 400 bytes
tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee);
// Sign it to make sure Bitcore doesn't complain about the fees
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey);
server.signTx({
txProposalId: tx.id,
signatures: signatures,
@ -1881,11 +1884,11 @@ 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 = 10000 / 1e8;
var change = 0.00000001;
var fee = 4095 / 1e8; // The exact fee of the resulting tx
var change = 100 / 1e8; // Below dust
var amount = 1 - fee - change;
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount, null, TestData.copayers[0].privKey_1H_0);
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount, null, TestData.copayers[0].privKey_1H_0, 10000);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('DUSTAMOUNT');
@ -1918,7 +1921,7 @@ describe('Wallet service', function() {
it('should create tx with 0 change output', function(done) {
helpers.stubUtxos(server, wallet, [1], function() {
var fee = Bitcore.Transaction.FEE_PER_KB / 1e8;
var fee = 4100 / 1e8; // The exact fee of the resulting tx
var amount = 1 - fee;
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount, null, TestData.copayers[0].privKey_1H_0);
@ -2090,17 +2093,21 @@ describe('Wallet service', function() {
balance.totalAmount.should.equal(helpers.toSatoshi(9));
balance.lockedAmount.should.equal(0);
balance.availableAmount.should.equal(helpers.toSatoshi(9));
balance.totalKbToSendMax.should.equal(3);
var max = (balance.totalAmount - balance.lockedAmount) - (balance.totalKbToSendMax * 10000);
balance.totalBytesToSendMax.should.equal(2896);
var sizeInKB = balance.totalBytesToSendMax / 1000;
var fee = parseInt((Math.ceil(sizeInKB * 10000 / 100) * 100).toFixed(0));
var max = balance.availableAmount - fee;
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', max / 1e8, null, TestData.copayers[0].privKey_1H_0);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.amount.should.equal(max);
tx.fee.should.equal(3 * 10000);
var estimatedFee = 2896 * 10000 / 1000;
tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee);
server.getBalance({}, function(err, balance) {
should.not.exist(err);
balance.lockedAmount.should.equal(helpers.toSatoshi(9));
balance.availableAmount.should.equal(0);
done();
});
});
@ -2117,14 +2124,17 @@ describe('Wallet service', function() {
balance.totalAmount.should.equal(helpers.toSatoshi(9));
balance.lockedAmount.should.equal(helpers.toSatoshi(4));
balance.availableAmount.should.equal(helpers.toSatoshi(5));
balance.totalKbToSendMax.should.equal(2);
var max = (balance.totalAmount - balance.lockedAmount) - (balance.totalKbToSendMax * 2000);
balance.totalBytesToSendMax.should.equal(1653);
var sizeInKB = balance.totalBytesToSendMax / 1000;
var fee = parseInt((Math.ceil(sizeInKB * 2000 / 100) * 100).toFixed(0));
var max = balance.availableAmount - fee;
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', max / 1e8, null, TestData.copayers[0].privKey_1H_0, 2000);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.amount.should.equal(max);
tx.fee.should.equal(2 * 2000);
var estimatedFee = 1653 * 2000 / 1000;
tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee);
server.getBalance({}, function(err, balance) {
should.not.exist(err);
balance.lockedAmount.should.equal(helpers.toSatoshi(9));

View File

@ -197,9 +197,11 @@ var aTXP = function(type) {
"inputPaths": ["m/2147483647/0/1"],
"requiredSignatures": 2,
"requiredRejections": 1,
"walletN": 2,
"status": "pending",
"actions": [],
"outputOrder": [0, 1],
"fee": 10000,
};
if (type == TxProposal.Types.MULTIPLEOUTPUTS) {
txp.outputs = [{