smart coin selection.

This commit is contained in:
Christopher Jeffrey 2017-02-22 09:56:24 -08:00
parent cef85d7294
commit 304f0e7e75
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
4 changed files with 186 additions and 9 deletions

View File

@ -311,6 +311,22 @@ HTTPServer.prototype._init = function _init() {
enforce(util.isUInt32(options.blocks), 'Blocks must be a number.');
}
if (params.selection != null) {
options.selection = params.selection;
enforce(typeof options.selection === 'string',
'selection must be a string.');
}
if (params.smart != null) {
if (typeof params.smart === 'string') {
options.smart = Boolean(params.smart);
} else {
options.smart = params.smart;
enforce(typeof options.smart === 'boolean',
'smart must be a boolean.');
}
}
if (params.subtractFee != null) {
if (typeof params.subtractFee === 'number') {
options.subtractFee = params.subtractFee;

View File

@ -379,7 +379,7 @@ TXDB.prototype.removeInput = function removeInput(tx, index) {
* @returns {Boolean}
*/
TXDB.prototype.resolveInput = co(function* resolveInput(tx, index, height, path) {
TXDB.prototype.resolveInput = co(function* resolveInput(tx, index, height, path, own) {
var hash = tx.hash('hex');
var spent = yield this.getSpent(hash, index);
var stx, credit;
@ -399,6 +399,7 @@ TXDB.prototype.resolveInput = co(function* resolveInput(tx, index, height, path)
// Crete the credit and add the undo coin.
credit = Credit.fromTX(tx, index, height);
credit.own = own;
this.spendCredit(credit, stx.tx, spent.index);
@ -801,6 +802,7 @@ TXDB.prototype.insert = co(function* insert(wtx, block) {
var hash = wtx.hash;
var height = block ? block.height : -1;
var details = new Details(this, wtx, block);
var own = false;
var updated = false;
var i, input, output, coin;
var prevout, credit, path, account;
@ -878,6 +880,7 @@ TXDB.prototype.insert = co(function* insert(wtx, block) {
}
updated = true;
own = true;
}
}
@ -893,12 +896,13 @@ TXDB.prototype.insert = co(function* insert(wtx, block) {
// Attempt to resolve an input we
// did not know was ours at the time.
if (yield this.resolveInput(tx, i, height, path)) {
if (yield this.resolveInput(tx, i, height, path, own)) {
updated = true;
continue;
}
credit = Credit.fromTX(tx, i, height);
credit.own = own;
this.pending.coin++;
this.pending.unconfirmed += output.value;
@ -2809,6 +2813,7 @@ function Credit(coin, spent) {
this.coin = coin || new Coin();
this.spent = spent || false;
this.own = false;
}
/**
@ -2821,6 +2826,12 @@ Credit.prototype.fromRaw = function fromRaw(data) {
var br = new BufferReader(data);
this.coin.fromReader(br);
this.spent = br.readU8() === 1;
this.own = true;
// Note: soft-fork
if (br.left() > 0)
this.own = br.readU8() === 1;
return this;
};
@ -2840,7 +2851,7 @@ Credit.fromRaw = function fromRaw(data) {
*/
Credit.prototype.getSize = function getSize() {
return this.coin.getSize() + 1;
return this.coin.getSize() + 2;
};
/**
@ -2853,6 +2864,7 @@ Credit.prototype.toRaw = function toRaw() {
var bw = new StaticWriter(size);
this.coin.toWriter(bw);
bw.writeU8(this.spent ? 1 : 0);
bw.writeU8(this.own ? 1 : 0);
return bw.render();
};
@ -2867,6 +2879,7 @@ Credit.prototype.toRaw = function toRaw() {
Credit.prototype.fromTX = function fromTX(tx, index, height) {
this.coin.fromTX(tx, index, height);
this.spent = false;
this.own = false;
return this;
};

View File

@ -1421,15 +1421,17 @@ Wallet.prototype._fund = co(function* fund(mtx, options) {
if (!account.initialized)
throw new Error('Account is not initialized.');
coins = yield this.getCoins(options.account);
rate = options.rate;
if (rate == null)
rate = yield this.db.estimateFee();
// Don't use any locked coins.
coins = this.txdb.filterLocked(coins);
if (options.smart) {
coins = yield this.getSmartCoins(options.account);
} else {
coins = yield this.getCoins(options.account);
coins = this.txdb.filterLocked(coins);
}
yield mtx.fund(coins, {
selection: options.selection,
@ -2373,6 +2375,58 @@ Wallet.prototype.getCoins = co(function* getCoins(acct) {
return yield this.txdb.getCoins(account);
});
/**
* Get all available credits.
* @param {(String|Number)?} account
* @returns {Promise} - Returns {@link Credit}[].
*/
Wallet.prototype.getCredits = co(function* getCredits(acct) {
var account = yield this.ensureIndex(acct);
return yield this.txdb.getCredits(account);
});
/**
* Get "smart" coins.
* @param {(String|Number)?} account
* @returns {Promise} - Returns {@link Coin}[].
*/
Wallet.prototype.getSmartCoins = co(function* getSmartCoins(acct) {
var credits = yield this.getCredits(acct);
var coins = [];
var i, credit, coin;
for (i = 0; i < credits.length; i++) {
credit = credits[i];
coin = credit.coin;
if (credit.spent)
continue;
if (this.txdb.isLocked(coin))
continue;
// Always used confirmed coins.
if (coin.height !== -1) {
coins.push(coin);
continue;
}
// Use unconfirmed only if they were
// created as a result of one of our
// _own_ transactions. i.e. they're
// not low-fee and not in danger of
// being double-spent by a bad actor.
if (!credit.own)
continue;
coins.push(coin);
}
return coins;
});
/**
* Get all pending/unconfirmed transactions.
* @param {(String|Number)?} acct

View File

@ -930,7 +930,7 @@ describe('Wallet', function() {
assert(t2.verify());
}));
it('should fill tx with inputs with subtract fee', co(function* () {
it('should fill tx with inputs with subtract fee (1)', co(function* () {
var w1 = yield walletdb.create();
var w2 = yield walletdb.create();
var t1, t2;
@ -959,7 +959,7 @@ describe('Wallet', function() {
assert.equal(t2.getFee(), 10000);
}));
it('should fill tx with inputs with subtract fee', co(function* () {
it('should fill tx with inputs with subtract fee (2)', co(function* () {
var w1 = yield walletdb.create();
var w2 = yield walletdb.create();
var options, t1, t2;
@ -993,6 +993,100 @@ describe('Wallet', function() {
assert.equal(t2.getFee(), 10000);
}));
it('should fill tx with smart coin selection', co(function* () {
var w1 = yield walletdb.create();
var w2 = yield walletdb.create();
var found = false;
var total = 0;
var i, options, t1, t2, t3, block, coins, coin;
// Coinbase
t1 = new MTX();
t1.addInput(dummy());
t1.addOutput(w1.getAddress(), 5460);
t1.addOutput(w1.getAddress(), 5460);
t1.addOutput(w1.getAddress(), 5460);
t1.addOutput(w1.getAddress(), 5460);
t1 = t1.toTX();
yield walletdb.addTX(t1);
// Coinbase
t2 = new MTX();
t2.addInput(dummy());
t2.addOutput(w1.getAddress(), 5460);
t2.addOutput(w1.getAddress(), 5460);
t2.addOutput(w1.getAddress(), 5460);
t2.addOutput(w1.getAddress(), 5460);
t2 = t2.toTX();
block = nextBlock();
yield walletdb.addBlock(block, [t2]);
coins = yield w1.getSmartCoins();
assert.equal(coins.length, 4);
for (i = 0; i < coins.length; i++) {
coin = coins[i];
assert.equal(coin.height, block.height);
}
// Create a change output for ourselves.
yield w1.send({
subtractFee: true,
rate: 1000,
depth: 1,
outputs: [{ address: w2.getAddress(), value: 1461 }]
});
coins = yield w1.getSmartCoins();
assert.equal(coins.length, 4);
for (i = 0; i < coins.length; i++) {
coin = coins[i];
if (coin.height === -1) {
assert(!found);
assert(coin.value < 5460);
found = true;
} else {
assert.equal(coin.height, block.height);
}
total += coin.value;
}
assert(found);
// Use smart selection
options = {
subtractFee: true,
smart: true,
rate: 10000,
outputs: [{ address: w2.getAddress(), value: total }]
};
t3 = yield w1.createTX(options);
assert.equal(t3.inputs.length, 4);
found = false;
for (i = 0; i < t3.inputs.length; i++) {
coin = t3.view.getCoin(t3.inputs[i]);
if (coin.height === -1) {
assert(!found);
assert(coin.value < 5460);
found = true;
} else {
assert.equal(coin.height, block.height);
}
}
assert(found);
yield w1.sign(t3);
assert(t3.verify());
}));
it('should get range of txs', co(function* () {
var w = wallet;
var txs = yield w.getRange({ start: util.now() - 1000 });