aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-03 16:00:39 -0700
committerAdam Soltys <[email protected]>2026-10-03 16:00:39 -0700
commit328f2db9af46cda473318bb3eada0f6e83bb42f1 (patch)
treeb4e850022ba260eff90940ebc465eebded023b63 /tools
parent141779ab97efad6abefaec0545bba5330fd1e12d (diff)
downloadcoinosv3-328f2db9af46cda473318bb3eada0f6e83bb42f1.tar.gz
coinosv3-328f2db9af46cda473318bb3eada0f6e83bb42f1.zip
A Lightning payment that can't be covered says why, not 'Payment failed'
A user renewed all ten of their coins (55,611 sats) from the coins page, tried to pay a 55,300-sat invoice three minutes later, and was told only 'Payment failed'. Nothing had failed: the renewal's round ran at once, but its coins stay unspendable until the round's transaction confirms on-chain, and that block took 64 minutes. The quote found no spendable coin and gave up with the generic message; nothing reached the server. - The quote now tells apart (lnShortfall): money held by another payment (wait, as before — now by sum, not one coin covering it all), money a renewal holds ('Nothing was sent. N sats are being renewed…'), and a real shortfall ('needs X sats with its fee, Y available'). Coins leaving in an offboard or exit are never waited for. - The manager's 'insufficient ark balance' names a renewal too. - The coins page says before the tap that renewed coins can't be spent until the round confirms, and that renewing every coin leaves nothing to pay with meanwhile. tools/ark/ln-shortfall-test.js Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
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);