Files
salvium-rs/test/utxo-selection.test.js
T
Matt Hess 1066ca2110 Add full wallet implementation with storage, sync, and transaction support
New Features:
  - Wallet class with multi-account and subaddress support
  - PersistentWallet with blockchain sync and storage (Memory/IndexedDB)
  - Connection manager for daemon/wallet RPC failover
  - Query system for filtering outputs and transactions
  - Offline transaction signing (cold wallet support)
  - Multisig wallet support (N-of-M threshold signing)

  Daemon RPC:
  - Salvium-specific: getSupplyInfo(), getYieldInfo()
  - Mining control: startMining(), stopMining(), miningStatus()
  - Bandwidth/peer management, admin controls

  Transaction:
  - Full transaction building with UTXO selection
  - Transaction parsing and summarization
  - Fee estimation with priority levels
  - Decoy selection with gamma distribution

  Tests:
  - Wallet class tests (accounts, subaddresses, recovery)
  - Transaction builder and parser tests
  - UTXO selection tests
2026-01-18 21:39:05 +00:00

280 lines
8.1 KiB
JavaScript

#!/usr/bin/env node
/**
* UTXO Selection Tests
*
* Tests for selectUTXOs function with various strategies:
* - LARGEST_FIRST: Minimize input count
* - SMALLEST_FIRST: Consume small UTXOs first
* - FIFO: Oldest UTXOs first
* - RANDOM: Randomized selection
*/
import {
UTXO_STRATEGY,
selectUTXOs,
CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
} from '../src/transaction.js';
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed++;
} catch (e) {
console.log(`${name}: ${e.message}`);
failed++;
}
}
function assert(condition, message) {
if (!condition) throw new Error(message || 'Assertion failed');
}
function assertEqual(a, b, message) {
if (a !== b) throw new Error(message || `Expected ${b}, got ${a}`);
}
function assertTrue(condition, message) {
if (!condition) throw new Error(message || 'Expected true');
}
// Create mock UTXOs for testing
function createMockUTXOs() {
return [
{ amount: 1000000000n, globalIndex: 100, blockHeight: 1000, txHash: 'a'.repeat(64) },
{ amount: 500000000n, globalIndex: 200, blockHeight: 1050, txHash: 'b'.repeat(64) },
{ amount: 250000000n, globalIndex: 300, blockHeight: 1100, txHash: 'c'.repeat(64) },
{ amount: 100000000n, globalIndex: 400, blockHeight: 1150, txHash: 'd'.repeat(64) },
{ amount: 50000000n, globalIndex: 500, blockHeight: 1200, txHash: 'e'.repeat(64) },
];
}
console.log('\n=== UTXO Selection Tests ===\n');
// Strategy constants
console.log('--- Strategy Constants ---');
test('UTXO_STRATEGY.LARGEST_FIRST exists', () => {
assertEqual(UTXO_STRATEGY.LARGEST_FIRST, 'largest_first');
});
test('UTXO_STRATEGY.SMALLEST_FIRST exists', () => {
assertEqual(UTXO_STRATEGY.SMALLEST_FIRST, 'smallest_first');
});
test('UTXO_STRATEGY.RANDOM exists', () => {
assertEqual(UTXO_STRATEGY.RANDOM, 'random');
});
test('UTXO_STRATEGY.FIFO exists', () => {
assertEqual(UTXO_STRATEGY.FIFO, 'fifo');
});
// Basic selection
console.log('\n--- Basic Selection ---');
test('selects sufficient UTXOs for target amount', () => {
const utxos = createMockUTXOs();
const result = selectUTXOs(utxos, 300000000n, 10000000n, { currentHeight: 2000 });
assertTrue(result.selected.length > 0, 'Should select UTXOs');
assertTrue(result.totalAmount >= 300000000n + result.estimatedFee, 'Should cover target + fee');
});
test('returns correct structure', () => {
const utxos = createMockUTXOs();
const result = selectUTXOs(utxos, 100000000n, 1000000n, { currentHeight: 2000 });
assert(Array.isArray(result.selected), 'Should have selected array');
assertEqual(typeof result.totalAmount, 'bigint', 'totalAmount should be bigint');
assertEqual(typeof result.changeAmount, 'bigint', 'changeAmount should be bigint');
assertEqual(typeof result.estimatedFee, 'bigint', 'estimatedFee should be bigint');
});
test('calculates change correctly', () => {
const utxos = [{ amount: 1000000000n, globalIndex: 1 }];
const targetAmount = 300000000n;
const feePerInput = 10000000n;
const result = selectUTXOs(utxos, targetAmount, feePerInput);
const expectedChange = 1000000000n - targetAmount - feePerInput;
assertEqual(result.changeAmount, expectedChange);
});
test('fee scales with input count', () => {
const utxos = [
{ amount: 100000000n, globalIndex: 1 },
{ amount: 100000000n, globalIndex: 2 },
{ amount: 100000000n, globalIndex: 3 }
];
const feePerInput = 5000000n;
const result = selectUTXOs(utxos, 250000000n, feePerInput);
// Need 3 inputs to reach 250M, so fee should be 3 * 5M = 15M
assertEqual(result.estimatedFee, 15000000n);
});
// Strategy tests
console.log('\n--- Selection Strategies ---');
test('LARGEST_FIRST selects biggest UTXOs first', () => {
const utxos = createMockUTXOs();
const result = selectUTXOs(utxos, 100000000n, 1000000n, {
strategy: UTXO_STRATEGY.LARGEST_FIRST,
currentHeight: 2000
});
assertEqual(result.selected[0].amount, 1000000000n, 'Should select 1 SAL first');
});
test('LARGEST_FIRST minimizes input count', () => {
const utxos = createMockUTXOs();
const largestFirst = selectUTXOs(utxos, 500000000n, 1000000n, {
strategy: UTXO_STRATEGY.LARGEST_FIRST,
currentHeight: 2000
});
const smallestFirst = selectUTXOs(utxos, 500000000n, 1000000n, {
strategy: UTXO_STRATEGY.SMALLEST_FIRST,
currentHeight: 2000,
dustThreshold: 10000000n
});
assertTrue(largestFirst.selected.length <= smallestFirst.selected.length,
'LARGEST_FIRST should use fewer or equal inputs');
});
test('SMALLEST_FIRST selects smallest UTXOs first', () => {
const utxos = createMockUTXOs();
const result = selectUTXOs(utxos, 40000000n, 1000000n, {
strategy: UTXO_STRATEGY.SMALLEST_FIRST,
currentHeight: 2000,
dustThreshold: 10000000n
});
assertEqual(result.selected[0].amount, 50000000n, 'Should select 50M first');
});
test('FIFO selects oldest UTXOs first', () => {
const utxos = createMockUTXOs();
const result = selectUTXOs(utxos, 100000000n, 1000000n, {
strategy: UTXO_STRATEGY.FIFO,
currentHeight: 2000
});
assertEqual(result.selected[0].blockHeight, 1000, 'Should select oldest first');
});
test('RANDOM produces valid selection', () => {
const utxos = createMockUTXOs();
const result = selectUTXOs(utxos, 100000000n, 1000000n, {
strategy: UTXO_STRATEGY.RANDOM,
currentHeight: 2000
});
assertTrue(result.selected.length > 0, 'Should select UTXOs');
assertTrue(result.totalAmount >= 100000000n + result.estimatedFee, 'Should cover amount');
});
// Filtering tests
console.log('\n--- Filtering ---');
test('respects minConfirmations', () => {
const utxos = createMockUTXOs();
// At height 1205, UTXOs at 1200 only have 5 confirmations
const result = selectUTXOs(utxos, 40000000n, 1000000n, {
currentHeight: 1205,
minConfirmations: 10
});
for (const utxo of result.selected) {
const confirmations = 1205 - utxo.blockHeight;
assertTrue(confirmations >= 10, `UTXO should have at least 10 confirmations, has ${confirmations}`);
}
});
test('respects dustThreshold', () => {
const utxos = [
{ amount: 100n, globalIndex: 1 },
{ amount: 500000n, globalIndex: 2 },
{ amount: 1000000000n, globalIndex: 3 }
];
const result = selectUTXOs(utxos, 100000000n, 1000000n, {
dustThreshold: 1000000n
});
for (const utxo of result.selected) {
assertTrue(utxo.amount >= 1000000n, 'Should not include dust UTXOs');
}
});
test('respects maxInputs', () => {
const utxos = [];
for (let i = 0; i < 20; i++) {
utxos.push({ amount: 100000000n, globalIndex: i }); // 100M each
}
// Request 300M - could be satisfied by 3 inputs, but limit to 5
const result = selectUTXOs(utxos, 300000000n, 1000000n, { maxInputs: 5 });
assertTrue(result.selected.length <= 5, 'Should not exceed maxInputs');
});
// Error cases
console.log('\n--- Error Handling ---');
test('throws on insufficient funds', () => {
const utxos = [{ amount: 1000000n, globalIndex: 1 }];
let threw = false;
try {
selectUTXOs(utxos, 1000000000n, 1000000n);
} catch (e) {
threw = true;
assertTrue(e.message.includes('Insufficient'), 'Should mention insufficient funds');
}
assertTrue(threw, 'Should throw error');
});
test('throws on no eligible UTXOs', () => {
const utxos = [{ amount: 100n, globalIndex: 1 }]; // Below dust threshold
let threw = false;
try {
selectUTXOs(utxos, 50n, 10n, { dustThreshold: 1000000n });
} catch (e) {
threw = true;
assertTrue(e.message.includes('No eligible'), 'Should mention no eligible UTXOs');
}
assertTrue(threw, 'Should throw error');
});
test('handles number inputs (converts to bigint)', () => {
const utxos = [{ amount: 1000000000, globalIndex: 1 }]; // Number, not bigint
const result = selectUTXOs(utxos, 100000000, 1000000);
assertTrue(result.selected.length > 0, 'Should handle number amounts');
});
// Summary
console.log(`\n--- Summary ---`);
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed === 0) {
console.log('\n✓ All UTXO selection tests passed!');
process.exit(0);
} else {
console.log('\n✗ Some tests failed');
process.exit(1);
}