aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/ark/ln-shortfall-test.js52
1 files changed, 52 insertions, 0 deletions
diff --git a/tools/ark/ln-shortfall-test.js b/tools/ark/ln-shortfall-test.js
new file mode 100644
index 0000000..f249ae1
--- /dev/null
+++ b/tools/ark/ln-shortfall-test.js
@@ -0,0 +1,52 @@
+// Why a Lightning payment can't be covered — so the wallet says "your coins
+// are being renewed" or "not enough with the fee" instead of "Payment failed".
+// The first case is the 2026-10-03 report: a user renewed all ten coins
+// (55,611 sats) from the coins page, tried to pay 55,300 three minutes later,
+// and the renewal's round took 64 minutes to confirm.
+// Run: bun tools/ark/ln-shortfall-test.js
+import { lnShortfall } from '../../src/ark/lightning.js';
+let ok = true;
+const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
+const coin = (id, amountSat, state) => ({ id, amountSat, state });
+
+const amounts = [3, 54, 54, 49500, 1000, 1000, 1000, 1000, 1000, 1000];
+const renewed = amounts.map((a, i) => coin('c' + i, a, 'pending'));
+const refresh = { type: 'refresh', step: 'submitted', inputIds: renewed.map((v) => v.id) };
+let r = lnShortfall(renewed, [refresh], 55300);
+check('every coin in a renewal: the payment is held by the renewal, by name', r.kind === 'renewing' && r.renewingSat === 55611 && r.spendableSat === 0, JSON.stringify(r));
+
+r = lnShortfall(renewed, [{ ...refresh, step: 'done' }], 55300);
+check('a finished renewal holds nothing', r.kind !== 'renewing' && r.renewingSat === 0, JSON.stringify(r));
+
+r = lnShortfall([coin('a', 60000, 'pending')], [{ type: 'lnpay', step: 'htlc', inputIds: ['a'] }], 55300);
+check('coins held by another payment: wait, they free themselves', r.kind === 'settling' && r.settlingSat === 60000, JSON.stringify(r));
+
+r = lnShortfall([coin('a', 30000, 'pending'), coin('b', 30000, 'spendable')], [], 55300);
+check('...counted together with what is spendable (it used to take one pending coin covering it all)', r.kind === 'settling', JSON.stringify(r));
+
+r = lnShortfall([coin('a', 55400, 'spendable')], [], 55300);
+check('enough for the amount but not its fee: short, not "failed"', r.kind === 'short' && r.spendableSat === 55400, JSON.stringify(r));
+
+r = lnShortfall([coin('a', 100, 'spendable'), coin('b', 200, 'pending')], [{ type: 'refresh', step: 'created', inputIds: ['b'] }], 55300);
+check('a renewal that wouldn\'t cover it anyway: short', r.kind === 'short' && r.renewingSat === 200, JSON.stringify(r));
+
+r = lnShortfall([coin('a', 40000, 'pending'), coin('b', 20000, 'pending')], [{ type: 'refresh', step: 'issued', inputIds: ['a'] }, { type: 'lnpay', step: 'htlc', inputIds: ['b'] }], 55300);
+check('part renewing, part settling, together enough: the renewal is what it waits on', r.kind === 'renewing', JSON.stringify(r));
+
+r = lnShortfall([coin('a', 60000, 'pending')], [{ type: 'offboard', step: 'submitted', inputIds: ['a'] }], 55300);
+check('coins leaving for Savings are not waited for', r.kind === 'short' && r.settlingSat === 0, JSON.stringify(r));
+r = lnShortfall([coin('a', 60000, 'pending')], [{ type: 'exit', step: 'chain', vtxoId: 'a' }], 55300);
+check('...nor coins in a unilateral exit', r.kind === 'short' && r.settlingSat === 0, JSON.stringify(r));
+
+// the manager's own error (ark sends, and the pay path behind the quote) names it too
+const { ArkManager } = await import('../../src/ark/manager.js');
+const mgr = new ArkManager({ storage: { save() {} }, arkUrl: 'http://test.invalid' });
+mgr.state = { nextKeyIndex: 1, movements: [], actions: [refresh], vtxos: renewed.map((v) => ({ ...v, expiryHeight: 973921 })) };
+const msg = mgr._insufficientMsg(969751);
+check('the manager\'s "insufficient" says a renewal holds the money', /55611 sat are being renewed/.test(msg), msg);
+mgr.state.actions = [];
+mgr.state.vtxos = [];
+check('...and stays plain when nothing is renewing', mgr._insufficientMsg(969751) === 'insufficient ark balance');
+
+console.log(ok ? '\n✅ a payment that can\'t be covered says why' : '\n❌ failed');
+process.exit(ok ? 0 : 1);