diff options
| author | Adam Soltys <[email protected]> | 2026-07-05 22:47:47 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-05 22:47:47 -0700 |
| commit | d24ba23df05bbc8072cbd97bec84a2a198951d03 (patch) | |
| tree | 6fb9e0f832f2a1b6560fd11ad723d772c557b84b /src | |
| parent | 1725e6ca03eacbe1ee16f563c8fdb27435080553 (diff) | |
| download | coinosv3-d24ba23df05bbc8072cbd97bec84a2a198951d03.tar.gz coinosv3-d24ba23df05bbc8072cbd97bec84a2a198951d03.zip | |
Zero-coin unilateral exit + honest actionable errors
A fresh gift-only wallet has no on-chain coin for the CPFP fee child,
so its exit stalled behind 'hit a snag — retrying automatically' — a
precondition dressed up as a transient. When no fee coin exists the
hop is now tried bare (a zero-minrelay regtest relays it; a gift
wallet can exit with literally nothing but the gift), and if the node
refuses, the real message shows: exiting needs an on-chain coin for
fees. Errors now carry an actionable flag so the status line knows
when to tell the truth verbatim.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/ark.js | 24 |
1 files changed, 21 insertions, 3 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index f886c0a..972dc53 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -302,7 +302,10 @@ export function arkFeature(ctx) { a.step === 'chain' ? t('arkExitChainStatus', { n: fmtAmount(a.amountSat) }) : a.step === 'timelock' ? t('arkExitTimelockStatus', { n: fmtAmount(a.amountSat), blocks: String(a.claimableAt) }) : t('arkExitClaimingStatus', { n: fmtAmount(a.amountSat) }), - a.lastError ? h('div', { class: 'small faint' }, t('arkExitRetrying')) : null)) + a.lastError + ? h('div', { class: a.actionable ? 'small err' : 'small faint' }, + a.actionable ? a.lastError : t('arkExitRetrying')) + : null)) ); } @@ -359,9 +362,10 @@ export function arkFeature(ctx) { for (const a of open) { try { await driveExit(mgr, a); - if (a.lastError) { delete a.lastError; mgr._save(); } // transient error resolved + if (a.lastError) { delete a.lastError; delete a.actionable; mgr._save(); } // resolved } catch (e) { a.lastError = e.message; + a.actionable = !!e.actionable; // user must act (e.g. fund fees) vs. plain retry mgr._save(); } } @@ -386,7 +390,21 @@ export function arkFeature(ctx) { // the child pays for the whole package (the parent is zero-fee) const feeSat = Math.ceil((txi.vsize + 130) * feeRate); // child ≈ 130 vB const coin = pickFeeCoin(Math.max(294, feeSat - txi.anchorValue + 294)); - if (!coin) throw new Error(t('arkExitNoFeeCoin')); + if (!coin) { + // No on-chain coin for the fee child. Try the hop bare — a node + // that relays zero-fee txs (regtest with minrelaytxfee=0) needs no + // bump. If it refuses, this is a PRECONDITION the user must fix, + // not a transient to retry behind a vague message. + try { + await submitPackage(mgr.esploraUrl, [txi.hex]); + mgr._save(); + return; + } catch { + const e = new Error(t('arkExitNoFeeCoin')); + e.actionable = true; + throw e; + } + } const changeAddr = wallet.freshChange().address; const child = buildBumpChild({ parentTxidInternal: txi.txidInternal, anchorVout: txi.anchorVout, anchorValue: txi.anchorValue, |
