From d24ba23df05bbc8072cbd97bec84a2a198951d03 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 5 Jul 2026 22:47:47 -0700 Subject: Zero-coin unilateral exit + honest actionable errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/features/ark.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) (limited to 'src') 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, -- cgit v1.2.3