wallet: add wallet coin source.
This commit is contained in:
parent
bbb44083a2
commit
1121e8eefe
4 changed files with 248 additions and 23 deletions
|
|
@ -49,7 +49,7 @@ class AbstractCoinSource {
|
|||
|
||||
/**
|
||||
* @param {BufferMap<Number>} inputs
|
||||
* @param {Coin[]} coins
|
||||
* @param {Coin[]} coins - Coin per input.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const base58 = require('bcrypto/lib/encoding/base58');
|
|||
const bio = require('bufio');
|
||||
const blake2b = require('bcrypto/lib/blake2b');
|
||||
const cleanse = require('bcrypto/lib/cleanse');
|
||||
const {BufferSet} = require('buffer-map');
|
||||
const TXDB = require('./txdb');
|
||||
const Path = require('./path');
|
||||
const common = require('./common');
|
||||
|
|
@ -38,12 +39,16 @@ const reserved = require('../covenants/reserved');
|
|||
const {ownership} = require('../covenants/ownership');
|
||||
const {states} = require('../covenants/namestate');
|
||||
const {types} = rules;
|
||||
const {BufferSet} = require('buffer-map');
|
||||
const Coin = require('../primitives/coin');
|
||||
const Outpoint = require('../primitives/outpoint');
|
||||
const {
|
||||
AbstractCoinSource,
|
||||
CoinSelector
|
||||
} = require('../utils/coinselector');
|
||||
|
||||
/** @typedef {import('bdb').DB} DB */
|
||||
/** @typedef {ReturnType<DB['batch']>} Batch */
|
||||
/** @typedef {import('buffer-map').BufferMap} BufferMap */
|
||||
/** @typedef {import('../types').Base58String} Base58String */
|
||||
/** @typedef {import('../types').Hash} Hash */
|
||||
/** @typedef {import('../types').Amount} Amount */
|
||||
|
|
@ -1226,12 +1231,10 @@ class Wallet extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fill a transaction with inputs without a lock.
|
||||
* @private
|
||||
* @see MTX#selectCoins
|
||||
* @see MTX#fill
|
||||
* Fill a transactions with inputs without a lock.
|
||||
* @param {MTX} mtx
|
||||
* @param {Object} [options]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async fill(mtx, options = {}) {
|
||||
|
|
@ -1245,18 +1248,10 @@ class Wallet extends EventEmitter {
|
|||
if (rate == null)
|
||||
rate = await this.wdb.estimateFee(options.blocks);
|
||||
|
||||
let coins = options.coins || [];
|
||||
assert(Array.isArray(coins));
|
||||
if (options.smart) {
|
||||
const smartCoins = await this.getSmartCoins(options.account);
|
||||
coins = coins.concat(smartCoins);
|
||||
} else {
|
||||
let availableCoins = await this.getCoins(options.account);
|
||||
availableCoins = this.txdb.filterLocked(availableCoins);
|
||||
coins = coins.concat(availableCoins);
|
||||
}
|
||||
|
||||
await mtx.fund(coins, {
|
||||
const selected = await this.select(mtx, {
|
||||
// we use options.account to maintain the same behaviour
|
||||
account: await this.ensureIndex(options.account),
|
||||
smart: options.smart,
|
||||
selection: options.selection,
|
||||
round: options.round,
|
||||
depth: options.depth,
|
||||
|
|
@ -1270,6 +1265,53 @@ class Wallet extends EventEmitter {
|
|||
maxFee: options.maxFee,
|
||||
estimate: prev => this.estimateSize(prev)
|
||||
});
|
||||
|
||||
mtx.fill(selected);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select coins for the transaction.
|
||||
* @param {MTX} mtx
|
||||
* @param {Object} [options]
|
||||
* @returns {Promise<CoinSelector>}
|
||||
*/
|
||||
|
||||
async select(mtx, options) {
|
||||
const selection = options.selection || 'dbvalue';
|
||||
|
||||
switch (selection) {
|
||||
case 'all':
|
||||
case 'random':
|
||||
case 'value':
|
||||
case 'age': {
|
||||
let coins = options.coins || [];
|
||||
|
||||
assert(Array.isArray(coins));
|
||||
if (options.smart) {
|
||||
const smartCoins = await this.getSmartCoins(options.account);
|
||||
coins = coins.concat(smartCoins);
|
||||
} else {
|
||||
let availableCoins = await this.getCoins(options.account);
|
||||
availableCoins = this.txdb.filterLocked(availableCoins);
|
||||
coins = coins.concat(availableCoins);
|
||||
}
|
||||
|
||||
return mtx.selectCoins(coins, options);
|
||||
}
|
||||
}
|
||||
|
||||
const source = new WalletCoinSource(this, options);
|
||||
await source.init();
|
||||
|
||||
if (selection === 'dball')
|
||||
options.selectAll = true;
|
||||
|
||||
const selector = new CoinSelector(mtx, source, options);
|
||||
await selector.select();
|
||||
await source.end();
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1965,9 +2007,6 @@ class Wallet extends EventEmitter {
|
|||
const nameHash = bidOutput.covenant.getHash(0);
|
||||
const height = bidOutput.covenant.getU32(1);
|
||||
|
||||
const coins = [];
|
||||
coins.push(bidCoin);
|
||||
|
||||
const blind = bidOutput.covenant.getHash(3);
|
||||
const bv = await this.getBlind(blind);
|
||||
if (!bv)
|
||||
|
|
@ -1980,10 +2019,11 @@ class Wallet extends EventEmitter {
|
|||
output.value = value;
|
||||
output.covenant.setReveal(nameHash, height, nonce);
|
||||
|
||||
reveal.addOutpoint(Outpoint.fromTX(bid, bidOuputIndex));
|
||||
reveal.addCoin(bidCoin);
|
||||
reveal.outputs.push(output);
|
||||
|
||||
await this.fill(reveal, { ...options, coins: coins });
|
||||
await this.fill(reveal, { ...options });
|
||||
|
||||
assert(
|
||||
reveal.inputs.length === 1,
|
||||
'Pre-signed REVEAL must not require additional inputs'
|
||||
|
|
@ -5590,6 +5630,160 @@ class Wallet extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coin source for wallet.
|
||||
* @alias module:wallet.CoinSource
|
||||
*/
|
||||
|
||||
class WalletCoinSource extends AbstractCoinSource {
|
||||
/**
|
||||
* @param {Wallet} wallet
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
constructor(wallet, options) {
|
||||
super();
|
||||
|
||||
this.wallet = wallet;
|
||||
this.wdb = wallet.wdb;
|
||||
this.txdb = wallet.txdb;
|
||||
|
||||
this.iter = null;
|
||||
this.done = false;
|
||||
|
||||
this.account = 0;
|
||||
this.selection = 'dbvalue';
|
||||
this.smart = false;
|
||||
this.skipDust = true;
|
||||
|
||||
if (options)
|
||||
this.fromOptions(options);
|
||||
}
|
||||
|
||||
fromOptions(options = {}) {
|
||||
if (options.account != null) {
|
||||
assert(typeof options.account === 'number',
|
||||
'Account must be a number.');
|
||||
this.account = options.account;
|
||||
}
|
||||
|
||||
if (options.selection != null) {
|
||||
assert(typeof options.selection === 'string',
|
||||
'Selection must be a string.');
|
||||
this.selection = options.selection;
|
||||
}
|
||||
|
||||
if (options.smart != null) {
|
||||
assert(typeof options.smart === 'boolean',
|
||||
'Smart must be a boolean.');
|
||||
|
||||
this.smart = options.smart;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async init() {
|
||||
switch (this.selection) {
|
||||
case 'dbvalue':
|
||||
case 'dball': {
|
||||
this.iter = this.txdb.getAccountCreditIterByValue(this.account, {
|
||||
reverse: true
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'dbage': {
|
||||
this.iter = this.txdb.getAccountCreditIterByHeight(this.account);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid selection: ${this.selection}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we done.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
hasNext() {
|
||||
return !this.done;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Coin?>}
|
||||
*/
|
||||
|
||||
async next() {
|
||||
if (this.done)
|
||||
return null;
|
||||
|
||||
// look for an usable credit.
|
||||
for (;;) {
|
||||
const item = await this.iter.next();
|
||||
|
||||
if (item.done) {
|
||||
this.done = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @type {Credit} */
|
||||
const credit = item.value;
|
||||
|
||||
if (credit.spent)
|
||||
continue;
|
||||
|
||||
const {coin} = credit;
|
||||
|
||||
if (this.txdb.isLocked(coin))
|
||||
continue;
|
||||
|
||||
if (this.smart && coin.height === -1 && !credit.own)
|
||||
continue;
|
||||
|
||||
return coin;
|
||||
}
|
||||
}
|
||||
|
||||
async end() {
|
||||
if (!this.iter)
|
||||
return;
|
||||
|
||||
this.iter.return();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve coins.
|
||||
* @param {BufferMap<Number>} inputs
|
||||
* @param {Coin[]} coins - Coin per input.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
async resolveInputsToCoins(inputs, coins) {
|
||||
for (const [key, idx] of inputs.entries()) {
|
||||
if (coins[idx] != null)
|
||||
continue;
|
||||
|
||||
const outpoint = Outpoint.fromKey(key);
|
||||
|
||||
if (this.account !== -1) {
|
||||
const hasCoin = await this.txdb.hasCoinByAccount(
|
||||
this.account,
|
||||
outpoint.hash,
|
||||
outpoint.index
|
||||
);
|
||||
|
||||
if (!hasCoin)
|
||||
continue;
|
||||
}
|
||||
|
||||
const coin = await this.txdb.getCoin(outpoint.hash, outpoint.index);
|
||||
coins[idx] = coin;
|
||||
inputs.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1955,6 +1955,36 @@ describe('Wallet Coin Selection', function() {
|
|||
]
|
||||
};
|
||||
|
||||
const reselect = (tests, selection) => {
|
||||
return tests.map((t) => {
|
||||
const options = {
|
||||
...t.options,
|
||||
selection
|
||||
};
|
||||
|
||||
return {
|
||||
...t,
|
||||
options
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Selection `value` and `dbvalue` are the same.
|
||||
SELECTION_TESTS['dbvalue'] = reselect(SELECTION_TESTS['value'], 'dbvalue');
|
||||
SELECTION_TESTS['dbvalue + smart'] = reselect(SELECTION_TESTS['value + smart'], 'dbvalue');
|
||||
SELECTION_TESTS['dbvalue + existing coins and inputs'] = reselect(
|
||||
SELECTION_TESTS['value + existing coins and inputs'], 'dbvalue');
|
||||
|
||||
// Same with `age` and `dbage`.
|
||||
SELECTION_TESTS['dbage'] = reselect(SELECTION_TESTS['age'], 'dbage');
|
||||
SELECTION_TESTS['dbage + smart'] = reselect(SELECTION_TESTS['age + smart'], 'dbage');
|
||||
SELECTION_TESTS['dbage + existing inputs'] = reselect(
|
||||
SELECTION_TESTS['age + existing inputs'], 'dbage');
|
||||
|
||||
SELECTION_TESTS['dball'] = reselect(SELECTION_TESTS['all'], 'dball');
|
||||
SELECTION_TESTS['dball + existing inputs'] = reselect(
|
||||
SELECTION_TESTS['all + existing inputs'], 'dball');
|
||||
|
||||
for (const [name, testCase] of Object.entries(SELECTION_TESTS)) {
|
||||
describe(`Wallet Coin Selection by ${name}`, function() {
|
||||
// fund wallet.
|
||||
|
|
|
|||
|
|
@ -3277,6 +3277,7 @@ describe('Wallet', function() {
|
|||
const fund = 10e6;
|
||||
// Store height of auction OPEN to be used in second bid.
|
||||
// The main test wallet, and wallet that will receive the FINALIZE.
|
||||
/** @type {Wallet} */
|
||||
let wallet;
|
||||
let unsentReveal;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue