From 304f0e7e75e91502cb136c2e873b9d1849e41ece Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 22 Feb 2017 09:56:24 -0800 Subject: [PATCH] smart coin selection. --- lib/http/server.js | 16 ++++++++ lib/wallet/txdb.js | 19 +++++++-- lib/wallet/wallet.js | 62 ++++++++++++++++++++++++++-- test/wallet-test.js | 98 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 186 insertions(+), 9 deletions(-) diff --git a/lib/http/server.js b/lib/http/server.js index e5830ba6..48c60fac 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -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; diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index ae2593ee..f2320ec6 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -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; }; diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 5fc50274..df8a97ac 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -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 diff --git a/test/wallet-test.js b/test/wallet-test.js index 7c25d388..e7f064a2 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -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 });