Merge PR #928 from 'nodech/wallet-coinselection'

This commit is contained in:
Nodari Chkuaselidze 2025-06-18 12:26:02 +04:00
commit f0a81dac67
No known key found for this signature in database
GPG key ID: B018A7BB437D1F05
33 changed files with 7791 additions and 726 deletions

View file

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

View file

@ -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=<number>]
* [--spendable=<number>] [--opens=<number>]
* [--per-block=<number>] [--cleanup]
* [--ops-per-type=<number>] [--skip-init]
* [--output=<file>] [--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<String, BenchmarkEntry[]> */
this.benchmarksPerType = new Map();
/** @type Map<String, BenchmarkResults> */
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<TX[]>}
*/
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<TX[]>}
*/
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<TX[]>}
*/
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<String[]>}
*/
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;
}

View file

@ -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({

View file

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

View file

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

View file

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

View file

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

View file

@ -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<CoinSelector>}
* @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<CoinSelector>}
*/
@ -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<CoinSelector>}
*/
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;

711
lib/utils/coinselector.js Normal file
View file

@ -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<Coin?>}
*/
next() {
throw new Error('Abstract method.');
}
/**
* @param {BufferMap<Number>} inputs
* @param {Coin[]} coins - Coin per input.
* @returns {Promise<void>}
*/
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<Coin?>}
*/
async next() {
if (!this.hasNext())
return null;
return this.coins[this.index++];
}
/**
* @param {BufferMap<Number>} inputs
* @param {Coin[]} coins
* @returns {Promise<void>}
*/
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<Number>} */
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<void>}
*/
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<void>}
*/
async selectHard() {
this.fee = this.hardFee;
await this.fund();
}
/**
* Fill the transaction with inputs.
* @returns {Promise<this>}
*/
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;

View file

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

View file

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

View file

@ -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']),

View file

@ -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<MigrationType>}
*/
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;

View file

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

View file

@ -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) {

View file

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

View file

@ -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<DB['batch']>} 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<CoinSelector>}
*/
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<Credit>}
*/
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<Credit>}
*/
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<Coin?>}
*/
async next() {
if (this.done)
return null;
// look for an usable credit.
for (;;) {
const item = await this.iter.next();
if (item.done) {
this.done = true;
return null;
}
/** @type {Credit} */
const credit = item.value;
if (credit.spent)
continue;
const {coin} = credit;
if (this.txdb.isLocked(coin))
continue;
if (this.smart && coin.height === -1 && !credit.own)
continue;
return coin;
}
}
async end() {
if (!this.iter)
return;
this.iter.return();
}
/**
* Resolve coins.
* @param {bufmap.BufferMap<Number>} inputs
* @param {Coin[]} coins - Coin per input.
* @returns {Promise<void>}
*/
async resolveInputsToCoins(inputs, coins) {
for (const [key, idx] of inputs.entries()) {
if (coins[idx] != null)
continue;
const outpoint = Outpoint.fromKey(key);
if (this.account !== -1) {
const hasCoin = await this.txdb.hasCoinByAccount(
this.account,
outpoint.hash,
outpoint.index
);
if (!hasCoin)
continue;
}
const coin = await this.txdb.getCoin(outpoint.hash, outpoint.index);
if (!coin)
continue;
coins[idx] = coin;
inputs.delete(key);
}
}
}
/*
* Expose
*/

View file

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

View file

@ -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), [

View file

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

View file

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

View file

@ -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"
}
}

View file

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

View file

@ -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<String[]>} - 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<String[]>} - 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,

View file

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

217
test/util/primitives.js Normal file
View file

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

View file

@ -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<Output>}
*/
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<TX[]>}
*/
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<TX[]>}
*/
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;
};

View file

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

File diff suppressed because it is too large Load diff

View file

@ -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();
});
});
});

View file

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

View file

@ -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), []);

View file

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