diff --git a/CHANGELOG.md b/CHANGELOG.md index a98a536b..e85f09b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased **When upgrading to this version of hsd, you must pass `--chain-migrate=4` -and `--wallet-migrate=6` when you run it for the first time.** +and `--wallet-migrate=7` when you run it for the first time.** ### Wallet Changes @@ -14,6 +14,14 @@ and `--wallet-migrate=6` when you run it for the first time.** the namestate when the wallet owns the name. - Introduce admin `POST /recalculate-balances`, useful if the post-migration recalculation was not triggered and wallet balances are not correct. + - The TX creation HTTP Endpoints now supports new values for the `selection` + property. These new strategies use database iterators instead of loading all + coins into RAM. + - `db-value` - This is a database alternative to `value` and new default. + - `db-age` - A database alternative `age`. + - `db-all` - A database alternative `all`. + - `db-sweepdust` - Select smallest coins first. + - Add `sweepdustMinValue` option for TX creation endpoints, default 1. #### Wallet/WalletDB API - `Wallet.zap` now returns the number of transactions zapped instead of their hashes. diff --git a/bench/wallet-coinselector.js b/bench/wallet-coinselector.js new file mode 100644 index 00000000..e1ec97cf --- /dev/null +++ b/bench/wallet-coinselector.js @@ -0,0 +1,738 @@ +/*! + * bench/wallet-coinselector.js - benchmark wallet coin selections. + * + * This can prepare coin set for the wallet and then run different + * coin selection algorithms on it. The wallet will run on the regtest. + * + * Usage: + * node bench/wallet-coinselector.js [--prefix=path] [--unspendable=] + * [--spendable=] [--opens=] + * [--per-block=] [--cleanup] + * [--ops-per-type=] [--skip-init] + * [--output=] [--no-print] [--no-logs] + * [--skip-sends] [--skip-bids] + * [--skip-updates] [--skip-renewals] + * [--skip-transfers] + * + * Options: + * - `prefix` The location to store the walletdb. If data exists, + * it will be used for the benchmark. (Default: tmp) + * - `opens` The number of 0 value OPEN coins. + * Default: 1 000. + * - `spendable` The number of SPENDABLE coins. + * Default: 2 000. + * - `unspendable` The number of UNSPENDABLE coins. + * Default: 1 500. + * - `per-block` The number of each coin type per block. + * Default: 300. + * - `cleanup` Remove the walletdb after the benchmark. + * Default: false. + * - `ops-per-type` The number of operations per type. + * Default: 200. + * - `max-pending` The maximum number of coins to be spent. Ops will zap + * all pending txs after every `max-pending` operations. + * Default: 50. + * - `skip-init` Skip the initialization of the wallet. This will + * only run the benchmarks on the existing data. + * Default: false. + * - `output` The output file to store the benchmark results. + * Default: null. + * - `no-print` Do not print the benchmark results to the console. + * Default: false. + * - `no-logs` Do not print the logs to the console. + * Default: false. + */ + +'use strict'; + +process.title = 'hsd-coinselector-bench'; + +const Config = require('bcfg'); +const path = require('path'); +const os = require('os'); +const bfs = require('bfile'); +const Covenant = require('../lib/primitives/covenant'); +const Network = require('../lib/protocol/network'); +const WalletDB = require('../lib/wallet/walletdb'); +const NameState = require('../lib/covenants/namestate'); +const {Resource} = require('../lib/dns/resource'); +const wcommon = require('../lib/wallet/common'); +const wutils = require('../test/util/wallet'); +const random = require('bcrypto/lib/random'); +const primutils = require('../test/util/primitives'); +const {DB_VALUE, DB_AGE} = wcommon.coinSelectionTypes; + +/** @typedef {import('../lib/covenants/rules').types} covenantTypes */ +/** @typedef {import('../lib/wallet/wallet')} Wallet */ + +(async () => { + const cfg = new Config('hsd'); + + cfg.load({ + argv: true, + env: true + }); + + const network = Network.get('regtest'); + const tmp = path.join(os.tmpdir(), 'hsd-bench'); + const prefix = cfg.str('prefix', tmp); + const options = { + opens: cfg.int('opens', 1_000), + spendable: cfg.int('spendable', 2_000), + unspendable: cfg.int('unspendable', 1_500), + perBlock: cfg.int('per-block', 400), + cleanup: cfg.bool('cleanup', false), + opsPerType: cfg.int('ops-per-type', 500), + maxPending: cfg.int('max-pending', 200), + skipInit: cfg.bool('skip-init', false), + noPrint: cfg.bool('no-print', false), + output: cfg.str('output', null), + noLogs: cfg.bool('no-logs', false), + + skipSends: cfg.bool('skip-sends', false), + skipBids: cfg.bool('skip-bids', false), + skipUpdates: cfg.bool('skip-updates', false), + skipRenewals: cfg.bool('skip-renewals', false), + skipTransfers: cfg.bool('skip-transfers', false) + }; + + if (options.maxPending > options.opsPerType) + throw new Error('max-pending cannot be greater than ops-per-type.'); + + options.opens = Math.max(options.opens, options.maxPending); + options.unspendable = Math.max(options.unspendable, options.maxPending); + + if (!await bfs.exists(prefix)) + await bfs.mkdirp(prefix); + + let consoleLog = console.log.bind(console); + let stdoutWrite = process.stdout.write.bind(process.stdout); + + if (options.noLogs) { + consoleLog = () => {}; + stdoutWrite = () => {}; + } + + consoleLog(`WalletDB location: ${prefix}`); + + const wdb = new WalletDB({ + memory: false, + network, + prefix + }); + + await wdb.open(); + await wdb.primary.zap(-1, 0); + + if (!options.skipInit) { + const left = { + opens: options.opens, + spendable: options.spendable, + unspendable: options.unspendable + }; + + consoleLog('Collect existing data.'); + const coins = await wdb.primary.getCoins(0); + + for (const coin of coins) { + if (coin.covenant.type === Covenant.types.OPEN) { + left.opens--; + continue; + } + + if (coin.covenant.type === Covenant.types.NONE + || coin.covenant.type === Covenant.types.REDEEM) { + left.spendable--; + continue; + } + + left.unspendable--; + } + + consoleLog(`Coins: ${coins.length}, Left to mine: + opens: ${left.opens} + spendable: ${left.spendable} + unspendable: ${left.unspendable}`); + + const opens = distributeCoinsPerBlock(left.opens, options.perBlock); + const spendable = distributeCoinsPerBlock(left.spendable, + options.perBlock); + const unspendable = distributeCoinsPerBlock(left.unspendable, + options.perBlock); + + const max = Math.max(opens.length, spendable.length, unspendable.length); + consoleLog(`Blocks to mine: ${max}`); + + for (let i = 0; i < max; i++) { + const openTXs = await createOpenTXs(wdb.primary, opens[i] || 0); + const spendTXs = await createSpendTXs(wdb.primary, spendable[i] || 0); + const unspendTXs = await createUnspendableTXs(wdb.primary, + unspendable[i] || 0); + + consoleLog(`Block: ${wdb.height + 1}, ` + + `opens: ${openTXs.length}, ` + + `spends: ${spendTXs.length}, ` + + `unspendables: ${unspendTXs.length}`); + + await wdb.addBlock(wutils.nextBlock(wdb), + [].concat(openTXs, spendTXs, unspendTXs)); + } + + const treeInterval = network.names.treeInterval; + const biddingPeriod = network.names.biddingPeriod; + const revealPeriod = network.names.revealPeriod; + + if (max) { + consoleLog('Progressing to the closed phase...'); + for (let i = 0; i < biddingPeriod + revealPeriod; i++) { + await wdb.addBlock(wutils.nextBlock(wdb), []); + } + } + + // Prepare bidding names + const existingBiddingNames = await getBiddableNames(wdb.primary); + + consoleLog(`Existing bidding names: ${existingBiddingNames.length}`); + if (existingBiddingNames.length < options.maxPending) { + stdoutWrite('Creating bidding names...'); + const biddingNames = Array.from({ length: options.maxPending }, () => { + return primutils.randomName(30); + }); + + const openInfos = biddingNames.map((name) => { + return { + value: 0, + covenant: { + type: Covenant.types.OPEN, + name + } + }; + }); + + const txs = await wutils.createInboundTXs(wdb.primary, openInfos, { + txPerOutput: true, + createAddress: true + }); + + await wdb.addBlock(wutils.nextBlock(wdb), txs); + + for (let i = 0; i < treeInterval + 1; i++) { + // progress to the bidding phase. + await wdb.addBlock(wutils.nextBlock(wdb), []); + } + + stdoutWrite(' Done.\n'); + } + + await wdb.primary.zap(-1, 0); + consoleLog('Wallet initialized.'); + } + + const wallet = wdb.primary; + + const benchmarks = new BenchmarkResults({ + opens: options.opens, + spendable: options.spendable, + unspendable: options.unspendable, + maxPending: options.maxPending + }); + + const runOperations = async (sendTXFn) => { + await wallet.zap(-1, 0); + + let pending = 0; + for (let i = 0; i < options.opsPerType; i++) { + await sendTXFn(pending); + pending++; + + if (i % options.maxPending === 0) { + await wallet.zap(-1, 0); + pending = 0; + } + } + + await wallet.zap(-1, 0); + }; + + // Benchmark normal sends. + consoleLog(`Running benchmarks... + ${options.opsPerType} operations per type. + ${options.maxPending} max pending.`); + + const selections = [ + 'random', + 'value', + DB_VALUE, + 'age', + DB_AGE + ]; + + for (const selection of selections) { + if (options.skipSends) + continue; + + stdoutWrite(`Sending ${selection} selection...`); + await runOperations(async (pending) => { + const min = Math.min(options.spendable * 1e5 / options.maxPending, + 1e6); + const max = Math.min(options.spendable * 1e5 / options.maxPending, + 1000e6); + const value = random.randomRange(min, max); + const address = primutils.randomP2PKAddress(); + const before = process.hrtime.bigint(); + await wallet.send({ + selection, + outputs: [{ + value, + address + }] + }); + + const after = process.hrtime.bigint(); + + const entry = new BenchmarkEntry('send', selection, + after - before,pending); + + benchmarks.addResult(entry); + }); + + stdoutWrite(' Done.\n'); + } + + for (const selection of selections) { + if (options.skipBids) + continue; + stdoutWrite(`Bidding ${selection} selection...`); + + const biddingNames = await getBiddableNames(wallet); + + if (biddingNames.length < options.maxPending) + throw new Error('Not enough bidding names to benchmark.'); + + await runOperations(async (pending) => { + const min = Math.min(options.spendable * 1e5 / options.maxPending, + 1e6); + const max = Math.min(options.spendable * 1e5 / options.maxPending, + 1000e6); + const value = random.randomRange(min, max); + const name = biddingNames[pending]; + const before = process.hrtime.bigint(); + await wallet.sendBid(name, value, value, { + selection + }); + + const after = process.hrtime.bigint(); + + const entry = new BenchmarkEntry('bid', selection, + after - before, pending); + + benchmarks.addResult(entry); + }); + + stdoutWrite(' Done.\n'); + } + + const namestates = await wallet.getNames(); + const selectedOwned = []; + + for (const ns of namestates) { + const {hash, index} = ns.owner; + const coin = await wallet.getCoin(hash, index); + + if (!coin) + continue; + + if (ns.state(wdb.height, network) === NameState.states.CLOSED) { + if (ns.isExpired(wdb.height, network)) + continue; + + selectedOwned.push(ns.name.toString('ascii')); + } + + if (selectedOwned.length >= options.maxPending) + break; + } + + if (selectedOwned.length < options.maxPending) + throw new Error('Not enough owned names to benchmark.'); + + const res = Resource.fromString('Resource'); + for (const selection of selections) { + if (options.skipUpdates) + continue; + stdoutWrite(`Updating ${selection} selection...`); + + await runOperations(async (pending) => { + const before = process.hrtime.bigint(); + await wallet.sendUpdate(selectedOwned[pending], res, { selection }); + const after = process.hrtime.bigint(); + + const entry = new BenchmarkEntry('update', selection, + after - before, pending); + benchmarks.addResult(entry); + }); + + stdoutWrite(' Done.\n'); + } + + for (const selection of selections) { + if (options.skipRenewals) + continue; + stdoutWrite(`Renewing ${selection} selection...`); + + await runOperations(async (pending) => { + const before = process.hrtime.bigint(); + await wallet.sendRenewal(selectedOwned[pending], { selection }); + const after = process.hrtime.bigint(); + + const entry = new BenchmarkEntry('renew', selection, + after - before, pending); + benchmarks.addResult(entry); + }); + + stdoutWrite(' Done.\n'); + } + + // do transfer at the end + for (const selection of selections) { + if (options.skipTransfers) + continue; + + stdoutWrite(`Transfering ${selection} selection...`); + + const addr = primutils.randomP2PKAddress(); + await runOperations(async (pending) => { + const before = process.hrtime.bigint(); + await wallet.sendTransfer(selectedOwned[pending], addr, { selection }); + const after = process.hrtime.bigint(); + + const entry = new BenchmarkEntry('transfer', selection, + after - before, pending); + benchmarks.addResult(entry); + }); + + stdoutWrite(' Done.\n'); + } + + benchmarks.calculateStats(); + + if (!options.noPrint) + benchmarks.print(); + + if (options.output) { + const json = benchmarks.toJSON(); + await bfs.writeFile(options.output, JSON.stringify(json, null, 2)); + } + + await wdb.close(); + + if (options.cleanup) + await bfs.rimraf(prefix); +})().catch((err) => { + console.error(err); + process.exit(1); +}); + +class BenchmarkEntry { + /** + * @param {String} type + * @param {String} selection + * @param {BigInt} elapsed + * @param {Number} pending + */ + constructor(type, selection, elapsed, pending) { + /** @type {String} */ + this.type = type; + /** @type {String} */ + this.selection = selection; + /** @type {BigInt} */ + this.elapsed = elapsed; + /** @type {Number} */ + this.pending = pending; + } + + get key() { + return `${this.type}-${this.selection}`; + } +} + +/** + * @typedef {Object} BenchmarkResults + * @property {String} type + * @property {String} selection + * @property {Number} opens + * @property {Number} spendable + * @property {Number} unspendable + * @property {Number} maxPending + * @property {Number} ops + * @property {BigInt} min + * @property {BigInt} max + * @property {BigInt} median + * @property {BigInt} percentile95 + * @property {BigInt} avg + */ + +class BenchmarkResults { + constructor(options = {}) { + this.opens = options.opens || 0; + this.spendable = options.spendable || 0; + this.unspendable = options.unspendable || 0; + this.maxPending = options.maxPending || 0; + /** @type Map */ + this.benchmarksPerType = new Map(); + + /** @type Map */ + this.results = new Map(); + } + + /** + * @param {BenchmarkEntry} entry + */ + + addResult(entry) { + const key = entry.key; + + if (!this.benchmarksPerType.has(key)) + this.benchmarksPerType.set(key, []); + + const entries = this.benchmarksPerType.get(key); + entries.push(entry); + } + + calculateStats() { + for (const [key, entries] of this.benchmarksPerType.entries()) { + const result = { + type: entries[0].type, + selection: entries[0].selection, + opens: this.opens, + spendable: this.spendable, + unspendable: this.unspendable, + maxPending: this.maxPending, + ops: entries.length, + min: BigInt(Number.MAX_SAFE_INTEGER), + max: 0n, + median: 0n, + percentile95: 0n, + avg: 0n + }; + + const sorted = entries.sort((a, b) => Number(a.elapsed - b.elapsed)); + const p95 = Math.floor(sorted.length * 0.95); + + for (let i = 0; i < sorted.length; i++) { + if (i === p95) + result.percentile95 = sorted[i].elapsed; + + if (sorted[i].elapsed < result.min) + result.min = sorted[i].elapsed; + + if (sorted[i].elapsed > result.max) + result.max = sorted[i].elapsed; + + result.avg += sorted[i].elapsed; + } + + if (sorted.length > 1 && sorted.length % 2 === 0) { + const mid1 = sorted[sorted.length / 2 - 1].elapsed; + const mid2 = sorted[sorted.length / 2].elapsed; + result.median = (mid1 + mid2) / 2n; + } else if (sorted.length > 0) { + result.median = sorted[Math.floor(sorted.length / 2)].elapsed; + } + + result.avg /= BigInt(sorted.length); + + this.results.set(key, result); + } + } + + toResultsArray() { + const resultTable = []; + + for (const entry of this.results.values()) { + resultTable.push({ + type: entry.type, + selection: entry.selection, + opens: entry.opens, + spendable: entry.spendable, + unspendable: entry.unspendable, + maxPending: entry.maxPending, + ops: entry.ops, + minMs: formatElapsedTime(entry.min), + maxMs: formatElapsedTime(entry.max), + medianMs: formatElapsedTime(entry.median), + percentile95ms: formatElapsedTime(entry.percentile95), + avgMs: formatElapsedTime(entry.avg) + }); + } + + return resultTable; + } + + print() { + if (this.results.size === 0) + throw new Error('No results to print.'); + + console.table(this.toResultsArray()); + } + + toJSON() { + if (this.results.size === 0) + throw new Error('No results to print.'); + + return { + data: this.toResultsArray() + }; + } +} + +function distributeCoinsPerBlock(left, perBlock) { + if (left <= 0) + return []; + + const full = Math.floor(left / perBlock); + const rest = left % perBlock; + const coins = new Array(full).fill(perBlock); + + if (rest > 0) + coins.push(rest); + + return coins; +} + +/** + * @param {Wallet} wallet + * @param {Number} opens + * @returns {Promise} + */ + +async function createOpenTXs(wallet, opens) { + /** @type {wutils.OutputInfo[]} */ + const infos = []; + + for (let i = 0; i < opens; i++) { + const info = { + // OPENs are mostly 0 values. It does not need to be this way, but it is. + value: 0, + covenant: { type: Covenant.types.OPEN } + }; + + infos.push(info); + } + + const txs = await wutils.createInboundTXs(wallet, infos, { + txPerOutput: true, + createAddress: true + }); + + return txs; +} + +/** + * @param {Wallet} wallet + * @param {Number} spendable + * @param {Object} options + * @param {Number} options.minValue + * @param {Number} options.maxValue + * @returns {Promise} + */ + +async function createSpendTXs(wallet, spendable, options = {}) { + /** @type {wutils.OutputInfo[]} */ + const infos = []; + const spendables = [ + Covenant.types.NONE, + Covenant.types.REDEEM + ]; + + const { + minValue = 1e5, + maxValue = 100e6 + } = options; + + for (let i = 0; i < spendable; i++) { + const covenant = { type: spendables[i % spendables.length] }; + const value = random.randomRange(minValue, maxValue); + const info = { value, covenant }; + + infos.push(info); + } + + const txs = await wutils.createInboundTXs(wallet, infos, { + txPerOutput: true, + createAddress: true + }); + + return txs; +} + +/** + * @param {Wallet} wallet + * @param {Number} unspendable + * @param {Object} options + * @param {Number} options.minValue + * @param {Number} options.maxValue + * @returns {Promise} + */ + +async function createUnspendableTXs(wallet, unspendable, options = {}) { + /** @type {wutils.OutputInfo[]} */ + const infos = []; + const unspendables = [ + // Covenant.types.REGISTER, + // Covenant.types.UPDATE, + // Covenant.types.RENEW, + Covenant.types.FINALIZE + ]; + + const { + minValue = 1e5, + maxValue = 100e6 + } = options; + + for (let i = 0; i < unspendable; i++) { + const covenant = { type: unspendables[i % unspendables.length] }; + const value = random.randomRange(minValue, maxValue); + const info = { value, covenant }; + + infos.push(info); + } + + const txs = await wutils.createInboundTXs(wallet, infos, { + txPerOutput: true, + createAddress: true + }); + + return txs; +} + +/** + * @param {BigInt} elapsedNanos + * @returns {Number} + */ + +function formatElapsedTime(elapsedNanos) { + const nsInMs = 1000000n; + + return Number(elapsedNanos) / Number(nsInMs); +} + +/** + * @param {Wallet} wallet + * @returns {Promise} + */ + +async function getBiddableNames(wallet) { + const height = wallet.wdb.height; + const network = wallet.network; + const names = await wallet.getNames(); + const biddable = []; + + for (const ns of names) { + if (ns.state(height, network) === NameState.states.BIDDING) { + biddable.push(ns.name.toString('ascii')); + } + } + + return biddable; +} diff --git a/lib/blockstore/index-browser.js b/lib/blockstore/index-browser.js index a5df0956..dae3b8cc 100644 --- a/lib/blockstore/index-browser.js +++ b/lib/blockstore/index-browser.js @@ -15,7 +15,7 @@ const LevelBlockStore = require('./level'); * @module blockstore */ -exports.create = (options) => { +exports.create = function create(options) { const location = join(options.prefix, 'blocks'); return new LevelBlockStore({ diff --git a/lib/blockstore/index.js b/lib/blockstore/index.js index df1cbc35..1eae7168 100644 --- a/lib/blockstore/index.js +++ b/lib/blockstore/index.js @@ -16,7 +16,7 @@ const FileBlockStore = require('./file'); * @module blockstore */ -exports.create = (options) => { +exports.create = function create(options) { if (options.memory) { return new LevelBlockStore({ network: options.network, diff --git a/lib/coins/coinview.js b/lib/coins/coinview.js index 93daef99..45102a0c 100644 --- a/lib/coins/coinview.js +++ b/lib/coins/coinview.js @@ -193,7 +193,7 @@ class CoinView extends View { /** * Spend an output. - * @param {Outpoint} prevout + * @param {Outpoint|Coin} prevout * @returns {CoinEntry|null} */ @@ -216,7 +216,7 @@ class CoinView extends View { /** * Remove an output. - * @param {Outpoint} prevout + * @param {Outpoint|Coin} prevout * @returns {CoinEntry|null} */ @@ -232,7 +232,7 @@ class CoinView extends View { /** * Test whether the view has an entry by prevout. - * @param {Outpoint} prevout + * @param {Outpoint|Coin} prevout * @returns {Boolean} */ @@ -248,7 +248,7 @@ class CoinView extends View { /** * Get a single entry by prevout. - * @param {Outpoint} prevout + * @param {Outpoint|Coin} prevout * @returns {CoinEntry|null} */ diff --git a/lib/migrations/migrator.js b/lib/migrations/migrator.js index a28a6894..81f807a2 100644 --- a/lib/migrations/migrator.js +++ b/lib/migrations/migrator.js @@ -434,6 +434,14 @@ class MigrationContext { async saveState() { await this.migrator.saveState(this.state); } + + /** + * @param {Batch} b + */ + + writeState(b) { + this.migrator.writeState(b, this.state); + } } exports.Migrator = Migrator; diff --git a/lib/primitives/covenant.js b/lib/primitives/covenant.js index db983e98..3ac9869d 100644 --- a/lib/primitives/covenant.js +++ b/lib/primitives/covenant.js @@ -383,17 +383,17 @@ class Covenant extends bio.Struct { /** * Set covenant to BID. * @param {Hash} nameHash - * @param {Number} start + * @param {Number} height * @param {Buffer} rawName * @param {Hash} blind * @returns {Covenant} */ - setBid(nameHash, start, rawName, blind) { + setBid(nameHash, height, rawName, blind) { this.type = types.BID; this.items = []; this.pushHash(nameHash); - this.pushU32(start); + this.pushU32(height); this.push(rawName); this.pushHash(blind); diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index cfea27a8..f5b7980c 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -8,7 +8,6 @@ const assert = require('bsert'); const {encoding} = require('bufio'); -const {BufferMap} = require('buffer-map'); const Script = require('../script/script'); const TX = require('./tx'); const Input = require('./input'); @@ -18,15 +17,18 @@ const Outpoint = require('./outpoint'); const CoinView = require('../coins/coinview'); const Path = require('../wallet/path'); const WalletCoinView = require('../wallet/walletcoinview'); -const Address = require('./address'); const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); -const Amount = require('../ui/amount'); const Stack = require('../script/stack'); const rules = require('../covenants/rules'); const util = require('../utils/util'); const {types} = rules; +const { + CoinSelector, + InMemoryCoinSource +} = require('../utils/coinselector'); + /** @typedef {import('../types').SighashType} SighashType */ /** @typedef {import('../types').Hash} Hash */ /** @typedef {import('../types').Amount} AmountValue */ @@ -34,6 +36,8 @@ const {types} = rules; /** @typedef {import('../protocol/network')} Network */ /** @typedef {import('../workers/workerpool')} WorkerPool */ /** @typedef {import('./keyring')} KeyRing */ +/** @typedef {import('./address')} Address */ +/** @typedef {import('../utils/coinselector')} coinselector */ /** * MTX @@ -227,6 +231,18 @@ class MTX extends TX { return output; } + /** + * Get the value of the change output. + * @returns {AmountValue} value - Returns -1 if no change output. + */ + + getChangeValue() { + if (this.changeIndex === -1) + return -1; + + return this.outputs[this.changeIndex].value; + } + /** * Verify all transaction inputs. * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] @@ -1018,14 +1034,26 @@ class MTX extends TX { /** * Select necessary coins based on total output value. * @param {Coin[]} coins - * @param {Object?} options + * @param {Object} options * @returns {Promise} * @throws on not enough funds available. */ - selectCoins(coins, options) { - const selector = new CoinSelector(this, options); - return selector.select(coins); + async selectCoins(coins, options) { + const source = new InMemoryCoinSource({ + coins, + selection: options.selection + }); + + await source.init(); + + if (options.selection === 'all') + options.selectAll = true; + + const selector = new CoinSelector(this, source, options); + await selector.select(); + + return selector; } /** @@ -1112,7 +1140,9 @@ class MTX extends TX { /** * Select coins and fill the inputs. * @param {Coin[]} coins - * @param {Object} options - See {@link MTX#selectCoins} options. + * @param {Object} options - See + * {@link CoinSelectorOptions} and + * {@link CoinSourceOptions} options. * @returns {Promise} */ @@ -1122,7 +1152,17 @@ class MTX extends TX { // Select necessary coins. const select = await this.selectCoins(coins, options); + this.fill(select); + return select; + } + /** + * Fill transaction with the selected inputs. + * @param {CoinSelector} select + * @returns {void} + */ + + fill(select) { // Make sure we empty the input array. this.inputs.length = 0; @@ -1153,8 +1193,6 @@ class MTX extends TX { this.changeIndex = this.outputs.length - 1; assert.strictEqual(this.getFee(), select.fee); } - - return select; } /** @@ -1175,7 +1213,14 @@ class MTX extends TX { const inputs = []; /** @type {Output[]} */ const outputs = []; - // [Input, Output][] + + /** + * @typedef {Array} Linked + * @property {Input} 0 + * @property {Output} 1 + */ + + /** @type {Linked[]} */ const linked = []; let i = 0; @@ -1411,525 +1456,26 @@ class MTX extends TX { } } -/** - * Coin Selector - * @alias module:primitives.CoinSelector - */ - -class CoinSelector { - /** - * Create a coin selector. - * @constructor - * @param {MTX} tx - * @param {Object?} options - */ - - constructor(tx, options) { - this.tx = tx.clone(); - this.view = tx.view; - this.coins = []; - this.outputValue = 0; - this.index = 0; - this.chosen = []; - this.change = 0; - this.fee = CoinSelector.MIN_FEE; - - this.selection = 'value'; - this.subtractFee = false; - this.subtractIndex = -1; - this.height = -1; - this.depth = -1; - this.hardFee = -1; - this.rate = CoinSelector.FEE_RATE; - this.maxFee = -1; - this.round = false; - this.coinbaseMaturity = 400; - this.changeAddress = null; - this.inputs = new BufferMap(); - - // Needed for size estimation. - this.estimate = null; - - this.injectInputs(); - - if (options) - this.fromOptions(options); - } - - /** - * Initialize selector options. - * @param {Object} options - * @private - */ - - fromOptions(options) { - if (options.selection) { - assert(typeof options.selection === 'string'); - this.selection = options.selection; - } - - if (options.subtractFee != null) { - if (typeof options.subtractFee === 'number') { - assert(Number.isSafeInteger(options.subtractFee)); - assert(options.subtractFee >= -1); - this.subtractIndex = options.subtractFee; - this.subtractFee = this.subtractIndex !== -1; - } else { - assert(typeof options.subtractFee === 'boolean'); - this.subtractFee = options.subtractFee; - } - } - - if (options.subtractIndex != null) { - assert(Number.isSafeInteger(options.subtractIndex)); - assert(options.subtractIndex >= -1); - this.subtractIndex = options.subtractIndex; - this.subtractFee = this.subtractIndex !== -1; - } - - if (options.height != null) { - assert(Number.isSafeInteger(options.height)); - assert(options.height >= -1); - this.height = options.height; - } - - if (options.confirmations != null) { - assert(Number.isSafeInteger(options.confirmations)); - assert(options.confirmations >= -1); - this.depth = options.confirmations; - } - - if (options.depth != null) { - assert(Number.isSafeInteger(options.depth)); - assert(options.depth >= -1); - this.depth = options.depth; - } - - if (options.hardFee != null) { - assert(Number.isSafeInteger(options.hardFee)); - assert(options.hardFee >= -1); - this.hardFee = options.hardFee; - } - - if (options.rate != null) { - assert(Number.isSafeInteger(options.rate)); - assert(options.rate >= 0); - this.rate = options.rate; - } - - if (options.maxFee != null) { - assert(Number.isSafeInteger(options.maxFee)); - assert(options.maxFee >= -1); - this.maxFee = options.maxFee; - } - - if (options.round != null) { - assert(typeof options.round === 'boolean'); - this.round = options.round; - } - - if (options.coinbaseMaturity != null) { - assert((options.coinbaseMaturity >>> 0) === options.coinbaseMaturity); - this.coinbaseMaturity = options.coinbaseMaturity; - } - - if (options.changeAddress) { - const addr = options.changeAddress; - if (typeof addr === 'string') { - this.changeAddress = Address.fromString(addr); - } else { - assert(addr instanceof Address); - this.changeAddress = addr; - } - } - - if (options.estimate) { - assert(typeof options.estimate === 'function'); - this.estimate = options.estimate; - } - - if (options.inputs) { - assert(Array.isArray(options.inputs)); - - const lastIndex = this.inputs.size; - for (let i = 0; i < options.inputs.length; i++) { - const prevout = options.inputs[i]; - assert(prevout && typeof prevout === 'object'); - const {hash, index} = prevout; - this.inputs.set(Outpoint.toKey(hash, index), lastIndex + i); - } - } - - return this; - } - - /** - * Attempt to inject existing inputs. - * @private - */ - - injectInputs() { - if (this.tx.inputs.length > 0) { - for (let i = 0; i < this.tx.inputs.length; i++) { - const {prevout} = this.tx.inputs[i]; - this.inputs.set(prevout.toKey(), i); - } - } - } - - /** - * Initialize the selector with coins to select from. - * @param {Coin[]} coins - */ - - init(coins) { - this.coins = coins.slice(); - this.outputValue = this.tx.getOutputValue(); - this.index = 0; - this.chosen = []; - this.change = 0; - this.fee = CoinSelector.MIN_FEE; - this.tx.inputs.length = 0; - - switch (this.selection) { - case 'all': - case 'random': - this.coins.sort(sortRandom); - break; - case 'age': - this.coins.sort(sortAge); - break; - case 'value': - this.coins.sort(sortValue); - break; - default: - throw new FundingError(`Bad selection type: ${this.selection}.`); - } - } - - /** - * Calculate total value required. - * @returns {AmountValue} - */ - - total() { - if (this.subtractFee) - return this.outputValue; - return this.outputValue + this.fee; - } - - /** - * Test whether the selector has - * completely funded the transaction. - * @returns {Boolean} - */ - - isFull() { - return this.tx.getInputValue() >= this.total(); - } - - /** - * Test whether a coin is spendable - * with regards to the options. - * @param {Coin} coin - * @returns {Boolean} - */ - - isSpendable(coin) { - if (this.tx.view.hasEntry(coin)) - return false; - - if (coin.covenant.isNonspendable()) - return false; - - if (this.height === -1) - return true; - - if (coin.coinbase) { - if (coin.height === -1) - return false; - - if (this.height + 1 < coin.height + this.coinbaseMaturity) - return false; - - return true; - } - - if (this.depth === -1) - return true; - - const depth = coin.getDepth(this.height); - - if (depth < this.depth) - return false; - - return true; - } - - /** - * Get the current fee based on a size. - * @param {Number} size - * @returns {AmountValue} - */ - - getFee(size) { - // This is mostly here for testing. - // i.e. A fee rounded to the nearest - // kb is easier to predict ahead of time. - if (this.round) - return policy.getRoundFee(size, this.rate); - - return policy.getMinFee(size, this.rate); - } - - /** - * Fund the transaction with more - * coins if the `output value + fee` - * total was updated. - */ - - fund() { - // Ensure all preferred inputs first. - this.resolveInputCoins(); - - if (this.isFull()) - return; - - while (this.index < this.coins.length) { - const coin = this.coins[this.index++]; - - if (!this.isSpendable(coin)) - continue; - - this.tx.addCoin(coin); - this.chosen.push(coin); - - if (this.selection === 'all') - continue; - - if (this.isFull()) - break; - } - } - - /** - * Initiate selection from `coins`. - * @param {Coin[]} coins - * @returns {Promise} - */ - - async select(coins) { - this.init(coins); - - if (this.hardFee !== -1) { - this.selectHard(); - } else { - // This is potentially asynchronous: - // it may invoke the size estimator - // required for redeem scripts (we - // may be calling out to a wallet - // or something similar). - await this.selectEstimate(); - } - - if (!this.isFull()) { - // Still failing to get enough funds. - throw new FundingError( - 'Not enough funds.', - this.tx.getInputValue(), - this.total()); - } - - // How much money is left after filling outputs. - this.change = this.tx.getInputValue() - this.total(); - - return this; - } - - /** - * Initialize selection based on size estimate. - */ - - async selectEstimate() { - // Set minimum fee and do - // an initial round of funding. - this.fee = CoinSelector.MIN_FEE; - this.fund(); - - // Add dummy output for change. - const change = new Output(); - - if (this.changeAddress) { - change.address = this.changeAddress; - } else { - // In case we don't have a change address, - // we use a fake p2pkh output to gauge size. - change.address.fromPubkeyhash(Buffer.allocUnsafe(20)); - } - - this.tx.outputs.push(change); - - // Keep recalculating the fee and funding - // until we reach some sort of equilibrium. - do { - const size = await this.tx.estimateSize(this.estimate); - - this.fee = this.getFee(size); - - if (this.maxFee > 0 && this.fee > this.maxFee) - throw new FundingError('Fee is too high.'); - - // Failed to get enough funds, add more coins. - if (!this.isFull()) - this.fund(); - } while (!this.isFull() && this.index < this.coins.length); - } - - /** - * Initiate selection based on a hard fee. - */ - - selectHard() { - this.fee = this.hardFee; - this.fund(); - } - - resolveInputCoins() { - if (this.inputs.size === 0) - return; - - const coins = []; - - for (let i = 0 ; i < this.inputs.size; i++) { - coins.push(null); - } - - // first resolve from coinview if possible. - for (const key of this.inputs.keys()) { - const prevout = Outpoint.fromKey(key); - - if (this.view.hasEntry(prevout)) { - const coinEntry = this.view.getEntry(prevout); - const i = this.inputs.get(key); - - if (i != null) { - assert(!coins[i]); - coins[i] = coinEntry.toCoin(prevout); - this.inputs.delete(key); - } - } - } - - // Now try to resolve from the passed coins array. - if (this.inputs.size > 0) { - for (const coin of this.coins) { - const {hash, index} = coin; - const key = Outpoint.toKey(hash, index); - const i = this.inputs.get(key); - - if (i != null) { - assert(!coins[i]); - coins[i] = coin; - this.inputs.delete(key); - } - } - } - - if (this.inputs.size > 0) - throw new Error('Could not resolve preferred inputs.'); - - for (const coin of coins) { - this.tx.addCoin(coin); - this.chosen.push(coin); - } - } -} - -/** - * Default fee rate - * for coin selection. - * @const {Amount} - * @default - */ - -CoinSelector.FEE_RATE = 10000; - -/** - * Minimum fee to start with - * during coin selection. - * @const {Amount} - * @default - */ - -CoinSelector.MIN_FEE = 10000; - -/** - * Funding Error - * An error thrown from the coin selector. - * @ignore - * @extends Error - * @property {String} message - Error message. - * @property {Amount} availableFunds - * @property {Amount} requiredFunds - */ - -class FundingError extends Error { - /** - * Create a funding error. - * @constructor - * @param {String} msg - * @param {AmountValue} [available] - * @param {AmountValue} [required] - */ - - constructor(msg, available, required) { - super(); - - this.type = 'FundingError'; - this.message = msg; - this.availableFunds = -1; - this.requiredFunds = -1; - - if (available != null) { - this.message += ` (available=${Amount.coin(available)},`; - this.message += ` required=${Amount.coin(required)})`; - this.availableFunds = available; - this.requiredFunds = required; - } - - if (Error.captureStackTrace) - Error.captureStackTrace(this, FundingError); - } -} - /* * Helpers */ -function sortAge(a, b) { - a = a.height === -1 ? 0x7fffffff : a.height; - b = b.height === -1 ? 0x7fffffff : b.height; - return a - b; -} - -function sortRandom(a, b) { - return Math.random() > 0.5 ? 1 : -1; -} - -function sortValue(a, b) { - if (a.height === -1 && b.height !== -1) - return 1; - - if (a.height !== -1 && b.height === -1) - return -1; - - return b.value - a.value; -} +/** + * @param {Input} a + * @param {Input} b + * @returns {Number} + */ function sortInputs(a, b) { return a.compare(b); } +/** + * @param {Output} a + * @param {Output} b + * @returns {Number} + */ + function sortOutputs(a, b) { return a.compare(b); } @@ -1942,9 +1488,6 @@ function sortLinked(a, b) { * Expose */ -exports = MTX; -exports.MTX = MTX; -exports.Selector = CoinSelector; -exports.FundingError = FundingError; +MTX.MTX = MTX; -module.exports = exports; +module.exports = MTX; diff --git a/lib/utils/coinselector.js b/lib/utils/coinselector.js new file mode 100644 index 00000000..fd3a0977 --- /dev/null +++ b/lib/utils/coinselector.js @@ -0,0 +1,711 @@ +/*! + * coinselector.js - Coin Selector + * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). + * Copyright (c) 2025, Nodari Chkuaselidze (MIT License) + * https://github.com/handshake-org/hsd + */ + +'use strict'; + +const assert = require('bsert'); +const Amount = require('../ui/amount'); +const Address = require('../primitives/address'); +const Output = require('../primitives/output'); +const Outpoint = require('../primitives/outpoint'); +const policy = require('../protocol/policy'); +const {BufferMap} = require('buffer-map'); + +/** @typedef {import('../types').Amount} AmountValue */ +/** @typedef {import('../types').Hash} Hash */ +/** @typedef {import('../coins/coinview')} CoinView */ +/** @typedef {import('../primitives/mtx').MTX} MTX */ +/** @typedef {import('../primitives/coin')} Coin */ + +class AbstractCoinSource { + /** + * Initialize the coin source. + * @returns {Promise} + */ + + async init() { + throw new Error('Abstract method.'); + } + + /** + * @returns {Boolean} + */ + + hasNext() { + throw new Error('Abstract method.'); + } + + /** + * @returns {Promise} + */ + + next() { + throw new Error('Abstract method.'); + } + + /** + * @param {BufferMap} inputs + * @param {Coin[]} coins - Coin per input. + * @returns {Promise} + */ + + async resolveInputsToCoins(inputs, coins) { + throw new Error('Abstract method.'); + } +} + +/** @typedef {'all'|'random'|'age'|'value'} MemSelectionType */ + +/** + * @typedef {Object} CoinSourceOptions + * @property {MemSelectionType} [selection] - Selection type. + * @property {Coin[]} [coins] - Coins to select from. + */ + +/** + * Coin Source with coins. + * @alias module:utils.CoinSource + */ + +class InMemoryCoinSource extends AbstractCoinSource { + /** + * @param {CoinSourceOptions} [options] + */ + + constructor(options = {}) { + super(); + + /** @type {Coin[]} */ + this.coins = []; + + /** @type {MemSelectionType} */ + this.selection = 'value'; + + this.index = -1; + + if (options) + this.fromOptions(options); + } + + /** + * @param {CoinSourceOptions} options + * @returns {this} + */ + + fromOptions(options = {}) { + if (options.coins != null) { + assert(Array.isArray(options.coins), 'Coins must be an array.'); + this.coins = options.coins.slice(); + } + + if (options.selection != null) { + assert(typeof options.selection === 'string', + 'Selection must be a string.'); + this.selection = options.selection; + } + + return this; + } + + async init() { + this.index = 0; + + switch (this.selection) { + case 'all': + case 'random': + shuffle(this.coins); + break; + case 'age': + this.coins.sort(sortAge); + break; + case 'value': + this.coins.sort(sortValue); + break; + default: + throw new FundingError(`Bad selection type: ${this.selection}`); + } + } + + hasNext() { + return this.index < this.coins.length; + } + + /** + * @returns {Promise} + */ + + async next() { + if (!this.hasNext()) + return null; + + return this.coins[this.index++]; + } + + /** + * @param {BufferMap} inputs + * @param {Coin[]} coins + * @returns {Promise} + */ + + async resolveInputsToCoins(inputs, coins) { + for (const coin of this.coins) { + const {hash, index} = coin; + const key = Outpoint.toKey(hash, index); + const i = inputs.get(key); + + if (i != null) { + assert(!coins[i]); + coins[i] = coin; + inputs.delete(key); + } + } + } +} + +/** + * @typedef {Object} InputOption + * @property {Hash} hash + * @property {Number} index + */ + +/** + * @typedef {Object} CoinSelectorOptions + * @property {Address} [changeAddress] - Change address. + * @property {Boolean} [subtractFee] - Subtract fee from output. + * @property {Number} [subtractIndex] - Index of output to subtract fee from. + * @property {Number} [height] - Current chain height. + * @property {Number} [depth] - Minimum confirmation depth of coins to spend. + * @property {Number} [confirmations] - depth alias. + * @property {Number} [coinbaseMaturity] - When do CBs become spendable. + * @property {Number} [hardFee] - Fixed fee. + * @property {Number} [rate] - Rate of dollarydoo per kB. + * @property {Number} [maxFee] - Maximum fee we are willing to pay. + * @property {Boolean} [round] - Round to the nearest kilobyte. + * @property {Function?} [estimate] - Input script size estimator. + * @property {Boolean} [selectAll] - Select all coins. + * @property {InputOption[]} [inputs] - Inputs to use for funding. + */ + +/** + * Coin Selector + * @alias module:utils.CoinSelector + * @property {MTX} tx - clone of the original mtx. + * @property {CoinView} view - reference to the original view. + */ + +class CoinSelector { + /** + * @param {MTX} mtx + * @param {AbstractCoinSource} source + * @param {CoinSelectorOptions?} [options] + */ + + constructor(mtx, source, options = {}) { + this.original = mtx; + /** @type {MTX} */ + this.tx = mtx.clone(); + /** @type {CoinView} */ + this.view = mtx.view; + this.source = source; + this.outputValue = 0; + this.fee = CoinSelector.MIN_FEE; + + /** @type {Coin[]} */ + this.chosen = []; + + this.selectAll = false; + this.subtractFee = false; + this.subtractIndex = -1; + this.height = -1; + this.depth = -1; + this.hardFee = -1; + this.rate = CoinSelector.FEE_RATE; + this.maxFee = -1; + this.round = false; + this.coinbaseMaturity = 400; + this.changeAddress = null; + this.estimate = null; + + /** @type {BufferMap} */ + this.inputs = new BufferMap(); + + this.injectInputs(); + + if (options) + this.fromOptions(options); + } + + /** + * @param {CoinSelectorOptions} [options] + * @returns {this} + */ + + fromOptions(options = {}) { + if (options.subtractFee != null) { + if (typeof options.subtractFee === 'number') { + assert(Number.isSafeInteger(options.subtractFee)); + assert(options.subtractFee >= -1); + this.subtractIndex = options.subtractFee; + this.subtractFee = this.subtractIndex !== -1; + } else { + assert(typeof options.subtractFee === 'boolean'); + this.subtractFee = options.subtractFee; + } + } + + if (options.subtractIndex != null) { + assert(Number.isSafeInteger(options.subtractIndex)); + assert(options.subtractIndex >= -1); + this.subtractIndex = options.subtractIndex; + this.subtractFee = this.subtractIndex !== -1; + } + + if (options.height != null) { + assert(Number.isSafeInteger(options.height)); + assert(options.height >= -1); + this.height = options.height; + } + + if (options.confirmations != null) { + assert(Number.isSafeInteger(options.confirmations)); + assert(options.confirmations >= -1); + this.depth = options.confirmations; + } + + if (options.depth != null) { + assert(Number.isSafeInteger(options.depth)); + assert(options.depth >= -1); + this.depth = options.depth; + } + + if (options.hardFee != null) { + assert(Number.isSafeInteger(options.hardFee)); + assert(options.hardFee >= -1); + this.hardFee = options.hardFee; + } + + if (options.rate != null) { + assert(Number.isSafeInteger(options.rate)); + assert(options.rate >= 0); + this.rate = options.rate; + } + + if (options.maxFee != null) { + assert(Number.isSafeInteger(options.maxFee)); + assert(options.maxFee >= -1); + this.maxFee = options.maxFee; + } + + if (options.round != null) { + assert(typeof options.round === 'boolean'); + this.round = options.round; + } + + if (options.coinbaseMaturity != null) { + assert((options.coinbaseMaturity >>> 0) === options.coinbaseMaturity); + this.coinbaseMaturity = options.coinbaseMaturity; + } + + if (options.changeAddress) { + const addr = options.changeAddress; + if (typeof addr === 'string') { + this.changeAddress = Address.fromString(addr); + } else { + assert(addr instanceof Address); + this.changeAddress = addr; + } + } + + if (options.estimate) { + assert(typeof options.estimate === 'function'); + this.estimate = options.estimate; + } + + if (options.selectAll != null) { + assert(typeof options.selectAll === 'boolean'); + this.selectAll = options.selectAll; + } + + if (options.inputs) { + assert(Array.isArray(options.inputs)); + + const lastIndex = this.inputs.size; + for (let i = 0; i < options.inputs.length; i++) { + const prevout = options.inputs[i]; + assert(prevout && typeof prevout === 'object'); + const {hash, index} = prevout; + this.inputs.set(Outpoint.toKey(hash, index), lastIndex + i); + } + } + + return this; + } + + /** + * Attempt to inject existing inputs. + * @private + */ + + injectInputs() { + if (this.tx.inputs.length > 0) { + for (let i = 0; i < this.tx.inputs.length; i++) { + const {prevout} = this.tx.inputs[i]; + this.inputs.set(prevout.toKey(), i); + } + } + } + + /** + * Initialize the selector with coins to select from. + */ + + init() { + this.outputValue = this.tx.getOutputValue(); + this.chosen = []; + this.change = 0; + this.fee = CoinSelector.MIN_FEE; + this.tx.inputs.length = 0; + } + + /** + * Calculate total value required. + * @returns {AmountValue} + */ + + total() { + if (this.subtractFee) + return this.outputValue; + + return this.outputValue + this.fee; + } + + /** + * Test whether filler + * completely funded the transaction. + * @returns {Boolean} + */ + + isFull() { + return this.tx.getInputValue() >= this.total(); + } + + /** + * Test whether a coin is spendable + * with regards to the options. + * @param {Coin} coin + * @returns {Boolean} + */ + + isSpendable(coin) { + if (this.tx.view.hasEntry(coin)) + return false; + + if (coin.covenant.isNonspendable()) + return false; + + if (this.height === -1) + return true; + + if (coin.coinbase) { + if (coin.height === -1) + return false; + + if (this.height + 1 < coin.height + this.coinbaseMaturity) + return false; + + return true; + } + + if (this.depth === -1) + return true; + + const depth = coin.getDepth(this.height); + + if (depth < this.depth) + return false; + + return true; + } + + /** + * Get the current fee based on a size. + * @param {Number} size + * @returns {AmountValue} + */ + + getFee(size) { + // This is mostly here for testing. + // i.e. A fee rounded to the nearest + // kb is easier to predict ahead of time. + if (this.round) + return policy.getRoundFee(size, this.rate); + + return policy.getMinFee(size, this.rate); + } + + /** + * Fund the transaction with more + * coins if the `output value + fee` + * total was updated. + * @returns {Promise} + */ + + async fund() { + // Ensure all preferred inputs first. + await this.resolveInputCoins(); + + if (this.isFull() && !this.selectAll) + return; + + for (;;) { + const coin = await this.source.next(); + + if (!coin) + break; + + if (!this.isSpendable(coin)) + continue; + + this.tx.addCoin(coin); + this.chosen.push(coin); + + if (this.selectAll) + continue; + + if (this.isFull()) + break; + } + } + + /** + * Initialize selection based on size estimate. + */ + + async selectEstimate() { + // Set minimum fee and do + // an initial round of funding. + this.fee = CoinSelector.MIN_FEE; + await this.fund(); + + // Add dummy output for change. + const change = new Output(); + + if (this.changeAddress) { + change.address = this.changeAddress; + } else { + // In case we don't have a change address, + // we use a fake p2pkh output to gauge size. + change.address.fromPubkeyhash(Buffer.allocUnsafe(20)); + } + + this.tx.outputs.push(change); + + // Keep recalculating the fee and funding + // until we reach some sort of equilibrium. + do { + const size = await this.tx.estimateSize(this.estimate); + + this.fee = this.getFee(size); + + if (this.maxFee > 0 && this.fee > this.maxFee) + throw new FundingError('Fee is too high.'); + + // Failed to get enough funds, add more coins. + if (!this.isFull()) + await this.fund(); + } while (!this.isFull() && this.source.hasNext()); + } + + /** + * Collect coins for the transaction. + * @returns {Promise} + */ + + async selectHard() { + this.fee = this.hardFee; + await this.fund(); + } + + /** + * Fill the transaction with inputs. + * @returns {Promise} + */ + + async select() { + this.init(); + + if (this.hardFee !== -1) { + await this.selectHard(); + } else { + // This is potentially asynchronous: + // it may invoke the size estimator + // required for redeem scripts (we + // may be calling out to a wallet + // or something similar). + await this.selectEstimate(); + } + + if (!this.isFull()) { + // Still failing to get enough funds. + throw new FundingError( + 'Not enough funds.', + this.tx.getInputValue(), + this.total()); + } + + // How much money is left after filling outputs. + this.change = this.tx.getInputValue() - this.total(); + + return this; + } + + async resolveInputCoins() { + if (this.inputs.size === 0) + return; + + /** @type {Coin[]} */ + const coins = []; + + for (let i = 0 ; i < this.inputs.size; i++) { + coins.push(null); + } + + // first resolve from coinview if possible. + for (const key of this.inputs.keys()) { + const prevout = Outpoint.fromKey(key); + + if (this.view.hasEntry(prevout)) { + const coinEntry = this.view.getEntry(prevout); + const i = this.inputs.get(key); + + if (i != null) { + assert(!coins[i]); + coins[i] = coinEntry.toCoin(prevout); + this.inputs.delete(key); + } + } + } + + if (this.inputs.size > 0) + await this.source.resolveInputsToCoins(this.inputs, coins); + + if (this.inputs.size > 0) + throw new Error('Could not resolve preferred inputs.'); + + for (const coin of coins) { + this.tx.addCoin(coin); + this.chosen.push(coin); + } + } +} + +/** + * Default fee rate + * for coin selection. + * @const {Amount} + * @default + */ + +CoinSelector.FEE_RATE = 10000; + +/** + * Minimum fee to start with + * during coin selection. + * @const {Amount} + * @default + */ + +CoinSelector.MIN_FEE = 10000; + +/** + * Funding Error + * An error thrown from the coin selector. + * @ignore + * @extends Error + * @property {String} message - Error message. + * @property {Amount} availableFunds + * @property {Amount} requiredFunds + */ + +class FundingError extends Error { + /** + * Create a funding error. + * @constructor + * @param {String} msg + * @param {AmountValue} [available] + * @param {AmountValue} [required] + */ + + constructor(msg, available, required) { + super(); + + this.type = 'FundingError'; + this.message = msg; + this.availableFunds = -1; + this.requiredFunds = -1; + + if (available != null) { + this.message += ` (available=${Amount.coin(available)},`; + this.message += ` required=${Amount.coin(required)})`; + this.availableFunds = available; + this.requiredFunds = required; + } + + if (Error.captureStackTrace) + Error.captureStackTrace(this, FundingError); + } +} + +/* + * Helpers + */ + +/** + * @param {Coin} a + * @param {Coin} b + * @returns {Number} + */ + +function sortAge(a, b) { + const ah = a.height === -1 ? 0x7fffffff : a.height; + const bh = b.height === -1 ? 0x7fffffff : b.height; + return ah - bh; +} + +/** + * @param {Coin[]} coins + * @returns {Coin[]} + */ + +function shuffle(coins) { + for (let i = coins.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [coins[i], coins[j]] = [coins[j], coins[i]]; + } + return coins; +} + +/** + * @param {Coin} a + * @param {Coin} b + * @returns {Number} + */ + +function sortValue(a, b) { + if (a.height === -1 && b.height !== -1) + return 1; + + if (a.height !== -1 && b.height === -1) + return -1; + + return b.value - a.value; +} + +exports.AbstractCoinSource = AbstractCoinSource; +exports.InMemoryCoinSource = InMemoryCoinSource; +exports.CoinSelector = CoinSelector; +exports.FundingError = FundingError; diff --git a/lib/wallet/common.js b/lib/wallet/common.js index 836df4b7..04d3eb26 100644 --- a/lib/wallet/common.js +++ b/lib/wallet/common.js @@ -150,3 +150,15 @@ common.sortDeps = function sortDeps(txs) { return result; }; + +/** + * Wallet coin selection types. + * @enum {String} + */ + +common.coinSelectionTypes = { + DB_ALL: 'db-all', + DB_VALUE: 'db-value', + DB_SWEEPDUST: 'db-sweepdust', + DB_AGE: 'db-age' +}; diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 1952fbd8..747f2f88 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -1878,6 +1878,7 @@ class TransactionOptions { this.rate = valid.u64('rate'); this.maxFee = valid.u64('maxFee'); this.selection = valid.str('selection'); + this.sweepdustMinValue = valid.u64('sweepdustMinValue'); this.smart = valid.bool('smart'); this.account = valid.str('account'); this.locktime = valid.u64('locktime'); @@ -1948,8 +1949,7 @@ function enforce(value, msg) { * Expose */ -exports = HTTP; -exports.HTTP = HTTP; -exports.TransactionOptions = TransactionOptions; +HTTP.HTTP = HTTP; +HTTP.TransactionOptions = TransactionOptions; -module.exports = exports; +module.exports = HTTP; diff --git a/lib/wallet/layout.js b/lib/wallet/layout.js index 31a38f88..02b1ae8f 100644 --- a/lib/wallet/layout.js +++ b/lib/wallet/layout.js @@ -118,6 +118,20 @@ exports.wdb = { * p[hash] -> dummy (pending tx) * P[account][tx-hash] -> dummy (pending tx by account) * + * Coin Selection + * -------------- + * Sv[value][tx-hash][index] -> dummy (confirmed coins by Value) + * SV[account][value][tx-hash][index] -> dummy + * (confirmed coins by account + Value) + * + * Su[value][tx-hash][index] -> dummy (Unconfirmed coins by value) + * SU[account][value][tx-hash][index] -> dummy + * (Unconfirmed coins by account + value) + * + * Sh[tx-hash][index] -> dummy (coins by account + Height) + * SH[account][height][tx-hash][index] -> dummy + * (coins by account + Height) + * * Count and Time Index * -------------------- * Ol - Latest Unconfirmed Index @@ -161,6 +175,20 @@ exports.txdb = { d: bdb.key('d', ['hash256', 'uint32']), s: bdb.key('s', ['hash256', 'uint32']), + // Coin Selector + // confirmed by Value + Sv: bdb.key('Sv', ['uint64', 'hash256', 'uint32']), + // confirmed by account + Value + SV: bdb.key('SV', ['uint32', 'uint64', 'hash256', 'uint32']), + // Unconfirmed by value + Su: bdb.key('Su', ['uint64', 'hash256', 'uint32']), + // Unconfirmed by account + value + SU: bdb.key('SU', ['uint32', 'uint64', 'hash256', 'uint32']), + // by Height + Sh: bdb.key('Sh', ['uint32', 'hash256', 'uint32']), + // by account + Height + SH: bdb.key('SH', ['uint32', 'uint32', 'hash256', 'uint32']), + // Transaction t: bdb.key('t', ['hash256']), T: bdb.key('T', ['uint32', 'hash256']), diff --git a/lib/wallet/migrations.js b/lib/wallet/migrations.js index 3608b4de..0c72b486 100644 --- a/lib/wallet/migrations.js +++ b/lib/wallet/migrations.js @@ -1352,6 +1352,226 @@ class MigrateMigrationStateV1 extends AbstractMigration { } } +class MigrateCoinSelection extends AbstractMigration { + /** + * @param {WalletMigratorOptions} options + * @constructor + */ + + constructor(options) { + super(options); + + /** @type {WalletMigratorOptions} */ + this.options = options; + this.logger = options.logger.context('wallet-migration-coin-selection'); + this.db = options.db; + this.ldb = options.ldb; + this.layout = MigrateCoinSelection.layout(); + + this.UNCONFIRMED_HEIGHT = 0xffffffff; + this.batchSize = 5000; + this.progress = { + wid: 0, + account: 0, + hash: consensus.ZERO_HASH, + index: 0 + }; + } + + /** + * We always migrate. + * @returns {Promise} + */ + + async check() { + return types.MIGRATE; + } + + /** + * Actual migration + * @param {Batch} b + * @param {WalletMigrationContext} ctx + * @returns {Promise} + */ + + async migrate(b, ctx) { + const wlayout = this.layout.wdb; + const wids = await this.ldb.keys({ + gte: wlayout.W.min(), + lte: wlayout.W.max(), + parse: key => wlayout.W.decode(key)[0] + }); + + await this.decodeProgress(ctx.state.inProgressData); + + for (const wid of wids) { + if (wid < this.progress.wid) { + this.logger.debug( + 'Skipping wallet %d (%d/%d), already migrated.', + wid, this.progress.wid, wids.length); + continue; + } + + this.logger.info( + 'Migrating wallet %d (%d/%d)', + wid, this.progress.wid, wids.length); + await this.migrateWallet(wid, ctx); + } + + this.db.writeVersion(b, 5); + } + + /** + * @param {Number} wid + * @param {WalletMigrationContext} ctx + * @returns {Promise} + */ + + async migrateWallet(wid, ctx) { + const txlayout = this.layout.txdb; + const prefix = txlayout.prefix.encode(wid); + const bucket = this.ldb.bucket(prefix); + + const min = txlayout.C.encode( + this.progress.account, + this.progress.hash, + this.progress.index + ); + + const coinsIter = bucket.iterator({ + gte: min, + lte: txlayout.C.max() + }); + + let parent = bucket.batch(); + let total = 0; + + for await (const {key} of coinsIter) { + const [account, hash, index] = txlayout.C.decode(key); + const rawCoin = await bucket.get(txlayout.c.encode(hash, index)); + const coin = Coin.decode(rawCoin); + + if (coin.isUnspendable() || coin.covenant.isNonspendable()) + continue; + + if (coin.height === -1) { + // index coins by value + parent.put(txlayout.Su.encode(coin.value, hash, index), null); + parent.put(txlayout.SU.encode(account, coin.value, hash, index), null); + + // index coins by height + parent.put(txlayout.Sh.encode(this.UNCONFIRMED_HEIGHT, hash, index), + null); + + parent.put( + txlayout.SH.encode(account, this.UNCONFIRMED_HEIGHT, hash, index), + null + ); + } else { + parent.put(txlayout.Sv.encode(coin.value, hash, index), null); + parent.put(txlayout.SV.encode(account, coin.value, hash, index), null); + + parent.put(txlayout.Sh.encode(coin.height, hash, index), null); + parent.put(txlayout.SH.encode(account, coin.height, hash, index), null); + } + + if (++total % this.batchSize === 0) { + // save progress + this.progress.wid = wid; + this.progress.account = account; + this.progress.hash = hash; + this.progress.index = index + 1; + + ctx.state.inProgressData = this.encodeProgress(); + ctx.writeState(parent.root()); + + await parent.write(); + parent = bucket.batch(); + } + }; + + this.progress.wid = wid + 1; + this.progress.account = 0; + this.progress.hash = consensus.ZERO_HASH; + this.progress.index = 0; + ctx.state.inProgressData = this.encodeProgress(); + ctx.writeState(parent.root()); + await parent.write(); + } + + /** + * @returns {Buffer} + */ + + encodeProgress() { + const bw = bio.write(44); + bw.writeU32(this.progress.wid); + bw.writeU32(this.progress.account); + bw.writeBytes(this.progress.hash); + bw.writeU32(this.progress.index); + return bw.render(); + } + + /** + * Get migration info. + * @param {Buffer} data + * @return {Object} + */ + + decodeProgress(data) { + if (data.length === 0) + return; + + assert(data.length === 44); + + const br = bio.read(data); + this.progress.wid = br.readU32(); + this.progress.account = br.readU32(); + this.progress.hash = br.readBytes(32); + this.progress.index = br.readU32(); + } + + static info() { + return { + name: 'Wallet Coin Selection Migration', + description: 'Reindex coins for better coin selection' + }; + } + + static layout() { + return { + wdb: { + V: bdb.key('V'), + + // W[wid] -> wallet id + W: bdb.key('W', ['uint32']) + }, + txdb: { + prefix: bdb.key('t', ['uint32']), + + // Coins + c: bdb.key('c', ['hash256', 'uint32']), + C: bdb.key('C', ['uint32', 'hash256', 'uint32']), + d: bdb.key('d', ['hash256', 'uint32']), + s: bdb.key('s', ['hash256', 'uint32']), + + // confirmed by Value + Sv: bdb.key('Sv', ['uint64', 'hash256', 'uint32']), + // confirmed by account + Value + SV: bdb.key('SV', ['uint32', 'uint64', 'hash256', 'uint32']), + // Unconfirmed by value + Su: bdb.key('Su', ['uint64', 'hash256', 'uint32']), + // Unconfirmed by account + value + SU: bdb.key('SU', ['uint32', 'uint64', 'hash256', 'uint32']), + // by height + Sh: bdb.key('Sh', ['uint32', 'hash256', 'uint32']), + // by account + height + SH: bdb.key('SH', ['uint32', 'uint32', 'hash256', 'uint32']) + } + }; + } +} + /** * Wallet migration results. * @alias module:blockchain.WalletMigrationResult @@ -1536,7 +1756,8 @@ WalletMigrator.migrations = { 3: MigrateTXDBBalances, 4: MigrateBidRevealEntries, 5: MigrateTXCountTimeIndex, - 6: MigrateMigrationStateV1 + 6: MigrateMigrationStateV1, + 7: MigrateCoinSelection }; // Expose migrations @@ -1547,5 +1768,6 @@ WalletMigrator.MigrateTXDBBalances = MigrateTXDBBalances; WalletMigrator.MigrateBidRevealEntries = MigrateBidRevealEntries; WalletMigrator.MigrateTXCountTimeIndex = MigrateTXCountTimeIndex; WalletMigrator.MigrateMigrationStateV1 = MigrateMigrationStateV1; +WalletMigrator.MigrateCoinSelection = MigrateCoinSelection; module.exports = WalletMigrator; diff --git a/lib/wallet/plugin.js b/lib/wallet/plugin.js index f4062ebb..71ad830f 100644 --- a/lib/wallet/plugin.js +++ b/lib/wallet/plugin.js @@ -12,6 +12,10 @@ const NodeClient = require('./nodeclient'); const HTTP = require('./http'); const RPC = require('./rpc'); +/** @typedef {import('../node/fullnode')} FullNode */ +/** @typedef {import('../node/spvnode')} SPVNode */ +/** @typedef {FullNode|SPVNode} Node */ + /** * @exports wallet/plugin */ @@ -114,9 +118,11 @@ plugin.id = 'walletdb'; /** * Plugin initialization. * @param {Node} node - * @returns {WalletDB} + * @returns {Plugin} */ plugin.init = function init(node) { return new Plugin(node); }; + +plugin.Plugin = Plugin; diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index afa1bbd1..a73830b2 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -33,6 +33,9 @@ const {EXP} = consensus; const RPCBase = bweb.RPC; const RPCError = bweb.RPCError; +/** @typedef {import('./node')} WalletNode */ +/** @typedef {import('./plugin').Plugin} Plugin */ + /* * Constants */ @@ -76,13 +79,12 @@ const MAGIC_STRING = `${pkg.currency} signed message:\n`; /** * Wallet RPC * @alias module:wallet.RPC - * @extends bweb.RPC */ class RPC extends RPCBase { /** * Create an RPC. - * @param {WalletDB} wdb + * @param {WalletNode|Plugin} node */ constructor(node) { diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 7d0b92ca..9f87c607 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -197,6 +197,86 @@ class TXDB { b.del(layout.d.encode(spender.hash, spender.index)); } + /** + * Coin selection index credit. + * @param {Batch} b + * @param {Credit} credit + * @param {Path} path + * @param {Number?} oldHeight + */ + + indexCSCredit(b, credit, path, oldHeight) { + const {coin} = credit; + + if (coin.isUnspendable() || coin.covenant.isNonspendable()) + return; + + // value index + if (coin.height === -1) { + // index unconfirmed coin by value + b.put(layout.Su.encode(coin.value, coin.hash, coin.index), null); + + // index unconfirmed coin by account + value. + b.put(layout.SU.encode( + path.account, coin.value, coin.hash, coin.index), null); + } else { + // index confirmed coin by value + b.put(layout.Sv.encode(coin.value, coin.hash, coin.index), null); + + // index confirmed coin by account + value. + b.put(layout.SV.encode( + path.account, coin.value, coin.hash, coin.index), null); + } + + // cleanup old value indexes. + if (oldHeight && oldHeight === -1) { + // remove unconfirmed indexes, now that it's confirmed. + b.del(layout.Su.encode(coin.value, coin.hash, coin.index)); + b.del(layout.SU.encode(path.account, coin.value, coin.hash, coin.index)); + } else if (oldHeight && oldHeight !== -1) { + // remove confirmed indexes, now that it's unconfirmed. + b.del(layout.Sv.encode(coin.value, coin.hash, coin.index)); + b.del(layout.SV.encode(path.account, coin.value, coin.hash, coin.index)); + } + + // handle height indexes + // index coin by account + height + const height = coin.height === -1 ? UNCONFIRMED_HEIGHT : coin.height; + b.put(layout.Sh.encode(height, coin.hash, coin.index), null); + b.put(layout.SH.encode(path.account, height, coin.hash, coin.index), null); + + if (oldHeight != null) { + const height = oldHeight === -1 ? UNCONFIRMED_HEIGHT : oldHeight; + b.del(layout.Sh.encode(height, coin.hash, coin.index)); + b.del(layout.SH.encode(path.account, height, coin.hash, coin.index)); + } + } + + /** + * Unindex Credit. + * @param {Batch} b + * @param {Credit} credit + * @param {Path} path + */ + + unindexCSCredit(b, credit, path) { + const {coin} = credit; + + // Remove coin by account + value. + if (coin.height === -1) { + b.del(layout.Su.encode(coin.value, coin.hash, coin.index)); + b.del(layout.SU.encode(path.account, coin.value, coin.hash, coin.index)); + } else { + b.del(layout.Sv.encode(coin.value, coin.hash, coin.index)); + b.del(layout.SV.encode(path.account, coin.value, coin.hash, coin.index)); + } + + // Remove coin by account + height + const height = coin.height === -1 ? UNCONFIRMED_HEIGHT : coin.height; + b.del(layout.Sh.encode(height, coin.hash, coin.index)); + b.del(layout.SH.encode(path.account, height, coin.hash, coin.index)); + } + /** * Spend credit by spender/input record. * Add undo coin to the input record. @@ -1118,6 +1198,7 @@ class TXDB { this.unlockBalances(state, credit, path, height); await this.removeCredit(b, credit, path); + this.unindexCSCredit(b, credit, path); view.addCoin(coin); } @@ -1162,6 +1243,7 @@ class TXDB { } await this.saveCredit(b, credit, path); + this.indexCSCredit(b, credit, path, null); await this.watchOpensEarly(b, output); } @@ -1340,6 +1422,7 @@ class TXDB { // entirely, now that we know it's also // been removed on-chain. await this.removeCredit(b, credit, path); + this.unindexCSCredit(b, credit, path); view.addCoin(coin); } @@ -1397,6 +1480,7 @@ class TXDB { credit.coin.height = height; await this.saveCredit(b, credit, path); + this.indexCSCredit(b, credit, path, -1); } // Handle names. @@ -1521,6 +1605,7 @@ class TXDB { credit.spent = false; await this.saveCredit(b, credit, path); + this.indexCSCredit(b, credit, path, null); } } @@ -1553,6 +1638,7 @@ class TXDB { } await this.removeCredit(b, credit, path); + this.unindexCSCredit(b, credit, path); } // Undo name state. @@ -1783,6 +1869,7 @@ class TXDB { credit.spent = true; own = true; await this.saveCredit(b, credit, path); + this.indexCSCredit(b, credit, path, null); } } @@ -1834,6 +1921,7 @@ class TXDB { // Update coin height and confirmed // balance. Save once again. + const oldHeight = credit.coin.height; credit.coin.height = -1; // If the coin was not discovered now, it means @@ -1846,6 +1934,7 @@ class TXDB { } await this.saveCredit(b, credit, path); + this.indexCSCredit(b, credit, path, oldHeight); } // Unconfirm will also index OPENs as the transaction is now part of the @@ -2199,13 +2288,13 @@ class TXDB { const time = medianTime; b.del(layout.Oi.encode(time, count.height, - count.index, options.hash)); + count.index, options.hash)); for (const [acct] of accounts) { b.del(layout.OT.encode(acct, count.height, count.index)); b.del(layout.OI.encode(acct, time, count.height, - count.index, options.hash)); + count.index, options.hash)); } } @@ -3372,8 +3461,8 @@ class TXDB { * Filter array of coins or outpoints * for only unlocked ones. * jsdoc can't express this type. - * @param {Coin[]|Outpoint[]} coins - * @returns {Coin[]|Outpoint[]} + * @param {Coin[]} coins + * @returns {Coin[]} */ filterLocked(coins) { @@ -3726,6 +3815,179 @@ class TXDB { return coins; } + /** + * Get credits iterator sorted by value. + * @param {Number} acct + * @param {Object} [options] + * @param {Number} [options.minValue=0] + * @param {Number} [options.maxValue=MAX_MONEY] + * @param {Number?} [options.limit] + * @param {Boolean} [options.reverse=false] + * @param {Boolean} [options.inclusive=true] + * @returns {AsyncGenerator} + */ + + async *getAccountCreditIterByValue(acct, options = {}) { + assert(typeof acct === 'number'); + assert(options && typeof options === 'object'); + + const { + minValue = 0, + maxValue = consensus.MAX_MONEY, + inclusive = true, + reverse = false, + limit + } = options; + + assert(typeof minValue === 'number'); + assert(typeof maxValue === 'number'); + assert(minValue <= maxValue); + + const iterOpts = { + limit: limit, + reverse: reverse, + keys: true, + values: false + }; + + const greater = inclusive ? 'gte' : 'gt'; + const lesser = inclusive ? 'lte' : 'lt'; + + let prefix, hashIdx; + let min, max; + + if (acct === -1) { + prefix = layout.Sv; + min = prefix.min(minValue); + max = prefix.max(maxValue); + hashIdx = 1; + } else { + prefix = layout.SV; + min = prefix.min(acct, minValue); + max = prefix.max(acct, maxValue); + hashIdx = 2; + } + + iterOpts[greater] = min; + iterOpts[lesser] = max; + + const iter = this.bucket.iterator(iterOpts); + let items = 0; + + for await (const key of iter.keysAsync()) { + const decoded = prefix.decode(key); + const hash = decoded[hashIdx]; + const index = decoded[hashIdx + 1]; + const credit = await this.getCredit(hash, index); + + assert(credit); + yield credit; + items++; + } + + // now process unconfirmed. + if (acct === -1) { + prefix = layout.Su; + min = prefix.min(minValue); + max = prefix.max(maxValue); + } else { + prefix = layout.SU; + min = prefix.min(acct, minValue); + max = prefix.max(acct, maxValue); + } + + iterOpts[greater] = min; + iterOpts[lesser] = max; + + if (limit != null && limit > 0) { + if (items >= limit) + return; + + iterOpts.limit = limit - items; + } + + const ucIter = this.bucket.iterator(iterOpts); + + for await (const key of ucIter.keysAsync()) { + const decoded = prefix.decode(key); + const hash = decoded[hashIdx]; + const index = decoded[hashIdx + 1]; + const credit = await this.getCredit(hash, index); + + assert(credit); + yield credit; + } + } + + /** + * Get credits iterator sorted by height. + * @param {Number} acct + * @param {Object} [options] + * @param {Number} [options.minHeight=0] + * @param {Number} [options.maxHeight=UNCONFIRMED_HEIGHT] + * @param {Number} [options.limit=-1] + * @param {Boolean} [options.reverse=false] + * @param {Boolean} [options.inclusive=true] + * @returns {AsyncGenerator} + */ + + async *getAccountCreditIterByHeight(acct, options = {}) { + assert(typeof acct === 'number'); + assert(options && typeof options === 'object'); + + const { + minHeight = 0, + maxHeight = UNCONFIRMED_HEIGHT, + inclusive = true, + reverse = false, + limit + } = options; + + assert(typeof minHeight === 'number'); + assert(typeof maxHeight === 'number'); + assert(minHeight <= maxHeight); + + const iterOpts = { + limit, + reverse, + keys: true, + values: false + }; + + const greater = inclusive ? 'gte' : 'gt'; + const lesser = inclusive ? 'lte' : 'lt'; + + let prefix, hashIdx; + let min, max; + + if (acct === -1) { + prefix = layout.Sh; + min = prefix.min(minHeight); + max = prefix.max(maxHeight); + hashIdx = 1; + } else { + prefix = layout.SH; + min = prefix.min(acct, minHeight); + max = prefix.max(acct, maxHeight); + hashIdx = 2; + } + + iterOpts[greater] = min; + iterOpts[lesser] = max; + + const iter = this.bucket.iterator(iterOpts); + + for await (const key of iter.keysAsync()) { + const decoded = prefix.decode(key); + const hash = decoded[hashIdx]; + const index = decoded[hashIdx + 1]; + const credit = await this.getCredit(hash, index); + + assert(credit); + yield credit; + } + } + /** * Get a coin viewpoint. * @param {TX} tx diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 264e5ecc..40ea10c0 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -13,6 +13,8 @@ const base58 = require('bcrypto/lib/encoding/base58'); const bio = require('bufio'); const blake2b = require('bcrypto/lib/blake2b'); const cleanse = require('bcrypto/lib/cleanse'); +const bufmap = require('buffer-map'); +const BufferSet = bufmap.BufferSet; const TXDB = require('./txdb'); const Path = require('./path'); const common = require('./common'); @@ -38,9 +40,12 @@ 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} Batch */ @@ -68,6 +73,8 @@ const Outpoint = require('../primitives/outpoint'); const EMPTY = Buffer.alloc(0); +const DEFAULT_SELECTION = common.coinSelectionTypes.DB_VALUE; + /** * @typedef {Object} AddResult * @property {Details} details @@ -1201,7 +1208,9 @@ class Wallet extends EventEmitter { * @param {(String|Number)?} options.account - If no account is * specified, coins from the entire wallet will be filled. * @param {String?} options.selection - Coin selection priority. Can - * be `age`, `random`, or `all`. (default=age). + * be `random`, `age`, `db-age`, `value`, `db-value`, `all`, `db-all` + * or `db-sweepdust` + * (default=`db-value`) * @param {Boolean} options.round - Whether to round to the nearest * kilobyte for fee calculation. * See {@link TX#getMinFee} vs. {@link TX#getRoundFee}. @@ -1213,6 +1222,8 @@ class Wallet extends EventEmitter { * calculating one. * @param {Number|Boolean} options.subtractFee - Whether to subtract the * fee from existing outputs rather than adding more inputs. + * @param {Number} [options.sweepdustMinValue=1] - Minimum value for + * sweepdust value. * @param {Boolean} [force] */ @@ -1227,19 +1238,14 @@ class Wallet extends EventEmitter { /** * Fill a transaction with inputs without a lock. - * @private - * @see MTX#selectCoins - * @see MTX#fill * @param {MTX} mtx * @param {Object} [options] + * @returns {Promise} */ - async fill(mtx, options) { - if (!options) - options = {}; - + async fill(mtx, options = {}) { const acct = options.account || 0; - const change = await this.changeAddress(acct); + const change = await this.changeAddress(acct === -1 ? 0 : acct); if (!change) throw new Error('Account not found.'); @@ -1248,19 +1254,14 @@ 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, + // sweepdust options + sweepdustMinValue: options.sweepdustMinValue, + round: options.round, depth: options.depth, hardFee: options.hardFee, @@ -1273,6 +1274,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} + */ + + async select(mtx, options) { + const selection = options.selection || DEFAULT_SELECTION; + + 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 === common.coinSelectionTypes.DB_ALL) + options.selectAll = true; + + const selector = new CoinSelector(mtx, source, options); + await selector.select(); + await source.end(); + + return selector; } /** @@ -1968,9 +2016,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) @@ -1983,10 +2028,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' @@ -2171,7 +2217,6 @@ class Wallet extends EventEmitter { continue; const ns = await this.getNameState(nameHash); - const name = ns.name; if (!ns) continue; @@ -2195,7 +2240,7 @@ class Wallet extends EventEmitter { const bv = await this.getBlind(blind); if (!bv) { - this.logger.warning(`Blind value not found for name: ${name}.`); + this.logger.warning(`Blind value not found for name: ${ns.name}.`); continue; } @@ -5151,6 +5196,36 @@ class Wallet extends EventEmitter { return this.txdb.getCredits(account); } + /** + * Get credits iterator sorted by value. + * @param {Number} acct + * @param {Object} [options] + * @param {Number} [options.minValue=0] + * @param {Number} [options.maxValue=MAX_MONEY] + * @param {Number} [options.limit=-1] + * @param {Boolean} [options.reverse=false] + * @returns {AsyncGenerator} + */ + + getAccountCreditIterByValue(acct, options = {}) { + return this.txdb.getAccountCreditIterByValue(acct, options); + } + + /** + * Get credits iterator sorted by height. + * @param {Number} acct + * @param {Object} [options] + * @param {Number} [options.minHeight=0] + * @param {Number} [options.maxHeight=UNCONFIRMED_HEIGHT] + * @param {Number} [options.limit=-1] + * @param {Boolean} [options.reverse=false] + * @returns {AsyncGenerator} + */ + + getAccountCreditIterByHeight(acct, options = {}) { + return this.txdb.getAccountCreditIterByHeight(acct, options); + } + /** * Get "smart" coins. * @param {(String|Number)?} acct @@ -5563,6 +5638,180 @@ 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 = DEFAULT_SELECTION; + this.smart = false; + + this.sweepdustMinValue = 1; + + 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; + } + + if (options.sweepdustMinValue != null) { + assert(typeof options.sweepdustMinValue === 'number', + 'Sweepdust min value must be a number.'); + assert((options.sweepdustMinValue >>> 0) === options.sweepdustMinValue, + 'Sweepdust min value must be a uint32.'); + + this.sweepdustMinValue = options.sweepdustMinValue; + } + + return this; + } + + async init() { + switch (this.selection) { + case common.coinSelectionTypes.DB_VALUE: + case common.coinSelectionTypes.DB_ALL: { + this.iter = this.txdb.getAccountCreditIterByValue(this.account, { + reverse: true + }); + break; + } + case common.coinSelectionTypes.DB_AGE: { + this.iter = this.txdb.getAccountCreditIterByHeight(this.account); + break; + } + case common.coinSelectionTypes.DB_SWEEPDUST: { + this.iter = this.txdb.getAccountCreditIterByValue(this.account, { + minValue: this.sweepdustMinValue + }); + break; + } + default: + throw new Error(`Invalid selection: ${this.selection}`); + } + } + + /** + * Are we done. + * @returns {Boolean} + */ + + hasNext() { + return !this.done; + } + + /** + * @returns {Promise} + */ + + 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 {bufmap.BufferMap} inputs + * @param {Coin[]} coins - Coin per input. + * @returns {Promise} + */ + + 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); + + if (!coin) + continue; + + coins[idx] = coin; + inputs.delete(key); + } + } +} + /* * Expose */ diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index a2527e5d..1ac57569 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -92,7 +92,7 @@ class WalletDB extends EventEmitter { /** @type {bdb.DB} */ this.db = bdb.create(this.options); this.name = 'wallet'; - this.version = 4; + this.version = 5; // chain state. this.hasStateCache = false; diff --git a/test/data/migrations/wallet-4-bid-reveal-gen.js b/test/data/migrations/wallet-4-bid-reveal-gen.js index b42934df..bd8aa777 100644 --- a/test/data/migrations/wallet-4-bid-reveal-gen.js +++ b/test/data/migrations/wallet-4-bid-reveal-gen.js @@ -10,6 +10,7 @@ const WalletDB = require('../../../lib/wallet/walletdb'); const MTX = require('../../../lib/primitives/mtx'); const wutils = require('../../../test/util/wallet'); const rules = require('../../../lib/covenants/rules'); +const {deterministicInput} = require('../../../test/util/primitives'); const layout = { wdb: { @@ -66,16 +67,16 @@ let timeCounter = 0; // fund wallets const mtx1 = new MTX(); - mtx1.addInput(wutils.deterministicInput(txID++)); + mtx1.addInput(deterministicInput(txID++)); mtx1.addOutput(await wallet1.receiveAddress(0), 10e6); const mtx2 = new MTX(); - mtx2.addInput(wutils.deterministicInput(txID++)); + mtx2.addInput(deterministicInput(txID++)); mtx2.addOutput(await wallet1.receiveAddress(1), 10e6); // fund second wallet. const mtx3 = new MTX(); - mtx3.addInput(wutils.deterministicInput(txID++)); + mtx3.addInput(deterministicInput(txID++)); mtx3.addOutput(await wallet2.receiveAddress(), 10e6); await wdb.addBlock(wutils.nextEntry(wdb), [ diff --git a/test/data/migrations/wallet-5-pagination-gen.js b/test/data/migrations/wallet-5-pagination-gen.js index fb495479..7f893b80 100644 --- a/test/data/migrations/wallet-5-pagination-gen.js +++ b/test/data/migrations/wallet-5-pagination-gen.js @@ -25,7 +25,8 @@ const ChainEntry = require('../../../lib/blockchain/chainentry'); const WalletDB = require('../../../lib/wallet/walletdb'); const MTX = require('../../../lib/primitives/mtx'); const rules = require('../../../lib/covenants/rules'); -const wutils = require('../../../test/util/wallet'); +const wutils = require('../../util/wallet'); +const primutils = require('../../util/primitives'); const layout = { wdb: { @@ -282,7 +283,7 @@ async function fundThree(wallet1, wallet2) { for (const [wallet, acct] of funds) { timeCounter++; const mtx1 = new MTX(); - mtx1.addInput(wutils.deterministicInput(txID++)); + mtx1.addInput(primutils.deterministicInput(txID++)); mtx1.addOutput(await wallet.receiveAddress(acct), 10e6); mtx1.addOutput(OUT_ADDR, 1e6); txs.push(mtx1.toTX()); diff --git a/test/data/migrations/wallet-7-coinselector-gen.js b/test/data/migrations/wallet-7-coinselector-gen.js new file mode 100644 index 00000000..a68292ff --- /dev/null +++ b/test/data/migrations/wallet-7-coinselector-gen.js @@ -0,0 +1,347 @@ +'use strict'; + +const assert = require('bsert'); +const bdb = require('bdb'); +const Network = require('../../../lib/protocol/network'); +const MTX = require('../../../lib/primitives/mtx'); +const {Resource} = require('../../../lib/dns/resource'); +const WalletDB = require('../../../lib/wallet/walletdb'); +const wutils = require('../../util/wallet'); +const mutils = require('../../util/migrations'); + +const network = Network.get('regtest'); + +const layout = { + wdb: { + V: bdb.key('V'), + + // W[wid] -> wallet id + W: bdb.key('W', ['uint32']) + }, + txdb: { + prefix: bdb.key('t', ['uint32']), + + // Coins + c: bdb.key('c', ['hash256', 'uint32']), + C: bdb.key('C', ['uint32', 'hash256', 'uint32']), + d: bdb.key('d', ['hash256', 'uint32']), + s: bdb.key('s', ['hash256', 'uint32']), + + // confirmed by Value + Sv: bdb.key('Sv', ['uint64', 'hash256', 'uint32']), + // confirmed by account + Value + SV: bdb.key('SV', ['uint32', 'uint64', 'hash256', 'uint32']), + // Unconfirmed by value + Su: bdb.key('Su', ['uint64', 'hash256', 'uint32']), + // Unconfirmed by account + value + SU: bdb.key('SU', ['uint32', 'uint64', 'hash256', 'uint32']), + // by height + Sh: bdb.key('Sh', ['uint32', 'hash256', 'uint32']), + // by account + height + SH: bdb.key('SH', ['uint32', 'uint32', 'hash256', 'uint32']) + } +}; + +/** @typedef {import('../../util/wallet').InboundTXOptions} InboundTXOptions */ +/** @typedef {import('../../util/wallet').OutputInfo} OutputInfo */ + +/* + * Generate a wallet with coins in multiple states, + * similar to coin selection indexes test at test/wallet-coinselection-test.js + */ + +const OUT_ADDR = 'rs1q2uqpaefgfjke38whrtvdzsve3478k38qcgg9ws'; + +const wallet1priv = 'rprvKE8qsHtkmUxUSPQdn2sFKFUcKyUQz9pKQhxjEWecnXg9hgJMsmJXcw' + + 'J77SqmHT1R6mcuNqVPzgT2EoGStsXaUN92VJKhQWUB6uZdL8gAZvez'; +const wallet2priv = 'rprvKE8qsHtkmUxUSR4jE7Lti9XV77hv7xxacAShw5MvxY6RfsAYVeB1WL' + + 'WtjiebDmqTruVJxmMeQUMkk61e83WDZbZidDnNPhHyQpeEwxjuSZuG'; + +(async () => { + // we use legacy selection to ensure + // deterministic coin selections. + const selection = 'value'; + + const wdb = new WalletDB({ + network: network, + memory: true + }); + + await wdb.open(); + + const wallet1 = await wdb.create({ + id: 'wallet1', + master: wallet1priv + }); + + await wallet1.createAccount('alt'); + + const wallet2 = await wdb.create({ + id: 'wallet2', + master: wallet2priv + }); + + for (let i = 0; i < 50; i++) { + await wdb.addBlock(wutils.nextEntry(wdb), []); + } + + /** @type {OutputInfo[]} */ + const outputs = Array.from({ length: 15 }, () => { + return { + value: 1e6 + }; + }); + + /** @type {InboundTXOptions} */ + const createOptions = { + createAddress: true, + txPerOutput: true, + deterministicInput: true + }; + + // unconfirm -> confirm + const w1a1txs = await wutils.createInboundTXs(wallet1, outputs, createOptions); + const w1a2txs = await wutils.createInboundTXs(wallet1, outputs.map(v => ({ + ...v, + account: 1 + })), createOptions); + const w2a1txs = await wutils.createInboundTXs(wallet2, outputs, createOptions); + const alltxs = [...w1a1txs, ...w1a2txs, ...w2a1txs]; + + for (const tx of alltxs) + await wdb.addTX(tx); + + // confirm. + await wdb.addBlock(wutils.nextEntry(wdb), alltxs); + + // new unconfirmed txs + const w1a1txs2 = await wutils.createInboundTXs(wallet1, outputs, createOptions); + const w1a2txs2 = await wutils.createInboundTXs(wallet1, outputs.map(v => ({ + ...v, + account: 1 + })), createOptions); + const w2a1txs2 = await wutils.createInboundTXs(wallet2, outputs, createOptions); + const alltxs2 = [...w1a1txs2, ...w1a2txs2, ...w2a1txs2]; + + for (const tx of alltxs2) { + await wdb.addTX(tx); + } + + // 1 coinbase for each + /** @type {OutputInfo[]} */ + const coinbase = [{ + value: 1e6, + coinbase: true + }]; + + const w1a1cb = await wutils.createInboundTXs(wallet1, coinbase, createOptions); + const w1a2cb = await wutils.createInboundTXs(wallet1, [{ + ...coinbase[0], + account: 1 + }], createOptions); + const w2a1cb = await wutils.createInboundTXs(wallet2, coinbase, createOptions); + const allcb = [...w1a1cb, ...w1a2cb, ...w2a1cb]; + + for (const tx of allcb) + await wdb.addBlock(wutils.nextEntry(wdb), [tx]); + + // send some coins + { + const sendOpts = { + outputs: [{ + address: OUT_ADDR, + value: 1e6 + }], + selection + }; + + const confirmSend1 = await wallet1.send(sendOpts); + const confirmSend2 = await wallet1.send({ account: 1, ...sendOpts }); + const confirmSend3 = await wallet2.send(sendOpts); + + await wallet1.send(sendOpts); + await wallet1.send({ account: 1, ...sendOpts }); + await wallet2.send(sendOpts); + + await wdb.addBlock(wutils.nextEntry(wdb), [ + confirmSend1, confirmSend2, confirmSend3 + ]); + } + + // unconfirmed + { + const sendOpts = { + depth: 2, + outputs: [{ + address: OUT_ADDR, + value: 1e6 + }], + selection + }; + + const mtx1 = await wallet1.createTX({ account: 0, ...sendOpts }); + const mtx2 = await wallet1.createTX({ account: 1, ...sendOpts }); + const mtx3 = await wallet2.createTX(sendOpts); + + const txs = [mtx1, mtx2, mtx3].map(mtx => mtx.toTX()); + await wdb.addBlock(wutils.nextEntry(wdb), txs); + } + + { + // double spend + const sendOpts = { + depth: 1, + outputs: [{ + address: OUT_ADDR, + value: 1e6 + }], + selection + }; + + const mtx1 = await wallet1.createTX({ account: 0, ...sendOpts }); + const mtx2 = await wallet1.createTX({ account: 1, ...sendOpts }); + const mtx3 = await wallet2.createTX(sendOpts); + + const txs = [mtx1, mtx2, mtx3].map(mtx => mtx.toTX()); + const entry = wutils.nextEntry(wdb); + await wdb.addBlock(entry, txs); + + const discedTXCount = await wdb.removeBlock(entry); + assert(discedTXCount === txs.length); + + const coins1 = mtx1.inputs.map(input => mtx1.view.getCoinFor(input)); + const coins2 = mtx2.inputs.map(input => mtx2.view.getCoinFor(input)); + const coins3 = mtx3.inputs.map(input => mtx3.view.getCoinFor(input)); + + const dblspend1 = new MTX(); + dblspend1.addOutput({ + address: OUT_ADDR, + value: 1e6 + }); + + for (const coin of coins1) + dblspend1.addCoin(coin); + + await wallet1.sign(dblspend1); + dblspend1.check(); + + const dblspend2 = new MTX(); + dblspend2.addOutput({ + address: OUT_ADDR, + value: 1e6 + }); + + for (const coin of coins2) + dblspend2.addCoin(coin); + + await wallet1.sign(dblspend2); + dblspend2.check(); + + const dblspend3 = new MTX(); + dblspend3.addOutput({ + address: OUT_ADDR, + value: 1e6 + }); + + for (const coin of coins3) + dblspend3.addCoin(coin); + + await wallet2.sign(dblspend3); + dblspend3.check(); + + await wdb.addBlock(wutils.nextEntry(wdb), [ + dblspend1.toTX(), + dblspend2.toTX(), + dblspend3.toTX() + ]); + } + + // do some non spendables as well. + { + const sendOpts = { selection }; + + const availableName = 'testname-1'; + await wallet1.importName(availableName); + await wallet2.importName(availableName); + + const openTX = await wallet1.sendOpen(availableName, sendOpts); + + await wdb.addBlock(wutils.nextEntry(wdb), [openTX]); + + for (let i = 0; i < network.names.treeInterval + 1; i++) + await wdb.addBlock(wutils.nextEntry(wdb), []); + + const bid1 = await wallet1.sendBid(availableName, 1e4, 1e4, sendOpts); + const bid2 = await wallet2.sendBid(availableName, 2e4, 2e5, sendOpts); + + await wdb.addBlock(wutils.nextEntry(wdb), [bid1, bid2]); + for (let i = 0; i < network.names.biddingPeriod; i++) + await wdb.addBlock(wutils.nextEntry(wdb), []); + + const reveal1 = await wallet1.sendReveal(availableName, sendOpts); + const reveal2 = await wallet2.sendReveal(availableName, sendOpts); + + await wdb.addBlock(wutils.nextEntry(wdb), [reveal1, reveal2]); + + for (let i = 0; i < network.names.revealPeriod; i++) + await wdb.addBlock(wutils.nextEntry(wdb), []); + + // Don't send this one, have it locked + // const redeem1 = await wallet1.sendRedeem(availableName, sendOpts); + const resource = Resource.fromJSON({records: []}); + const register = await wallet2.sendUpdate(availableName, resource, sendOpts); + + await wdb.addBlock(wutils.nextEntry(wdb), [register]); + } + + const { + dump, + prefixes + } = await getMigrationDump(wdb); + console.log(JSON.stringify({ + prefixes, + data: dump + }, null, 2)); + + await wdb.close(); +})().catch((err) => { + console.error(err); + process.exit(1); +}); + +/** + * @param {WalletDB} wdb + * @returns {Object} + */ + +async function getMigrationDump(wdb) { + const prefixes = [ + layout.wdb.V.encode().toString('hex') + ]; + + for (let i = 1; i < 3; i++) { + prefixes.push(layout.wdb.W.encode(i).toString('hex')); + } + + for (let i = 1; i < 3; i++) { + const tprefix = layout.txdb.prefix.encode(i).toString('hex'); + + for (const key of Object.keys(layout.txdb)) { + if (key === 'prefix') + continue; + + const val = layout.txdb[key]; + + assert(val.id.toString('hex') === mutils.prefix2hex(key)); + const prefix = mutils.prefix2hex(key); + prefixes.push(tprefix + prefix); + } + } + + const dump = await wutils.dumpWDB(wdb, prefixes); + + return { + dump, + prefixes + }; +} diff --git a/test/data/migrations/wallet-7-coinselector.json b/test/data/migrations/wallet-7-coinselector.json new file mode 100644 index 00000000..841d6a13 --- /dev/null +++ b/test/data/migrations/wallet-7-coinselector.json @@ -0,0 +1,956 @@ +{ + "description": "Migration for coin selection in the wallet", + "prefixes": [ + "56", + "5700000001", + "5700000002", + "740000000163", + "740000000143", + "740000000164", + "740000000173", + "740000000153", + "740000000153", + "740000000153", + "740000000153", + "740000000153", + "740000000153", + "740000000263", + "740000000243", + "740000000264", + "740000000273", + "740000000253", + "740000000253", + "740000000253", + "740000000253", + "740000000253", + "740000000253" + ], + "before": { + "56": "77616c6c657404000000", + "5700000001": "0777616c6c657431", + "5700000002": "0777616c6c657432", + "740000000143000000000a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "00", + "740000000143000000000c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000": "00", + "7400000001430000000017b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000": "00", + "74000000014300000000240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000": "00", + "7400000001430000000026ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000": "00", + "7400000001430000000028dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "00", + "740000000143000000003896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "00", + "740000000143000000003dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000": "00", + "740000000143000000005ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000001": "00", + "740000000143000000006b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000": "00", + "740000000143000000006e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000": "00", + "740000000143000000007284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000": "00", + "740000000143000000007541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000": "00", + "7400000001430000000099fe9437e4242bb7c6107f5b9131f807ead2ad0c70dfe039b34d1f801a51bc0500000000": "00", + "740000000143000000009c98d2db8df0a5d4b5b1ee3074f872541b4c9fccd5e13256915ff22049faaf1d00000000": "00", + "74000000014300000000ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "00", + "74000000014300000000ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000001": "00", + "74000000014300000000b88b14dfc52612e8e1b84550d5ab7136d591302185938a7c59b54077c5a723f000000000": "00", + "74000000014300000000c86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000": "00", + "74000000014300000000cf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000": "00", + "74000000014300000000d52fb57ff1d3a4e5e2f923421015e2b5ce392f8031dfcdbbf6e3d425fef19e5a00000000": "00", + "74000000014300000000d5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000": "00", + "74000000014300000000d9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000": "00", + "74000000014300000000de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "00", + "74000000014300000000e63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000": "00", + "74000000014300000000eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000000": "00", + "74000000014300000000eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "00", + "74000000014300000000eb9dc1183d33bbb09bf6daa19e2a9c485fa5dff57386872a020f01e1fa14bf3c00000000": "00", + "74000000014300000000ed9f7353e70f6b38e26b0433245dde90b348ac1996f50d10c827a29c5240c24100000000": "00", + "74000000014300000000ee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000": "00", + "74000000014300000000fa5c2be8b0a338c4bd7200c9a6cfcdc2657f80acfa9dbef5a39e0e36a60720cf00000000": "00", + "7400000001430000000110fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000": "00", + "740000000143000000011e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000": "00", + "7400000001430000000124ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "00", + "7400000001430000000134d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "00", + "7400000001430000000135631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000": "00", + "740000000143000000013f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000": "00", + "740000000143000000014341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "00", + "740000000143000000014d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "00", + "740000000143000000014f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "00", + "740000000143000000015491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000": "00", + "740000000143000000015e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000": "00", + "74000000014300000001711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000": "00", + "7400000001430000000176cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000": "00", + "740000000143000000018bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000": "00", + "74000000014300000001943082f6babcfc5a5799956e8149574ab83cd8f474c6d2958fd8a78ed5b5e73d00000000": "00", + "74000000014300000001996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000": "00", + "74000000014300000001aa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000": "00", + "74000000014300000001acdda30e345a698a43e7d105719c9bbae846351caa6e59c5234cc1eb4840e6a300000000": "00", + "74000000014300000001b4bec8d2a53d512fe9c55d5af3c9565302e233cb8dab73b937791abb9cc41faf00000000": "00", + "74000000014300000001b6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000": "00", + "74000000014300000001bdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000": "00", + "74000000014300000001da74c9e20a2fef35b4363a266dda1141a65d1c79d2d59ab85a1dd201731b3f8400000000": "00", + "74000000014300000001e66914b1f93cf6d3317090f01c11aa2614f2c61fca5e22bfbd0192771222676900000000": "00", + "74000000014300000001e7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000": "00", + "74000000014300000001f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "00", + "74000000014300000001f74e991d682a2b3b97e69bfebff3e7a446b528f44db2f59bf508fa206808c13800000000": "00", + "74000000014300000001f75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000": "00", + "7400000001630a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "000000003700000028320f00000000000014a62c71694f8bf7583bfe73bd1b2235b81ee125580000000001", + "7400000001630c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000": "00000000ffffffff40420f00000000000014cc0fdce38328d8df1f900a638544b15f9d81f5520000000000", + "74000000016310fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000": "00000000ffffffff40420f000000000000143ddd945091f3ec8dc5e0c0cefce32875e5037aeb0000000000", + "74000000016317b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000": "00000000ffffffff40420f00000000000014c53ec93147b4fa7e1e1bf56f0edbdd319895e2530000000000", + "7400000001631e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000": "00000000ffffffff40420f0000000000001427595b8603244dc6dd42d5a89d6e77a18f18e23b0000000000", + "740000000163240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000": "00000000ffffffff40420f00000000000014e4a65896f371f330fd6aa119e933fc346cb1223b0000000000", + "74000000016324ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "000000003700000028320f00000000000014f0f269d0cb5eb1e065b1084d1d787a7ec5b6ed8c0000000001", + "74000000016326ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000": "00000000ffffffff40420f0000000000001401ef877cb2f639d44cb257bb2eb983d408c8893c0000000000", + "74000000016328dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "00000000ffffffff28320f000000000000141fcdaa5bd5efa8811106f6390e5ecc327083426b0000000001", + "74000000016334d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "000000003300000040420f000000000000145cb324e109b920b19494549b3e540f57d43c4ef10000000100", + "74000000016335631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000": "00000000ffffffff40420f00000000000014ad37f7abb46c43e2ac8238c1c9e4a98841b3fa6c0000000000", + "7400000001633896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "000000003300000040420f00000000000014b9e7138dd8a5fdd11fee499ccb7a6cd131edbaa30000000100", + "7400000001633dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000": "00000000ffffffff40420f00000000000014198a5f555dc8f78bcc35bf23baefc03c954e70530000000000", + "7400000001633f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000": "00000000ffffffff40420f00000000000014a2affbcd20614676abcac636b5db306e964411210000000000", + "7400000001634341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "000000003300000040420f000000000000142667d9e317cf8f0aac2543fd27f38e0ab9da3ecb0000000100", + "7400000001634d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "000000003300000040420f00000000000014d20cf3c6172ea38a0ec42514435978a76a99b1eb0000000100", + "7400000001634f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "000000003800000028320f00000000000014c674ba19506d9e2c178ab0eb4acd47b7011927050000000001", + "7400000001635491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000": "00000000ffffffff40420f000000000000140ca598cff494738bfbfe1b06f416bce1def2dfae0000000000", + "7400000001635e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000": "00000000ffffffff40420f00000000000014045216fd9b9f0aeff2be3c18327c3083fc4ffd4a0000000000", + "7400000001635ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000001": "0000000041000000d8090f0000000000001460976bb97754c7ab48bac4c894d27a4126bc23d10000000001", + "7400000001636b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000": "00000000ffffffff40420f00000000000014433b4a5bdd977ac6ab958e07834a08fbd2e7f9610000000000", + "7400000001636e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000": "00000000ffffffff40420f00000000000014edda79fa7ce1811ea0c0a9a2ffca881c509269020000000000", + "740000000163711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000": "00000000ffffffff40420f000000000000144eca2ab57c2a64d356a40fab260140a48fb39ca40000000000", + "7400000001637284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000": "00000000ffffffff40420f000000000000144725f4cf8e0fbeb8d220c54f3e38d6d1f73cba1a0000000000", + "7400000001637541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000": "00000000ffffffff40420f000000000000142e79df92c411266e738b4488a54686c6fe0d27b80000000000", + "74000000016376cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000": "00000000ffffffff40420f0000000000001417e6329645e6f871f3b2f01b2e8115ffc01fa0dd0000000000", + "7400000001638bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000": "00000000ffffffff40420f000000000000149dfcfcd6ce44031193a6960f878460c3f1157b0b0000000000", + "740000000163943082f6babcfc5a5799956e8149574ab83cd8f474c6d2958fd8a78ed5b5e73d00000000": "000000003300000040420f00000000000014646f81d8c9569f04c93d1559a606da8185cb49c10000000000", + "740000000163996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000": "00000000ffffffff40420f0000000000001431bbc5580d28765f2172cd537369614a87c99b4d0000000000", + "74000000016399fe9437e4242bb7c6107f5b9131f807ead2ad0c70dfe039b34d1f801a51bc0500000000": "000000003300000040420f00000000000014cc2953e1188c162d1689206ce79fabdd54c0a5750000000000", + "7400000001639c98d2db8df0a5d4b5b1ee3074f872541b4c9fccd5e13256915ff22049faaf1d00000000": "000000003300000040420f000000000000147220144f51a799765a1cfebc74a6ff789e0cf0980000000000", + "740000000163aa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000": "00000000ffffffff40420f000000000000141860e07aa099551e6f6d4794de9eb48aac172a450000000000", + "740000000163ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "000000003a000000000000000000000000144a476eeca3f6b9f47b7ac6ea4953f5aec201fda002032075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d04000000000a746573746e616d652d31000001", + "740000000163ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000001": "000000003a0000007c330f00000000000014a7cd0f0c61bcdb86967f1b11128b7d3ba7d78ec70000000001", + "740000000163acdda30e345a698a43e7d105719c9bbae846351caa6e59c5234cc1eb4840e6a300000000": "000000003500000040420f00000000000014359d9335692983142de60a57df5a0197fd13ab760000010000", + "740000000163b4bec8d2a53d512fe9c55d5af3c9565302e233cb8dab73b937791abb9cc41faf00000000": "000000003300000040420f000000000000147db1e3611d2409939fb385976712f032de7629ef0000000000", + "740000000163b6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000": "00000000ffffffff40420f000000000000141b5a09ead7ea9f4c460d1a248d6980a98186f3c70000000000", + "740000000163b88b14dfc52612e8e1b84550d5ab7136d591302185938a7c59b54077c5a723f000000000": "000000003400000040420f00000000000014332479f977c3cd9a6a915a25e7ca4e95bbfb92c00000010000", + "740000000163bdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000": "00000000ffffffff40420f0000000000001434079c84c47cf7b4ce95b38b8b19040043b5a58b0000000000", + "740000000163c86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000": "00000000ffffffff40420f000000000000149df7d0326747749b602967421b892f1efe19fd730000000000", + "740000000163cf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000": "00000000ffffffff40420f000000000000147f0a0bb9e20f94b934fb814e4938ef3a783e6f970000000000", + "740000000163d52fb57ff1d3a4e5e2f923421015e2b5ce392f8031dfcdbbf6e3d425fef19e5a00000000": "000000003300000040420f0000000000001441136683ea0485bbef46e9d9f6a2ff766128efad0000000000", + "740000000163d5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000": "00000000ffffffff40420f0000000000001468a14e77be363ef2ee92406d5ce969a1785110130000000000", + "740000000163d9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000": "00000000ffffffff40420f00000000000014a37f94f98c1e2cb4714d5ce5465f9d7a82f3e36e0000000000", + "740000000163da74c9e20a2fef35b4363a266dda1141a65d1c79d2d59ab85a1dd201731b3f8400000000": "000000003300000040420f00000000000014452b000bd69cd9c4a99322d1c41e972cdcd662fe0000000000", + "740000000163de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "000000003800000028320f00000000000014222740511246fe2b10957049ae10e0bbe7fa996c0000000001", + "740000000163e63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000": "00000000ffffffff40420f00000000000014bfc1d60d296b8a15c489d32d5647f21e8c6cb8950000000000", + "740000000163e66914b1f93cf6d3317090f01c11aa2614f2c61fca5e22bfbd0192771222676900000000": "000000003300000040420f000000000000142cd7a7b49b1013603d53b6a234493ee1208471750000000000", + "740000000163e7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000": "00000000ffffffff40420f00000000000014f59d25508e19abeec49c3377bedd48ad7947af440000000000", + "740000000163eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000000": "0000000047000000102700000000000000142315e1a3850c7afd6f1be87df18dea1d3939d0a904032075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a00000020c616d48ae4039aad2438bc47a11e0ec0d883f5dfc1f34aa10944085c57d79a13000001", + "740000000163eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "00000000470000009c2c0f000000000000147a98b6621e4ccc528560645f6a7682633a24fad80000000001", + "740000000163eb9dc1183d33bbb09bf6daa19e2a9c485fa5dff57386872a020f01e1fa14bf3c00000000": "000000003300000040420f000000000000146a94c2244bd9a4b5257a8c946b68af72f4118b350000000000", + "740000000163ed9f7353e70f6b38e26b0433245dde90b348ac1996f50d10c827a29c5240c24100000000": "000000003300000040420f000000000000142f6d31b494bdfa42034719ca28894496cdb893590000000000", + "740000000163ee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000": "00000000ffffffff40420f000000000000147e7a6df077a7d3a39fba31246574634375439e3c0000000000", + "740000000163f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "00000000ffffffff28320f000000000000145258eb6f21709d00f66fa39deab6e8d5391eae5f0000000001", + "740000000163f74e991d682a2b3b97e69bfebff3e7a446b528f44db2f59bf508fa206808c13800000000": "000000003300000040420f00000000000014cfe3810f02a4af8dd98092bb6650841bac0ff2c40000000000", + "740000000163f75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000": "00000000ffffffff40420f000000000000144931bd2996fb46b2f17a5f4bae7d0b2a04ee65bc0000000000", + "740000000163fa5c2be8b0a338c4bd7200c9a6cfcdc2657f80acfa9dbef5a39e0e36a60720cf00000000": "000000003300000040420f000000000000144c43158dc848068c85406c14957b0cab073f343b0000000000", + "7400000001640a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "000000003300000040420f000000000000144f9644e04b6bbc4f53ae49e444d005524cc1422f000000", + "7400000001640a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000001": "000000003300000040420f000000000000140441cbe166229b9e2adb6aa90751052523cb298d000000", + "74000000016424ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "000000003300000040420f00000000000014f4f1b2cd73eebe34bef87c174e5d4903e86493ff000000", + "74000000016424ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000001": "000000003300000040420f000000000000146337e3ecd9354ba7a5fbe0ce2feb6d3bc5558a54000000", + "74000000016428dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "000000003300000040420f000000000000145cb324e109b920b19494549b3e540f57d43c4ef1000000", + "74000000016428dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000001": "000000003300000040420f00000000000014b9e7138dd8a5fdd11fee499ccb7a6cd131edbaa3000000", + "740000000164320e6e6cffd8d9b904d97f904536c6c410f47d7ed856e354b0836b6bd47ec44b00000000": "000000003300000040420f000000000000145428d3cebe215a9f78bf3ca6d3ebc055b828f3d9000000", + "740000000164320e6e6cffd8d9b904d97f904536c6c410f47d7ed856e354b0836b6bd47ec44b00000001": "000000003300000040420f000000000000146dddb81cee99d34e617876a2d4bd091fea7da844000000", + "7400000001644f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "000000003300000040420f00000000000014f7c2c1cf022472febc2deb36c91ae2e450c36b29000000", + "7400000001644f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000001": "000000003300000040420f00000000000014309a0821ae883849b41aeb44c24b075af338b8c0000000", + "7400000001645ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000000": "000000003300000040420f000000000000142a52b414589aca0f9e6b79be9563e3e0b8e053aa000000", + "74000000016499e639a2ab769f77f51c3752ca4d85436abd3d6ee0fa28d249fc67a7f4e8422200000000": "000000003300000040420f00000000000014e154c1b406841cc45d1d63fbd0c36003b5e34cc9000000", + "74000000016499e639a2ab769f77f51c3752ca4d85436abd3d6ee0fa28d249fc67a7f4e8422200000001": "000000003300000040420f000000000000149d03a12b2facd319be3b174ac49e78effae0f9fc000000", + "740000000164ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "000000003300000040420f00000000000014b27ecd0a361b0fe9acafecdc47841e021a4ebf50000000", + "740000000164de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "000000003300000040420f0000000000001499b6d711eb6ba45313ed8ff37d9a8ec5df572750000000", + "740000000164de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000001": "000000003300000040420f000000000000149259252916ee5b5573f1f911ed49c99592b1db44000000", + "740000000164eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000000": "0000000041000000102700000000000000142315e1a3850c7afd6f1be87df18dea1d3939d0a903042075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a0000000a746573746e616d652d3120d239a4dc0ce73cc36bf38204e3b0a438d353a7c6d57889fa6e88fff91265145500", + "740000000164eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "000000003300000040420f000000000000148d375a0132f2077de9c4a96c66e15c24c5501851000000", + "740000000164f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "000000003300000040420f000000000000142667d9e317cf8f0aac2543fd27f38e0ab9da3ecb000000", + "740000000164f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000001": "000000003300000040420f00000000000014d20cf3c6172ea38a0ec42514435978a76a99b1eb000000", + "74000000017303681f8ee2b9bd28df7af7445a589052965b60c4d89d13269b483aafd0ea9db600000000": "6b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000", + "7400000001730fff2e108fcc22c30f2e5117182fc26f74cd2a79c3372d196e7eff08416276d500000000": "0a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000", + "74000000017310576dd1a93f478542464d6721bc6053cdd310f68241cf7f94864edc80d8f23b00000000": "bdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000", + "74000000017313642f3ae0597fae5368a9ab3aa5002159ee421b622fff2dfaf4431259e117fb00000000": "0a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546401000000", + "7400000001731372f66217b7c16d46080b70af90951addb9fdf9a148566208957b82b5b1771200000000": "24ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000", + "7400000001731d14f5deb4d7480fe6568ede44151397fc4fa3da9ed32b6c405ca946d12e937200000000": "1e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000", + "7400000001732858e5aedda20aa45293fa3aa64722c13ea3d18adc8e952728739e450ef2023b00000000": "b6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000", + "7400000001732e1c6abb0f6ed5b4fcc53b6ebccaf5f155e0fa7d763a12a31ae68ac9408d02fd00000000": "26ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000", + "740000000173323b9dd49fb66e770de1fdd46bb57028a778e140de37a7ca5596a57100defbf900000000": "24ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59901000000", + "74000000017334d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "28dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000", + "74000000017336b9062567c977a2b897f8cbd547ce78fa3bd598ef1c6b5499e71229514fe3ed00000000": "e63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000", + "74000000017337066d4d4d245fc5bc01fe8c55005c9b92d7a9f8deb443a9369d4423acd9570000000000": "ee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000", + "7400000001733896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "28dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7701000000", + "7400000001733a3126fbbf206d75ecfbb40d5da8f39068263a6dbaa4e047529fd3ddb11fd8b800000000": "de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000", + "7400000001733ad9ed2ba729a6419907e522c55e246fa70eebaae3a01a3794b2df42fc36efb500000000": "7284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000", + "74000000017340660cb50912d86a4ec527cd5af973233d7f9bdee7361bf7fadfdf1243bfe43800000000": "240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000", + "740000000173408c946546253b8ba7642a80c1089bfe1a0fa76c33627d6f2d3684ee17b55ebe00000000": "de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d401000000", + "740000000173425ac3ec38bb16ae168a5fc25a8702bc65a6d9cd09f6896e1dc28499e2b18da800000000": "3dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000", + "7400000001734341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000", + "7400000001734561925263b2563548dd72b116034abee46210e88371f763563b6cd33eb0053500000000": "99e639a2ab769f77f51c3752ca4d85436abd3d6ee0fa28d249fc67a7f4e8422200000000", + "7400000001734d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf01000000", + "7400000001734dfc35d2d60a286f00bcff7fe5a868279ca21ec1903c1ef089facae0131bcbb600000000": "99e639a2ab769f77f51c3752ca4d85436abd3d6ee0fa28d249fc67a7f4e8422201000000", + "740000000173515b89fe7e7cd16dea4e1981b4f75e0a02b896ad4d30d2200daf82d8956ba8af00000000": "10fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000", + "74000000017353cb0828e3d769a6c997319c775e387c41280157d65fda2c9b997af84d7f90db00000000": "d9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000", + "7400000001735442c34bd79edaa96143c2fb37fa65fd304c65fc65e154cfc67468645c402f1b00000000": "35631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000", + "74000000017354f473602bcde9d4f9f719fd4e3edabf323f5a3593adb409e49c10d549ce834800000000": "4f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000", + "7400000001735991ca040bea6b28ed9f7b388ecd3e8b5afe19fec959bda94c6ea523aef0b30000000000": "3f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000", + "7400000001735ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000000": "eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000000", + "74000000017362ffbfd09fd83be044075929214ab559e3b9994197a792cc60b48ad80caff85700000000": "ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000", + "7400000001736b96d4992c1901179bcff23c0cb649ae98f6b878ab8961341d359944494874d000000000": "aa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000", + "7400000001736cdc4bd0ac996647c5e8b59a7f1616ce613ce3b4a9725a62a3abf4c7c43d114f00000000": "5ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000000", + "7400000001736dda8d21c1c543e52fa2201dd186dda5bce740cbe979e31d8c92bf6936e68bfa00000000": "996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000", + "74000000017377404f33697b378fb347d8d1fc6d341468f19cf2aaf7a2daa21679e0e5b4c5df00000000": "e7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000", + "7400000001737858cecbeefa2985c7ff7a126143e1099f6c2a1e6bb8f8b49876284c5aef11b500000000": "4f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a01000000", + "7400000001737a669bbd9b6071db8e6032adfdc266ece43a0375cbdda6e77925b855e2f066e000000000": "320e6e6cffd8d9b904d97f904536c6c410f47d7ed856e354b0836b6bd47ec44b00000000", + "7400000001737e905920371ec5f99176f13c5d5010758493c830fe520e97cc4f9d66d80b3efe00000000": "c86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000", + "7400000001737f161a9bd7c781ec7c25e094a012bd1931dec18faa2eaa5fd56b20ee19b1447100000000": "6e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000", + "740000000173807957571a3add9343a9d932cb90955446f9797c984c8582176e5d8e5790f12600000000": "eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed01000000", + "7400000001738486b24023f38c8f595dcb48880e9673e1e0434d601f1e8871ed60a8acda154d00000000": "f75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000", + "74000000017389220598e0a127f887cf16b37c62f26d9cace85a448243522f9e41b408ade26400000000": "17b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000", + "7400000001738dbb3eb02c8f8cf6cf03346a72d733d4ca9b7e7e5dacc1afeddfb5697376c30800000000": "0c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000", + "7400000001738eff95c81bcf3d5467c32ce9abf81872481c568c9f90947580122e8fe177dc7000000000": "320e6e6cffd8d9b904d97f904536c6c410f47d7ed856e354b0836b6bd47ec44b01000000", + "740000000173a2ba1d077eded3608cdf54c41c514c59b02aefe28af3e4e602f460626be42d3e00000000": "76cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000", + "740000000173a854a5b169d381f72109048ba56b41dd547ba2ac6b0b838d60d675fdcdf5310700000000": "cf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000", + "740000000173b352f75f0642ce5589167154fe760a84fb047418a871d5225fd1d66c53c4ebd100000000": "711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000", + "740000000173b37353c0edbb420e054eff5a0f05b3e0e99e852393f449c854057527a312fc3c00000000": "5491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000", + "740000000173b609f6515a3dcdf32b896678a89b1d867f1bd93b7485f82d1e901c51e875b2c200000000": "d5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000", + "740000000173e8634b9bc9031ba1617a827f1d51785c25bf5164692257ccbbfd3c5d2918c30d00000000": "8bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000", + "740000000173e899425912717de079a89b958274948c7056308ae920c899666e0cf3cfc0ffa100000000": "7541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000", + "740000000173ecf456658d108c368e6bb7473305a5278cae99cc199c55bfeb618420d6a6423100000000": "5e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000", + "740000000243000000001ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000": "00", + "740000000243000000001cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000": "00", + "740000000243000000001e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000": "00", + "740000000243000000002ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "00", + "740000000243000000003cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000": "00", + "74000000024300000000458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "00", + "740000000243000000004eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000": "00", + "7400000002430000000053956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000": "00", + "74000000024300000000554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "00", + "740000000243000000005dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000001": "00", + "7400000002430000000063864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000001": "00", + "74000000024300000000683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "00", + "7400000002430000000086387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000": "00", + "740000000243000000008b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000": "00", + "740000000243000000008eba67a23de92df548ecbb4f40be648bbf870a1de67191ad0b576c1fab59ea8b00000000": "00", + "74000000024300000000952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "00", + "74000000024300000000a7d8e41fbc89aacc349effd58b140f08b84ab4d33775bb967a678e8bf4d8c00a00000000": "00", + "74000000024300000000ad6c09f1d7c9eacd449af12280c6ac397061a27bed995bc5dfe7b24ce2c49e3a00000000": "00", + "74000000024300000000b37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000": "00", + "74000000024300000000bf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000": "00", + "74000000024300000000d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000000": "00", + "74000000024300000000d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000001": "00", + "74000000024300000000d949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000": "00", + "74000000024300000000db3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000": "00", + "74000000024300000000db40da9ef39f8b52f0cfc84fe1bd4320f9dcab3f0c7425af1e505cfd59ecedbb00000000": "00", + "74000000024300000000ddef667255e254fc9d885ee04fe4852dba08d4b7ba84e86562afe6351d62098000000000": "00", + "74000000024300000000e674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000": "00", + "74000000024300000000ee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000": "00", + "74000000024300000000ee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000": "00", + "74000000024300000000fa72230236318c48e79926683a93baa4ccf09e856a78dfef9e1bf20f9b947dbe00000000": "00", + "74000000024300000000fc7c46c4caa0d66e55e2b70c82fe65a68fef5fabfb5c903cc264afc68ea6b7bd00000000": "00", + "7400000002631ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000": "00000000ffffffff40420f00000000000014ac46b87e7e6708a4f2b7e278b78ae7ba02c04b3b0000000000", + "7400000002631cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000": "00000000ffffffff40420f00000000000014975caef4f934d0494c828c38140a139019dc71540000000000", + "7400000002631e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000": "00000000ffffffff40420f00000000000014f4a5f44033305254c96ee91dd253f903af5944aa0000000000", + "7400000002632ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "000000003800000028320f00000000000014fb392be3e9bd032482a3b24ec077f27c4fcf00b20000000001", + "7400000002633cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000": "00000000ffffffff40420f000000000000145493e23f073a3dba4442e529038ffd00c3cb72700000000000", + "740000000263458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "000000003300000040420f00000000000014bd835f1fd372fcc462a59b7e088dcaf1bdc8ca540000000100", + "7400000002634eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000": "00000000ffffffff40420f00000000000014a956453db168abea91324667b05b286fa5c03bd50000000000", + "74000000026353956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000": "00000000ffffffff40420f000000000000141ec0760b28899bb7b5ef6c617915c3db1d7324580000000000", + "740000000263554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "000000003300000040420f00000000000014dbba0b358b90b46b04a86b01a46e0b844ea7c3680000000100", + "7400000002635dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000001": "0000000041000000a8230c000000000000142e9187f08252e4be2044f7a41cd25ceb7ae96d460000000001", + "74000000026363864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000001": "0000000047000000a4ae020000000000001432aa9c919d911fed1b053a452dcbf3704a759e940000000001", + "740000000263683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "00000000ffffffff28320f00000000000014bfe57d3d51c357a99aa9c84195fe8e000273399a0000000001", + "74000000026386387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000": "00000000ffffffff40420f00000000000014ce48fa97354847c1f35d85763e776bd9e372ec240000000000", + "7400000002638b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000": "00000000ffffffff40420f0000000000001468a4797e126034fa8d58cdb8f709d61bda3fd7660000000000", + "7400000002638eba67a23de92df548ecbb4f40be648bbf870a1de67191ad0b576c1fab59ea8b00000000": "000000003300000040420f00000000000014093a264c767174696332bf57a93ffceaabe23bde0000000000", + "740000000263952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "000000003700000028320f00000000000014b44aae19323f53e63e5cd184dba19a71616808150000000001", + "740000000263a7d8e41fbc89aacc349effd58b140f08b84ab4d33775bb967a678e8bf4d8c00a00000000": "000000003600000040420f000000000000147ed369a0353f353c38c282be4b153f1f883b04de0000010000", + "740000000263ad6c09f1d7c9eacd449af12280c6ac397061a27bed995bc5dfe7b24ce2c49e3a00000000": "000000003300000040420f0000000000001478d8fc4cbe36b0fde3b3d337cebd4520dae1e4c60000000000", + "740000000263b37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000": "00000000ffffffff40420f000000000000148c446d6b2942f2ed67b8e78c64a0a84cfa079f920000000000", + "740000000263bf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000": "00000000ffffffff40420f000000000000141e3feab7c10d898c5762226c1a3d6969f470a89e0000000000", + "740000000263d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000000": "0000000052000000102700000000000000146ff78bc8fc05796fca3c60163568ff5c9dd22acc06042075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a000000010020ae3895cf597eff05b19e02a70ceeeecb9dc72dbfe6504a50e9343a72f06a87c5000001", + "740000000263d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000001": "00000000520000006c16000000000000001463ee2af024ea3dd2411e4de306e7f13fb26659df0000000001", + "740000000263d949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000": "00000000ffffffff40420f0000000000001454b98d922ac0a29f3dd4a71280bea1aa35297c130000000000", + "740000000263db3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000": "00000000ffffffff40420f00000000000014a7f6ac09b90a612149c8853772515fcc849361520000000000", + "740000000263db40da9ef39f8b52f0cfc84fe1bd4320f9dcab3f0c7425af1e505cfd59ecedbb00000000": "000000003300000040420f000000000000143229a7234fc49dbd38ea2f203e01e4a7f15c4bd40000000000", + "740000000263ddef667255e254fc9d885ee04fe4852dba08d4b7ba84e86562afe6351d62098000000000": "000000003300000040420f0000000000001483bd567dae4fb1abf5f6c53379e23c87ec036e770000000000", + "740000000263e674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000": "00000000ffffffff40420f0000000000001419703fb89be3e452f3cf0541d02f6118cda5eced0000000000", + "740000000263ee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000": "00000000ffffffff40420f000000000000142965055c105f10931d54d690ae8d5aca7aa5434d0000000000", + "740000000263ee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000": "00000000ffffffff40420f000000000000140b2909a9cc84726cb1f01c32e45288f5b49349ad0000000000", + "740000000263fa72230236318c48e79926683a93baa4ccf09e856a78dfef9e1bf20f9b947dbe00000000": "000000003300000040420f00000000000014784cc5364704372f14e7b68600aafbb2cd4aea390000000000", + "740000000263fc7c46c4caa0d66e55e2b70c82fe65a68fef5fabfb5c903cc264afc68ea6b7bd00000000": "000000003300000040420f000000000000149b18d692909cfbd2b7d9342d41932ed4e5a7e7930000000000", + "7400000002642ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "000000003300000040420f0000000000001446e641691144a03add0db507a26404313606782e000000", + "7400000002642ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000001": "000000003300000040420f0000000000001449fec1e6fcbd2ccc2597e1b12986fc6ec8e64c07000000", + "7400000002645dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000000": "000000003300000040420f000000000000149c1222d5dd6094fb7ee38bdb79e5d8eb7bdfd37b000000", + "74000000026463864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000000": "0000000041000000400d03000000000000146ff78bc8fc05796fca3c60163568ff5c9dd22acc03042075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a0000000a746573746e616d652d3120909aec88e869c4d284606d5cc8054853bfa7c4e24c6041c4d8598f1b29bf282400", + "740000000264683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "000000003300000040420f00000000000014bd835f1fd372fcc462a59b7e088dcaf1bdc8ca54000000", + "740000000264683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000001": "000000003300000040420f00000000000014dbba0b358b90b46b04a86b01a46e0b844ea7c368000000", + "740000000264952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "000000003300000040420f00000000000014852e98d8b8ab2946445f20b051e6b055b492c506000000", + "740000000264952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000001": "000000003300000040420f00000000000014195fe465b1df13832ebe58e00aaefa05702ad39b000000", + "740000000264c2d3e816222195ea0b04be6b7bb664e0b617f9c91a898682ea7214d5168c293700000000": "000000003300000040420f000000000000148e659d46d7ceb9464cc04f3aeb73bc2702c1e7a3000000", + "740000000264c2d3e816222195ea0b04be6b7bb664e0b617f9c91a898682ea7214d5168c293700000001": "000000003300000040420f0000000000001424fd6822d0ad3e981832391cb004484cbcab15da000000", + "740000000264d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000000": "0000000047000000204e00000000000000146ff78bc8fc05796fca3c60163568ff5c9dd22acc04032075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a000000200cc30191083dcbf8e46ecaa8972af321588068915326f7f0851b2cee9872050500", + "7400000002730cd502099a8d25b5b574bc381cf60b84f45163ccb32cb44d64d53375a2d8a97a00000000": "4eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000", + "7400000002731012b7e8316dcc4c65e0b80bc8b964962088c25629483bc27a81fb072726432800000000": "952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000", + "7400000002731c820c68189da48293553b1cd7465cf7f75ac3bf2db9172f5f9ddebf3d10ad8500000000": "ee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000", + "74000000027325cf4fe01b9170e6641cd56aa6d2110ed7755c831648f6b209010a65cd17bfc900000000": "952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e01000000", + "7400000002732c2a543be67720878d34455f26e69f830c741b4dfbf33cfcd4998b881a9c97bd00000000": "8b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000", + "7400000002732dd1d165fc1bfa68e3bfec4e134141df8da1e37b9daa8436109cc0fb4b1805f600000000": "ee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000", + "740000000273458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000", + "7400000002734e26855b7519729fbbd5d525ded78b92308ebb2acc11f595793997d358db2dc900000000": "b37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000", + "740000000273554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331101000000", + "7400000002735dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000000": "63864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000000", + "7400000002735f8e95f14b4655f2cd5f476b02390b083dd4208ccf1c73bc3bf3f7ae294d728b00000000": "2ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000", + "74000000027361886f1b56565fb2455e0b7d890eb674e01679dab480319320607e03c218912800000000": "d949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000", + "74000000027363849657fbafa56c0be2daa74185c51ff8323d1984b3d62becafb83a9f43866c00000000": "2ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da01000000", + "74000000027363864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000000": "d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000000", + "74000000027365b569f91f6b28d447f12c5e093a68933f58ef7d60ab129fa236e61d406e5c8200000000": "c2d3e816222195ea0b04be6b7bb664e0b617f9c91a898682ea7214d5168c293700000000", + "7400000002737250af6eb0c99bc0390f068687ee0f51e32340c16872da8c56427dd02c17aaa300000000": "86387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000", + "7400000002737b1e678d97e5ac5cf768247a497c5b76b3e6e2eaf5dab87abef949e30385d19400000000": "c2d3e816222195ea0b04be6b7bb664e0b617f9c91a898682ea7214d5168c293701000000", + "74000000027384440206ad559e1539aaf0d6b81f24c7bcca619032684024df17cb0b48a90f2d00000000": "5dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000000", + "74000000027386416be467af312440c129867613049d435ba837f71091aa56cef7829440f18e00000000": "1e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000", + "7400000002738a306eeb3501386cb9462e357d8574d0a51ab1a3904fcf6ad7fb516901bd90ff00000000": "bf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000", + "7400000002738ab5e6fc1290ea699b3f2c98e99226212e6b7217a0f774e17697bf88d77fa2ff00000000": "53956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000", + "7400000002738ef53020504301017d883fed0eed75001cb2242d219a372bb334895ef588789900000000": "1cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000", + "740000000273d007a06dd5e690763c223421b6126ba058d04216a3a02e4c164348513b6495ea00000000": "3cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000", + "740000000273dfc49e74ea08407f6722adf59ca4e8c782ee0183beac232e02675863fc5fbbad00000000": "e674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000", + "740000000273f8237a38c8e06c92a717eee4301299d785dd32765027b2fe30c683721c97379e00000000": "db3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000", + "740000000273fc12d97b516e6e7fe68e9bbcb622784543b45d5b89c00d720115d1e697eca4f500000000": "1ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000" + }, + "after": { + "56": "77616c6c657405000000", + "5700000001": "0777616c6c657431", + "5700000002": "0777616c6c657432", + "740000000143000000000a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "00", + "740000000143000000000c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000": "00", + "7400000001430000000017b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000": "00", + "74000000014300000000240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000": "00", + "7400000001430000000026ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000": "00", + "7400000001430000000028dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "00", + "740000000143000000003896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "00", + "740000000143000000003dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000": "00", + "740000000143000000005ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000001": "00", + "740000000143000000006b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000": "00", + "740000000143000000006e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000": "00", + "740000000143000000007284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000": "00", + "740000000143000000007541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000": "00", + "7400000001430000000099fe9437e4242bb7c6107f5b9131f807ead2ad0c70dfe039b34d1f801a51bc0500000000": "00", + "740000000143000000009c98d2db8df0a5d4b5b1ee3074f872541b4c9fccd5e13256915ff22049faaf1d00000000": "00", + "74000000014300000000ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "00", + "74000000014300000000ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000001": "00", + "74000000014300000000b88b14dfc52612e8e1b84550d5ab7136d591302185938a7c59b54077c5a723f000000000": "00", + "74000000014300000000c86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000": "00", + "74000000014300000000cf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000": "00", + "74000000014300000000d52fb57ff1d3a4e5e2f923421015e2b5ce392f8031dfcdbbf6e3d425fef19e5a00000000": "00", + "74000000014300000000d5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000": "00", + "74000000014300000000d9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000": "00", + "74000000014300000000de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "00", + "74000000014300000000e63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000": "00", + "74000000014300000000eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000000": "00", + "74000000014300000000eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "00", + "74000000014300000000eb9dc1183d33bbb09bf6daa19e2a9c485fa5dff57386872a020f01e1fa14bf3c00000000": "00", + "74000000014300000000ed9f7353e70f6b38e26b0433245dde90b348ac1996f50d10c827a29c5240c24100000000": "00", + "74000000014300000000ee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000": "00", + "74000000014300000000fa5c2be8b0a338c4bd7200c9a6cfcdc2657f80acfa9dbef5a39e0e36a60720cf00000000": "00", + "7400000001430000000110fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000": "00", + "740000000143000000011e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000": "00", + "7400000001430000000124ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "00", + "7400000001430000000134d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "00", + "7400000001430000000135631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000": "00", + "740000000143000000013f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000": "00", + "740000000143000000014341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "00", + "740000000143000000014d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "00", + "740000000143000000014f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "00", + "740000000143000000015491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000": "00", + "740000000143000000015e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000": "00", + "74000000014300000001711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000": "00", + "7400000001430000000176cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000": "00", + "740000000143000000018bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000": "00", + "74000000014300000001943082f6babcfc5a5799956e8149574ab83cd8f474c6d2958fd8a78ed5b5e73d00000000": "00", + "74000000014300000001996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000": "00", + "74000000014300000001aa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000": "00", + "74000000014300000001acdda30e345a698a43e7d105719c9bbae846351caa6e59c5234cc1eb4840e6a300000000": "00", + "74000000014300000001b4bec8d2a53d512fe9c55d5af3c9565302e233cb8dab73b937791abb9cc41faf00000000": "00", + "74000000014300000001b6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000": "00", + "74000000014300000001bdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000": "00", + "74000000014300000001da74c9e20a2fef35b4363a266dda1141a65d1c79d2d59ab85a1dd201731b3f8400000000": "00", + "74000000014300000001e66914b1f93cf6d3317090f01c11aa2614f2c61fca5e22bfbd0192771222676900000000": "00", + "74000000014300000001e7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000": "00", + "74000000014300000001f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "00", + "74000000014300000001f74e991d682a2b3b97e69bfebff3e7a446b528f44db2f59bf508fa206808c13800000000": "00", + "74000000014300000001f75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000": "00", + "7400000001534800000000000000333896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "00", + "74000000015348000000000000003399fe9437e4242bb7c6107f5b9131f807ead2ad0c70dfe039b34d1f801a51bc0500000000": "00", + "7400000001534800000000000000339c98d2db8df0a5d4b5b1ee3074f872541b4c9fccd5e13256915ff22049faaf1d00000000": "00", + "740000000153480000000000000033d52fb57ff1d3a4e5e2f923421015e2b5ce392f8031dfcdbbf6e3d425fef19e5a00000000": "00", + "740000000153480000000000000033eb9dc1183d33bbb09bf6daa19e2a9c485fa5dff57386872a020f01e1fa14bf3c00000000": "00", + "740000000153480000000000000033ed9f7353e70f6b38e26b0433245dde90b348ac1996f50d10c827a29c5240c24100000000": "00", + "740000000153480000000000000033fa5c2be8b0a338c4bd7200c9a6cfcdc2657f80acfa9dbef5a39e0e36a60720cf00000000": "00", + "740000000153480000000000000034b88b14dfc52612e8e1b84550d5ab7136d591302185938a7c59b54077c5a723f000000000": "00", + "7400000001534800000000000000370a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "00", + "740000000153480000000000000038de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "00", + "74000000015348000000000000003aac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "00", + "74000000015348000000000000003aac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000001": "00", + "7400000001534800000000000000415ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000001": "00", + "740000000153480000000000000047eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "00", + "7400000001534800000000ffffffff0c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000": "00", + "7400000001534800000000ffffffff17b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000": "00", + "7400000001534800000000ffffffff240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000": "00", + "7400000001534800000000ffffffff26ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000": "00", + "7400000001534800000000ffffffff28dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "00", + "7400000001534800000000ffffffff3dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000": "00", + "7400000001534800000000ffffffff6b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000": "00", + "7400000001534800000000ffffffff6e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000": "00", + "7400000001534800000000ffffffff7284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000": "00", + "7400000001534800000000ffffffff7541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000": "00", + "7400000001534800000000ffffffffc86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000": "00", + "7400000001534800000000ffffffffcf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000": "00", + "7400000001534800000000ffffffffd5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000": "00", + "7400000001534800000000ffffffffd9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000": "00", + "7400000001534800000000ffffffffe63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000": "00", + "7400000001534800000000ffffffffee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000": "00", + "74000000015348000000010000003334d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "00", + "7400000001534800000001000000334341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "00", + "7400000001534800000001000000334d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "00", + "740000000153480000000100000033943082f6babcfc5a5799956e8149574ab83cd8f474c6d2958fd8a78ed5b5e73d00000000": "00", + "740000000153480000000100000033b4bec8d2a53d512fe9c55d5af3c9565302e233cb8dab73b937791abb9cc41faf00000000": "00", + "740000000153480000000100000033da74c9e20a2fef35b4363a266dda1141a65d1c79d2d59ab85a1dd201731b3f8400000000": "00", + "740000000153480000000100000033e66914b1f93cf6d3317090f01c11aa2614f2c61fca5e22bfbd0192771222676900000000": "00", + "740000000153480000000100000033f74e991d682a2b3b97e69bfebff3e7a446b528f44db2f59bf508fa206808c13800000000": "00", + "740000000153480000000100000035acdda30e345a698a43e7d105719c9bbae846351caa6e59c5234cc1eb4840e6a300000000": "00", + "74000000015348000000010000003724ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "00", + "7400000001534800000001000000384f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "00", + "7400000001534800000001ffffffff10fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000": "00", + "7400000001534800000001ffffffff1e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000": "00", + "7400000001534800000001ffffffff35631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000": "00", + "7400000001534800000001ffffffff3f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000": "00", + "7400000001534800000001ffffffff5491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000": "00", + "7400000001534800000001ffffffff5e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000": "00", + "7400000001534800000001ffffffff711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000": "00", + "7400000001534800000001ffffffff76cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000": "00", + "7400000001534800000001ffffffff8bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000": "00", + "7400000001534800000001ffffffff996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000": "00", + "7400000001534800000001ffffffffaa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000": "00", + "7400000001534800000001ffffffffb6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000": "00", + "7400000001534800000001ffffffffbdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000": "00", + "7400000001534800000001ffffffffe7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000": "00", + "7400000001534800000001fffffffff101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "00", + "7400000001534800000001fffffffff75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000": "00", + "740000000153550000000000000000000f322828dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "00", + "740000000153550000000000000000000f42400c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000": "00", + "740000000153550000000000000000000f424017b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000": "00", + "740000000153550000000000000000000f4240240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000": "00", + "740000000153550000000000000000000f424026ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000": "00", + "740000000153550000000000000000000f42403dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000": "00", + "740000000153550000000000000000000f42406b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000": "00", + "740000000153550000000000000000000f42406e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000": "00", + "740000000153550000000000000000000f42407284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000": "00", + "740000000153550000000000000000000f42407541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000": "00", + "740000000153550000000000000000000f4240c86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000": "00", + "740000000153550000000000000000000f4240cf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000": "00", + "740000000153550000000000000000000f4240d5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000": "00", + "740000000153550000000000000000000f4240d9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000": "00", + "740000000153550000000000000000000f4240e63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000": "00", + "740000000153550000000000000000000f4240ee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000": "00", + "740000000153550000000100000000000f3228f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "00", + "740000000153550000000100000000000f424010fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000": "00", + "740000000153550000000100000000000f42401e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000": "00", + "740000000153550000000100000000000f424035631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000": "00", + "740000000153550000000100000000000f42403f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000": "00", + "740000000153550000000100000000000f42405491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000": "00", + "740000000153550000000100000000000f42405e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000": "00", + "740000000153550000000100000000000f4240711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000": "00", + "740000000153550000000100000000000f424076cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000": "00", + "740000000153550000000100000000000f42408bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000": "00", + "740000000153550000000100000000000f4240996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000": "00", + "740000000153550000000100000000000f4240aa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000": "00", + "740000000153550000000100000000000f4240b6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000": "00", + "740000000153550000000100000000000f4240bdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000": "00", + "740000000153550000000100000000000f4240e7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000": "00", + "740000000153550000000100000000000f4240f75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000": "00", + "74000000015356000000000000000000000000ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "00", + "740000000153560000000000000000000f09d85ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000001": "00", + "740000000153560000000000000000000f2c9ceb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "00", + "740000000153560000000000000000000f32280a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "00", + "740000000153560000000000000000000f3228de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "00", + "740000000153560000000000000000000f337cac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000001": "00", + "740000000153560000000000000000000f42403896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "00", + "740000000153560000000000000000000f424099fe9437e4242bb7c6107f5b9131f807ead2ad0c70dfe039b34d1f801a51bc0500000000": "00", + "740000000153560000000000000000000f42409c98d2db8df0a5d4b5b1ee3074f872541b4c9fccd5e13256915ff22049faaf1d00000000": "00", + "740000000153560000000000000000000f4240b88b14dfc52612e8e1b84550d5ab7136d591302185938a7c59b54077c5a723f000000000": "00", + "740000000153560000000000000000000f4240d52fb57ff1d3a4e5e2f923421015e2b5ce392f8031dfcdbbf6e3d425fef19e5a00000000": "00", + "740000000153560000000000000000000f4240eb9dc1183d33bbb09bf6daa19e2a9c485fa5dff57386872a020f01e1fa14bf3c00000000": "00", + "740000000153560000000000000000000f4240ed9f7353e70f6b38e26b0433245dde90b348ac1996f50d10c827a29c5240c24100000000": "00", + "740000000153560000000000000000000f4240fa5c2be8b0a338c4bd7200c9a6cfcdc2657f80acfa9dbef5a39e0e36a60720cf00000000": "00", + "740000000153560000000100000000000f322824ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "00", + "740000000153560000000100000000000f32284f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "00", + "740000000153560000000100000000000f424034d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "00", + "740000000153560000000100000000000f42404341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "00", + "740000000153560000000100000000000f42404d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "00", + "740000000153560000000100000000000f4240943082f6babcfc5a5799956e8149574ab83cd8f474c6d2958fd8a78ed5b5e73d00000000": "00", + "740000000153560000000100000000000f4240acdda30e345a698a43e7d105719c9bbae846351caa6e59c5234cc1eb4840e6a300000000": "00", + "740000000153560000000100000000000f4240b4bec8d2a53d512fe9c55d5af3c9565302e233cb8dab73b937791abb9cc41faf00000000": "00", + "740000000153560000000100000000000f4240da74c9e20a2fef35b4363a266dda1141a65d1c79d2d59ab85a1dd201731b3f8400000000": "00", + "740000000153560000000100000000000f4240e66914b1f93cf6d3317090f01c11aa2614f2c61fca5e22bfbd0192771222676900000000": "00", + "740000000153560000000100000000000f4240f74e991d682a2b3b97e69bfebff3e7a446b528f44db2f59bf508fa206808c13800000000": "00", + "740000000153680000003334d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "00", + "74000000015368000000333896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "00", + "74000000015368000000334341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "00", + "74000000015368000000334d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "00", + "7400000001536800000033943082f6babcfc5a5799956e8149574ab83cd8f474c6d2958fd8a78ed5b5e73d00000000": "00", + "740000000153680000003399fe9437e4242bb7c6107f5b9131f807ead2ad0c70dfe039b34d1f801a51bc0500000000": "00", + "74000000015368000000339c98d2db8df0a5d4b5b1ee3074f872541b4c9fccd5e13256915ff22049faaf1d00000000": "00", + "7400000001536800000033b4bec8d2a53d512fe9c55d5af3c9565302e233cb8dab73b937791abb9cc41faf00000000": "00", + "7400000001536800000033d52fb57ff1d3a4e5e2f923421015e2b5ce392f8031dfcdbbf6e3d425fef19e5a00000000": "00", + "7400000001536800000033da74c9e20a2fef35b4363a266dda1141a65d1c79d2d59ab85a1dd201731b3f8400000000": "00", + "7400000001536800000033e66914b1f93cf6d3317090f01c11aa2614f2c61fca5e22bfbd0192771222676900000000": "00", + "7400000001536800000033eb9dc1183d33bbb09bf6daa19e2a9c485fa5dff57386872a020f01e1fa14bf3c00000000": "00", + "7400000001536800000033ed9f7353e70f6b38e26b0433245dde90b348ac1996f50d10c827a29c5240c24100000000": "00", + "7400000001536800000033f74e991d682a2b3b97e69bfebff3e7a446b528f44db2f59bf508fa206808c13800000000": "00", + "7400000001536800000033fa5c2be8b0a338c4bd7200c9a6cfcdc2657f80acfa9dbef5a39e0e36a60720cf00000000": "00", + "7400000001536800000034b88b14dfc52612e8e1b84550d5ab7136d591302185938a7c59b54077c5a723f000000000": "00", + "7400000001536800000035acdda30e345a698a43e7d105719c9bbae846351caa6e59c5234cc1eb4840e6a300000000": "00", + "74000000015368000000370a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "00", + "740000000153680000003724ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "00", + "74000000015368000000384f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "00", + "7400000001536800000038de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "00", + "740000000153680000003aac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "00", + "740000000153680000003aac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000001": "00", + "74000000015368000000415ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000001": "00", + "7400000001536800000047eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "00", + "74000000015368ffffffff0c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000": "00", + "74000000015368ffffffff10fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000": "00", + "74000000015368ffffffff17b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000": "00", + "74000000015368ffffffff1e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000": "00", + "74000000015368ffffffff240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000": "00", + "74000000015368ffffffff26ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000": "00", + "74000000015368ffffffff28dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "00", + "74000000015368ffffffff35631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000": "00", + "74000000015368ffffffff3dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000": "00", + "74000000015368ffffffff3f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000": "00", + "74000000015368ffffffff5491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000": "00", + "74000000015368ffffffff5e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000": "00", + "74000000015368ffffffff6b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000": "00", + "74000000015368ffffffff6e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000": "00", + "74000000015368ffffffff711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000": "00", + "74000000015368ffffffff7284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000": "00", + "74000000015368ffffffff7541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000": "00", + "74000000015368ffffffff76cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000": "00", + "74000000015368ffffffff8bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000": "00", + "74000000015368ffffffff996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000": "00", + "74000000015368ffffffffaa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000": "00", + "74000000015368ffffffffb6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000": "00", + "74000000015368ffffffffbdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000": "00", + "74000000015368ffffffffc86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000": "00", + "74000000015368ffffffffcf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000": "00", + "74000000015368ffffffffd5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000": "00", + "74000000015368ffffffffd9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000": "00", + "74000000015368ffffffffe63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000": "00", + "74000000015368ffffffffe7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000": "00", + "74000000015368ffffffffee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000": "00", + "74000000015368fffffffff101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "00", + "74000000015368fffffffff75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000": "00", + "7400000001537500000000000f322828dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "00", + "7400000001537500000000000f3228f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "00", + "7400000001537500000000000f42400c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000": "00", + "7400000001537500000000000f424010fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000": "00", + "7400000001537500000000000f424017b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000": "00", + "7400000001537500000000000f42401e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000": "00", + "7400000001537500000000000f4240240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000": "00", + "7400000001537500000000000f424026ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000": "00", + "7400000001537500000000000f424035631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000": "00", + "7400000001537500000000000f42403dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000": "00", + "7400000001537500000000000f42403f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000": "00", + "7400000001537500000000000f42405491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000": "00", + "7400000001537500000000000f42405e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000": "00", + "7400000001537500000000000f42406b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000": "00", + "7400000001537500000000000f42406e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000": "00", + "7400000001537500000000000f4240711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000": "00", + "7400000001537500000000000f42407284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000": "00", + "7400000001537500000000000f42407541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000": "00", + "7400000001537500000000000f424076cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000": "00", + "7400000001537500000000000f42408bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000": "00", + "7400000001537500000000000f4240996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000": "00", + "7400000001537500000000000f4240aa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000": "00", + "7400000001537500000000000f4240b6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000": "00", + "7400000001537500000000000f4240bdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000": "00", + "7400000001537500000000000f4240c86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000": "00", + "7400000001537500000000000f4240cf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000": "00", + "7400000001537500000000000f4240d5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000": "00", + "7400000001537500000000000f4240d9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000": "00", + "7400000001537500000000000f4240e63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000": "00", + "7400000001537500000000000f4240e7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000": "00", + "7400000001537500000000000f4240ee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000": "00", + "7400000001537500000000000f4240f75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000": "00", + "740000000153760000000000000000ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "00", + "7400000001537600000000000f09d85ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000001": "00", + "7400000001537600000000000f2c9ceb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "00", + "7400000001537600000000000f32280a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "00", + "7400000001537600000000000f322824ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "00", + "7400000001537600000000000f32284f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "00", + "7400000001537600000000000f3228de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "00", + "7400000001537600000000000f337cac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000001": "00", + "7400000001537600000000000f424034d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "00", + "7400000001537600000000000f42403896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "00", + "7400000001537600000000000f42404341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "00", + "7400000001537600000000000f42404d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "00", + "7400000001537600000000000f4240943082f6babcfc5a5799956e8149574ab83cd8f474c6d2958fd8a78ed5b5e73d00000000": "00", + "7400000001537600000000000f424099fe9437e4242bb7c6107f5b9131f807ead2ad0c70dfe039b34d1f801a51bc0500000000": "00", + "7400000001537600000000000f42409c98d2db8df0a5d4b5b1ee3074f872541b4c9fccd5e13256915ff22049faaf1d00000000": "00", + "7400000001537600000000000f4240acdda30e345a698a43e7d105719c9bbae846351caa6e59c5234cc1eb4840e6a300000000": "00", + "7400000001537600000000000f4240b4bec8d2a53d512fe9c55d5af3c9565302e233cb8dab73b937791abb9cc41faf00000000": "00", + "7400000001537600000000000f4240b88b14dfc52612e8e1b84550d5ab7136d591302185938a7c59b54077c5a723f000000000": "00", + "7400000001537600000000000f4240d52fb57ff1d3a4e5e2f923421015e2b5ce392f8031dfcdbbf6e3d425fef19e5a00000000": "00", + "7400000001537600000000000f4240da74c9e20a2fef35b4363a266dda1141a65d1c79d2d59ab85a1dd201731b3f8400000000": "00", + "7400000001537600000000000f4240e66914b1f93cf6d3317090f01c11aa2614f2c61fca5e22bfbd0192771222676900000000": "00", + "7400000001537600000000000f4240eb9dc1183d33bbb09bf6daa19e2a9c485fa5dff57386872a020f01e1fa14bf3c00000000": "00", + "7400000001537600000000000f4240ed9f7353e70f6b38e26b0433245dde90b348ac1996f50d10c827a29c5240c24100000000": "00", + "7400000001537600000000000f4240f74e991d682a2b3b97e69bfebff3e7a446b528f44db2f59bf508fa206808c13800000000": "00", + "7400000001537600000000000f4240fa5c2be8b0a338c4bd7200c9a6cfcdc2657f80acfa9dbef5a39e0e36a60720cf00000000": "00", + "7400000001630a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "000000003700000028320f00000000000014a62c71694f8bf7583bfe73bd1b2235b81ee125580000000001", + "7400000001630c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000": "00000000ffffffff40420f00000000000014cc0fdce38328d8df1f900a638544b15f9d81f5520000000000", + "74000000016310fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000": "00000000ffffffff40420f000000000000143ddd945091f3ec8dc5e0c0cefce32875e5037aeb0000000000", + "74000000016317b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000": "00000000ffffffff40420f00000000000014c53ec93147b4fa7e1e1bf56f0edbdd319895e2530000000000", + "7400000001631e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000": "00000000ffffffff40420f0000000000001427595b8603244dc6dd42d5a89d6e77a18f18e23b0000000000", + "740000000163240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000": "00000000ffffffff40420f00000000000014e4a65896f371f330fd6aa119e933fc346cb1223b0000000000", + "74000000016324ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "000000003700000028320f00000000000014f0f269d0cb5eb1e065b1084d1d787a7ec5b6ed8c0000000001", + "74000000016326ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000": "00000000ffffffff40420f0000000000001401ef877cb2f639d44cb257bb2eb983d408c8893c0000000000", + "74000000016328dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "00000000ffffffff28320f000000000000141fcdaa5bd5efa8811106f6390e5ecc327083426b0000000001", + "74000000016334d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "000000003300000040420f000000000000145cb324e109b920b19494549b3e540f57d43c4ef10000000100", + "74000000016335631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000": "00000000ffffffff40420f00000000000014ad37f7abb46c43e2ac8238c1c9e4a98841b3fa6c0000000000", + "7400000001633896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "000000003300000040420f00000000000014b9e7138dd8a5fdd11fee499ccb7a6cd131edbaa30000000100", + "7400000001633dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000": "00000000ffffffff40420f00000000000014198a5f555dc8f78bcc35bf23baefc03c954e70530000000000", + "7400000001633f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000": "00000000ffffffff40420f00000000000014a2affbcd20614676abcac636b5db306e964411210000000000", + "7400000001634341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "000000003300000040420f000000000000142667d9e317cf8f0aac2543fd27f38e0ab9da3ecb0000000100", + "7400000001634d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "000000003300000040420f00000000000014d20cf3c6172ea38a0ec42514435978a76a99b1eb0000000100", + "7400000001634f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "000000003800000028320f00000000000014c674ba19506d9e2c178ab0eb4acd47b7011927050000000001", + "7400000001635491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000": "00000000ffffffff40420f000000000000140ca598cff494738bfbfe1b06f416bce1def2dfae0000000000", + "7400000001635e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000": "00000000ffffffff40420f00000000000014045216fd9b9f0aeff2be3c18327c3083fc4ffd4a0000000000", + "7400000001635ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000001": "0000000041000000d8090f0000000000001460976bb97754c7ab48bac4c894d27a4126bc23d10000000001", + "7400000001636b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000": "00000000ffffffff40420f00000000000014433b4a5bdd977ac6ab958e07834a08fbd2e7f9610000000000", + "7400000001636e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000": "00000000ffffffff40420f00000000000014edda79fa7ce1811ea0c0a9a2ffca881c509269020000000000", + "740000000163711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000": "00000000ffffffff40420f000000000000144eca2ab57c2a64d356a40fab260140a48fb39ca40000000000", + "7400000001637284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000": "00000000ffffffff40420f000000000000144725f4cf8e0fbeb8d220c54f3e38d6d1f73cba1a0000000000", + "7400000001637541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000": "00000000ffffffff40420f000000000000142e79df92c411266e738b4488a54686c6fe0d27b80000000000", + "74000000016376cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000": "00000000ffffffff40420f0000000000001417e6329645e6f871f3b2f01b2e8115ffc01fa0dd0000000000", + "7400000001638bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000": "00000000ffffffff40420f000000000000149dfcfcd6ce44031193a6960f878460c3f1157b0b0000000000", + "740000000163943082f6babcfc5a5799956e8149574ab83cd8f474c6d2958fd8a78ed5b5e73d00000000": "000000003300000040420f00000000000014646f81d8c9569f04c93d1559a606da8185cb49c10000000000", + "740000000163996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000": "00000000ffffffff40420f0000000000001431bbc5580d28765f2172cd537369614a87c99b4d0000000000", + "74000000016399fe9437e4242bb7c6107f5b9131f807ead2ad0c70dfe039b34d1f801a51bc0500000000": "000000003300000040420f00000000000014cc2953e1188c162d1689206ce79fabdd54c0a5750000000000", + "7400000001639c98d2db8df0a5d4b5b1ee3074f872541b4c9fccd5e13256915ff22049faaf1d00000000": "000000003300000040420f000000000000147220144f51a799765a1cfebc74a6ff789e0cf0980000000000", + "740000000163aa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000": "00000000ffffffff40420f000000000000141860e07aa099551e6f6d4794de9eb48aac172a450000000000", + "740000000163ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "000000003a000000000000000000000000144a476eeca3f6b9f47b7ac6ea4953f5aec201fda002032075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d04000000000a746573746e616d652d31000001", + "740000000163ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000001": "000000003a0000007c330f00000000000014a7cd0f0c61bcdb86967f1b11128b7d3ba7d78ec70000000001", + "740000000163acdda30e345a698a43e7d105719c9bbae846351caa6e59c5234cc1eb4840e6a300000000": "000000003500000040420f00000000000014359d9335692983142de60a57df5a0197fd13ab760000010000", + "740000000163b4bec8d2a53d512fe9c55d5af3c9565302e233cb8dab73b937791abb9cc41faf00000000": "000000003300000040420f000000000000147db1e3611d2409939fb385976712f032de7629ef0000000000", + "740000000163b6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000": "00000000ffffffff40420f000000000000141b5a09ead7ea9f4c460d1a248d6980a98186f3c70000000000", + "740000000163b88b14dfc52612e8e1b84550d5ab7136d591302185938a7c59b54077c5a723f000000000": "000000003400000040420f00000000000014332479f977c3cd9a6a915a25e7ca4e95bbfb92c00000010000", + "740000000163bdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000": "00000000ffffffff40420f0000000000001434079c84c47cf7b4ce95b38b8b19040043b5a58b0000000000", + "740000000163c86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000": "00000000ffffffff40420f000000000000149df7d0326747749b602967421b892f1efe19fd730000000000", + "740000000163cf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000": "00000000ffffffff40420f000000000000147f0a0bb9e20f94b934fb814e4938ef3a783e6f970000000000", + "740000000163d52fb57ff1d3a4e5e2f923421015e2b5ce392f8031dfcdbbf6e3d425fef19e5a00000000": "000000003300000040420f0000000000001441136683ea0485bbef46e9d9f6a2ff766128efad0000000000", + "740000000163d5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000": "00000000ffffffff40420f0000000000001468a14e77be363ef2ee92406d5ce969a1785110130000000000", + "740000000163d9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000": "00000000ffffffff40420f00000000000014a37f94f98c1e2cb4714d5ce5465f9d7a82f3e36e0000000000", + "740000000163da74c9e20a2fef35b4363a266dda1141a65d1c79d2d59ab85a1dd201731b3f8400000000": "000000003300000040420f00000000000014452b000bd69cd9c4a99322d1c41e972cdcd662fe0000000000", + "740000000163de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "000000003800000028320f00000000000014222740511246fe2b10957049ae10e0bbe7fa996c0000000001", + "740000000163e63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000": "00000000ffffffff40420f00000000000014bfc1d60d296b8a15c489d32d5647f21e8c6cb8950000000000", + "740000000163e66914b1f93cf6d3317090f01c11aa2614f2c61fca5e22bfbd0192771222676900000000": "000000003300000040420f000000000000142cd7a7b49b1013603d53b6a234493ee1208471750000000000", + "740000000163e7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000": "00000000ffffffff40420f00000000000014f59d25508e19abeec49c3377bedd48ad7947af440000000000", + "740000000163eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000000": "0000000047000000102700000000000000142315e1a3850c7afd6f1be87df18dea1d3939d0a904032075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a00000020c616d48ae4039aad2438bc47a11e0ec0d883f5dfc1f34aa10944085c57d79a13000001", + "740000000163eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "00000000470000009c2c0f000000000000147a98b6621e4ccc528560645f6a7682633a24fad80000000001", + "740000000163eb9dc1183d33bbb09bf6daa19e2a9c485fa5dff57386872a020f01e1fa14bf3c00000000": "000000003300000040420f000000000000146a94c2244bd9a4b5257a8c946b68af72f4118b350000000000", + "740000000163ed9f7353e70f6b38e26b0433245dde90b348ac1996f50d10c827a29c5240c24100000000": "000000003300000040420f000000000000142f6d31b494bdfa42034719ca28894496cdb893590000000000", + "740000000163ee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000": "00000000ffffffff40420f000000000000147e7a6df077a7d3a39fba31246574634375439e3c0000000000", + "740000000163f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "00000000ffffffff28320f000000000000145258eb6f21709d00f66fa39deab6e8d5391eae5f0000000001", + "740000000163f74e991d682a2b3b97e69bfebff3e7a446b528f44db2f59bf508fa206808c13800000000": "000000003300000040420f00000000000014cfe3810f02a4af8dd98092bb6650841bac0ff2c40000000000", + "740000000163f75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000": "00000000ffffffff40420f000000000000144931bd2996fb46b2f17a5f4bae7d0b2a04ee65bc0000000000", + "740000000163fa5c2be8b0a338c4bd7200c9a6cfcdc2657f80acfa9dbef5a39e0e36a60720cf00000000": "000000003300000040420f000000000000144c43158dc848068c85406c14957b0cab073f343b0000000000", + "7400000001640a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000": "000000003300000040420f000000000000144f9644e04b6bbc4f53ae49e444d005524cc1422f000000", + "7400000001640a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000001": "000000003300000040420f000000000000140441cbe166229b9e2adb6aa90751052523cb298d000000", + "74000000016424ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000": "000000003300000040420f00000000000014f4f1b2cd73eebe34bef87c174e5d4903e86493ff000000", + "74000000016424ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000001": "000000003300000040420f000000000000146337e3ecd9354ba7a5fbe0ce2feb6d3bc5558a54000000", + "74000000016428dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000": "000000003300000040420f000000000000145cb324e109b920b19494549b3e540f57d43c4ef1000000", + "74000000016428dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000001": "000000003300000040420f00000000000014b9e7138dd8a5fdd11fee499ccb7a6cd131edbaa3000000", + "740000000164320e6e6cffd8d9b904d97f904536c6c410f47d7ed856e354b0836b6bd47ec44b00000000": "000000003300000040420f000000000000145428d3cebe215a9f78bf3ca6d3ebc055b828f3d9000000", + "740000000164320e6e6cffd8d9b904d97f904536c6c410f47d7ed856e354b0836b6bd47ec44b00000001": "000000003300000040420f000000000000146dddb81cee99d34e617876a2d4bd091fea7da844000000", + "7400000001644f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000": "000000003300000040420f00000000000014f7c2c1cf022472febc2deb36c91ae2e450c36b29000000", + "7400000001644f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000001": "000000003300000040420f00000000000014309a0821ae883849b41aeb44c24b075af338b8c0000000", + "7400000001645ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000000": "000000003300000040420f000000000000142a52b414589aca0f9e6b79be9563e3e0b8e053aa000000", + "74000000016499e639a2ab769f77f51c3752ca4d85436abd3d6ee0fa28d249fc67a7f4e8422200000000": "000000003300000040420f00000000000014e154c1b406841cc45d1d63fbd0c36003b5e34cc9000000", + "74000000016499e639a2ab769f77f51c3752ca4d85436abd3d6ee0fa28d249fc67a7f4e8422200000001": "000000003300000040420f000000000000149d03a12b2facd319be3b174ac49e78effae0f9fc000000", + "740000000164ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000": "000000003300000040420f00000000000014b27ecd0a361b0fe9acafecdc47841e021a4ebf50000000", + "740000000164de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000": "000000003300000040420f0000000000001499b6d711eb6ba45313ed8ff37d9a8ec5df572750000000", + "740000000164de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000001": "000000003300000040420f000000000000149259252916ee5b5573f1f911ed49c99592b1db44000000", + "740000000164eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000000": "0000000041000000102700000000000000142315e1a3850c7afd6f1be87df18dea1d3939d0a903042075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a0000000a746573746e616d652d3120d239a4dc0ce73cc36bf38204e3b0a438d353a7c6d57889fa6e88fff91265145500", + "740000000164eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000001": "000000003300000040420f000000000000148d375a0132f2077de9c4a96c66e15c24c5501851000000", + "740000000164f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000": "000000003300000040420f000000000000142667d9e317cf8f0aac2543fd27f38e0ab9da3ecb000000", + "740000000164f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000001": "000000003300000040420f00000000000014d20cf3c6172ea38a0ec42514435978a76a99b1eb000000", + "74000000017303681f8ee2b9bd28df7af7445a589052965b60c4d89d13269b483aafd0ea9db600000000": "6b1c5e65d3a687a5a016e242dea9aa49e55c870c925c65ac040011f52d9f1f2500000000", + "7400000001730fff2e108fcc22c30f2e5117182fc26f74cd2a79c3372d196e7eff08416276d500000000": "0a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546400000000", + "74000000017310576dd1a93f478542464d6721bc6053cdd310f68241cf7f94864edc80d8f23b00000000": "bdab5449ac51a2a6c35c9ccff7aaecec0d2568fda855be526c32588ec72d69aa00000000", + "74000000017313642f3ae0597fae5368a9ab3aa5002159ee421b622fff2dfaf4431259e117fb00000000": "0a1ad3d1abddd4df701750e546180af3fe4d48961022190397e30676d478546401000000", + "7400000001731372f66217b7c16d46080b70af90951addb9fdf9a148566208957b82b5b1771200000000": "24ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59900000000", + "7400000001731d14f5deb4d7480fe6568ede44151397fc4fa3da9ed32b6c405ca946d12e937200000000": "1e97ca90dc61c77eadb5db80fa3ac3062656798774adc73d70aee6c06a50463a00000000", + "7400000001732858e5aedda20aa45293fa3aa64722c13ea3d18adc8e952728739e450ef2023b00000000": "b6426c7489bacaeeb20e1f604b6ff5c7eb68d3c584bcfc82bdb68b4fb9bba6c800000000", + "7400000001732e1c6abb0f6ed5b4fcc53b6ebccaf5f155e0fa7d763a12a31ae68ac9408d02fd00000000": "26ff1a281d3266513c3cb4ff75bf500254648353ef3b7e28e057867baf0e751400000000", + "740000000173323b9dd49fb66e770de1fdd46bb57028a778e140de37a7ca5596a57100defbf900000000": "24ee0ed46218285f10508ceedfec794a59e1d44bd91bad90361e8479aaced59901000000", + "74000000017334d0a5fad161ea5893dd922e816752e4fc3652eb9ad25379e67691f6614ab92e00000000": "28dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7700000000", + "74000000017336b9062567c977a2b897f8cbd547ce78fa3bd598ef1c6b5499e71229514fe3ed00000000": "e63ce0cd1c74654ac8e84cb2978a0c78cfc24778e41665531326cae89ee351cd00000000", + "74000000017337066d4d4d245fc5bc01fe8c55005c9b92d7a9f8deb443a9369d4423acd9570000000000": "ee075af0b9b68a3bdf7ccaca9da87c8204a7cdbe2bf4a3590dfd8f6c8e795de000000000", + "7400000001733896a471d160df8172db2c4e2de390e495c8f2372ecfcc189aa90244ee41f39400000000": "28dbd27362bb650701ee1471cb8fd79c44009abc8e60d032bc319ac619819d7701000000", + "7400000001733a3126fbbf206d75ecfbb40d5da8f39068263a6dbaa4e047529fd3ddb11fd8b800000000": "de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d400000000", + "7400000001733ad9ed2ba729a6419907e522c55e246fa70eebaae3a01a3794b2df42fc36efb500000000": "7284e421e893d7eccdde27728d97f211d8fe772d724e042ef4b145c0c0fcee7b00000000", + "74000000017340660cb50912d86a4ec527cd5af973233d7f9bdee7361bf7fadfdf1243bfe43800000000": "240829d7a55927ea680e422b87dac74216025c62649342751826c2756726bdcc00000000", + "740000000173408c946546253b8ba7642a80c1089bfe1a0fa76c33627d6f2d3684ee17b55ebe00000000": "de2437c61766ef9b1b53fc660f88c79e23473d34e80d84737da2f3e14526f7d401000000", + "740000000173425ac3ec38bb16ae168a5fc25a8702bc65a6d9cd09f6896e1dc28499e2b18da800000000": "3dc0c6fa07af38bbfb9820ebd7b4965129c733676557be966c8669618a27449400000000", + "7400000001734341692bca57bec186da5537e3718d59b1296bddfbbc758be9f8e20b27e6cac800000000": "f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf00000000", + "7400000001734561925263b2563548dd72b116034abee46210e88371f763563b6cd33eb0053500000000": "99e639a2ab769f77f51c3752ca4d85436abd3d6ee0fa28d249fc67a7f4e8422200000000", + "7400000001734d32785a36482a631d9e21961cfc72a5c1cc9e24ae88c7604d01e331af81fb8000000000": "f101ad4568fae51d9a9ce3bcf425eaf5e48ced07a25b96b08569226f36e49aaf01000000", + "7400000001734dfc35d2d60a286f00bcff7fe5a868279ca21ec1903c1ef089facae0131bcbb600000000": "99e639a2ab769f77f51c3752ca4d85436abd3d6ee0fa28d249fc67a7f4e8422201000000", + "740000000173515b89fe7e7cd16dea4e1981b4f75e0a02b896ad4d30d2200daf82d8956ba8af00000000": "10fabcf64220ce07b43d1a127f4a97df50e4e0123541ecb02bfef2422307375d00000000", + "74000000017353cb0828e3d769a6c997319c775e387c41280157d65fda2c9b997af84d7f90db00000000": "d9352f22bac4f4ecb764cd61281ff7798bf15124749a6b0a3ea0ba9d3bd9db0500000000", + "7400000001735442c34bd79edaa96143c2fb37fa65fd304c65fc65e154cfc67468645c402f1b00000000": "35631837f407b50a52a2d998683dcc7b9ec10d821676937810c02e7a19e6e1bb00000000", + "74000000017354f473602bcde9d4f9f719fd4e3edabf323f5a3593adb409e49c10d549ce834800000000": "4f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a00000000", + "7400000001735991ca040bea6b28ed9f7b388ecd3e8b5afe19fec959bda94c6ea523aef0b30000000000": "3f62e0a98011e9944e9620a822da03b9012438fee2c1372062ed98abcd32a85400000000", + "7400000001735ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000000": "eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed00000000", + "74000000017362ffbfd09fd83be044075929214ab559e3b9994197a792cc60b48ad80caff85700000000": "ac60693db941398eceb9e4413b3c5c1508e47d2cb7545caa58fb9c03200ddf4700000000", + "7400000001736b96d4992c1901179bcff23c0cb649ae98f6b878ab8961341d359944494874d000000000": "aa64d26f673b9b27bb62d455a968cb95971f85070ad2915c5c94969136cc70f200000000", + "7400000001736cdc4bd0ac996647c5e8b59a7f1616ce613ce3b4a9725a62a3abf4c7c43d114f00000000": "5ea79940ff73064890402b5b6580072e4959182fccfb5db3b89140c481694a5f00000000", + "7400000001736dda8d21c1c543e52fa2201dd186dda5bce740cbe979e31d8c92bf6936e68bfa00000000": "996bf17c792ea9070d646bd82bf3fdc78b823e426c9bf85206ae0608ba27bbfe00000000", + "74000000017377404f33697b378fb347d8d1fc6d341468f19cf2aaf7a2daa21679e0e5b4c5df00000000": "e7ef08ed598381b76a34363425477658dc01c38a833b728cec7d937cad72156100000000", + "7400000001737858cecbeefa2985c7ff7a126143e1099f6c2a1e6bb8f8b49876284c5aef11b500000000": "4f4c5a016a435ee5f8ab1ff3642be1baff3f44882449d3e7dbf1251ba7b1603a01000000", + "7400000001737a669bbd9b6071db8e6032adfdc266ece43a0375cbdda6e77925b855e2f066e000000000": "320e6e6cffd8d9b904d97f904536c6c410f47d7ed856e354b0836b6bd47ec44b00000000", + "7400000001737e905920371ec5f99176f13c5d5010758493c830fe520e97cc4f9d66d80b3efe00000000": "c86df0977b2121bcdd8cc465c3393d74f004f8f1588520964df696d4685033a900000000", + "7400000001737f161a9bd7c781ec7c25e094a012bd1931dec18faa2eaa5fd56b20ee19b1447100000000": "6e6a8c6f3d1b6e2cc085eb20443b20fbd704a807a5309249bb5ab5dd02b70d7500000000", + "740000000173807957571a3add9343a9d932cb90955446f9797c984c8582176e5d8e5790f12600000000": "eb3a4a4640e638d26a3f7d42c34867e0d49635db3a42329061ae5262357a29ed01000000", + "7400000001738486b24023f38c8f595dcb48880e9673e1e0434d601f1e8871ed60a8acda154d00000000": "f75e805a1eab20b20b22a3fe820f38fb984a80fe4e13eb0cba96598585b3177300000000", + "74000000017389220598e0a127f887cf16b37c62f26d9cace85a448243522f9e41b408ade26400000000": "17b8aff83a89964237b449c9871e4db5f1c4912faa77af62cadf977b6d166d0100000000", + "7400000001738dbb3eb02c8f8cf6cf03346a72d733d4ca9b7e7e5dacc1afeddfb5697376c30800000000": "0c20979f6b1d9816391792dc327ed2aa94016d929ccf6cf1cf4b4b3b87ebf68100000000", + "7400000001738eff95c81bcf3d5467c32ce9abf81872481c568c9f90947580122e8fe177dc7000000000": "320e6e6cffd8d9b904d97f904536c6c410f47d7ed856e354b0836b6bd47ec44b01000000", + "740000000173a2ba1d077eded3608cdf54c41c514c59b02aefe28af3e4e602f460626be42d3e00000000": "76cc5e32ce0628d7daf6a7c3c738b374aa74f9b8377c82a147bb0d77a487a35700000000", + "740000000173a854a5b169d381f72109048ba56b41dd547ba2ac6b0b838d60d675fdcdf5310700000000": "cf044f7f4562759bc3bdce5480e567433d5bde459a981591fb2cb9a32561a71b00000000", + "740000000173b352f75f0642ce5589167154fe760a84fb047418a871d5225fd1d66c53c4ebd100000000": "711776151e5423d47f15b71435481ad94b8081abc10eb7c2901bf2f1419e466800000000", + "740000000173b37353c0edbb420e054eff5a0f05b3e0e99e852393f449c854057527a312fc3c00000000": "5491dba32385f57519e5723789e9cab1d341cd77587b24b1ad05a619772afe9400000000", + "740000000173b609f6515a3dcdf32b896678a89b1d867f1bd93b7485f82d1e901c51e875b2c200000000": "d5a322102a990e4d1eff9867ee69657cca464fed60bfb8bbdb8f0f0bfd7a9c8e00000000", + "740000000173e8634b9bc9031ba1617a827f1d51785c25bf5164692257ccbbfd3c5d2918c30d00000000": "8bc203ae3cabb037f1032e4fb403895ebc6aa60b146b5048b6a07e2578f34df000000000", + "740000000173e899425912717de079a89b958274948c7056308ae920c899666e0cf3cfc0ffa100000000": "7541d1e00d5c900ea5113bd20f1ede588e8aefa7f07d7795e03ee151056ecaeb00000000", + "740000000173ecf456658d108c368e6bb7473305a5278cae99cc199c55bfeb618420d6a6423100000000": "5e28176286da842abc6921ecc60d50b6c9b2976f8a40a3041bf14d3a34c6efec00000000", + "740000000243000000001ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000": "00", + "740000000243000000001cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000": "00", + "740000000243000000001e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000": "00", + "740000000243000000002ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "00", + "740000000243000000003cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000": "00", + "74000000024300000000458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "00", + "740000000243000000004eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000": "00", + "7400000002430000000053956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000": "00", + "74000000024300000000554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "00", + "740000000243000000005dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000001": "00", + "7400000002430000000063864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000001": "00", + "74000000024300000000683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "00", + "7400000002430000000086387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000": "00", + "740000000243000000008b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000": "00", + "740000000243000000008eba67a23de92df548ecbb4f40be648bbf870a1de67191ad0b576c1fab59ea8b00000000": "00", + "74000000024300000000952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "00", + "74000000024300000000a7d8e41fbc89aacc349effd58b140f08b84ab4d33775bb967a678e8bf4d8c00a00000000": "00", + "74000000024300000000ad6c09f1d7c9eacd449af12280c6ac397061a27bed995bc5dfe7b24ce2c49e3a00000000": "00", + "74000000024300000000b37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000": "00", + "74000000024300000000bf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000": "00", + "74000000024300000000d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000000": "00", + "74000000024300000000d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000001": "00", + "74000000024300000000d949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000": "00", + "74000000024300000000db3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000": "00", + "74000000024300000000db40da9ef39f8b52f0cfc84fe1bd4320f9dcab3f0c7425af1e505cfd59ecedbb00000000": "00", + "74000000024300000000ddef667255e254fc9d885ee04fe4852dba08d4b7ba84e86562afe6351d62098000000000": "00", + "74000000024300000000e674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000": "00", + "74000000024300000000ee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000": "00", + "74000000024300000000ee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000": "00", + "74000000024300000000fa72230236318c48e79926683a93baa4ccf09e856a78dfef9e1bf20f9b947dbe00000000": "00", + "74000000024300000000fc7c46c4caa0d66e55e2b70c82fe65a68fef5fabfb5c903cc264afc68ea6b7bd00000000": "00", + "740000000253480000000000000033458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "00", + "740000000253480000000000000033554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "00", + "7400000002534800000000000000338eba67a23de92df548ecbb4f40be648bbf870a1de67191ad0b576c1fab59ea8b00000000": "00", + "740000000253480000000000000033ad6c09f1d7c9eacd449af12280c6ac397061a27bed995bc5dfe7b24ce2c49e3a00000000": "00", + "740000000253480000000000000033db40da9ef39f8b52f0cfc84fe1bd4320f9dcab3f0c7425af1e505cfd59ecedbb00000000": "00", + "740000000253480000000000000033ddef667255e254fc9d885ee04fe4852dba08d4b7ba84e86562afe6351d62098000000000": "00", + "740000000253480000000000000033fa72230236318c48e79926683a93baa4ccf09e856a78dfef9e1bf20f9b947dbe00000000": "00", + "740000000253480000000000000033fc7c46c4caa0d66e55e2b70c82fe65a68fef5fabfb5c903cc264afc68ea6b7bd00000000": "00", + "740000000253480000000000000036a7d8e41fbc89aacc349effd58b140f08b84ab4d33775bb967a678e8bf4d8c00a00000000": "00", + "740000000253480000000000000037952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "00", + "7400000002534800000000000000382ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "00", + "7400000002534800000000000000415dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000001": "00", + "74000000025348000000000000004763864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000001": "00", + "740000000253480000000000000052d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000001": "00", + "7400000002534800000000ffffffff1ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000": "00", + "7400000002534800000000ffffffff1cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000": "00", + "7400000002534800000000ffffffff1e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000": "00", + "7400000002534800000000ffffffff3cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000": "00", + "7400000002534800000000ffffffff4eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000": "00", + "7400000002534800000000ffffffff53956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000": "00", + "7400000002534800000000ffffffff683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "00", + "7400000002534800000000ffffffff86387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000": "00", + "7400000002534800000000ffffffff8b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000": "00", + "7400000002534800000000ffffffffb37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000": "00", + "7400000002534800000000ffffffffbf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000": "00", + "7400000002534800000000ffffffffd949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000": "00", + "7400000002534800000000ffffffffdb3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000": "00", + "7400000002534800000000ffffffffe674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000": "00", + "7400000002534800000000ffffffffee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000": "00", + "7400000002534800000000ffffffffee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000": "00", + "740000000253550000000000000000000f3228683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "00", + "740000000253550000000000000000000f42401ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000": "00", + "740000000253550000000000000000000f42401cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000": "00", + "740000000253550000000000000000000f42401e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000": "00", + "740000000253550000000000000000000f42403cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000": "00", + "740000000253550000000000000000000f42404eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000": "00", + "740000000253550000000000000000000f424053956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000": "00", + "740000000253550000000000000000000f424086387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000": "00", + "740000000253550000000000000000000f42408b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000": "00", + "740000000253550000000000000000000f4240b37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000": "00", + "740000000253550000000000000000000f4240bf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000": "00", + "740000000253550000000000000000000f4240d949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000": "00", + "740000000253550000000000000000000f4240db3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000": "00", + "740000000253550000000000000000000f4240e674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000": "00", + "740000000253550000000000000000000f4240ee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000": "00", + "740000000253550000000000000000000f4240ee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000": "00", + "7400000002535600000000000000000000166cd0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000001": "00", + "7400000002535600000000000000000002aea463864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000001": "00", + "740000000253560000000000000000000c23a85dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000001": "00", + "740000000253560000000000000000000f32282ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "00", + "740000000253560000000000000000000f3228952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "00", + "740000000253560000000000000000000f4240458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "00", + "740000000253560000000000000000000f4240554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "00", + "740000000253560000000000000000000f42408eba67a23de92df548ecbb4f40be648bbf870a1de67191ad0b576c1fab59ea8b00000000": "00", + "740000000253560000000000000000000f4240a7d8e41fbc89aacc349effd58b140f08b84ab4d33775bb967a678e8bf4d8c00a00000000": "00", + "740000000253560000000000000000000f4240ad6c09f1d7c9eacd449af12280c6ac397061a27bed995bc5dfe7b24ce2c49e3a00000000": "00", + "740000000253560000000000000000000f4240db40da9ef39f8b52f0cfc84fe1bd4320f9dcab3f0c7425af1e505cfd59ecedbb00000000": "00", + "740000000253560000000000000000000f4240ddef667255e254fc9d885ee04fe4852dba08d4b7ba84e86562afe6351d62098000000000": "00", + "740000000253560000000000000000000f4240fa72230236318c48e79926683a93baa4ccf09e856a78dfef9e1bf20f9b947dbe00000000": "00", + "740000000253560000000000000000000f4240fc7c46c4caa0d66e55e2b70c82fe65a68fef5fabfb5c903cc264afc68ea6b7bd00000000": "00", + "7400000002536800000033458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "00", + "7400000002536800000033554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "00", + "74000000025368000000338eba67a23de92df548ecbb4f40be648bbf870a1de67191ad0b576c1fab59ea8b00000000": "00", + "7400000002536800000033ad6c09f1d7c9eacd449af12280c6ac397061a27bed995bc5dfe7b24ce2c49e3a00000000": "00", + "7400000002536800000033db40da9ef39f8b52f0cfc84fe1bd4320f9dcab3f0c7425af1e505cfd59ecedbb00000000": "00", + "7400000002536800000033ddef667255e254fc9d885ee04fe4852dba08d4b7ba84e86562afe6351d62098000000000": "00", + "7400000002536800000033fa72230236318c48e79926683a93baa4ccf09e856a78dfef9e1bf20f9b947dbe00000000": "00", + "7400000002536800000033fc7c46c4caa0d66e55e2b70c82fe65a68fef5fabfb5c903cc264afc68ea6b7bd00000000": "00", + "7400000002536800000036a7d8e41fbc89aacc349effd58b140f08b84ab4d33775bb967a678e8bf4d8c00a00000000": "00", + "7400000002536800000037952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "00", + "74000000025368000000382ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "00", + "74000000025368000000415dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000001": "00", + "740000000253680000004763864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000001": "00", + "7400000002536800000052d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000001": "00", + "74000000025368ffffffff1ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000": "00", + "74000000025368ffffffff1cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000": "00", + "74000000025368ffffffff1e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000": "00", + "74000000025368ffffffff3cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000": "00", + "74000000025368ffffffff4eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000": "00", + "74000000025368ffffffff53956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000": "00", + "74000000025368ffffffff683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "00", + "74000000025368ffffffff86387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000": "00", + "74000000025368ffffffff8b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000": "00", + "74000000025368ffffffffb37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000": "00", + "74000000025368ffffffffbf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000": "00", + "74000000025368ffffffffd949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000": "00", + "74000000025368ffffffffdb3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000": "00", + "74000000025368ffffffffe674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000": "00", + "74000000025368ffffffffee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000": "00", + "74000000025368ffffffffee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000": "00", + "7400000002537500000000000f3228683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "00", + "7400000002537500000000000f42401ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000": "00", + "7400000002537500000000000f42401cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000": "00", + "7400000002537500000000000f42401e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000": "00", + "7400000002537500000000000f42403cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000": "00", + "7400000002537500000000000f42404eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000": "00", + "7400000002537500000000000f424053956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000": "00", + "7400000002537500000000000f424086387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000": "00", + "7400000002537500000000000f42408b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000": "00", + "7400000002537500000000000f4240b37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000": "00", + "7400000002537500000000000f4240bf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000": "00", + "7400000002537500000000000f4240d949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000": "00", + "7400000002537500000000000f4240db3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000": "00", + "7400000002537500000000000f4240e674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000": "00", + "7400000002537500000000000f4240ee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000": "00", + "7400000002537500000000000f4240ee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000": "00", + "74000000025376000000000000166cd0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000001": "00", + "74000000025376000000000002aea463864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000001": "00", + "7400000002537600000000000c23a85dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000001": "00", + "7400000002537600000000000f32282ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "00", + "7400000002537600000000000f3228952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "00", + "7400000002537600000000000f4240458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "00", + "7400000002537600000000000f4240554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "00", + "7400000002537600000000000f42408eba67a23de92df548ecbb4f40be648bbf870a1de67191ad0b576c1fab59ea8b00000000": "00", + "7400000002537600000000000f4240a7d8e41fbc89aacc349effd58b140f08b84ab4d33775bb967a678e8bf4d8c00a00000000": "00", + "7400000002537600000000000f4240ad6c09f1d7c9eacd449af12280c6ac397061a27bed995bc5dfe7b24ce2c49e3a00000000": "00", + "7400000002537600000000000f4240db40da9ef39f8b52f0cfc84fe1bd4320f9dcab3f0c7425af1e505cfd59ecedbb00000000": "00", + "7400000002537600000000000f4240ddef667255e254fc9d885ee04fe4852dba08d4b7ba84e86562afe6351d62098000000000": "00", + "7400000002537600000000000f4240fa72230236318c48e79926683a93baa4ccf09e856a78dfef9e1bf20f9b947dbe00000000": "00", + "7400000002537600000000000f4240fc7c46c4caa0d66e55e2b70c82fe65a68fef5fabfb5c903cc264afc68ea6b7bd00000000": "00", + "7400000002631ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000": "00000000ffffffff40420f00000000000014ac46b87e7e6708a4f2b7e278b78ae7ba02c04b3b0000000000", + "7400000002631cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000": "00000000ffffffff40420f00000000000014975caef4f934d0494c828c38140a139019dc71540000000000", + "7400000002631e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000": "00000000ffffffff40420f00000000000014f4a5f44033305254c96ee91dd253f903af5944aa0000000000", + "7400000002632ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "000000003800000028320f00000000000014fb392be3e9bd032482a3b24ec077f27c4fcf00b20000000001", + "7400000002633cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000": "00000000ffffffff40420f000000000000145493e23f073a3dba4442e529038ffd00c3cb72700000000000", + "740000000263458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "000000003300000040420f00000000000014bd835f1fd372fcc462a59b7e088dcaf1bdc8ca540000000100", + "7400000002634eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000": "00000000ffffffff40420f00000000000014a956453db168abea91324667b05b286fa5c03bd50000000000", + "74000000026353956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000": "00000000ffffffff40420f000000000000141ec0760b28899bb7b5ef6c617915c3db1d7324580000000000", + "740000000263554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "000000003300000040420f00000000000014dbba0b358b90b46b04a86b01a46e0b844ea7c3680000000100", + "7400000002635dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000001": "0000000041000000a8230c000000000000142e9187f08252e4be2044f7a41cd25ceb7ae96d460000000001", + "74000000026363864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000001": "0000000047000000a4ae020000000000001432aa9c919d911fed1b053a452dcbf3704a759e940000000001", + "740000000263683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "00000000ffffffff28320f00000000000014bfe57d3d51c357a99aa9c84195fe8e000273399a0000000001", + "74000000026386387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000": "00000000ffffffff40420f00000000000014ce48fa97354847c1f35d85763e776bd9e372ec240000000000", + "7400000002638b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000": "00000000ffffffff40420f0000000000001468a4797e126034fa8d58cdb8f709d61bda3fd7660000000000", + "7400000002638eba67a23de92df548ecbb4f40be648bbf870a1de67191ad0b576c1fab59ea8b00000000": "000000003300000040420f00000000000014093a264c767174696332bf57a93ffceaabe23bde0000000000", + "740000000263952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "000000003700000028320f00000000000014b44aae19323f53e63e5cd184dba19a71616808150000000001", + "740000000263a7d8e41fbc89aacc349effd58b140f08b84ab4d33775bb967a678e8bf4d8c00a00000000": "000000003600000040420f000000000000147ed369a0353f353c38c282be4b153f1f883b04de0000010000", + "740000000263ad6c09f1d7c9eacd449af12280c6ac397061a27bed995bc5dfe7b24ce2c49e3a00000000": "000000003300000040420f0000000000001478d8fc4cbe36b0fde3b3d337cebd4520dae1e4c60000000000", + "740000000263b37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000": "00000000ffffffff40420f000000000000148c446d6b2942f2ed67b8e78c64a0a84cfa079f920000000000", + "740000000263bf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000": "00000000ffffffff40420f000000000000141e3feab7c10d898c5762226c1a3d6969f470a89e0000000000", + "740000000263d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000000": "0000000052000000102700000000000000146ff78bc8fc05796fca3c60163568ff5c9dd22acc06042075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a000000010020ae3895cf597eff05b19e02a70ceeeecb9dc72dbfe6504a50e9343a72f06a87c5000001", + "740000000263d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000001": "00000000520000006c16000000000000001463ee2af024ea3dd2411e4de306e7f13fb26659df0000000001", + "740000000263d949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000": "00000000ffffffff40420f0000000000001454b98d922ac0a29f3dd4a71280bea1aa35297c130000000000", + "740000000263db3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000": "00000000ffffffff40420f00000000000014a7f6ac09b90a612149c8853772515fcc849361520000000000", + "740000000263db40da9ef39f8b52f0cfc84fe1bd4320f9dcab3f0c7425af1e505cfd59ecedbb00000000": "000000003300000040420f000000000000143229a7234fc49dbd38ea2f203e01e4a7f15c4bd40000000000", + "740000000263ddef667255e254fc9d885ee04fe4852dba08d4b7ba84e86562afe6351d62098000000000": "000000003300000040420f0000000000001483bd567dae4fb1abf5f6c53379e23c87ec036e770000000000", + "740000000263e674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000": "00000000ffffffff40420f0000000000001419703fb89be3e452f3cf0541d02f6118cda5eced0000000000", + "740000000263ee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000": "00000000ffffffff40420f000000000000142965055c105f10931d54d690ae8d5aca7aa5434d0000000000", + "740000000263ee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000": "00000000ffffffff40420f000000000000140b2909a9cc84726cb1f01c32e45288f5b49349ad0000000000", + "740000000263fa72230236318c48e79926683a93baa4ccf09e856a78dfef9e1bf20f9b947dbe00000000": "000000003300000040420f00000000000014784cc5364704372f14e7b68600aafbb2cd4aea390000000000", + "740000000263fc7c46c4caa0d66e55e2b70c82fe65a68fef5fabfb5c903cc264afc68ea6b7bd00000000": "000000003300000040420f000000000000149b18d692909cfbd2b7d9342d41932ed4e5a7e7930000000000", + "7400000002642ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000": "000000003300000040420f0000000000001446e641691144a03add0db507a26404313606782e000000", + "7400000002642ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000001": "000000003300000040420f0000000000001449fec1e6fcbd2ccc2597e1b12986fc6ec8e64c07000000", + "7400000002645dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000000": "000000003300000040420f000000000000149c1222d5dd6094fb7ee38bdb79e5d8eb7bdfd37b000000", + "74000000026463864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000000": "0000000041000000400d03000000000000146ff78bc8fc05796fca3c60163568ff5c9dd22acc03042075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a0000000a746573746e616d652d3120909aec88e869c4d284606d5cc8054853bfa7c4e24c6041c4d8598f1b29bf282400", + "740000000264683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000": "000000003300000040420f00000000000014bd835f1fd372fcc462a59b7e088dcaf1bdc8ca54000000", + "740000000264683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000001": "000000003300000040420f00000000000014dbba0b358b90b46b04a86b01a46e0b844ea7c368000000", + "740000000264952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000": "000000003300000040420f00000000000014852e98d8b8ab2946445f20b051e6b055b492c506000000", + "740000000264952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000001": "000000003300000040420f00000000000014195fe465b1df13832ebe58e00aaefa05702ad39b000000", + "740000000264c2d3e816222195ea0b04be6b7bb664e0b617f9c91a898682ea7214d5168c293700000000": "000000003300000040420f000000000000148e659d46d7ceb9464cc04f3aeb73bc2702c1e7a3000000", + "740000000264c2d3e816222195ea0b04be6b7bb664e0b617f9c91a898682ea7214d5168c293700000001": "000000003300000040420f0000000000001424fd6822d0ad3e981832391cb004484cbcab15da000000", + "740000000264d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000000": "0000000047000000204e00000000000000146ff78bc8fc05796fca3c60163568ff5c9dd22acc04032075c670d4f561a616175bb3e15282381fd8e08beafc861fe16568209c849e909d043a000000200cc30191083dcbf8e46ecaa8972af321588068915326f7f0851b2cee9872050500", + "7400000002730cd502099a8d25b5b574bc381cf60b84f45163ccb32cb44d64d53375a2d8a97a00000000": "4eaedb2e215e18ac11b72228c0ebb6a4b056e9333e188b6a63f54d937d0a9ed000000000", + "7400000002731012b7e8316dcc4c65e0b80bc8b964962088c25629483bc27a81fb072726432800000000": "952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e00000000", + "7400000002731c820c68189da48293553b1cd7465cf7f75ac3bf2db9172f5f9ddebf3d10ad8500000000": "ee1f7646c89e2171132e0ee25021933051f7c12ecd704e8db06d9ffe4a8c8d5200000000", + "74000000027325cf4fe01b9170e6641cd56aa6d2110ed7755c831648f6b209010a65cd17bfc900000000": "952e6e77a47172d5bf30feca405b92a500c7527c5b684eceb966b0df00e9335e01000000", + "7400000002732c2a543be67720878d34455f26e69f830c741b4dfbf33cfcd4998b881a9c97bd00000000": "8b775b6dc4d7ac26c7f527fcc5b54a4d33f9aa8c75b6973b5e20da2cfd9e747700000000", + "7400000002732dd1d165fc1bfa68e3bfec4e134141df8da1e37b9daa8436109cc0fb4b1805f600000000": "ee9f9b4d9d1b4ad59ae6ef2013d219f1036d2dcde4f5e6647713446ff829439a00000000", + "740000000273458a24e82424fa1c01664265ac9af70cf065080937ee8e3bbe1744c1c8025db400000000": "683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331100000000", + "7400000002734e26855b7519729fbbd5d525ded78b92308ebb2acc11f595793997d358db2dc900000000": "b37ce98e5ba10c3da50ecd80c5bd47fd930d8aeaadfa87470206a2a88908a3ac00000000", + "740000000273554604839464bb5f55aa3a865c8ccda578647fadd974c9c4542335f0dfbeb3f700000000": "683182a33ad31deb2353a16675f80115242428cb8297e788098b49551dd6331101000000", + "7400000002735dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000000": "63864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000000", + "7400000002735f8e95f14b4655f2cd5f476b02390b083dd4208ccf1c73bc3bf3f7ae294d728b00000000": "2ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da00000000", + "74000000027361886f1b56565fb2455e0b7d890eb674e01679dab480319320607e03c218912800000000": "d949b231cfa7f782ece5481a05973c95c6babe1ba7852aa0820e3f7eaec802ec00000000", + "74000000027363849657fbafa56c0be2daa74185c51ff8323d1984b3d62becafb83a9f43866c00000000": "2ac00957d4ae882ff47c3af0ce782a0e8538593016af1e06d0190059dc72b7da01000000", + "74000000027363864c0c27e6f92c7a37315ec0064c0f303626cd3c1348ba778545db9d57ebeb00000000": "d0012d0eee1130d4c3a8b407fb4ad08daad7b678b972e92cf3df85a2c72e579800000000", + "74000000027365b569f91f6b28d447f12c5e093a68933f58ef7d60ab129fa236e61d406e5c8200000000": "c2d3e816222195ea0b04be6b7bb664e0b617f9c91a898682ea7214d5168c293700000000", + "7400000002737250af6eb0c99bc0390f068687ee0f51e32340c16872da8c56427dd02c17aaa300000000": "86387e082d90691017258560c4e75fc4fe132cee113a6311dd4d3c40c6cc267e00000000", + "7400000002737b1e678d97e5ac5cf768247a497c5b76b3e6e2eaf5dab87abef949e30385d19400000000": "c2d3e816222195ea0b04be6b7bb664e0b617f9c91a898682ea7214d5168c293701000000", + "74000000027384440206ad559e1539aaf0d6b81f24c7bcca619032684024df17cb0b48a90f2d00000000": "5dcd9e696320eac817ead4c9ac30648611d291a9e0f92031d7504e7357efcba200000000", + "74000000027386416be467af312440c129867613049d435ba837f71091aa56cef7829440f18e00000000": "1e31f64ba260272c9a6691f622d34ba236171b46cad336fedf66c79e92bdc9b000000000", + "7400000002738a306eeb3501386cb9462e357d8574d0a51ab1a3904fcf6ad7fb516901bd90ff00000000": "bf725da6643ce3b35c92c1dafaf985158a7bc26b04e8b28a646aa4d5d04549a100000000", + "7400000002738ab5e6fc1290ea699b3f2c98e99226212e6b7217a0f774e17697bf88d77fa2ff00000000": "53956ef1284e8b109112c30b5b248d0a7f3d38c31ebfb55aefbafe6956d3db9000000000", + "7400000002738ef53020504301017d883fed0eed75001cb2242d219a372bb334895ef588789900000000": "1cfa3bc68820e8a3bcd36085cb3cf389c00939c92b5700455eedb91dfb2eb9a100000000", + "740000000273d007a06dd5e690763c223421b6126ba058d04216a3a02e4c164348513b6495ea00000000": "3cde6d7e145fe413d71d7d5d06a0b5bfba09e76668c19357d2dac07b027fc7af00000000", + "740000000273dfc49e74ea08407f6722adf59ca4e8c782ee0183beac232e02675863fc5fbbad00000000": "e674dc40410a1edac73a3bdae5605acc25b198b77b26c47de9b8867700fd0b8a00000000", + "740000000273f8237a38c8e06c92a717eee4301299d785dd32765027b2fe30c683721c97379e00000000": "db3ab3342cf4b4ad2bd8ef7b944330e54595ecff2e39e60e78f7939a992c012900000000", + "740000000273fc12d97b516e6e7fe68e9bbcb622784543b45d5b89c00d720115d1e697eca4f500000000": "1ad7aff76118c5ea7ac811195ba9c12a25170adfc879f7233bf031876c0b8bb400000000" + } +} diff --git a/test/mtx-test.js b/test/mtx-test.js index 70dea97b..e67badef 100644 --- a/test/mtx-test.js +++ b/test/mtx-test.js @@ -1,13 +1,13 @@ 'use strict'; const assert = require('bsert'); -const random = require('bcrypto/lib/random'); const CoinView = require('../lib/coins/coinview'); const WalletCoinView = require('../lib/wallet/walletcoinview'); -const Coin = require('../lib/primitives/coin'); const MTX = require('../lib/primitives/mtx'); const Path = require('../lib/wallet/path'); const MemWallet = require('./util/memwallet'); +const primutils = require('./util/primitives'); +const {randomP2PKAddress, makeCoin} = primutils; const mtx1json = require('./data/mtx1.json'); const mtx2json = require('./data/mtx2.json'); @@ -138,16 +138,318 @@ describe('MTX', function() { }); }); - describe('Fund', function() { + describe('Fund with in memory coin selectors', function() { + const createCoins = (values) => { + return values.map(value => makeCoin({ value })); + }; + + it('should fund with sorted values', async () => { + const coins = createCoins([1e6, 2e6, 3e6, 4e6, 5e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 7e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 0 + }); + + assert.strictEqual(mtx.inputs.length, 2); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.outputs[0].value, 7e6); + assert.strictEqual(mtx.outputs[1].value, 2e6); + }); + + it('should fund with random selection', async () => { + const coins = createCoins([1e6, 1e6, 1e6, 1e6, 1e6, 1e6, 1e6, 1e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 5e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 1e5, + selection: 'random' + }); + + assert.strictEqual(mtx.inputs.length, 6); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.outputs[0].value, 5e6); + assert.strictEqual(mtx.getFee(), 1e5); + assert.strictEqual(mtx.outputs[1].value, 9e5); + }); + + it('should fund with all selection type', async () => { + const coins = createCoins([1e6, 2e6, 3e6, 4e6, 5e6, 6e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 2e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 0, + selection: 'all' + }); + + assert.strictEqual(mtx.inputs.length, 6); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.outputs[0].value, 2e6); + assert.strictEqual(mtx.getFee(), 0); + assert.strictEqual(mtx.outputs[1].value, 19e6); + }); + + it('should fund with age-based selection', async () => { + const coins = [ + makeCoin({ value: 2e6, height: 100 }), + makeCoin({ value: 3e6, height: 200 }), + makeCoin({ value: 1e6, height: 50 }) + ]; + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 1e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 1e5, + selection: 'age' + }); + + assert.strictEqual(mtx.inputs.length, 2); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.getFee(), 1e5); + // Should select the oldest (lowest height) coins first + assert.strictEqual(mtx.inputs[0].prevout.hash.equals(coins[2].hash), true); + assert.strictEqual(mtx.inputs[1].prevout.hash.equals(coins[0].hash), true); + }); + + it('should fund with value-based selection', async () => { + const coins = [ + makeCoin({ value: 1e6 }), + makeCoin({ value: 5e6 }), + makeCoin({ value: 2e6 }) + ]; + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 4e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 1e5, + selection: 'value' + }); + + assert.strictEqual(mtx.inputs.length, 1); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.getFee(), 1e5); + // Should select the highest value coin first + assert.strictEqual(mtx.inputs[0].prevout.hash.equals(coins[1].hash), true); + }); + + it('should handle subtractFee option', async () => { + const coins = createCoins([2e6, 3e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 5e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 1e5, + subtractFee: true + }); + + assert.strictEqual(mtx.inputs.length, 2); + assert.strictEqual(mtx.outputs.length, 1); + assert.strictEqual(mtx.outputs[0].value, 4.9e6); // 5e6 - 1e5 = 4.9e6 + assert.strictEqual(mtx.getFee(), 1e5); + }); + + it('should handle subtractIndex option', async () => { + const coins = createCoins([3e6, 3e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 3e6); + mtx.addOutput(randomP2PKAddress(), 3e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 2e5, + subtractFee: true, + subtractIndex: 1 + }); + + assert.strictEqual(mtx.inputs.length, 2); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.outputs[0].value, 3e6); + assert.strictEqual(mtx.outputs[1].value, 2.8e6); // 3e6 - 2e5 = 2.8e6 + assert.strictEqual(mtx.getFee(), 2e5); + }); + + it('should throw with insufficient funds', async () => { + const coins = createCoins([1e6, 1e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 5e6); + + let err; + try { + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 0 + }); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message.includes('Not enough funds'), true); + }); + + it('should throw when fee is too high', async () => { + const coins = createCoins([1e6, 1e6, 1e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 2e6); + + let err; + try { + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + rate: 1e6, // Extremely high fee rate + maxFee: 1e5 // But with a low maxFee + }); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message.includes('Fee is too high'), true); + }); + + it('should handle dust change', async () => { + const coins = createCoins([1e6, 1e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 1.999e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 1e3 + }); + + assert.strictEqual(mtx.inputs.length, 2); + assert.strictEqual(mtx.outputs.length, 1); + assert.strictEqual(mtx.getFee(), 1e3); + assert.strictEqual(mtx.changeIndex, -1); + }); + + it('should fund with exact amount needed', async () => { + const coins = createCoins([1e6, 2e6, 3e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 3e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 0 + }); + + assert.strictEqual(mtx.inputs.length, 1); + assert.strictEqual(mtx.outputs.length, 1); + assert.strictEqual(mtx.outputs[0].value, 3e6); + assert.strictEqual(mtx.getFee(), 0); + assert.strictEqual(mtx.changeIndex, -1); + }); + + it('should add coin based on minimum required', async () => { + const wallet = new MemWallet(); + const coins = [ + makeCoin({ address: wallet.getAddress(), value: 1e5 }), + makeCoin({ address: wallet.getAddress(), value: 2e5 }), + makeCoin({ address: wallet.getAddress(), value: 5e5 }), + makeCoin({ address: wallet.getAddress(), value: 1e6 }), + makeCoin({ address: wallet.getAddress(), value: 2e6 }) + ]; + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 1.5e6); + + await mtx.fund(coins, { + changeAddress: wallet.getChange(), + hardFee: 1e4 + }); + + // Should select the 2e6 coin (largest value first selection) + assert.strictEqual(mtx.inputs.length, 1); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.outputs[0].value, 1.5e6); + assert.strictEqual(mtx.outputs[1].value, 2e6 - 1.5e6 - 1e4); + assert.bufferEqual(mtx.inputs[0].prevout.hash, coins[4].hash); + }); + + it('should combine multiple coins when necessary', async () => { + const coins = createCoins([1e5, 2e5, 3e5, 4e5, 5e5]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 1e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 5e4 + }); + + // Should need to combine multiple coins to reach 1e6 + 5e4 + assert.ok(mtx.inputs.length > 1); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.outputs[0].value, 1e6); + assert.strictEqual(mtx.getFee(), 5e4); + }); + + it('should correctly set changeIndex', async () => { + const coins = createCoins([5e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 2e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + hardFee: 1e5 + }); + + assert.strictEqual(mtx.inputs.length, 1); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.changeIndex, 1); + assert.strictEqual(mtx.outputs[1].value, 2.9e6); // 5e6 - 2e6 - 1e5 = 2.9e6 + }); + + it('should handle fee rates properly', async () => { + const coins = createCoins([1e6, 2e6, 3e6]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 4e6); + + await mtx.fund(coins, { + changeAddress: randomP2PKAddress(), + rate: 5000 // dollarydoos per kb + }); + + // The exact fee will depend on the estimated tx size + assert.strictEqual(mtx.inputs.length, 2); + assert.strictEqual(mtx.outputs.length, 2); + assert.ok(mtx.getFee() > 0); + assert.ok(mtx.getFee() < 1e5); // Reasonable upper bound for test + }); + }); + + describe('Fund preferred & existing', function() { const wallet1 = new MemWallet(); const wallet2 = new MemWallet(); const coins1 = [ - dummyCoin(wallet1.getAddress(), 1000000), - dummyCoin(wallet1.getAddress(), 1000000), - dummyCoin(wallet1.getAddress(), 1000000), - dummyCoin(wallet1.getAddress(), 1000000), - dummyCoin(wallet1.getAddress(), 1000000) + makeCoin({ address: wallet1.getAddress(), value: 1000000 }), + makeCoin({ address: wallet1.getAddress(), value: 1000000 }), + makeCoin({ address: wallet1.getAddress(), value: 1000000 }), + makeCoin({ address: wallet1.getAddress(), value: 1000000 }), + makeCoin({ address: wallet1.getAddress(), value: 1000000 }) ]; const last1 = coins1[coins1.length - 1]; @@ -239,7 +541,10 @@ describe('MTX', function() { it('should fund with preferred inputs - view', async () => { const mtx = new MTX(); - const coin = dummyCoin(wallet1.getAddress(), 1000000); + const coin = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); mtx.addOutput(wallet2.getAddress(), 1500000); mtx.view.addCoin(coin); @@ -266,7 +571,10 @@ describe('MTX', function() { it('should fund with preferred inputs - coins && view', async () => { const mtx = new MTX(); - const viewCoin = dummyCoin(wallet1.getAddress(), 1000000); + const viewCoin = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const lastCoin = last1; mtx.addOutput(wallet2.getAddress(), 1500000); @@ -304,7 +612,10 @@ describe('MTX', function() { it('should not fund with preferred inputs and no coin info', async () => { const mtx = new MTX(); - const coin = dummyCoin(wallet1.getAddress(), 1000000); + const coin = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); mtx.addOutput(wallet2.getAddress(), 1500000); @@ -357,7 +668,10 @@ describe('MTX', function() { it('should fund with existing inputs view - view', async () => { const mtx = new MTX(); - const coin = dummyCoin(wallet1.getAddress(), 1000000); + const coin = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); mtx.addInput({ prevout: { @@ -388,7 +702,10 @@ describe('MTX', function() { it('should fund with existing inputs view - coins && view', async () => { const mtx = new MTX(); - const viewCoin = dummyCoin(wallet1.getAddress(), 1000000); + const viewCoin = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const lastCoin = last1; mtx.addInput({ @@ -434,7 +751,10 @@ describe('MTX', function() { it('should not fund with existing inputs and no coin info', async () => { const mtx = new MTX(); - const coin = dummyCoin(wallet1.getAddress(), 1000000); + const coin = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); mtx.addInput({ prevout: { @@ -502,8 +822,14 @@ describe('MTX', function() { it('should fund with preferred & existing inputs - view', async () => { const mtx = new MTX(); - const coin1 = dummyCoin(wallet1.getAddress(), 1000000); - const coin2 = dummyCoin(wallet1.getAddress(), 1000000); + const coin1 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); + const coin2 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); mtx.addInput({ prevout: { @@ -546,11 +872,17 @@ describe('MTX', function() { it('should fund with preferred & existing inputs', async () => { const mtx = new MTX(); // existing - const coin1 = dummyCoin(wallet1.getAddress(), 1000000); + const coin1 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const coinLast1 = last1; // preferred - const coin2 = dummyCoin(wallet1.getAddress(), 1000000); + const coin2 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const coinLast2 = last2; mtx.addInput({ @@ -620,11 +952,17 @@ describe('MTX', function() { it('should not fund with missing coin info (both)', async () => { const mtx = new MTX(); // existing - const coin1 = dummyCoin(wallet1.getAddress(), 1000000); + const coin1 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const coinLast1 = last1; // preferred - const coin2 = dummyCoin(wallet1.getAddress(), 1000000); + const coin2 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const coinLast2 = last2; mtx.addInput({ @@ -666,11 +1004,17 @@ describe('MTX', function() { it('should not fund with missing coin info(only existing)', async () => { const mtx = new MTX(); // existing - const coin1 = dummyCoin(wallet1.getAddress(), 1000000); + const coin1 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const coinLast1 = last1; // preferred - const coin2 = dummyCoin(wallet1.getAddress(), 1000000); + const coin2 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const coinLast2 = last2; mtx.addInput({ @@ -713,11 +1057,17 @@ describe('MTX', function() { it('should not fund with missing coin info(only preferred)', async () => { const mtx = new MTX(); // existing - const coin1 = dummyCoin(wallet1.getAddress(), 1000000); + const coin1 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const coinLast1 = last1; // preferred - const coin2 = dummyCoin(wallet1.getAddress(), 1000000); + const coin2 = makeCoin({ + address: wallet1.getAddress(), + value: 1000000 + }); const coinLast2 = last2; mtx.addInput({ @@ -758,10 +1108,3 @@ describe('MTX', function() { }); }); }); - -function dummyCoin(address, value) { - const hash = random.randomBytes(32); - const index = 0; - - return new Coin({address, value, hash, index}); -} diff --git a/test/util/migrations.js b/test/util/migrations.js index 08095fcf..c684d5a4 100644 --- a/test/util/migrations.js +++ b/test/util/migrations.js @@ -13,6 +13,10 @@ const consensus = require('../../lib/protocol/consensus'); const BlockTemplate = require('../../lib/mining/template'); const bdb = require('bdb'); +/** @typedef {import('../../lib/blockchain/chain')} Chain */ +/** @typedef {import('../../lib/mining/miner')} Miner */ +/** @typedef {import('../../lib/primitives/address')} Address */ + let Migrator = class {}; try { @@ -190,7 +194,7 @@ exports.mockLayout = mockLayout; exports.oldMockLayout = oldMockLayout; exports.DB_FLAG_ERROR = DB_FLAG_ERROR; -exports.migrationError = (migrations, ids, flagError) => { +exports.migrationError = function migrationError(migrations, ids, flagError) { let error = 'Database needs migration(s):\n'; for (const id of ids) { @@ -207,8 +211,12 @@ exports.prefix2hex = function prefix2hex(prefix) { return Buffer.from(prefix, 'ascii').toString('hex'); }; -exports.dumpDB = async (db, prefixes) => { +exports.dumpDB = async function dumpDB(db, prefixes) { const data = await db.dump(); + return exports.filteredObject(data, prefixes); +}; + +exports.filteredObject = function filteredObject(data, prefixes) { const filtered = {}; for (const [key, value] of Object.entries(data)) { @@ -223,7 +231,7 @@ exports.dumpDB = async (db, prefixes) => { return filtered; }; -exports.dumpChainDB = async (chaindb, prefixes) => { +exports.dumpChainDB = async function dumpChainDB(chaindb, prefixes) { return exports.dumpDB(chaindb.db, prefixes); }; @@ -238,7 +246,7 @@ exports.dumpChainDB = async (chaindb, prefixes) => { * @returns {Promise} - errors. */ -exports.checkEntries = async (ldb, options) => { +exports.checkEntries = async function checkEntries(ldb, options) { const errors = []; options.before = options.before || {}; @@ -296,7 +304,64 @@ exports.checkEntries = async (ldb, options) => { return errors; }; -exports.fillEntries = async (ldb, data) => { +/** + * @param {bdb.DB} ldb + * @param {String[]} prefixes + * @param {Object} options + * @param {Object} options.after - key value pairs to check. + * @param {Boolean} options.throw - throw on error. + * @param {Boolean} options.bail - bail on first error. + * @param {Boolean} options.logErrors - log errors. + * @returns {Promise} - errors. + */ + +exports.checkExactEntries = async function checkExactEntries(ldb, prefixes, options) { + const dumped = await exports.dumpDB(ldb, prefixes); + const after = exports.filteredObject(options.after, prefixes); + + const checks = new Set(Object.keys(after)); + const errors = []; + + for (const [key, value] of Object.entries(dumped)) { + if (errors.length > 0 && options.bail) { + if (options.throw) + throw new Error(errors[0]); + + break; + } + + if (!checks.has(key)) { + errors.push(`Unexpected key found in db: ${key}`); + continue; + } + + if (value !== after[key]) { + errors.push(`Value for ${key}: ${value} does not match expected: ${after[key]}`); + continue; + } + + checks.delete(key); + } + + if (checks.size > 0) { + for (const key of checks) { + errors.push(`Expected key ${key} not found in db.`); + } + } + + if (options.logErrors && errors.length !== 0) { + console.error( + JSON.stringify(errors, null, 2) + ); + } + + if (errors.length > 0 && options.throw) + throw new Error(`Check exact entries failed with ${errors.length} errors.`); + + return errors; +}; + +exports.fillEntries = async function fillEntries(ldb, data) { const batch = await ldb.batch(); for (const [key, value] of Object.entries(data)) { @@ -309,7 +374,7 @@ exports.fillEntries = async (ldb, data) => { await batch.write(); }; -exports.writeVersion = (b, key, name, version) => { +exports.writeVersion = function writeVersion(b, key, name, version) { const value = Buffer.alloc(name.length + 4); value.write(name, 0, 'ascii'); @@ -318,7 +383,7 @@ exports.writeVersion = (b, key, name, version) => { b.put(key, value); }; -exports.getVersion = (data, name) => { +exports.getVersion = function getVersion(data, name) { const error = 'version mismatch'; if (data.length !== name.length + 4) @@ -330,7 +395,7 @@ exports.getVersion = (data, name) => { return data.readUInt32LE(name.length); }; -exports.checkVersion = async (ldb, versionDBKey, expectedVersion) => { +exports.checkVersion = async function checkVersion(ldb, versionDBKey, expectedVersion) { const data = await ldb.get(versionDBKey); const version = exports.getVersion(data, 'wallet'); @@ -352,7 +417,7 @@ const getBlockTime = height => REGTEST_TIME + (height * 10 * 60); * @returns {BlockTemplate} */ -exports.createBlock = async (options) => { +exports.createBlock = async function createBlock(options) { const { chain, miner, diff --git a/test/util/pagination.js b/test/util/pagination.js index 32777cc1..57badaf9 100644 --- a/test/util/pagination.js +++ b/test/util/pagination.js @@ -3,7 +3,7 @@ const assert = require('bsert'); const {forEventCondition} = require('./common'); -exports.generateInitialBlocks = async (options) => { +exports.generateInitialBlocks = async function generateInitialBlocks(options) { const { nodeCtx, coinbase, diff --git a/test/util/primitives.js b/test/util/primitives.js new file mode 100644 index 00000000..99e7bca5 --- /dev/null +++ b/test/util/primitives.js @@ -0,0 +1,217 @@ +'use strict'; + +const assert = require('bsert'); +const blake2b = require('bcrypto/lib/blake2b'); +const random = require('bcrypto/lib/random'); +const rules = require('../../lib/covenants/rules'); +const Input = require('../../lib/primitives/input'); +const Address = require('../../lib/primitives/address'); +const Output = require('../../lib/primitives/output'); +const Outpoint = require('../../lib/primitives/outpoint'); +const Coin = require('../../lib/primitives/coin'); +const Covenant = require('../../lib/primitives/covenant'); + +/** @typedef {import('../../lib/types').Hash} Hash */ + +exports.coinbaseInput = function coinbaseInput() { + return Input.fromOutpoint(new Outpoint()); +}; + +exports.dummyInput = function dummyInput () { + const hash = random.randomBytes(32); + return Input.fromOutpoint(new Outpoint(hash, 0)); +}; + +exports.deterministicInput = function deterministicInput(id) { + const hash = blake2b.digest(fromU32(id)); + return Input.fromOutpoint(new Outpoint(hash, 0)); +}; + +/** + * @typedef {Object} OutputOptions + * @property {Number} value + * @property {Address} [address] + * @property {CovenantOptions} [covenant] + */ + +/** + * @param {OutputOptions} options + * @returns {Output} + */ + +exports.makeOutput = function makeOutput(options) { + const address = options.address || exports.randomP2PKAddress(); + const output = new Output(); + output.address = address; + output.value = options.value; + + if (options.covenant) + output.covenant = exports.makeCovenant(options.covenant); + + return output; +}; + +/** + * @typedef {Object} CovenantOptions + * @property {String} [name] + * @property {Hash} [nameHash] + * @property {Covenant.types} [type=Covenant.types.NONE] + * @property {Number} [height] + * @property {Array} [args] - leftover args for the covenant except + * for nameHash, name and height. + */ + +/** + * @param {CovenantOptions} options + * @returns {Covenant} + */ + +exports.makeCovenant = function makeCovenant(options) { + const covenant = new Covenant(); + covenant.type = options.type || Covenant.types.NONE; + + const args = options.args || []; + const height = options.height || 0; + let nameHash = options.nameHash; + let name = options.name; + + if (name) { + nameHash = rules.hashName(name); + } else if (!nameHash) { + name = exports.randomName(30); + nameHash = rules.hashName(name); + } + + switch (covenant.type) { + case Covenant.types.NONE: + break; + case Covenant.types.OPEN: { + assert(args.length === 0, 'Pass `options.name` instead.'); + const rawName = Buffer.from(name, 'ascii'); + covenant.setOpen(nameHash, rawName); + break; + } + case Covenant.types.BID: { + assert(args.length <= 1, 'Pass [blind?] instead.'); + const blind = args[0] || random.randomBytes(32); + const rawName = Buffer.from(name, 'ascii'); + covenant.setBid(nameHash, height, rawName, blind); + break; + } + case Covenant.types.REVEAL: { + assert(args.length <= 1, 'Pass [nonce?] instead.'); + const nonce = args[0] || random.randomBytes(32); + covenant.setReveal(nameHash, height, nonce); + break; + } + case Covenant.types.REDEEM: { + assert(args.length === 0, 'No args for redeem.'); + covenant.setRedeem(nameHash, height); + break; + } + case Covenant.types.REGISTER: { + assert(args.length <= 2, 'Pass [record?, blockHash?] instead.'); + const record = args[0] || Buffer.alloc(0); + const blockHash = args[1] || random.randomBytes(32); + covenant.setRegister(nameHash, height, record, blockHash); + break; + } + case Covenant.types.UPDATE: { + assert(args.length <= 1, 'Pass [resource?] instead.'); + const resource = args[0] || Buffer.alloc(0); + covenant.setUpdate(nameHash, height, resource); + break; + } + case Covenant.types.RENEW: { + assert(args.length <= 1, 'Pass [blockHash?] instead.'); + const blockHash = args[0] || random.randomBytes(32); + covenant.setRenew(nameHash, height, blockHash); + break; + } + case Covenant.types.TRANSFER: { + assert(args.length <= 1, 'Pass [address?] instead.'); + const address = args[0] || exports.randomP2PKAddress(); + covenant.setTransfer(nameHash, height, address); + break; + } + case Covenant.types.FINALIZE: { + assert(args.length <= 4, 'Pass [flags?, claimed?, renewal?, blockHash?] instead.'); + const rawName = Buffer.from(name, 'ascii'); + const flags = args[0] || 0; + const claimed = args[1] || 0; + const renewal = args[2] || 0; + const blockHash = args[3] || random.randomBytes(32); + + covenant.setFinalize( + nameHash, + height, + rawName, + flags, + claimed, + renewal, + blockHash + ); + break; + } + case Covenant.types.REVOKE: { + assert(args.length === 0, 'No args for revoke.'); + covenant.setRevoke(nameHash, height); + break; + } + default: + throw new Error(`Invalid covenant type ${covenant.type}.`); + } + + return covenant; +}; + +exports.randomP2PKAddress = function randomP2PKAddress() { + const key = random.randomBytes(33); + return Address.fromPubkey(key); +}; + +/** + * @typedef {Object} CoinOptions + * @param {String} [options.version=1] + * @param {String} [options.height=-1] + * @param {String} [options.value=0] + * @param {String} [options.address] + * @param {Object} [options.covenant] + * @param {Boolean} [options.coinbase=false] + * @param {Buffer} [options.hash] + * @param {Number} [options.index=0] + */ + +/** + * @param {CoinOptions} options + * @returns {Coin} + */ + +exports.makeCoin = function makeCoin(options) { + return Coin.fromOptions({ + hash: options.hash || random.randomBytes(32), + address: options.address || Address.fromPubkey(random.randomBytes(33)), + ...options + }); +}; + +function fromU32(num) { + const data = Buffer.allocUnsafe(4); + data.writeUInt32LE(num, 0, true); + return data; +} + +exports.randomName = function randomName(len) { + assert((len >>> 0) === len); + + let s = ''; + + for (let i = 0; i < len; i++) { + const n = Math.random() * (0x7b - 0x61) + 0x61; + const c = Math.floor(n); + + s += String.fromCharCode(c); + } + + return s; +}; diff --git a/test/util/wallet.js b/test/util/wallet.js index 05b173a4..ed44d234 100644 --- a/test/util/wallet.js +++ b/test/util/wallet.js @@ -2,11 +2,16 @@ const assert = require('bsert'); const blake2b = require('bcrypto/lib/blake2b'); -const random = require('bcrypto/lib/random'); const ChainEntry = require('../../lib/blockchain/chainentry'); -const Input = require('../../lib/primitives/input'); -const Outpoint = require('../../lib/primitives/outpoint'); +const MTX = require('../../lib/primitives/mtx'); const {ZERO_HASH} = require('../../lib/protocol/consensus'); +const primutils = require('./primitives'); +const {coinbaseInput, makeOutput} = primutils; + +/** @typedef {import('../../lib/types').Amount} Amount */ +/** @typedef {import('../../lib/covenants/rules').types} covenantTypes */ +/** @typedef {import('../../lib/primitives/output')} Output */ +/** @typedef {import('../../lib/wallet/wallet')} Wallet */ const walletUtils = exports; @@ -35,16 +40,6 @@ walletUtils.fakeBlock = (height, prevSeed = 0, seed = prevSeed) => { }; }; -walletUtils.dummyInput = () => { - const hash = random.randomBytes(32); - return Input.fromOutpoint(new Outpoint(hash, 0)); -}; - -walletUtils.deterministicInput = (id) => { - const hash = blake2b.digest(fromU32(id)); - return Input.fromOutpoint(new Outpoint(hash, 0)); -}; - walletUtils.nextBlock = (wdb, prevSeed = 0, seed = prevSeed) => { return walletUtils.fakeBlock(wdb.state.height + 1, prevSeed, seed); }; @@ -88,3 +83,144 @@ walletUtils.dumpWDB = async (wdb, prefixes) => { return filtered; }; + +/** + * @typedef {Object} OutputInfo + * @property {String} [address] + * @property {Number} [account=0] - address generation account. + * @property {Amount} [value] + * @property {covenantTypes} [covenant] + * @property {Boolean} [coinbase=false] + */ + +/** + * @param {Wallet} wallet + * @param {primutils.OutputOptions} outputInfo + * @param {Object} options + * @param {Boolean} [options.createAddress=true] - create address if not provided. + * @returns {Promise} + */ + +async function mkOutput(wallet, outputInfo, options = {}) { + const info = { ...outputInfo }; + + const { + createAddress = true + } = options; + + if (!info.address) { + const account = outputInfo.account || 0; + + if (createAddress) { + const walletKey = await wallet.createReceive(account); + info.address = walletKey.getAddress(); + } else { + info.address = await wallet.receiveAddress(account); + } + } + + return makeOutput(info); +} + +walletUtils.deterministicId = 0; + +/** + * Create Inbound TX Options + * @typedef {Object} InboundTXOptions + * @property {Boolean} [txPerOutput=true] + * @property {Boolean} [createAddress=true] + * @property {Boolean} [deterministicInput=false] + */ + +/** + * Create funding MTXs for a wallet. + * @param {Wallet} wallet + * @param {OutputInfo[]} outputInfos + * @param {InboundTXOptions} options + * @returns {Promise} + */ + +walletUtils.createInboundTXs = async function createInboundTXs(wallet, outputInfos, options = {}) { + assert(Array.isArray(outputInfos)); + + const { + txPerOutput = true, + createAddress = true + } = options; + + let hadCoinbase = false; + + const txs = []; + + let mtx = new MTX(); + + let getInput = primutils.dummyInput; + + if (options.deterministicInput) { + getInput = () => { + const id = walletUtils.deterministicId++; + return primutils.deterministicInput(id); + }; + } + + for (const info of outputInfos) { + if (txPerOutput) + mtx = new MTX(); + + if (info.coinbase && hadCoinbase) + throw new Error('Coinbase already added.'); + + if (info.coinbase && !hadCoinbase) { + if (!txPerOutput) + hadCoinbase = true; + mtx.addInput(coinbaseInput()); + } else if (!hadCoinbase) { + mtx.addInput(getInput()); + } + + const output = await mkOutput(wallet, info, { createAddress }); + mtx.addOutput(output); + + if (output.covenant.isLinked()) + mtx.addInput(getInput()); + + if (txPerOutput) + txs.push(mtx.toTX()); + } + + if (!txPerOutput) + txs.push(mtx.toTX()); + + return txs; +}; + +/** + * Fund wallet options + * @typedef {Object} FundOptions + * @property {Boolean} [txPerOutput=true] + * @property {Boolean} [createAddress=true] + * @property {Boolean} [blockPerTX=false] + */ + +/** + * @param {Wallet} wallet + * @param {OutputInfo[]} outputInfos + * @param {FundOptions} options + * @returns {Promise} + */ + +walletUtils.fundWallet = async function fundWallet(wallet, outputInfos, options = {}) { + const txs = await walletUtils.createInboundTXs(wallet, outputInfos, options); + + if (!options.blockPerTX) { + await wallet.wdb.addBlock(walletUtils.nextBlock(wallet.wdb), txs); + return txs; + } + + for (const tx of txs) { + await wallet.wdb.addTX(tx); + await wallet.wdb.addBlock(walletUtils.nextBlock(wallet.wdb), [tx]); + } + + return txs; +}; diff --git a/test/wallet-chainstate-test.js b/test/wallet-chainstate-test.js index 43f6b16d..b92bb72f 100644 --- a/test/wallet-chainstate-test.js +++ b/test/wallet-chainstate-test.js @@ -7,10 +7,8 @@ const MTX = require('../lib/primitives/mtx'); const WorkerPool = require('../lib/workers/workerpool'); const WalletDB = require('../lib/wallet/walletdb'); const wutils = require('./util/wallet'); -const { - dummyInput, - nextEntry -} = wutils; +const {nextEntry} = wutils; +const {dummyInput} = require('./util/primitives'); const enabled = true; const size = 2; diff --git a/test/wallet-coinselection-test.js b/test/wallet-coinselection-test.js index b909644d..2a7a9e6c 100644 --- a/test/wallet-coinselection-test.js +++ b/test/wallet-coinselection-test.js @@ -1,72 +1,3056 @@ 'use strict'; const assert = require('bsert'); -const {BlockMeta} = require('../lib/wallet/records'); -const util = require('../lib/utils/util'); +const {BufferMap} = require('buffer-map'); const Network = require('../lib/protocol/network'); const MTX = require('../lib/primitives/mtx'); +const Covenant = require('../lib/primitives/covenant'); +const Coin = require('../lib/primitives/coin'); +const Input = require('../lib/primitives/input'); +const wcommon = require('../lib/wallet/common'); const WalletDB = require('../lib/wallet/walletdb'); const policy = require('../lib/protocol/policy'); +const wutils = require('./util/wallet'); +const primutils = require('./util/primitives'); +const {randomP2PKAddress} = primutils; +const {DB_VALUE, DB_AGE, DB_ALL, DB_SWEEPDUST} = wcommon.coinSelectionTypes; +const { + nextBlock, + curBlock, + createInboundTXs, + fundWallet +} = wutils; + +/** @typedef {import('../lib/wallet/wallet')} Wallet */ +/** @typedef {import('../lib/primitives/tx')} TX */ +/** @typedef {import('./util/primitives').CoinOptions} CoinOptions */ +/** @typedef {wutils.OutputInfo} OutputInfo */ + +const UNCONFIRMED_HEIGHT = 0xffffffff; // Use main instead of regtest because (deprecated) // CoinSelector.MAX_FEE was network agnostic const network = Network.get('main'); -function dummyBlock(tipHeight) { - const height = tipHeight + 1; - const hash = Buffer.alloc(32); - hash.writeUInt16BE(height); +const DEFAULT_ACCOUNT = 'default'; +const ALT_ACCOUNT = 'alt'; - const prevHash = Buffer.alloc(32); - prevHash.writeUInt16BE(tipHeight); +describe('Wallet Coin Selection', function() { + const TX_START_BAK = network.txStart; + /** @type {WalletDB?} */ + let wdb; + /** @type {Wallet?} */ + let wallet; - const dummyBlock = { - hash, - height, - time: util.now(), - prevBlock: prevHash + const beforeFn = async () => { + network.txStart = 0; + wdb = new WalletDB({ network }); + + await wdb.open(); + await wdb.addBlock(nextBlock(wdb), []); + wallet = wdb.primary; + + await wallet.createAccount({ + name: ALT_ACCOUNT + }); }; - return dummyBlock; -} + const afterFn = async () => { + network.txStart = TX_START_BAK; + await wdb.close(); -async function fundWallet(wallet, amounts) { - assert(Array.isArray(amounts)); + wdb = null; + wallet = null; + }; - const mtx = new MTX(); - const addr = await wallet.receiveAddress(); - for (const amt of amounts) { - mtx.addOutput(addr, amt); + const indexes = [ + 'value-asc', + 'value-desc', + 'height-asc', + 'height-desc' + ]; + + for (const indexType of indexes) { + describe(`Coin Selection Indexes (${indexType})`, function() { + const TX_OPTIONS = [ + { value: 2e6, address: randomP2PKAddress() }, + // address will be generated using wallet. + { value: 1e6, covenant: { type: Covenant.types.OPEN } }, + { value: 5e6, covenant: { type: Covenant.types.REDEEM } }, + { value: 2e6 }, + // alt account + { value: 4e6, account: ALT_ACCOUNT }, + { value: 6e6, account: ALT_ACCOUNT, covenant: { type: Covenant.types.OPEN } }, + { value: 3e6, account: ALT_ACCOUNT, covenant: { type: Covenant.types.REDEEM } }, + // non spendable coins must not get indexed. + { value: 4e6, covenant: { type: Covenant.types.BID } }, + { value: 5e6, covenant: { type: Covenant.types.REVEAL } }, + { value: 6e6, covenant: { type: Covenant.types.REGISTER } }, + { value: 7e6, covenant: { type: Covenant.types.UPDATE } }, + { value: 8e6, covenant: { type: Covenant.types.RENEW } }, + { value: 9e6, covenant: { type: Covenant.types.TRANSFER } }, + { value: 10e6, covenant: { type: Covenant.types.FINALIZE } }, + { value: 11e6, covenant: { type: Covenant.types.REVOKE } } + ]; + + const ACCT_0_COINS = 3; + const ACCT_0_FUNDS = 1e6 + 2e6 + 5e6; + const ACCT_1_COINS = 3; + const ACCT_1_FUNDS = 3e6 + 4e6 + 6e6; + const TOTAL_COINS = ACCT_0_COINS + ACCT_1_COINS; + const TOTAL_FUNDS = ACCT_0_FUNDS + ACCT_1_FUNDS; + + let isSorted, getCredits; + const sumCredits = credits => credits.reduce((acc, c) => acc + c.coin.value, 0); + const checkWithLimits = async (credits, wallet, acct) => { + for (let i = 1; i < credits.length; i++) { + const creditsLimit = await getCredits(wallet, acct, { + limit: i + }); + assert.strictEqual(creditsLimit.length, i); + assert(isSorted(creditsLimit), 'Credits not sorted.'); + assert.deepStrictEqual(creditsLimit, credits.slice(0, i)); + assert(sumCredits(creditsLimit) === sumCredits(credits.slice(0, i))); + } + }; + + before(() => { + switch (indexType) { + case 'value-asc': + isSorted = isSortedByValueAsc; + getCredits = (wallet, acct = 0, opts = {}) => { + return collectIter(wallet.getAccountCreditIterByValue(acct, opts)); + }; + break; + case 'value-desc': + isSorted = isSortedByValueDesc; + getCredits = (wallet, acct = 0, opts = {}) => { + return collectIter(wallet.getAccountCreditIterByValue(acct, { + ...opts, + reverse: true + })); + }; + break; + case 'height-asc': + isSorted = isSortedByHeightAsc; + getCredits = (wallet, acct = 0, opts = {}) => { + return collectIter(wallet.getAccountCreditIterByHeight(acct, opts)); + }; + break; + case 'height-desc': + isSorted = isSortedByHeightDesc; + getCredits = (wallet, acct = 0, opts = {}) => { + return collectIter(wallet.getAccountCreditIterByHeight(acct, { + ...opts, + reverse: true + })); + }; + break; + default: + throw new Error('Invalid index type.'); + } + }); + + beforeEach(beforeFn); + afterEach(afterFn); + + it('should index unconfirmed tx output', async () => { + const txs = await createInboundTXs(wallet, TX_OPTIONS); + + for (const tx of txs) + await wallet.wdb.addTX(tx); + + const credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + const credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + const both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert.strictEqual(credit.coin.height, -1); + assert.strictEqual(credit.spent, false); + } + }); + + it('should index unconfirmed tx input', async () => { + const currentBlock = curBlock(wdb); + await fundWallet(wallet, TX_OPTIONS, { + blockPerTX: true + }); + + const spendAll = await wallet.createTX({ + hardFee: 0, + outputs: [{ value: TOTAL_FUNDS, address: randomP2PKAddress() }] + }); + + await wdb.addTX(spendAll.toTX()); + + // We still have the coin, even thought it is flagged: .spent = true + const credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + const credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + const both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert(credit.coin.height > currentBlock.height); + assert.strictEqual(credit.spent, true); + } + }); + + it('should index insert (block) tx output', async () => { + const currentBlock = curBlock(wdb); + await fundWallet(wallet, TX_OPTIONS, { blockPerTX: true }); + + const credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + const credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + const both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert(credit.coin.height > currentBlock.height); + assert.strictEqual(credit.spent, false); + } + }); + + it('should index insert (block) tx input', async () => { + await fundWallet(wallet, TX_OPTIONS, { + blockPerTX: false + }); + const currentBlock = curBlock(wdb); + + const spendAll = await wallet.createTX({ + hardFee: 0, + outputs: [{ value: TOTAL_FUNDS, address: randomP2PKAddress() }] + }); + + let credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + let credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + let both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert.strictEqual(credit.coin.height, currentBlock.height); + assert.strictEqual(credit.spent, false); + } + + await wdb.addBlock(nextBlock(wdb), [spendAll.toTX()]); + + credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, 0); + + credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, 0); + + both = await getCredits(wallet, -1); + assert.strictEqual(both.length, 0); + }); + + it('should index confirm tx output', async () => { + const txs = await createInboundTXs(wallet, TX_OPTIONS); + for (const tx of txs) + await wdb.addTX(tx); + + let credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + let credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + let both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert.strictEqual(credit.coin.height, -1); + assert.strictEqual(credit.spent, false); + } + + await wdb.addBlock(nextBlock(wdb), txs); + const currentBlock = curBlock(wdb); + + credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert.strictEqual(credit.coin.height, currentBlock.height); + assert.strictEqual(credit.spent, false); + } + }); + + it('should index confirm tx input', async () => { + const currentBlock = curBlock(wdb); + await fundWallet(wallet, TX_OPTIONS, { + blockPerTX: true + }); + + const spendAll = await wallet.createTX({ + hardFee: 0, + outputs: [{ value: TOTAL_FUNDS, address: randomP2PKAddress() }] + }); + const spendAllTX = spendAll.toTX(); + + await wdb.addTX(spendAllTX); + + let credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + let credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + let both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert(credit.coin.height > currentBlock.height); + assert.strictEqual(credit.spent, true); + } + + await wdb.addBlock(nextBlock(wdb), [spendAllTX]); + + credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, 0); + + credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, 0); + + both = await getCredits(wallet, -1); + assert.strictEqual(both.length, 0); + }); + + it('should index disconnect tx output', async () => { + const currentBlock = curBlock(wdb); + await fundWallet(wallet, TX_OPTIONS, { + blockPerTX: true + }); + + let credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + let credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + let both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert(credit.coin.height > currentBlock.height); + assert.strictEqual(credit.spent, false); + } + + // disconnect last block. + await wdb.rollback(currentBlock.height); + + // Only thing that must change is the HEIGHT. + credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert.strictEqual(credit.coin.height, -1); + assert.strictEqual(credit.spent, false); + } + }); + + it('should index disconnect tx input', async () => { + const startingHeight = curBlock(wdb).height; + await fundWallet(wallet, TX_OPTIONS, { blockPerTX: true }); + const createCoinHeight = curBlock(wdb).height; + + const spendAll = await wallet.createTX({ + hardFee: 0, + outputs: [{ value: TOTAL_FUNDS, address: randomP2PKAddress() }] + }); + + const spendAllTX = spendAll.toTX(); + await wdb.addBlock(nextBlock(wdb), [spendAllTX]); + + let credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, 0); + + let credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, 0); + + let both = await getCredits(wallet, -1); + assert.strictEqual(both.length, 0); + + await wdb.rollback(createCoinHeight); + + credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert(credit.coin.height > startingHeight); + assert.strictEqual(credit.spent, true); + } + }); + + it('should index erase tx output', async () => { + const txs = await createInboundTXs(wallet, TX_OPTIONS); + + for (const tx of txs) + await wdb.addTX(tx); + + let credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + let credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + let both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert.strictEqual(credit.coin.height, -1); + assert.strictEqual(credit.spent, false); + } + + // double spend original txs. + const mtx = new MTX(); + for (const tx of txs) + mtx.addInput(tx.inputs[0]); + mtx.addOutput(randomP2PKAddress(), 1e6); + + await wdb.addBlock(nextBlock(wdb), [mtx.toTX()]); + + credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, 0); + + credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, 0); + + both = await getCredits(wallet, -1); + assert.strictEqual(both.length, 0); + }); + + it('should index erase tx input', async () => { + const txs = await createInboundTXs(wallet, TX_OPTIONS); + for (const tx of txs) + await wdb.addTX(tx); + + const spendAll = await wallet.createTX({ + hardFee: 0, + outputs: [{ value: TOTAL_FUNDS, address: randomP2PKAddress() }] + }); + + await wdb.addTX(spendAll.toTX()); + + let credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + let credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + let both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert.strictEqual(credit.coin.height, -1); + assert.strictEqual(credit.spent, true); + } + + // double spend original tx. + const mtx = new MTX(); + for (const tx of txs) + mtx.addInput(tx.inputs[0]); + mtx.addOutput(randomP2PKAddress(), 1e6); + + await wdb.addBlock(nextBlock(wdb), [mtx.toTX()]); + + credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, 0); + + credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, 0); + + both = await getCredits(wallet, -1); + assert.strictEqual(both.length, 0); + }); + + it('should index erase (block) tx output', async () => { + const txOptions = [...TX_OPTIONS]; + for (const opt of txOptions) + opt.coinbase = true; + + const startingHeight = curBlock(wdb).height; + const txs = await fundWallet(wallet, txOptions, { blockPerTX: true }); + + for (const tx of txs) + assert(tx.isCoinbase()); + + let credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, ACCT_0_COINS); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === ACCT_0_FUNDS); + + let credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, ACCT_1_COINS); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === ACCT_1_FUNDS); + + let both = await getCredits(wallet, -1); + assert.strictEqual(both.length, TOTAL_COINS); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === TOTAL_FUNDS); + + for (const credit of [...credits0, ...credits1, ...both]) { + assert(credit.coin.height > startingHeight); + assert.strictEqual(credit.spent, false); + } + + await wdb.rollback(startingHeight); + + credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, 0); + + credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, 0); + + both = await getCredits(wallet, -1); + assert.strictEqual(both.length, 0); + }); + + it('should index block and mempool', async () => { + const txOptionsConfirmed = [ + { value: 4e6 }, + { value: 7e6 }, + { value: 2e6, account: ALT_ACCOUNT }, + { value: 5e6, account: ALT_ACCOUNT } + ]; + await fundWallet(wallet, txOptionsConfirmed, { + blockPerTX: false + }); + + const txOptionsUnconfirmed = [ + { value: 8e6 }, + { value: 3e6 }, + { value: 6e6, account: ALT_ACCOUNT }, + { value: 1e6, account: ALT_ACCOUNT } + ]; + const txs = await createInboundTXs(wallet, txOptionsUnconfirmed, { + txPerOutput: false + }); + await wdb.addTX(txs[0]); + + const sum0 = 3e6 + 4e6 + 7e6 + 8e6; + const sum1 = 1e6 + 2e6 + 5e6 + 6e6; + + const credits0 = await getCredits(wallet); + assert.strictEqual(credits0.length, 4); + assert(isSorted(credits0), 'Credits not sorted.'); + assert(sumCredits(credits0) === sum0); + + await checkWithLimits(credits0, wallet); + + const credits1 = await getCredits(wallet, 1); + assert.strictEqual(credits1.length, 4); + assert(isSorted(credits1), 'Credits not sorted.'); + assert(sumCredits(credits1) === sum1); + + await checkWithLimits(credits1, wallet, 1); + + const both = await getCredits(wallet, -1); + assert.strictEqual(both.length, 8); + assert(isSorted(both), 'Credits not sorted.'); + assert(sumCredits(both) === sum0 + sum1); + + await checkWithLimits(both, wallet, -1); + }); + }); } - const dummy = dummyBlock(wallet.wdb.height); - await wallet.wdb.addBlock(dummy, [mtx.toTX()]); -} + /** @type {OutputInfo[]} */ + const PER_BLOCK_COINS = [ + // confirmed per block. + { value: 2e6 }, + { value: 2e6 }, + { value: 1e6, account: ALT_ACCOUNT }, + { value: 12e6 }, // LOCKED + { value: 8e6 }, + { value: 10e6, account: ALT_ACCOUNT }, // LOCKED + { value: 5e6, account: ALT_ACCOUNT } + ]; -describe('Wallet Coin Selection', function () { - describe('Fees', function () { - const wdb = new WalletDB({network}); - let wallet; + /** @type {OutputInfo[]} */ + const UNCONFIRMED_COINS = [ + // unconfirmed + { value: 3e6 }, // own + { value: 6e6 }, + { value: 11e6 }, // LOCKED + { value: 4e6, account: ALT_ACCOUNT }, // own + { value: 7e6, account: ALT_ACCOUNT }, + { value: 9e6, account: ALT_ACCOUNT } // LOCKED + ]; + const LOCK = [9e6, 10e6, 11e6, 12e6]; + const OWN = [ + { account: DEFAULT_ACCOUNT, value: 3e6 }, + { account: ALT_ACCOUNT, value: 4e6 } + ]; + + const ACCT_0_CONFIRMED = 2e6 + 2e6 + 8e6; // 10e6 + const ACCT_0_UNCONFIRMED = 3e6 + 6e6; // 9e6 + const ACCT_0_FOREIGN = 6e6; + const ACCT_0_FUNDS = ACCT_0_CONFIRMED + ACCT_0_UNCONFIRMED; // 19e6 + + const ACCT_1_CONFIRMED = 1e6 + 5e6; // 6e6 + const ACCT_1_UNCONFIRMED = 4e6 + 7e6; // 11e6 + const ACCT_1_FOREIGN = 7e6; + const ACCT_1_FUNDS = ACCT_1_CONFIRMED + ACCT_1_UNCONFIRMED; // 17e6 + + /** + * @typedef {Object} SelectionTest + * @property {String} name + * @property {Object} options + * @property {Amount} value + * @property {Amount[]} [existingInputs] - use some coins that are resolved later. + * Use only unique value Coins. + * @property {CoinOptions[]} [existingCoins] - Coins that don't belong to the wallet, + * but are used in the mtx. + * @property {Amount[]} expectedOrdered + * @property {Object} [expectedSome] - Some of this must exist in mtx. + * * This is for AGE unconfirmed, which is not deterministic. + * @property {Number} expectedSome.count - Number of items that must exist. + * @property {Amount[]} expectedSome.items + */ + + /** @type {Object} */ + const SELECTION_TESTS = { + 'value': [ + // wallet by value + { + name: 'select 1 coin (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value' + }, + value: 1e6, + expectedOrdered: [8e6] + }, + { + name: 'select all confirmed coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value' + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED, + expectedOrdered: [8e6, 5e6, 2e6, 2e6, 1e6] + }, + { + name: 'select all confirmed and an unconfirmed (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value' + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [8e6, 5e6, 2e6, 2e6, 1e6, 7e6] + }, + { + name: 'select all coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value' + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS, + expectedOrdered: [8e6, 5e6, 2e6, 2e6, 1e6, 7e6, 6e6, 4e6, 3e6] + }, + { + // test locked filters. + name: 'throw funding error (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value' + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS + 1e6, + error: { + availableFunds: ACCT_0_FUNDS + ACCT_1_FUNDS, + requiredFunds: ACCT_0_FUNDS + ACCT_1_FUNDS + 1e6, + type: 'FundingError' + } + }, + + // default account by value + { + name: 'select 1 coin (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: 1e6, + expectedOrdered: [8e6] + }, + { + name: 'select all confirmed coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: ACCT_0_CONFIRMED, + expectedOrdered: [8e6, 2e6, 2e6] + }, + { + name: 'select all confirmed and an unconfirmed (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: ACCT_0_CONFIRMED + 1e6, + expectedOrdered: [8e6, 2e6, 2e6, 6e6] + }, + { + name: 'select all coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: ACCT_0_FUNDS, + expectedOrdered: [8e6, 2e6, 2e6, 6e6, 3e6] + }, + { + // test locked filters. + name: 'throw funding error (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: ACCT_0_FUNDS + 1e6, + error: { + availableFunds: ACCT_0_FUNDS, + requiredFunds: ACCT_0_FUNDS + 1e6, + type: 'FundingError' + } + }, + + // alt account by value + { + name: 'select 1 coin (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: 1e6, + expectedOrdered: [5e6] + }, + { + name: 'select all confirmed coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: ACCT_1_CONFIRMED, + expectedOrdered: [5e6, 1e6] + }, + { + name: 'select all confirmed and an unconfirmed (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [5e6, 1e6, 7e6] + }, + { + name: 'select all coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: ACCT_1_FUNDS, + expectedOrdered: [5e6, 1e6, 7e6, 4e6] + }, + { + // test locked filters. + name: 'throw funding error (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: ACCT_1_FUNDS + 1e6, + error: { + availableFunds: ACCT_1_FUNDS, + requiredFunds: ACCT_1_FUNDS + 1e6, + type: 'FundingError' + } + } + ], + 'value + smart': [ + // Test smart option. + // smart selection (wallet) + { + name: 'select all confirmed and an unconfirmed + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value', + smart: true + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [8e6, 5e6, 2e6, 2e6, 1e6, 4e6] + }, + { + name: 'select all coins + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value', + smart: true + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS - ACCT_0_FOREIGN - ACCT_1_FOREIGN, + expectedOrdered: [8e6, 5e6, 2e6, 2e6, 1e6, 4e6, 3e6] + }, + { + name: 'throw funding error + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value', + smart: true + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS, + error: { + availableFunds: ACCT_0_FUNDS + ACCT_1_FUNDS - ACCT_0_FOREIGN - ACCT_1_FOREIGN, + requiredFunds: ACCT_0_FUNDS + ACCT_1_FUNDS, + type: 'FundingError' + } + }, + // smart selection (default) + { + name: 'select all confirmed and an unconfirmed + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value', + smart: true + }, + value: ACCT_0_CONFIRMED + 1e6, + expectedOrdered: [8e6, 2e6, 2e6, 3e6] + }, + { + name: 'select all coins + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value', + smart: true + }, + value: ACCT_0_FUNDS - ACCT_0_FOREIGN, + expectedOrdered: [8e6, 2e6, 2e6, 3e6] + }, + { + name: 'throw funding error + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value', + smart: true + }, + value: ACCT_0_FUNDS, + error: { + availableFunds: ACCT_0_FUNDS - ACCT_0_FOREIGN, + requiredFunds: ACCT_0_FUNDS, + type: 'FundingError' + } + }, + // smart selection (alt) + { + name: 'select all confirmed and an unconfirmed + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value', + smart: true + }, + value: ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [5e6, 1e6, 4e6] + }, + { + name: 'select all coins + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value', + smart: true + }, + value: ACCT_1_FUNDS - ACCT_1_FOREIGN, + expectedOrdered: [5e6, 1e6, 4e6] + }, + { + name: 'throw funding error + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value', + smart: true + }, + value: ACCT_1_FUNDS, + error: { + availableFunds: ACCT_1_FUNDS - ACCT_1_FOREIGN, + requiredFunds: ACCT_1_FUNDS, + type: 'FundingError' + } + } + ], + // Existing coins = views + inputs + // Existing inputs = inputs (no view, needs extra resolving) + 'value + existing coins and inputs': [ + // existing coins (wallet) + { + name: 'select coins + existing coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value' + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 1e6 + } + ], + expectedOrdered: [1e6, 8e6, 5e6] + }, + // existing coins (default) + { + name: 'select coins + existing coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 1e6 + } + ], + expectedOrdered: [1e6, 8e6, 2e6] + }, + // existing coins (alt) + { + name: 'select coins + existing coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 1e6 + } + ], + expectedOrdered: [1e6, 5e6, 1e6, 7e6] + }, + { + name: 'select coins + existing inputs (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'value' + }, + value: 10e6, + existingInputs: [5e6], + expectedOrdered: [5e6, 8e6] + }, + // existing coins (default) + { + name: 'select coins + existing inputs (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: 10e6, + existingInputs: [3e6], + expectedOrdered: [3e6, 8e6] + }, + // existing coins (alt) + { + name: 'select coins + existing coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: 10e6, + existingInputs: [4e6], + expectedOrdered: [4e6, 5e6, 1e6] + }, + // fail existing inputs (cross account) + { + name: 'fail cross account existing inputs (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'value' + }, + value: 10e6, + existingInputs: [5e6], // this belongs to alt account + error: { + message: 'Could not resolve preferred inputs.' + } + } + ], + 'age': [ + // wallet by age + { + name: 'select 1 coin (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age' + }, + value: 1e6, + expectedOrdered: [2e6] + }, + { + name: 'select all confirmed coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age' + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED, + expectedOrdered: [2e6, 2e6, 1e6, 8e6, 5e6] + }, + { + name: 'select all confirmed and an unconfirmed (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age' + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [2e6, 2e6, 1e6, 8e6, 5e6], + expectedSome: { + count: 1, + items: [3e6, 6e6, 4e6, 7e6] + } + }, + { + name: 'select all coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age' + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS, + expectedOrdered: [2e6, 2e6, 1e6, 8e6, 5e6], + expectedSome: { + count: 4, + items: [3e6, 6e6, 4e6, 7e6] + } + }, + { + // test locked filters. + name: 'throw funding error (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age' + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS + 1e6, + error: { + availableFunds: ACCT_0_FUNDS + ACCT_1_FUNDS, + requiredFunds: ACCT_0_FUNDS + ACCT_1_FUNDS + 1e6, + type: 'FundingError' + } + }, + + // default account by age + { + name: 'select 1 coin (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: 1e6, + expectedOrdered: [2e6] + }, + { + name: 'select all confirmed coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: ACCT_0_CONFIRMED, + expectedOrdered: [2e6, 2e6, 8e6] + }, + { + name: 'select all confirmed and an unconfirmed (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: ACCT_0_CONFIRMED + 1e6, + expectedOrdered: [2e6, 2e6, 8e6], + expectedSome: { + count: 1, + items: [3e6, 6e6] + } + }, + { + name: 'select all coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: ACCT_0_FUNDS, + expectedOrdered: [2e6, 2e6, 8e6], + expectedSome: { + count: 2, + items: [3e6, 6e6] + } + }, + { + // test locked filters. + name: 'throw funding error (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: ACCT_0_FUNDS + 1e6, + error: { + availableFunds: ACCT_0_FUNDS, + requiredFunds: ACCT_0_FUNDS + 1e6, + type: 'FundingError' + } + }, + + // alt account by age + { + name: 'select 1 coin (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: 1e6, + expectedOrdered: [1e6] + }, + { + name: 'select all confirmed coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: ACCT_1_CONFIRMED, + expectedOrdered: [1e6, 5e6] + }, + { + name: 'select all confirmed and an unconfirmed (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [1e6, 5e6], + expectedSome: { + count: 1, + items: [4e6, 7e6] + } + }, + { + name: 'select all coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: ACCT_1_FUNDS, + expectedOrdered: [1e6, 5e6], + expectedSome: { + count: 2, + items: [4e6, 7e6] + } + }, + { + // test locked filters. + name: 'throw funding error (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: ACCT_1_FUNDS + 1e6, + error: { + availableFunds: ACCT_1_FUNDS, + requiredFunds: ACCT_1_FUNDS + 1e6, + type: 'FundingError' + } + } + ], + 'age + smart': [ + // Test smart option. + // smart selection (wallet) + { + name: 'select all confirmed and an unconfirmed + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age', + smart: true + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [2e6, 2e6, 1e6, 8e6, 5e6], + expectedSome: { + count: 1, + items: [3e6, 6e6, 4e6, 7e6] + } + }, + { + name: 'select all coins + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age', + smart: true + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS - ACCT_0_FOREIGN - ACCT_1_FOREIGN, + expectedOrdered: [2e6, 2e6, 1e6, 8e6, 5e6], + expectedSome: { + count: 2, + items: [3e6, 4e6] + } + }, + { + name: 'throw funding error + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age', + smart: true + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS, + error: { + availableFunds: ACCT_0_FUNDS + ACCT_1_FUNDS - ACCT_0_FOREIGN - ACCT_1_FOREIGN, + requiredFunds: ACCT_0_FUNDS + ACCT_1_FUNDS, + type: 'FundingError' + } + }, + // smart selection (default) + { + name: 'select all confirmed and an unconfirmed + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age', + smart: true + }, + value: ACCT_0_CONFIRMED + 1e6, + expectedOrdered: [2e6, 2e6, 8e6], + expectedSome: { + count: 1, + items: [3e6] + } + }, + { + name: 'select all coins + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age', + smart: true + }, + value: ACCT_0_FUNDS - ACCT_0_FOREIGN, + expectedOrdered: [2e6, 2e6, 8e6], + expectedSome: { + count: 1, + items: [3e6] + } + }, + { + name: 'throw funding error + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age', + smart: true + }, + value: ACCT_0_FUNDS, + error: { + availableFunds: ACCT_0_FUNDS - ACCT_0_FOREIGN, + requiredFunds: ACCT_0_FUNDS, + type: 'FundingError' + } + }, + // smart selection (alt) + { + name: 'select all confirmed and an unconfirmed + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age', + smart: true + }, + value: ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [1e6, 5e6], + expectedSome: { + count: 1, + items: [4e6] + } + }, + { + name: 'select all coins + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age', + smart: true + }, + value: ACCT_1_FUNDS - ACCT_1_FOREIGN, + expectedOrdered: [1e6, 5e6], + expectedSome: { + count: 1, + items: [4e6] + } + }, + { + name: 'throw funding error + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age', + smart: true + }, + value: ACCT_1_FUNDS, + error: { + availableFunds: ACCT_1_FUNDS - ACCT_1_FOREIGN, + requiredFunds: ACCT_1_FUNDS, + type: 'FundingError' + } + } + ], + // Existing coins = views + inputs + // Existing inputs = inputs (no view, needs extra resolving) + 'age + existing inputs': [ + // existing coins (wallet) + { + name: 'select coins + existing coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age' + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 1e6 + } + ], + expectedOrdered: [1e6, 2e6, 2e6, 1e6, 8e6] + }, + // existing coins (default) + { + name: 'select coins + existing coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 1e6 + } + ], + expectedOrdered: [1e6, 2e6, 2e6, 8e6] + }, + // existing coins (alt) + { + name: 'select coins + existing coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 1e6 + } + ], + expectedOrdered: [1e6, 1e6, 5e6], + expectedSome: { + count: 1, + items: [4e6, 7e6] + } + }, + { + name: 'select coins + existing inputs (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'age' + }, + value: 10e6, + existingInputs: [5e6], + expectedOrdered: [5e6, 2e6, 2e6, 1e6] + }, + // existing coins (default) + { + name: 'select coins + existing inputs (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: 10e6, + existingInputs: [3e6], + expectedOrdered: [3e6, 2e6, 2e6, 8e6] + }, + // existing coins (alt) + { + name: 'select coins + existing coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: 10e6, + existingInputs: [4e6], + expectedOrdered: [4e6, 1e6, 5e6] + }, + // fail existing inputs (cross account) + { + name: 'fail cross account existing inputs (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'age' + }, + value: 10e6, + existingInputs: [5e6], // this belongs to alt account + error: { + message: 'Could not resolve preferred inputs.' + } + } + ], + 'all': [ + // wallet by all + { + name: 'select all coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'all' + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 9, + items: [ + 2e6, 2e6, 1e6, 8e6, 5e6, + 3e6, 6e6, 4e6, 7e6 + ] + } + }, + { + name: 'select all coins + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'all', + smart: true + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 7, + items: [ + 2e6, 2e6, 1e6, 8e6, 5e6, + 3e6, 4e6 + ] + } + }, + { + name: 'select all coins + depth = 0 (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'all', + depth: 0 + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 9, + items: [ + 2e6, 2e6, 1e6, 8e6, 5e6, + 3e6, 6e6, 4e6, 7e6 + ] + } + }, + { + name: 'select all coins + depth = 1 (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'all', + depth: 1 + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 5, + items: [ + 2e6, 2e6, 1e6, 8e6, 5e6 + ] + } + }, + { + name: 'select all coins + depth = 3 (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'all', + depth: 3 + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 4, + items: [ + 2e6, 2e6, 1e6, 8e6 + ] + } + }, + + // wallet by default + { + name: 'select all coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'all' + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 5, + items: [ + 2e6, 2e6, 8e6, + 3e6, 6e6 + ] + } + }, + { + name: 'select all coins + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'all', + smart: true + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 4, + items: [ + 2e6, 2e6, 8e6, + 3e6 + ] + } + }, + { + name: 'select all coins + depth = 0 (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'all', + depth: 0 + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 5, + items: [ + 2e6, 2e6, 8e6, + 3e6, 6e6 + ] + } + }, + { + name: 'select all coins + depth = 1 (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'all', + depth: 1 + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 3, + items: [ + 2e6, 2e6, 8e6 + ] + } + }, + { + name: 'select all coins + depth = 4 (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'all', + depth: 4 + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 2, + items: [ + 2e6, 2e6 + ] + } + }, + + // wallet by alt + { + name: 'select all coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'all' + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 4, + items: [ + 1e6, 5e6, + 4e6, 7e6 + ] + } + }, + { + name: 'select all coins + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'all', + smart: true + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 3, + items: [ + 1e6, 5e6, + 4e6 + ] + } + }, + { + name: 'select all coins + depth = 0 (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'all', + depth: 0 + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 4, + items: [ + 1e6, 5e6, + 4e6, 7e6 + ] + } + }, + { + name: 'select all coins + depth = 1 (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'all', + depth: 1 + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 2, + items: [ + 1e6, 5e6 + ] + } + }, + { + name: 'select all coins + depth = 4 (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'all', + depth: 4 + }, + value: 1e6, // should select all regardless. + expectedOrdered: [], + expectedSome: { + count: 1, + items: [ + 1e6 + ] + } + } + ], + // Existing coins = views + inputs + // Existing inputs = inputs (no view, needs extra resolving) + 'all + existing inputs': [ + { + name: 'select all + existing coin (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'all' + }, + value: 2e6, + existingCoins: [ + { + height: -1, + value: 1e6 + } + ], + expectedOrdered: [1e6], + expectedSome: { + count: 9, + items: [ + 2e6, 2e6, 1e6, 8e6, 5e6, + 3e6, 6e6, 4e6, 7e6 + ] + } + }, + { + name: 'select all + existing coin (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'all' + }, + value: 2e6, + existingCoins: [ + { + height: -1, + value: 1e6 + } + ], + expectedOrdered: [1e6], + expectedSome: { + count: 5, + items: [ + 2e6, 2e6, 8e6, + 3e6, 6e6 + ] + } + }, + { + name: 'select all + existing coin (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'all' + }, + value: 2e6, + existingCoins: [ + { + height: -1, + value: 3e6 + } + ], + expectedOrdered: [3e6], + expectedSome: { + count: 4, + items: [ + 1e6, 5e6, + 4e6, 7e6 + ] + } + }, + { + name: 'select all + existing input (wallet)', + options: { + account: -1, + hardFee: 0, + selection: 'all' + }, + value: 2e6, + existingInputs: [8e6], + expectedOrdered: [8e6], + expectedSome: { + count: 8, + items: [ + 2e6, 2e6, 1e6, 5e6, + 3e6, 6e6, 4e6, 7e6 + ] + } + }, + { + name: 'select all + existing input (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'all' + }, + value: 2e6, + existingInputs: [8e6], + expectedOrdered: [8e6], + expectedSome: { + count: 4, + items: [ + 2e6, 2e6, + 3e6, 6e6 + ] + } + }, + { + name: 'select all + existing input (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: 'all' + }, + value: 2e6, + existingInputs: [5e6], + expectedOrdered: [5e6], + expectedSome: { + count: 3, + items: [1e6, 4e6, 7e6] + } + }, + { + name: 'select all + existing input + estimate (wallet)', + options: { + account: -1, + selection: 'all', + rate: 5e7 + }, + value: 2e6, + existingInputs: [8e6], + expectedOrdered: [8e6], + expectedSome: { + count: 8, + items: [ + 2e6, 2e6, 1e6, 5e6, + 3e6, 6e6, 4e6, 7e6 + ] + } + }, + { + name: 'fail cross account existing inputs (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: 'all' + }, + value: 2e6, + existingInputs: [5e6], // this belongs to alt account + error: { + message: 'Could not resolve preferred inputs.' + } + } + ], + 'sweepdust': [ + // wallet by sweep + { + name: 'select 1 coin (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 1e6, + expectedOrdered: [1e6] + }, + { + name: 'select 1 coin, minvalue (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 1e6 + 1 + }, + value: 1e6, + expectedOrdered: [2e6] + }, + { + name: 'select all confirmed coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED, + expectedOrdered: [1e6, 2e6, 2e6, 5e6, 8e6] + }, + { + name: 'select all confirmed coins, minvalue (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 1e6 + 1 + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED - 1e6, + expectedOrdered: [2e6, 2e6, 5e6, 8e6] + }, + { + name: 'select all confirmed and an unconfirmed (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [1e6, 2e6, 2e6, 5e6, 8e6, 3e6] + }, + { + name: 'select all confirmed and an unconfirmed, minvalue (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1 + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED - 5e6 + 1e6, + expectedOrdered: [5e6, 8e6, 3e6] + }, + { + name: 'select all coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS, + expectedOrdered: [1e6, 2e6, 2e6, 5e6, 8e6, 3e6, 4e6, 6e6, 7e6] + }, + { + name: 'select all coins, minvalue (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1 + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS - 5e6, + expectedOrdered: [5e6, 8e6, 3e6, 4e6, 6e6, 7e6] + }, + { + // test locked filters. + name: 'throw funding error (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS + 1e6, + error: { + availableFunds: ACCT_0_FUNDS + ACCT_1_FUNDS, + requiredFunds: ACCT_0_FUNDS + ACCT_1_FUNDS + 1e6, + type: 'FundingError' + } + }, + { + // test locked filters. + name: 'throw funding error, filterall (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 100e6 + }, + value: 1e6, + error: { + availableFunds: 0, + requiredFunds: 1e6, + type: 'FundingError' + } + }, + + // default account by value + { + name: 'select 1 coin (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 1e6, + expectedOrdered: [2e6] + }, + { + name: 'select 1 coin, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1 + }, + value: 2e6, + expectedOrdered: [8e6] + }, + { + name: 'select all confirmed coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_0_CONFIRMED, + expectedOrdered: [2e6, 2e6, 8e6] + }, + { + name: 'select all confirmed coins, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1 + }, + value: 8e6, + expectedOrdered: [8e6] + }, + { + name: 'select all confirmed and an unconfirmed (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_0_CONFIRMED + 1e6, + expectedOrdered: [2e6, 2e6, 8e6, 3e6] + }, + { + name: 'select all confirmed and an unconfirmed, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 3e6 + 1 + }, + value: 8e6 + 1e6, + expectedOrdered: [8e6, 6e6] + }, + { + name: 'select all coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_0_FUNDS, + expectedOrdered: [2e6, 2e6, 8e6, 3e6, 6e6] + }, + { + name: 'select all coins, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 3e6 + 1 + }, + value: 8e6 + 6e6, + expectedOrdered: [8e6, 6e6] + }, + { + // test locked filters. + name: 'throw funding error (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_0_FUNDS + 1e6, + error: { + availableFunds: ACCT_0_FUNDS, + requiredFunds: ACCT_0_FUNDS + 1e6, + type: 'FundingError' + } + }, + { + // test locked filters. + name: 'throw funding error, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1 + }, + value: ACCT_0_FUNDS + 1e6 - (4e6), + error: { + availableFunds: ACCT_0_FUNDS - 4e6, + requiredFunds: ACCT_0_FUNDS - 4e6 + 1e6, + type: 'FundingError' + } + }, + + // alt account by value + { + name: 'select 1 coin (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 1e6, + expectedOrdered: [1e6] + }, + { + name: 'select 1 coin, minvalue (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 1e6 + 1 + }, + value: 1e6, + expectedOrdered: [5e6] + }, + { + name: 'select all confirmed coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_1_CONFIRMED, + expectedOrdered: [1e6, 5e6] + }, + { + name: 'select all confirmed coins, minvalue (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 1e6 + 1 + }, + value: ACCT_1_CONFIRMED - 1e6, + expectedOrdered: [5e6] + }, + { + name: 'select all confirmed and an unconfirmed (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [1e6, 5e6, 4e6] + }, + { + name: 'select all confirmed and an unconfirmed (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 4e6 + 1 + }, + value: ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [5e6, 7e6] + }, + { + name: 'select all coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_1_FUNDS, + expectedOrdered: [1e6, 5e6, 4e6, 7e6] + }, + { + name: 'select all coins, minvalue (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 4e6 + 1 + }, + value: ACCT_1_FUNDS - 5e6, + expectedOrdered: [5e6, 7e6] + }, + { + // test locked filters. + name: 'throw funding error (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: ACCT_1_FUNDS + 1e6, + error: { + availableFunds: ACCT_1_FUNDS, + requiredFunds: ACCT_1_FUNDS + 1e6, + type: 'FundingError' + } + }, + { + // test locked filters. + name: 'throw funding error (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 4e6 + 1 + }, + value: ACCT_1_FUNDS + 1e6, + error: { + availableFunds: ACCT_1_FUNDS - 5e6, + requiredFunds: ACCT_1_FUNDS + 1e6, + type: 'FundingError' + } + } + ], + 'sweepdust + smart': [ + // Test smart option. + // smart selection (wallet) + { + name: 'select all confirmed and an unconfirmed + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + smart: true + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [1e6, 2e6, 2e6, 5e6, 8e6, 3e6] + }, + { + name: 'select all confirmed and an unconfirmed + smart, minvalue (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 3e6 + 1, + smart: true + }, + value: ACCT_0_CONFIRMED + ACCT_1_CONFIRMED + 1e6 - 5e6, + expectedOrdered: [5e6, 8e6, 4e6] + }, + { + name: 'select all coins + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + smart: true + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS - ACCT_0_FOREIGN - ACCT_1_FOREIGN, + expectedOrdered: [1e6, 2e6, 2e6, 5e6, 8e6, 3e6, 4e6] + }, + { + name: 'select all coins + smart, minvalue (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 3e6 + 1, + smart: true + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS - ACCT_0_FOREIGN - ACCT_1_FOREIGN - 5e6 - 3e6, + expectedOrdered: [5e6, 8e6, 4e6] + }, + { + name: 'throw funding error + smart (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + smart: true + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS, + error: { + availableFunds: ACCT_0_FUNDS + ACCT_1_FUNDS - ACCT_0_FOREIGN - ACCT_1_FOREIGN, + requiredFunds: ACCT_0_FUNDS + ACCT_1_FUNDS, + type: 'FundingError' + } + }, + { + name: 'throw funding error + smart, minvalue (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 3e6 + 1, + smart: true + }, + value: ACCT_0_FUNDS + ACCT_1_FUNDS, + error: { + availableFunds: ACCT_0_FUNDS + ACCT_1_FUNDS - ACCT_0_FOREIGN - ACCT_1_FOREIGN - 5e6 - 3e6, + requiredFunds: ACCT_0_FUNDS + ACCT_1_FUNDS, + type: 'FundingError' + } + }, + // smart selection (default) + { + name: 'select all confirmed and an unconfirmed + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + smart: true + }, + value: ACCT_0_CONFIRMED + 1e6, + expectedOrdered: [2e6, 2e6, 8e6, 3e6] + }, + { + name: 'select all confirmed and an unconfirmed + smart, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1, + smart: true + }, + value: ACCT_0_CONFIRMED + 1e6 - 4e6, + expectedOrdered: [8e6, 3e6] + }, + { + name: 'select all coins + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + smart: true + }, + value: ACCT_0_FUNDS - ACCT_0_FOREIGN, + expectedOrdered: [2e6, 2e6, 8e6, 3e6] + }, + { + name: 'select all coins + smart, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1, + smart: true + }, + value: ACCT_0_FUNDS - ACCT_0_FOREIGN - 4e6, + expectedOrdered: [8e6, 3e6] + }, + { + name: 'throw funding error + smart (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + smart: true + }, + value: ACCT_0_FUNDS, + error: { + availableFunds: ACCT_0_FUNDS - ACCT_0_FOREIGN, + requiredFunds: ACCT_0_FUNDS, + type: 'FundingError' + } + }, + { + name: 'throw funding error + smart, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1, + smart: true + }, + value: ACCT_0_FUNDS, + error: { + availableFunds: ACCT_0_FUNDS - ACCT_0_FOREIGN - 4e6, + requiredFunds: ACCT_0_FUNDS, + type: 'FundingError' + } + }, + // smart selection (alt) + { + name: 'select all confirmed and an unconfirmed + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + smart: true + }, + value: ACCT_1_CONFIRMED + 1e6, + expectedOrdered: [1e6, 5e6, 4e6] + }, + { + name: 'select all confirmed and an unconfirmed + smart, minvalue (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 1e6 + 1, + smart: true + }, + value: ACCT_1_CONFIRMED + 1e6 - 1e6, + expectedOrdered: [5e6, 4e6] + }, + { + name: 'select all coins + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + smart: true + }, + value: ACCT_1_FUNDS - ACCT_1_FOREIGN, + expectedOrdered: [1e6, 5e6, 4e6] + }, + { + name: 'select all coins + smart, minvalue (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 1e6 + 1, + smart: true + }, + value: ACCT_1_FUNDS - ACCT_1_FOREIGN - 1e6, + expectedOrdered: [5e6, 4e6] + }, + { + name: 'throw funding error + smart (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + smart: true + }, + value: ACCT_1_FUNDS, + error: { + availableFunds: ACCT_1_FUNDS - ACCT_1_FOREIGN, + requiredFunds: ACCT_1_FUNDS, + type: 'FundingError' + } + }, + { + name: 'throw funding error + smart, minvalue (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 1e6 + 1, + smart: true + }, + value: ACCT_1_FUNDS, + error: { + availableFunds: ACCT_1_FUNDS - ACCT_1_FOREIGN - 1e6, + requiredFunds: ACCT_1_FUNDS, + type: 'FundingError' + } + } + ], + // Existing coins = views + inputs + // Existing inputs = inputs (no view, needs extra resolving) + 'sweepdust + existing coins and inputs': [ + // existing coins (wallet) + { + name: 'select coins + existing coins (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 8e6 + } + ], + expectedOrdered: [8e6, 1e6, 2e6] + }, + { + name: 'select coins + existing coins, minvalue (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1 + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 8e6 + } + ], + expectedOrdered: [8e6, 5e6] + }, + // existing coins (default) + { + name: 'select coins + existing coins (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 7e6 + } + ], + expectedOrdered: [7e6, 2e6, 2e6] + }, + { + name: 'select coins + existing coins, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1 + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 7e6 + } + ], + expectedOrdered: [7e6, 8e6] + }, + // existing coins (alt) + { + name: 'select coins + existing coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 10e6, + existingCoins: [ + { + height: -1, + value: 1e6 + } + ], + expectedOrdered: [1e6, 1e6, 5e6, 4e6] + }, + { + name: 'select coins + existing inputs (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 10e6, + existingInputs: [5e6], + expectedOrdered: [5e6, 1e6, 2e6, 2e6] + }, + { + name: 'select coins + existing inputs, minvalue (wallet)', + options: { + account: -1, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1 + }, + value: 10e6, + existingInputs: [5e6], + expectedOrdered: [5e6, 8e6] + }, + // existing coins (default) + { + name: 'select coins + existing inputs (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 10e6, + existingInputs: [3e6], + expectedOrdered: [3e6, 2e6, 2e6, 8e6] + }, + { + name: 'select coins + existing inputs, minvalue (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 2e6 + 1 + }, + value: 10e6, + existingInputs: [2e6, 3e6], + expectedOrdered: [2e6, 3e6, 8e6] + }, + // existing coins (alt) + { + name: 'select coins + existing coins (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 10e6, + existingInputs: [4e6], + expectedOrdered: [4e6, 1e6, 5e6] + }, + { + name: 'select coins + existing coins, minvalue (alt)', + options: { + account: ALT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST, + sweepdustMinValue: 1e6 + 1 + }, + value: 9e6, + existingInputs: [4e6], + expectedOrdered: [4e6, 5e6] + }, + // fail existing inputs (cross account) + { + name: 'fail cross account existing inputs (default)', + options: { + account: DEFAULT_ACCOUNT, + hardFee: 0, + selection: DB_SWEEPDUST + }, + value: 10e6, + existingInputs: [5e6], // this belongs to alt account + error: { + message: 'Could not resolve preferred inputs.' + } + } + ] + }; + + 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'], DB_VALUE); + SELECTION_TESTS['dbvalue + smart'] = reselect(SELECTION_TESTS['value + smart'], DB_VALUE); + SELECTION_TESTS['dbvalue + existing coins and inputs'] = reselect( + SELECTION_TESTS['value + existing coins and inputs'], DB_VALUE); + + // Same with `age` and `dbage`. + SELECTION_TESTS['db-age'] = reselect(SELECTION_TESTS['age'], DB_AGE); + SELECTION_TESTS['db-age + smart'] = reselect(SELECTION_TESTS['age + smart'], DB_AGE); + SELECTION_TESTS['db-age + existing inputs'] = reselect( + SELECTION_TESTS['age + existing inputs'], DB_AGE); + + SELECTION_TESTS['db-all'] = reselect(SELECTION_TESTS['all'], DB_ALL); + SELECTION_TESTS['db-all + existing inputs'] = reselect( + SELECTION_TESTS['all + existing inputs'], DB_ALL); + + for (const [name, testCase] of Object.entries(SELECTION_TESTS)) { + describe(`Wallet Coin Selection by ${name}`, function() { + // fund wallet. + const valueByCoin = new BufferMap(); + // This is used for OWN and LOCK descriptions. + // The values must be unique in the UTXO set. + const coinByValue = new Map(); + + /** + * Fund the same coin in multiple different ways. + * @param {OutputInfo} output + * @returns {OutputInfo[]} + */ + + const fundCoinOptions = (output) => { + const spendables = [ + Covenant.types.NONE, + Covenant.types.OPEN, + Covenant.types.REDEEM + ]; + + const nonSpendables = [ + Covenant.types.BID, + Covenant.types.REVEAL, + Covenant.types.REGISTER, + Covenant.types.UPDATE, + Covenant.types.RENEW, + Covenant.types.TRANSFER, + Covenant.types.FINALIZE, + Covenant.types.REVOKE + ]; + + const account = output.account || 0; + const value = output.value; + const oneSpendable = spendables[Math.floor(Math.random() * spendables.length)]; + + return [{ value, account, covenant: { type: oneSpendable }}] + .concat(nonSpendables.map(t => ({ value, account, covenant: { type: t }}))); + }; + + // NOTE: tests themselves don't modify the wallet state, so before instead + // of beforeEach should be fine. before(async () => { - await wdb.open(); - wdb.height = network.txStart + 1; - wdb.state.height = wdb.height; + await beforeFn(); - const dummy = dummyBlock(network.txStart + 1); - const record = BlockMeta.fromEntry(dummy); - await wdb.setTip(record); - wallet = wdb.primary; + valueByCoin.clear(); + coinByValue.clear(); + + for (const coinOptions of PER_BLOCK_COINS) { + const outputInfos = fundCoinOptions(coinOptions); + const txs = await fundWallet(wallet, outputInfos, { + txPerOutput: true + }); + + for (const [i, tx] of txs.entries()) { + if (tx.outputs.length !== 1) + continue; + + if (tx.output(0).isUnspendable() || tx.output(0).covenant.isNonspendable()) + continue; + + const coin = Coin.fromTX(tx, 0, i + 1); + valueByCoin.set(coin.toKey(), tx.output(0).value); + coinByValue.set(tx.output(0).value, coin); + } + } + + for (const coinOptions of UNCONFIRMED_COINS) { + const options = fundCoinOptions(coinOptions); + const txs = await createInboundTXs(wallet, options); + + for (const tx of txs) { + await wallet.wdb.addTX(tx); + + if (tx.outputs.length !== 1) + continue; + + if (tx.output(0).isUnspendable() || tx.output(0).covenant.isNonspendable()) + continue; + + const coin = Coin.fromTX(tx, 0, -1); + valueByCoin.set(coin.toKey(), tx.output(0).value); + coinByValue.set(tx.output(0).value, coin); + } + } + + for (const value of LOCK) { + const coin = coinByValue.get(value); + wallet.lockCoin(coin); + } + + for (const {account, value} of OWN) { + const coin = coinByValue.get(value); + const mtx = new MTX(); + mtx.addOutput(await wallet.receiveAddress(account), value); + mtx.addCoin(coin); + await wallet.finalize(mtx); + await wallet.sign(mtx); + const tx = mtx.toTX(); + await wdb.addTX(tx); + + valueByCoin.delete(coin.toKey()); + coinByValue.delete(coin.value); + + const ownedCoin = Coin.fromTX(mtx, 0, -1); + valueByCoin.set(ownedCoin.toKey(), mtx.output(0).value); + coinByValue.set(mtx.output(0).value, ownedCoin); + } }); - after(async () => { - await wdb.close(); + after(afterFn); + + for (const fundingTest of testCase) { + it(`should ${fundingTest.name}`, async () => { + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), fundingTest.value); + + if (fundingTest.existingInputs) { + for (const inputVal of fundingTest.existingInputs) { + const coin = coinByValue.get(inputVal); + assert(coin, `Coin not found for value ${inputVal}.`); + + const input = Input.fromCoin(coin); + mtx.addInput(input); + } + } + + if (fundingTest.existingCoins) { + for (const coinOptions of fundingTest.existingCoins) { + const coin = primutils.makeCoin(coinOptions); + valueByCoin.set(coin.toKey(), coin.value); + mtx.addCoin(coin); + } + } + + let err; + + try { + await wallet.fund(mtx, fundingTest.options); + } catch (e) { + err = e; + } + + if (fundingTest.error) { + assert(err); + assert.strictEqual(err.type, fundingTest.error.type); + assert.strictEqual(err.availableFunds, fundingTest.error.availableFunds); + assert.strictEqual(err.requiredFunds, fundingTest.error.requiredFunds); + + if (fundingTest.error.message) + assert.strictEqual(err.message, fundingTest.error.message); + return; + } + + assert(!err, err); + + const inputVals = mtx.inputs.map(({prevout}) => valueByCoin.get(prevout.toKey())); + + assert(inputVals.length >= fundingTest.expectedOrdered.length, + 'Not enough inputs selected.'); + + assert.deepStrictEqual( + inputVals.slice(0, fundingTest.expectedOrdered.length), + fundingTest.expectedOrdered); + + const left = inputVals.slice(fundingTest.expectedOrdered.length); + + if (!fundingTest.expectedSome) { + assert(left.length === 0, 'Extra inputs selected.'); + return; + } + + let count = fundingTest.expectedSome.count; + const items = fundingTest.expectedSome.items.slice(); + + for (const value of left) { + assert(items.includes(value), `Value ${value} not in expected.`); + assert(count > 0, 'Too many inputs selected.'); + + const idx = items.indexOf(value); + items.splice(idx, 1); + count--; + } + + assert(count === 0, 'Not enough inputs selected.'); + }); + } + }); + } + + describe('Selection types', function() { + beforeEach(beforeFn); + afterEach(afterFn); + + it('should select all spendable coins', async () => { + const spendableCovs = [ + Covenant.types.NONE, + Covenant.types.OPEN, + Covenant.types.REDEEM + ]; + + const nonSpendableCovs = [ + Covenant.types.BID, + Covenant.types.REVEAL, + Covenant.types.REGISTER, + Covenant.types.UPDATE, + Covenant.types.RENEW, + Covenant.types.TRANSFER, + Covenant.types.FINALIZE, + Covenant.types.REVOKE + ]; + + const mkopt = type => ({ value: 1e6, covenant: { type }}); + await fundWallet(wallet, [...nonSpendableCovs, ...spendableCovs].map(mkopt)); + + const coins = await wallet.getCoins(); + assert.strictEqual(coins.length, spendableCovs.length + nonSpendableCovs.length); + + const spendables = await collectIter(wallet.getAccountCreditIterByValue(0)); + assert.strictEqual(spendables.length, spendableCovs.length); + + const mtx = new MTX(); + await wallet.fund(mtx, { + selection: 'all' + }); + + assert.strictEqual(mtx.inputs.length, spendableCovs.length); }); + it('should select coin by descending value', async () => { + const values = [1e6, 4e6, 3e6, 5e6, 2e6]; + await fundWallet(wallet, values.map(value => ({ value }))); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 9e6); + + await wallet.fund(mtx, { + selection: 'value', + hardFee: 0 + }); + + // 4 + 5 + assert.strictEqual(mtx.inputs.length, 2); + assert.strictEqual(mtx.outputs.length, 1); + assert.strictEqual(mtx.outputs[0].value, 9e6); + }); + + it('should select coins by descending age', async () => { + const values = [1e6, 2e6, 3e6, 4e6, 5e6]; + + for (const value of values) + await fundWallet(wallet, [{ value }]); + + const mtx = new MTX(); + mtx.addOutput(randomP2PKAddress(), 9e6); + await wallet.fund(mtx, { + selection: 'age', + hardFee: 0 + }); + + // 1 + 2 + 3 + 4 = 10 + assert.strictEqual(mtx.inputs.length, 4); + assert.strictEqual(mtx.outputs.length, 2); + assert.strictEqual(mtx.outputs[0].value, 9e6); + assert.strictEqual(mtx.outputs[1].value, 1e6); + }); + }); + + describe('Fees', function() { + before(beforeFn); + after(afterFn); + it('should fund wallet', async () => { - await fundWallet(wallet, [100e6, 10e6, 1e6, 100000, 10000]); + const vals = [100e6, 10e6, 1e6, 0.1e6, 0.01e6]; + await fundWallet(wallet, vals.map(value => ({ value }))); const bal = await wallet.getBalance(); - assert.strictEqual(bal.confirmed, 111110000); + assert.strictEqual(bal.confirmed, 111.11e6); }); it('should pay default fee rate for small tx', async () => { @@ -121,16 +3105,22 @@ describe('Wallet Coin Selection', function () { it('should fail to pay absurd fee rate for small tx', async () => { const address = await wallet.receiveAddress(); - await assert.rejects( - wallet.send({ + let err; + + try { + await wallet.send({ outputs: [{ address, value: 5e6 }], rate: (policy.ABSURD_FEE_FACTOR + 1) * network.minRelay - }), - {message: 'Fee exceeds absurd limit.'} - ); + }); + } catch (e) { + err = e; + } + + assert(err, 'Error not thrown.'); + assert.strictEqual(err.message, 'Fee exceeds absurd limit.'); }); it('should pay fee just under the absurd limit', async () => { @@ -177,3 +3167,111 @@ describe('Wallet Coin Selection', function () { }); }); }); + +/** + * Collect iterator items. + * @template T + * @param {AsyncGenerator} iter + * @returns {Promise} + */ + +async function collectIter(iter) { + const items = []; + + for await (const item of iter) + items.push(item); + + return items; +} + +/** + * @param {Credit[]} credits + * @returns {Boolean} + */ + +function isSortedByValueAsc(credits) { + for (let i = 1; i < credits.length; i++) { + const prev = credits[i - 1].coin; + const cur = credits[i].coin; + + if (prev.height === -1 && cur.height !== -1) + return false; + + if (prev.height !== -1 && cur.height === -1) + continue; + + if (prev.value > cur.value) + return false; + } + + return true; +} + +/** + * @param {Credit[]} credits + * @returns {Boolean} + */ + +function isSortedByValueDesc(credits) { + for (let i = 1; i < credits.length; i++) { + const prev = credits[i - 1].coin; + const cur = credits[i].coin; + + if (prev.height === -1 && cur.height !== -1) + return false; + + if (prev.height !== -1 && cur.height === -1) + continue; + + if (prev.value < cur.value) + return false; + } + + return true; +} + +/** + * @param {Credit[]} credits + * @returns {Boolean} + */ + +function isSortedByHeightAsc(credits) { + for (let i = 1; i < credits.length; i++) { + let prevHeight = credits[i - 1].coin.height; + let curHeight = credits[i].coin.height; + + if (prevHeight === -1) + prevHeight = UNCONFIRMED_HEIGHT; + + if (curHeight === -1) + curHeight = UNCONFIRMED_HEIGHT; + + if (prevHeight > curHeight) + return false; + } + + return true; +} + +/** + * @param {Credit[]} credits + * @returns {Boolean} + */ + +function isSortedByHeightDesc(credits) { + for (let i = 1; i < credits.length; i++) { + let prevHeight = credits[i - 1].coin.height; + let curHeight = credits[i].coin.height; + + if (prevHeight === -1) + prevHeight = UNCONFIRMED_HEIGHT; + + if (curHeight === -1) + curHeight = UNCONFIRMED_HEIGHT; + + if (prevHeight < curHeight) + return false; + } + + return true; +} diff --git a/test/wallet-migration-test.js b/test/wallet-migration-test.js index 6549d0f4..a6973428 100644 --- a/test/wallet-migration-test.js +++ b/test/wallet-migration-test.js @@ -25,6 +25,7 @@ const { getVersion, checkVersion, checkEntries, + checkExactEntries, fillEntries } = migutils; const {rimraf, testdir} = require('./util/common'); @@ -957,7 +958,6 @@ describe('Wallet Migrations', function() { describe(`TX Count and time indexing migration (integration ${i})`, function() { const location = testdir('wallet-tx-count-time'); const migrationsBAK = WalletMigrator.migrations; - // const data = require('./data/migrations/wallet-5-pagination.json'); const Migration = WalletMigrator.MigrateTXCountTimeIndex; const layout = Migration.layout(); @@ -1107,4 +1107,144 @@ describe('Wallet Migrations', function() { await walletDB.close(); }); }); + + describe('Migrate coin selection (data)', function() { + const location = testdir('wallet-migrate-coin-selection'); + const data = require('./data/migrations/wallet-7-coinselector.json'); + const migrationsBAK = WalletMigrator.migrations; + const Migration = WalletMigrator.MigrateCoinSelection; + + const walletOptions = { + prefix: location, + memory: false, + network + }; + + let walletDB, ldb; + beforeEach(async () => { + WalletMigrator.migrations = {}; + await fs.mkdirp(location); + + walletDB = new WalletDB(walletOptions); + ldb = walletDB.db; + + await walletDB.open(); + await fillEntries(walletDB.db, data.before); + await walletDB.close(); + }); + + afterEach(async () => { + WalletMigrator.migrations = migrationsBAK; + await rimraf(location); + }); + + it('should migrate', async () => { + WalletMigrator.migrations = { + 0: Migration + }; + + walletDB.options.walletMigrate = 0; + + await walletDB.open(); + + // Check that we have removed and added + // the expected entries. + await checkEntries(ldb, { + before: data.before, + after: data.after, + throw: true + }); + + // check that we have not created extra entries in the db + // that is not present in the data dump. + await checkExactEntries(ldb, data.prefixes, { + after: data.after, + throw: true + }); + + await walletDB.close(); + }); + + it('should resume the progress of migration if interrupted', async () => { + // patch the db buckets to throw after each write. + const patchDB = () => { + // throw after each bucket write. + const ldbBucket = walletDB.db.bucket; + + walletDB.db.bucket = (prefix) => { + const bucket = ldbBucket.call(ldb, prefix); + const bucketBatch = bucket.batch; + + bucket.batch = () => { + const batch = bucketBatch.call(bucket); + const originalWrite = batch.write; + + batch.write = async () => { + await originalWrite.call(batch); + throw new Error('Interrupt migration'); + }; + + return batch; + }; + + return bucket; + }; + + return () => { + walletDB.db.bucket = ldbBucket; + }; + }; + + WalletMigrator.migrations = { + 0: class extends Migration { + constructor(options) { + super(options); + + this.batchSize = 10; + } + + async migrate(b, ctx) { + const unpatch = patchDB(); + await super.migrate(b, ctx); + unpatch(); + } + } + }; + + await walletDB.db.open(); + + const migrator = new WalletMigrator({ + walletMigrate: 0, + walletDB: walletDB, + dbVersion: 5 + }); + + let err; + + do { + try { + await migrator.migrate(); + err = null; + } catch (e) { + if (e.message !== 'Interrupt migration') + throw e; + + err = e; + } + } while (err); + + await checkEntries(ldb, { + before: data.before, + after: data.after, + throw: true + }); + + await checkExactEntries(ldb, data.prefixes, { + after: data.after, + throw: true + }); + + await walletDB.db.close(); + }); + }); }); diff --git a/test/wallet-pagination-test.js b/test/wallet-pagination-test.js index c2898717..2e577086 100644 --- a/test/wallet-pagination-test.js +++ b/test/wallet-pagination-test.js @@ -7,10 +7,8 @@ const WalletDB = require('../lib/wallet/walletdb'); const consensus = require('../lib/protocol/consensus'); const util = require('../lib/utils/util'); const wutils = require('./util/wallet'); -const { - dummyInput, - nextEntry -} = wutils; +const {nextEntry} = wutils; +const {dummyInput} = require('./util/primitives'); /** @typedef {import('../lib/wallet/wallet')} Wallet */ diff --git a/test/wallet-test.js b/test/wallet-test.js index e1dab594..82ecb51a 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -28,12 +28,12 @@ const wutils = require('./util/wallet'); const {ownership} = require('../lib/covenants/ownership'); const {CachedStubResolver, STUB_SERVERS} = require('./util/stub'); const { - dummyInput, curBlock, nextBlock, curEntry, nextEntry } = wutils; +const {dummyInput} = require('./util/primitives'); const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ'; @@ -401,7 +401,7 @@ describe('Wallet', function() { assert.strictEqual(balanceBefore.tx, 2); assert.strictEqual(balanceBefore.coin, 2); - await wdb.removeBlock(block, [cbTX.toTX(), normalTX.toTX()]); + await wdb.removeBlock(block); const pending = await wallet.getPending(); assert.strictEqual(pending.length, 1); @@ -2224,23 +2224,16 @@ describe('Wallet', function() { // Store balance data before rescan to ensure rescan was complete let recipBalBefore, senderBalBefore; - // Hack required to focus test on txdb mechanics. - // We don't otherwise need WalletDB or Blockchain - // TODO: Remove this after #888 is merged. - wdb.getRenewalBlock = () => { - return network.genesis.hash; - }; - before(async () => { await wdb.open(); await wdb.connect(); wallet = await wdb.create(); recip = await wdb.create(); - // rollout all names - wdb.height = 52 * 144 * 7; + network.names.noRollout = true; }); after(async () => { + network.names.noRollout = false; await wdb.disconnect(); await wdb.close(); }); @@ -2610,21 +2603,15 @@ describe('Wallet', function() { let start; let wallet; - // Hack required to focus test on txdb mechanics. - // We don't otherwise need WalletDB or Blockchain - // TODO: Remove this after #888 is merged. - wdb.getRenewalBlock = () => { - return network.genesis.hash; - }; - before(async () => { await wdb.open(); wallet = await wdb.create(); // rollout all names - wdb.height = 52 * 144 * 7; + network.names.noRollout = true; }); after(async () => { + network.names.noRollout = false; await wdb.close(); }); @@ -3290,24 +3277,19 @@ 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; - // Hack required to focus test on txdb mechanics. - // We don't otherwise need WalletDB or Blockchain - // TODO: Remove this after #888 is merged. - wdb.getRenewalBlock = () => { - return network.genesis.hash; - }; - before(async () => { await wdb.open(); wallet = await wdb.create(); // rollout all names - wdb.height = 52 * 144 * 7; + network.names.noRollout = true; }); after(async () => { + network.names.noRollout = false; await wdb.close(); }); @@ -3748,13 +3730,6 @@ describe('Wallet', function() { const network = Network.get('regtest'); const wdb = new WalletDB({ network }); - // Hack required to focus test on txdb mechanics. - // We don't otherwise need WalletDB or Blockchain - // TODO: Remove this after #888 is merged. - wdb.getRenewalBlock = () => { - return network.genesis.hash; - }; - const mineBlocks = async (count) => { for (let i = 0; i < count; i++) { await wdb.addBlock(nextEntry(wdb), []); diff --git a/test/wallet-unit-test.js b/test/wallet-unit-test.js index 1e6a5530..8bb9c2a5 100644 --- a/test/wallet-unit-test.js +++ b/test/wallet-unit-test.js @@ -13,7 +13,8 @@ const WalletDB = require('../lib/wallet/walletdb'); const Wallet = require('../lib/wallet/wallet'); const Account = require('../lib/wallet/account'); const wutils = require('./util/wallet'); -const {nextEntry, fakeEntry} = require('./util/wallet'); +const {nextEntry, fakeEntry} = wutils; +const {dummyInput} = require('./util/primitives'); const MemWallet = require('./util/memwallet'); /** @typedef {import('../lib/primitives/tx')} TX */ @@ -541,7 +542,7 @@ describe('Wallet Unit Tests', () => { function fakeTX(addr) { const tx = new MTX(); - tx.addInput(wutils.dummyInput()); + tx.addInput(dummyInput()); tx.addOutput({ address: addr, value: 5460