test: add more coin selection tests to mtx and coinselection.

This commit is contained in:
Nodari Chkuaselidze 2025-04-24 14:29:01 +04:00
parent a49a68f87a
commit cd3399b312
No known key found for this signature in database
GPG key ID: B018A7BB437D1F05
12 changed files with 804 additions and 151 deletions

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

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

View file

@ -41,22 +41,27 @@ class AbstractCoinSource {
}
}
/** @typedef {'all'|'random'|'age'|'value'} SelectionType */
/** @typedef {'all'|'random'|'age'|'value'} MemSelectionType */
/**
* @typedef {Object} CoinSourceOptions
* @property {SelectionType} [selection] - Selection type.
* @property {MemSelectionType} [selection] - Selection type.
* @property {Coin[]} [coins] - Coins to select from.
*/
class CoinSource extends AbstractCoinSource {
/**
* Coin Source with coins.
* @alias module:utils.CoinSource
*/
class InMemoryCoinSource extends AbstractCoinSource {
constructor(options = {}) {
super();
/** @type {Coin[]} */
this.coins = [];
/** @type {SelectionType} */
/** @type {MemSelectionType} */
this.selection = 'value';
this.index = -1;
@ -166,7 +171,7 @@ class CoinSource extends AbstractCoinSource {
/**
* Coin Selector
* @alias module:primitives.CoinSelector
* @alias module:utils.CoinSelector
* @property {MTX} tx - clone of the original mtx.
* @property {CoinView} view - reference to the original view.
*/
@ -174,7 +179,7 @@ class CoinSource extends AbstractCoinSource {
class CoinSelector {
/**
* @param {MTX} tx
* @param {CoinSource} source
* @param {InMemoryCoinSource} source
* @param {CoinSelectorOptions?} [options]
*/
@ -506,7 +511,7 @@ class CoinSelector {
/**
* Fill the transaction with inputs.
* @returns {Promise<CoinSelector>}
* @returns {Promise<this>}
*/
async select() {
@ -680,6 +685,6 @@ function sortValue(a, b) {
}
exports.AbstractCoinSource = AbstractCoinSource;
exports.CoinSource = CoinSource;
exports.InMemoryCoinSource = InMemoryCoinSource;
exports.CoinSelector = CoinSelector;
exports.FundingError = FundingError;

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

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

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

@ -0,0 +1,213 @@
'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 = () => {
return Input.fromOutpoint(new Outpoint(Buffer.alloc(32), 0));
};
exports.dummyInput = () => {
const hash = random.randomBytes(32);
return Input.fromOutpoint(new Outpoint(hash, 0));
};
exports.deterministicInput = (id) => {
const hash = blake2b.digest(fromU32(id));
return Input.fromOutpoint(new Outpoint(hash, 0));
};
/**
* @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.
*/
/**
* @typedef {Object} OutputOptions
* @property {Number} value
* @property {Address} [address]
* @property {CovenantOptions} [covenant]
*/
/**
* @param {OutputOptions} options
* @returns {Output}
*/
exports.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;
};
/**
* @param {CovenantOptions} options
* @returns {Covenant}
*/
exports.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 = randomString(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 = () => {
const key = random.randomBytes(33);
return Address.fromPubkey(key);
};
/**
* @param {Object} options
* @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]
* @returns {Coin}
*/
exports.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;
}
function randomString(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,10 +2,7 @@
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 {ZERO_HASH} = require('../../lib/protocol/consensus');
const walletUtils = exports;
@ -35,16 +32,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);
};

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;

View file

@ -1,72 +1,130 @@
'use strict';
const assert = require('bsert');
const {BlockMeta} = require('../lib/wallet/records');
const util = require('../lib/utils/util');
const Network = require('../lib/protocol/network');
const MTX = require('../lib/primitives/mtx');
const Covenant = require('../lib/primitives/covenant');
const WalletDB = require('../lib/wallet/walletdb');
const policy = require('../lib/protocol/policy');
const wutils = require('./util/wallet');
const {nextBlock} = wutils;
const primutils = require('./util/primitives');
const {coinbaseInput, dummyInput} = primutils;
/** @typedef {import('../lib/wallet/wallet')} Wallet */
/** @typedef {import('../lib/covenants/rules').types} covenantTypes */
// Use main instead of regtest because (deprecated)
// CoinSelector.MAX_FEE was network agnostic
const network = Network.get('main');
function dummyBlock(tipHeight) {
const height = tipHeight + 1;
const hash = Buffer.alloc(32);
hash.writeUInt16BE(height);
describe('Wallet Coin Selection', function () {
const TX_START_BAK = network.txStart;
/** @type {WalletDB?} */
let wdb;
/** @type {Wallet?} */
let wallet;
const prevHash = Buffer.alloc(32);
prevHash.writeUInt16BE(tipHeight);
const beforeFn = async () => {
network.txStart = 0;
wdb = new WalletDB({ network });
const dummyBlock = {
hash,
height,
time: util.now(),
prevBlock: prevHash
await wdb.open();
await wdb.addBlock(nextBlock(wdb), []);
wallet = wdb.primary;
};
return dummyBlock;
}
const afterFn = async () => {
network.txStart = TX_START_BAK;
await wdb.close();
async function fundWallet(wallet, amounts) {
assert(Array.isArray(amounts));
wdb = null;
wallet = null;
};
const mtx = new MTX();
const addr = await wallet.receiveAddress();
for (const amt of amounts) {
mtx.addOutput(addr, amt);
}
describe('Selection types', function () {
beforeEach(beforeFn);
afterEach(afterFn);
const dummy = dummyBlock(wallet.wdb.height);
await wallet.wdb.addBlock(dummy, [mtx.toTX()]);
}
it('should select all spendable coins', async () => {
const spendableCovs = [
Covenant.types.NONE,
Covenant.types.OPEN,
Covenant.types.REDEEM
];
const nonSpendableCovs = [
Covenant.types.BID,
Covenant.types.REVEAL,
Covenant.types.REGISTER,
Covenant.types.UPDATE,
Covenant.types.RENEW,
Covenant.types.TRANSFER,
Covenant.types.FINALIZE,
Covenant.types.REVOKE
];
const mkopt = type => ({ value: 1e6, covenant: { type }});
await fundWallet(wallet, [...nonSpendableCovs, ...spendableCovs].map(mkopt));
const coins = await wallet.getCoins();
assert.strictEqual(coins.length, spendableCovs.length + nonSpendableCovs.length);
const mtx = new MTX();
await wallet.fund(mtx, {
selection: 'all'
});
assert.strictEqual(mtx.inputs.length, spendableCovs.length);
});
it('should select coin by descending value', async () => {
const values = [5e6, 4e6, 3e6, 2e6, 1e6];
await fundWallet(wallet, values.map(value => ({ value })));
const mtx = new MTX();
mtx.addOutput(primutils.randomP2PKAddress(), 9e6);
await wallet.fund(mtx, {
selection: 'value',
hardFee: 0
});
assert.strictEqual(mtx.inputs.length, 2);
assert.strictEqual(mtx.outputs.length, 1);
assert.strictEqual(mtx.outputs[0].value, 9e6);
});
it('should select coins by descending age', async () => {
const values = [1e6, 2e6, 3e6, 4e6, 5e6];
for (const value of values)
await fundWallet(wallet, [{ value }]);
const mtx = new MTX();
mtx.addOutput(primutils.randomP2PKAddress(), 9e6);
await wallet.fund(mtx, {
selection: 'age',
hardFee: 0
});
// 1 + 2 + 3 + 4 = 10
assert.strictEqual(mtx.inputs.length, 4);
assert.strictEqual(mtx.outputs.length, 2);
assert.strictEqual(mtx.outputs[0].value, 9e6);
assert.strictEqual(mtx.outputs[1].value, 1e6);
});
});
describe('Wallet Coin Selection', function () {
describe('Fees', function () {
const wdb = new WalletDB({network});
let wallet;
before(async () => {
await wdb.open();
wdb.height = network.txStart + 1;
wdb.state.height = wdb.height;
const dummy = dummyBlock(network.txStart + 1);
const record = BlockMeta.fromEntry(dummy);
await wdb.setTip(record);
wallet = wdb.primary;
});
after(async () => {
await wdb.close();
});
before(beforeFn);
after(afterFn);
it('should fund wallet', async () => {
await fundWallet(wallet, [100e6, 10e6, 1e6, 100000, 10000]);
const vals = [100e6, 10e6, 1e6, 0.1e6, 0.01e6];
await fundWallet(wallet, vals.map(value => ({ value })));
const bal = await wallet.getBalance();
assert.strictEqual(bal.confirmed, 111110000);
assert.strictEqual(bal.confirmed, 111.11e6);
});
it('should pay default fee rate for small tx', async () => {
@ -121,16 +179,22 @@ describe('Wallet Coin Selection', function () {
it('should fail to pay absurd fee rate for small tx', async () => {
const address = await wallet.receiveAddress();
await assert.rejects(
wallet.send({
let err;
try {
await wallet.send({
outputs: [{
address,
value: 5e6
}],
rate: (policy.ABSURD_FEE_FACTOR + 1) * network.minRelay
}),
{message: 'Fee exceeds absurd limit.'}
);
});
} catch (e) {
err = e;
}
assert(err, 'Error not thrown.');
assert.strictEqual(err.message, 'Fee exceeds absurd limit.');
});
it('should pay fee just under the absurd limit', async () => {
@ -177,3 +241,60 @@ describe('Wallet Coin Selection', function () {
});
});
});
/**
* @typedef {Object} OutputInfo
* @property {String} [address]
* @property {Number} [value]
* @property {covenantTypes} [covenant]
* @property {Boolean} [coinbase=false]
*/
/**
* @param {Wallet} wallet
* @param {primutils.OutputOptions} outputInfo
* @returns {Promise<Output>}
*/
async function mkOutput(wallet, outputInfo) {
if (!outputInfo.address)
outputInfo.address = await wallet.receiveAddress();
return primutils.makeOutput(outputInfo);
}
/**
* @param {Wallet} wallet
* @param {OutputInfo[]} outputInfos
*/
async function fundWallet(wallet, outputInfos) {
assert(Array.isArray(outputInfos));
let hadCoinbase = false;
const txs = [];
for (const info of outputInfos) {
const mtx = new MTX();
if (info.coinbase && hadCoinbase)
throw new Error('Coinbase already added.');
if (info.coinbase && !hadCoinbase) {
hadCoinbase = true;
mtx.addInput(coinbaseInput());
} else {
mtx.addInput(dummyInput());
}
const output = await mkOutput(wallet, info);
mtx.addOutput(output);
if (output.covenant.isLinked())
mtx.addInput(dummyInput());
txs.push(mtx.toTX());
}
await wallet.wdb.addBlock(nextBlock(wallet.wdb), txs);
}

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';
@ -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();
});
@ -3293,21 +3280,15 @@ describe('Wallet', function() {
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 +3729,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