wallet: add wallet coin source.

This commit is contained in:
Nodari Chkuaselidze 2025-05-20 18:50:15 +04:00
parent bbb44083a2
commit 1121e8eefe
No known key found for this signature in database
GPG key ID: B018A7BB437D1F05
4 changed files with 248 additions and 23 deletions

View file

@ -49,7 +49,7 @@ class AbstractCoinSource {
/**
* @param {BufferMap<Number>} inputs
* @param {Coin[]} coins
* @param {Coin[]} coins - Coin per input.
* @returns {Promise<void>}
*/

View file

@ -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
*/

View file

@ -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.

View file

@ -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;