From 22b27b8b350c4c84eee73dac62b6536a654d894b Mon Sep 17 00:00:00 2001 From: jwinterm Date: Wed, 18 Feb 2026 19:35:01 -0500 Subject: [PATCH] improve split script dry run and status --- scripts/splitOutputs.js | 183 ++++++++++++++++++++++++++++++++-------- 1 file changed, 147 insertions(+), 36 deletions(-) diff --git a/scripts/splitOutputs.js b/scripts/splitOutputs.js index 9ee21d9..7b99fa5 100755 --- a/scripts/splitOutputs.js +++ b/scripts/splitOutputs.js @@ -12,7 +12,7 @@ * Options: * --amount Target output size in WOW (default: 10) * --count Number of outputs to create (default: 20) - * --dry-run Show what would happen without sending + * --status Show current output breakdown and exit * --endpoint 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 Target output size in WOW (default: 10) --count Number of outputs to create (default: 20) - --dry-run Show what would happen without sending --endpoint 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);