improve split script dry run and status

This commit is contained in:
jwinterm
2026-02-18 19:35:01 -05:00
parent 57ebe00dd7
commit 22b27b8b35
+147 -36
View File
@@ -12,7 +12,7 @@
* Options:
* --amount <WOW> Target output size in WOW (default: 10)
* --count <N> Number of outputs to create (default: 20)
* --dry-run Show what would happen without sending
* --status Show current output breakdown and exit
* --endpoint <url> Wallet RPC endpoint (overrides PRIMARY_WALLET_ENDPOINT)
*/
@@ -47,7 +47,7 @@ function parseArgs() {
const opts = {
amount: 10,
count: 20,
dryRun: false,
status: false,
endpoint: process.env.PRIMARY_WALLET_ENDPOINT || 'http://127.0.0.1:34570'
};
@@ -67,8 +67,8 @@ function parseArgs() {
process.exit(1);
}
break;
case '--dry-run':
opts.dryRun = true;
case '--status':
opts.status = true;
break;
case '--endpoint':
opts.endpoint = args[++i];
@@ -80,14 +80,20 @@ splitOutputs.js — Split wallet outputs for concurrent payout capacity.
Usage: node scripts/splitOutputs.js [options]
Commands:
--status Show current output breakdown and exit
(default) Split outputs
Options:
--amount <WOW> Target output size in WOW (default: 10)
--count <N> Number of outputs to create (default: 20)
--dry-run Show what would happen without sending
--endpoint <url> Wallet RPC endpoint (default: PRIMARY_WALLET_ENDPOINT env)
--help Show this help message
Example:
Examples:
node scripts/splitOutputs.js --status
Show all spendable outputs with locked/unlocked status.
node scripts/splitOutputs.js --amount 10 --count 30
Creates 30 outputs of 10 WOW each (requires 300+ WOW unlocked balance).
`);
@@ -136,23 +142,130 @@ function rpcCall(endpoint, method, params = {}) {
});
}
async function main() {
const opts = parseArgs();
function formatWow(atomic) {
return (atomic / ATOMIC_DIVISOR).toFixed(4);
}
async function showStatus(endpoint) {
console.log('=== Wallet Output Status ===');
console.log(`Endpoint: ${endpoint}`);
console.log('');
// Get balance summary
const balResp = await rpcCall(endpoint, 'get_balance', { account_index: 0 });
const bal = balResp.result;
console.log(`Total balance: ${formatWow(bal.balance)} WOW`);
console.log(`Unlocked balance: ${formatWow(bal.unlocked_balance)} WOW`);
const locked = bal.balance - bal.unlocked_balance;
if (locked > 0) {
console.log(`Locked balance: ${formatWow(locked)} WOW`);
}
console.log('');
// Get all transfers to build output picture
const transfersResp = await rpcCall(endpoint, 'incoming_transfers', {
transfer_type: 'available',
account_index: 0
});
const outputs = transfersResp.result?.transfers || [];
if (outputs.length === 0) {
console.log('No spendable outputs found.');
return;
}
// Separate unlocked vs locked
const unlocked = outputs.filter(o => !o.frozen && o.unlocked !== false);
const lockedOutputs = outputs.filter(o => o.frozen || o.unlocked === false);
// Sort by amount descending
unlocked.sort((a, b) => b.amount - a.amount);
lockedOutputs.sort((a, b) => b.amount - a.amount);
console.log(`Spendable outputs: ${unlocked.length}`);
if (unlocked.length > 0) {
for (const o of unlocked) {
const age = o.block_height ? `block ${o.block_height}` : 'mempool';
console.log(` ${formatWow(o.amount).padStart(12)} WOW (${age})`);
}
}
if (lockedOutputs.length > 0) {
console.log('');
console.log(`Locked outputs: ${lockedOutputs.length}`);
for (const o of lockedOutputs) {
const age = o.block_height ? `block ${o.block_height}` : 'mempool';
const reason = o.frozen ? 'frozen' : 'locked';
console.log(` ${formatWow(o.amount).padStart(12)} WOW (${age}, ${reason})`);
}
}
console.log('');
const totalSpendable = unlocked.reduce((s, o) => s + o.amount, 0);
console.log(`Total spendable: ${unlocked.length} outputs, ${formatWow(totalSpendable)} WOW`);
if (lockedOutputs.length > 0) {
console.log(`Total locked: ${lockedOutputs.length} outputs, ${formatWow(lockedOutputs.reduce((s, o) => s + o.amount, 0))} WOW`);
}
// If amount/count provided, simulate which outputs would likely be consumed
if (opts.amount && opts.count) {
const amountAtomic = Math.round(opts.amount * ATOMIC_DIVISOR);
const totalNeeded = amountAtomic * opts.count;
const estimatedFees = Math.round(0.01 * ATOMIC_DIVISOR * opts.count);
const totalWithFees = totalNeeded + estimatedFees;
console.log('');
console.log(`--- Split Simulation: ${opts.count} x ${opts.amount} WOW ---`);
console.log(`Need: ~${formatWow(totalWithFees)} WOW (${formatWow(totalNeeded)} + ~${formatWow(estimatedFees)} fees)`);
if (totalWithFees > totalSpendable) {
console.log(`INSUFFICIENT: only ${formatWow(totalSpendable)} WOW spendable`);
return;
}
// Simulate output selection: wallet typically picks smallest sufficient
// combination to minimize change. Sort ascending and accumulate.
const candidates = [...unlocked].sort((a, b) => a.amount - b.amount);
const selected = [];
let accumulated = 0;
for (const o of candidates) {
selected.push(o);
accumulated += o.amount;
if (accumulated >= totalWithFees) break;
}
console.log(`Likely inputs (${selected.length} output${selected.length > 1 ? 's' : ''}):`);
for (const o of selected) {
const age = o.block_height ? `block ${o.block_height}` : 'mempool';
console.log(` ${formatWow(o.amount).padStart(12)} WOW (${age})`);
}
const change = accumulated - totalWithFees;
console.log(`Change output: ~${formatWow(change)} WOW`);
// Show what remains untouched
const remaining = unlocked.filter(o => !selected.includes(o));
console.log(`Untouched outputs: ${remaining.length} (${formatWow(remaining.reduce((s, o) => s + o.amount, 0))} WOW, still spendable)`);
console.log('');
console.log(`After split: ${remaining.length} existing + ${opts.count} new + 1 change = ${remaining.length + opts.count + 1} total outputs`);
}
}
async function splitOutputs(endpoint, opts) {
const amountAtomic = Math.round(opts.amount * ATOMIC_DIVISOR);
const totalNeeded = amountAtomic * opts.count;
console.log('=== Wownero Output Splitter ===');
console.log(`Endpoint: ${opts.endpoint}`);
console.log(`Endpoint: ${endpoint}`);
console.log(`Output size: ${opts.amount} WOW (${amountAtomic} atomic)`);
console.log(`Count: ${opts.count} outputs`);
console.log(`Total: ${(totalNeeded / ATOMIC_DIVISOR).toFixed(4)} WOW (+ fees)`);
if (opts.dryRun) console.log('Mode: DRY RUN');
console.log(`Total: ${formatWow(totalNeeded)} WOW (+ fees)`);
console.log('');
// Step 1: Get wallet address
// Get wallet address
let ownAddress;
try {
const addrResp = await rpcCall(opts.endpoint, 'get_address', {
const addrResp = await rpcCall(endpoint, 'get_address', {
account_index: 0,
address_index: [0]
});
@@ -160,17 +273,17 @@ async function main() {
console.log(`Wallet: ${ownAddress.substring(0, 12)}...${ownAddress.substring(ownAddress.length - 6)}`);
} catch (err) {
console.error(`Failed to get wallet address: ${err.message}`);
console.error('Make sure wownero-wallet-rpc is running on', opts.endpoint);
console.error('Make sure wownero-wallet-rpc is running on', endpoint);
process.exit(1);
}
// Step 2: Check balance
// Check balance
let balance;
try {
const balResp = await rpcCall(opts.endpoint, 'get_balance', { account_index: 0 });
const balResp = await rpcCall(endpoint, 'get_balance', { account_index: 0 });
balance = balResp.result;
console.log(`Balance: ${(balance.balance / ATOMIC_DIVISOR).toFixed(4)} WOW`);
console.log(`Unlocked: ${(balance.unlocked_balance / ATOMIC_DIVISOR).toFixed(4)} WOW`);
console.log(`Balance: ${formatWow(balance.balance)} WOW`);
console.log(`Unlocked: ${formatWow(balance.unlocked_balance)} WOW`);
console.log('');
} catch (err) {
console.error(`Failed to get balance: ${err.message}`);
@@ -183,34 +296,22 @@ async function main() {
if (balance.unlocked_balance < totalWithFees) {
console.error(`Insufficient unlocked balance!`);
console.error(` Need: ~${(totalWithFees / ATOMIC_DIVISOR).toFixed(4)} WOW (including estimated fees)`);
console.error(` Have: ${(balance.unlocked_balance / ATOMIC_DIVISOR).toFixed(4)} WOW unlocked`);
console.error(` Need: ~${formatWow(totalWithFees)} WOW (including estimated fees)`);
console.error(` Have: ${formatWow(balance.unlocked_balance)} WOW unlocked`);
process.exit(1);
}
// Step 3: Build destinations (all to own address)
// Build destinations (all to own address)
const destinations = [];
for (let i = 0; i < opts.count; i++) {
destinations.push({
amount: amountAtomic,
address: ownAddress
});
destinations.push({ amount: amountAtomic, address: ownAddress });
}
console.log(`Splitting into ${opts.count} outputs of ${opts.amount} WOW each...`);
if (opts.dryRun) {
console.log('');
console.log('DRY RUN — no transaction sent.');
console.log(`Would create ${opts.count} outputs of ${amountAtomic} atomic units each.`);
console.log(`Estimated total (with fees): ~${(totalWithFees / ATOMIC_DIVISOR).toFixed(4)} WOW`);
console.log('Remaining balance would be a change output.');
process.exit(0);
}
// Step 4: Send transfer_split to self
// Send transfer_split to self
try {
const txResp = await rpcCall(opts.endpoint, 'transfer_split', {
const txResp = await rpcCall(endpoint, 'transfer_split', {
destinations,
account_index: 0,
priority: 1,
@@ -236,6 +337,16 @@ async function main() {
}
}
async function main() {
const opts = parseArgs();
if (opts.status) {
await showStatus(opts.endpoint);
} else {
await splitOutputs(opts.endpoint, opts);
}
}
main().catch(err => {
console.error('Unexpected error:', err.message);
process.exit(1);