make select fn async

This commit is contained in:
Ivan Socolsky 2016-03-07 12:30:10 -03:00
parent 60f6300fbc
commit d3faad0639
2 changed files with 277 additions and 304 deletions

View File

@ -1397,7 +1397,7 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
return (size / 1000.).toFixed(4) + 'kB';
};
function select(utxos) {
function select(utxos, cb) {
var txpAmount = txp.getTotalAmount();
var baseTxpSize = txp.getEstimatedSize();
var baseTxpFee = baseTxpSize * txp.feePerKb / 1000.;
@ -1408,11 +1408,11 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
var netValueInUtxos = totalValueInUtxos - baseTxpFee - (utxos.length * feePerInput);
if (totalValueInUtxos < txpAmount) {
log.debug('Total value in all utxos (' + formatAmount(totalValueInUtxos) + ') is insufficient to cover for txp amount (' + formatAmount(txpAmount) + ')');
return Errors.INSUFFICIENT_FUNDS;
return cb(Errors.INSUFFICIENT_FUNDS);
}
if (netValueInUtxos < txpAmount) {
log.debug('Value after fees in all utxos (' + formatAmount(netValueInUtxos) + ') is insufficient to cover for txp amount (' + formatAmount(txpAmount) + ')');
return Errors.INSUFFICIENT_FUNDS_FOR_FEE;
return cb(Errors.INSUFFICIENT_FUNDS_FOR_FEE);
}
var bigInputThreshold = txpAmount * Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR + (baseTxpFee + feePerInput);
@ -1499,10 +1499,10 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
if (_.isEmpty(selected)) {
log.debug('Could not find enough funds within this utxo subset');
return error || Errors.INSUFFICIENT_FUNDS_FOR_FEE;
return cb(error || Errors.INSUFFICIENT_FUNDS_FOR_FEE);
}
return selected;
return cb(null, selected);
};
log.debug('Selecting inputs for a ' + formatAmount(txp.getTotalAmount()) + ' txp');
@ -1535,11 +1535,18 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
log.debug('Considering ' + utxos.length + ' utxos (' + formatInputs(utxos) + ')');
var groups = [6, 1];
if (!txp.excludeUnconfirmedUtxos) groups.push(0);
var inputs = [];
var groups = [6, 1, 0];
var error;
var selectionError;
var i = 0;
var lastGroupLength;
_.each(groups, function(group) {
async.whilst(function() {
return i < groups.length && _.isEmpty(inputs);
}, function(next) {
var group = groups[i++];
var candidateUtxos = _.filter(utxos, function(utxo) {
return utxo.confirmations >= group;
});
@ -1549,39 +1556,42 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
// If this group does not have any new elements, skip it
if (lastGroupLength === candidateUtxos.length) {
log.debug('This group is identical to the one already explored');
return;
return next();
}
log.debug('Candidate utxos: ' + formatInputs(candidateUtxos));
lastGroupLength = candidateUtxos.length;
var result = select(candidateUtxos);
if (result && !_.isArray(result)) {
error = result;
select(candidateUtxos, function(err, selected) {
if (err) {
log.debug('No inputs selected on this group: ', err);
selectionError = err;
return next();
}
selectionError = null;
inputs = selected;
log.debug('Selected inputs from this group: ' + formatInputs(inputs));
return next();
});
}, function(err) {
if (err) return cb(err);
if (selectionError || _.isEmpty(inputs)) return cb(selectionError || new Error('Could not select tx inputs'));
txp.setInputs(inputs);
var err = self._checkTxAndEstimateFee(txp);
if (!err) {
log.debug('Successfully built transaction. Total fees: ', formatAmount(txp.fee));
} else {
inputs = result;
error = null;
log.warn('Error building transaction', err);
}
log.debug('Selected inputs from this group: ' + formatInputs(inputs));
if (!_.isEmpty(inputs)) return false;
return cb(err);
});
if (error) return cb(error);
txp.setInputs(inputs);
var err = self._checkTxAndEstimateFee(txp);
if (!err) {
log.debug('Successfully built transaction. Total fees: ', formatAmount(txp.fee));
} else {
log.warn('Error building transaction', err);
}
return cb(err);
});
};

View File

@ -3169,7 +3169,7 @@ describe('Wallet service', function() {
});
});
describe('UTXO selection', function() {
describe('UTXO Selection', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
@ -3179,32 +3179,248 @@ describe('Wallet service', function() {
});
});
it('should create a tx', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
it('should select a single utxo if within thresholds relative to tx amount', function(done) {
helpers.stubUtxos(server, wallet, [1, '350bit', '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8 * 1e8,
amount: 200e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
console.log('*** [server.js ln3193] err:', err); // TODO
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(35000);
done();
});
});
});
it('should select smaller utxos if within fee constraints', function(done) {
helpers.stubUtxos(server, wallet, [1, '800bit', '800bit', '800bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 2000e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(3);
_.all(txp.inputs, function(input) {
return input == 100e2;
});
done();
});
});
});
it('should select smallest big utxo if small utxos are insufficient', function(done) {
helpers.stubUtxos(server, wallet, [3, 1, 2, '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 300e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
done();
});
});
});
it('should account for fee when selecting smallest big utxo', function(done) {
// log.level = 'debug';
var _old = Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR;
Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR = 2;
// The 605 bits input cannot be selected even if it is > 2 * tx amount
// because it cannot cover for fee on its own.
helpers.stubUtxos(server, wallet, [1, '605bit', '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 300e2,
}],
feePerKb: 1200e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR = _old;
done();
});
});
});
it('should select smallest big utxo if small utxos exceed maximum fee', function(done) {
helpers.stubUtxos(server, wallet, [3, 1, 2].concat(_.times(20, function() {
return '1000bit';
})), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 12000e2,
}],
feePerKb: 20e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
done();
});
});
});
it('should select smallest big utxo if small utxos are below accepted ratio of txp amount', function(done) {
helpers.stubUtxos(server, wallet, [9, 1, 1, 0.5, 0.2, 0.2, 0.2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 3e8,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(9e8);
done();
});
});
});
it('should not fail with tx exceeded max size if there is at least 1 big input', function(done) {
var _old1 = Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR;
var _old2 = Defaults.MAX_TX_SIZE_IN_KB;
Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR = 0.0001;
Defaults.MAX_TX_SIZE_IN_KB = 3;
helpers.stubUtxos(server, wallet, [100].concat(_.range(1, 20, 0)), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 15e8,
}],
feePerKb: 120e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(100e8);
Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR = _old1;
Defaults.MAX_TX_SIZE_IN_KB = _old2;
done();
});
});
});
it('should ignore utxos not contributing enough to cover increase in fee', function(done) {
helpers.stubUtxos(server, wallet, ['100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 80e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(3);
txOpts.feePerKb = 120e2;
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
done();
});
});
});
});
it('should fail to select utxos if not enough to cover tx amount', function(done) {
helpers.stubUtxos(server, wallet, ['100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 400e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.code.should.equal('INSUFFICIENT_FUNDS');
done();
});
});
});
it('should fail to select utxos if not enough to cover fees', function(done) {
helpers.stubUtxos(server, wallet, ['100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 299e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE');
done();
});
});
});
it('should prefer a higher fee (breaking all limits) if inputs have 6+ confirmations', function(done) {
helpers.stubUtxos(server, wallet, ['2c 2000bit'].concat(_.times(20, function() {
return '100bit';
})), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1500e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
_.all(txp.inputs, function(input) {
return input == 100e2;
});
done();
});
});
});
it('should select unconfirmed utxos if not enough confirmed utxos', function(done) {
// log.level = 'debug';
helpers.stubUtxos(server, wallet, ['u 1btc', '0.5btc'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8e8,
}],
feePerKb: 100e2,
};
server.createTx(txOpts, function(err, tx) {
server.createTx(txOpts, function(err, txp) {
console.log('*** [server.js ln3417] err:', err); // TODO
should.not.exist(err);
should.exist(tx);
tx.walletM.should.equal(2);
tx.walletN.should.equal(3);
tx.requiredRejections.should.equal(2);
tx.requiredSignatures.should.equal(2);
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));
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
done();
});
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
done();
});
});
});
@ -5731,257 +5947,4 @@ describe('Wallet service', function() {
});
});
});
describe('UTXO Selection', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should select a single utxo if within thresholds relative to tx amount', function(done) {
helpers.stubUtxos(server, wallet, [1, '350bit', '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(35000);
done();
});
});
});
it('should select smaller utxos if within fee constraints', function(done) {
helpers.stubUtxos(server, wallet, [1, '800bit', '800bit', '800bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 2000e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(3);
_.all(txp.inputs, function(input) {
return input == 100e2;
});
done();
});
});
});
it('should select smallest big utxo if small utxos are insufficient', function(done) {
helpers.stubUtxos(server, wallet, [3, 1, 2, '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 300e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
done();
});
});
});
it('should account for fee when selecting smallest big utxo', function(done) {
// log.level = 'debug';
var _old = Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR;
Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR = 2;
// The 605 bits input cannot be selected even if it is > 2 * tx amount
// because it cannot cover for fee on its own.
helpers.stubUtxos(server, wallet, [1, '605bit', '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 300e2,
}],
feePerKb: 1200e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR = _old;
done();
});
});
});
it('should select smallest big utxo if small utxos exceed maximum fee', function(done) {
helpers.stubUtxos(server, wallet, [3, 1, 2].concat(_.times(20, function() {
return '1000bit';
})), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 12000e2,
}],
feePerKb: 20e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
done();
});
});
});
it('should select smallest big utxo if small utxos are below accepted ratio of txp amount', function(done) {
helpers.stubUtxos(server, wallet, [9, 1, 1, 0.5, 0.2, 0.2, 0.2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 3e8,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(9e8);
done();
});
});
});
it('should not fail with tx exceeded max size if there is at least 1 big input', function(done) {
var _old1 = Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR;
var _old2 = Defaults.MAX_TX_SIZE_IN_KB;
Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR = 0.0001;
Defaults.MAX_TX_SIZE_IN_KB = 3;
helpers.stubUtxos(server, wallet, [100].concat(_.range(1, 20, 0)), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 15e8,
}],
feePerKb: 120e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(100e8);
Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR = _old1;
Defaults.MAX_TX_SIZE_IN_KB = _old2;
done();
});
});
});
it('should ignore utxos not contributing enough to cover increase in fee', function(done) {
helpers.stubUtxos(server, wallet, ['100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 80e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(3);
txOpts.feePerKb = 120e2;
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
done();
});
});
});
});
it('should fail to select utxos if not enough to cover tx amount', function(done) {
helpers.stubUtxos(server, wallet, ['100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 400e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.code.should.equal('INSUFFICIENT_FUNDS');
done();
});
});
});
it('should fail to select utxos if not enough to cover fees', function(done) {
helpers.stubUtxos(server, wallet, ['100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 299e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE');
done();
});
});
});
it('should prefer a higher fee (breaking all limits) if inputs have 6+ confirmations', function(done) {
helpers.stubUtxos(server, wallet, ['2c 2000bit'].concat(_.times(20, function() {
return '100bit';
})), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1500e2,
}],
feePerKb: 10e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
_.all(txp.inputs, function(input) {
return input == 100e2;
});
done();
});
});
});
it('should select unconfirmed utxos if not enough confirmed utxos', function(done) {
helpers.stubUtxos(server, wallet, ['u 1btc', '0.5btc'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8e8,
}],
feePerKb: 100e2,
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
done();
});
});
});
});
});