aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-05 22:47:47 -0700
committerAdam Soltys <[email protected]>2026-07-05 22:47:47 -0700
commitd24ba23df05bbc8072cbd97bec84a2a198951d03 (patch)
tree6fb9e0f832f2a1b6560fd11ad723d772c557b84b /src
parent1725e6ca03eacbe1ee16f563c8fdb27435080553 (diff)
downloadcoinosv3-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.js24
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,