From efd504ece47e684f77d85de990e97d90af0c221b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 28 Aug 2026 19:37:52 -0700 Subject: The tradeoff gets a quiet advanced card, not a quiz: when coins run four or more hops deep, Settings grows an "exit cost" card naming today's trustless-exit price and offering a one-tap renewal that flattens the history — invisible to everyone whose coins are shallow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi --- src/features/ark.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) (limited to 'src/features') diff --git a/src/features/ark.js b/src/features/ark.js index 269cfb9..66eef18 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1466,6 +1466,33 @@ export function arkFeature(ctx) { } } + // The exit-cost tradeoff, surfaced only when it's real: every off-chain + // hop a coin accumulates makes its trustless exit one on-chain transaction + // more expensive; a renewal flattens the history back to a single hop. + // Most users never need to think about this — the card only appears once + // coins run deep enough for the difference to matter. + const EXIT_DEPTH_ADVISORY = 4; + function exitDepthCard() { + if (!arkAvailable() || wallet.watchOnly || !ark || !ark.state) return null; + const spend = (ark.state.vtxos || []).filter((v) => v.state === 'spendable'); + if (!spend.length) return null; + let depth = 0; + for (const v of spend) { try { depth = Math.max(depth, ark._decoded(v).genesis.length); } catch {} } + if (depth < EXIT_DEPTH_ADVISORY) return null; + let exitFee = 0; try { exitFee = estimateExitFeeSat(ark); } catch {} + let renewFee = 0; try { renewFee = ark.refreshFee(spend, ark._tipH || 0); } catch {} + return h('div', { class: 'card col', style: 'gap:8px' }, + h('h3', {}, t('arkDepthTitle')), + h('p', { class: 'small muted', style: 'margin:0' }, + t('arkDepthBody', { d: String(depth), fee: fmtAmount(exitFee), renew: renewFee > 0 ? fmtAmount(renewFee) + ' sats' : t('arkDepthFree') })), + h('button', { class: 'btn-block', disabled: !!ui.arkBusy, onClick: () => { + // fire and let the round machinery carry it — rounds can be an hour out + ark.refresh(spend.map((v) => v.id)).catch((e) => toast(e.message)); + toast(t('arkDepthRenewed')); + render(); + } }, t('arkDepthBtn'))); + } + function autoWithdrawCard() { if (!arkAvailable() || wallet.watchOnly) return null; const st = awState(); @@ -2325,7 +2352,7 @@ export function arkFeature(ctx) { // swaps feature): the ASP pays invoices natively over ark. canLnPay() { return arkAvailable() && !wallet.watchOnly && !!(ark && ark.info); }, lnSpendableSat() { const b = arkBalance(); return b ? b.spendableSat : 0; }, - settingsCards() { return [autoWithdrawCard()]; }, + settingsCards() { return [autoWithdrawCard(), exitDepthCard()]; }, // The onboarding wizard's top-up step borrows the board form wholesale. arkBoardForm() { if (!arkAvailable() || wallet.watchOnly) return null; -- cgit v1.2.3