diff options
| author | Adam Soltys <[email protected]> | 2026-10-03 16:00:39 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-03 16:00:39 -0700 |
| commit | 328f2db9af46cda473318bb3eada0f6e83bb42f1 (patch) | |
| tree | b4e850022ba260eff90940ebc465eebded023b63 /src/features | |
| parent | 141779ab97efad6abefaec0545bba5330fd1e12d (diff) | |
| download | coinosv3-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 'src/features')
| -rw-r--r-- | src/features/ark.js | 22 |
1 files changed, 16 insertions, 6 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index fb6d2a3..523a3a2 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -9,7 +9,7 @@ import { sha256 } from '@noble/hashes/sha256'; import { ArkManager } from '../ark/manager.js'; import { loadBg, saveBg, buildBg, disarmSiblingRecords } from '../nwc-bg.js'; import { boardFee, p2trAddress } from '../ark/board.js'; -import { maybeBolt11, maybeLnInvoice, lnSendFee } from '../ark/lightning.js'; +import { maybeBolt11, maybeLnInvoice, lnSendFee, lnShortfall } from '../ark/lightning.js'; import { decodeVtxo, getVtxoStatus, VTXO_STATE_SPENT, concatBytes, vtxoBytesFromStr, vtxoBytesToHex, decodeAddress, arkIdFromServerPubkey, getArkInfo } from '../ark/proto.js'; import { arkStore } from '../ark/store.js'; import { signedExitTxs, exitTxVsizes, buildBumpChild, buildExitClaim, submitPackage } from '../ark/exit.js'; @@ -1062,8 +1062,8 @@ export function arkFeature(ctx) { } } if (fee == null) { - const pendingCover = mgr.vtxos().some((v) => v.state === 'pending' && v.amountSat >= p.amountSat); - if (pendingCover) { + const why = lnShortfall(mgr.vtxos(), mgr.state.actions, p.amountSat); + if (why.kind === 'settling') { p.status = 'fundsPending'; render(); mgr.resumePending().catch(() => {}); // nudge whatever holds them @@ -1072,9 +1072,16 @@ export function arkFeature(ctx) { }, 4000); return; } - // can't cover once fees are in + // Say why. A renewal holds its coins until the round confirms on-chain + // (not seconds: no spinner for that); otherwise there simply isn't + // enough once the fee is in — and that is not "Payment failed". ui.arkLnPay = null; - ui.sendError = t('arkLnPayFailed'); + ui.sendError = why.kind === 'renewing' + ? t('arkLnRenewing', { sats: fmtSats(why.renewingSat) }) + : t('arkLnShort', { + need: fmtSats(p.amountSat + lnSendFee(p.amountSat, mgr.info.lnSendFees, spendables, tip) + routing), + have: fmtSats(why.spendableSat), + }); render(); return; } @@ -2412,7 +2419,10 @@ export function arkFeature(ctx) { !selCoins.length ? t('arkCoinsRenewNone') : selCoins.length < spend.length ? t('arkCoinsRenewSome', { n: selCoins.length, fee: selFee > 0 ? fmtSats(selFee) + ' sats' : t('arkDepthFree') }) - : selFee > 0 ? t('arkCoinsRenewNowFee', { fee: fmtSats(selFee) + ' sats' }) : t('arkDepthRenewBtn'))), + : selFee > 0 ? t('arkCoinsRenewNowFee', { fee: fmtSats(selFee) + ' sats' }) : t('arkDepthRenewBtn')), + // said before the tap: a renewal takes its coins out of reach until + // the round's transaction confirms, and by default it takes them all + !spend.length ? null : h('div', { class: 'notice info small ark-renew-lock' }, t('arkCoinsRenewLock'))), h('div', { class: 'card col', style: 'gap:8px' }, h('h4', { style: 'margin:0' }, t('arkCoinsCoopTitle')), h('p', { class: 'small muted', style: 'margin:0' }, t('arkCoopDesc')), |
