From 5603cc9c0d75690678cdb182d5c3e41962c1180b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 21 Sep 2026 14:38:53 -0700 Subject: Spending coins: "Unilateral exit", with an Exit now button behind a confirmation. The confirmation shows the coin count, the amount, the estimated mining fees (paid from Savings as the exit unrolls) and what Savings holds for them, refuses when Savings can't carry the fees, and says the funds only land once the timelocks run out and that an exit can't be stopped. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp --- src/features/ark.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'src/features') diff --git a/src/features/ark.js b/src/features/ark.js index 8df45fe..96abcb1 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -2128,6 +2128,33 @@ export function arkFeature(ctx) { const tick = (checked, onChange, title) => h('input', { type: 'checkbox', checked, title, style: 'flex:0 0 auto;margin:0', onChange, }); + // "Exit now" asks once, with the numbers on the table, before anything + // is published: an exit cannot be called back. + if (ui.arkExitConfirm) { + const savings = wallet.spendable || 0; + const short = savings < exitFee; + const row = (k, v, cls = '') => h('div', { class: 'row between' }, h('span', { class: 'small muted' }, k), h('span', { class: 'small' + (cls ? ' ' + cls : '') }, v)); + return h('div', { class: 'col', style: 'gap:16px' }, + h('div', { class: 'card col', style: 'gap:10px' }, + h('h3', { style: 'margin:0' }, t('arkExitConfirmTitle')), + h('p', { class: 'small muted', style: 'margin:0' }, t('arkExitConfirmBody')), + h('div', { class: 'col', style: 'gap:4px' }, + row(t('arkExitConfirmCoins'), String(spend.length)), + row(t('arkExitConfirmAmount'), fmtSats(totalSat) + ' sats'), + row(t('arkExitConfirmFee'), fmtSats(exitFee) + ' sats'), + row(t('arkExitConfirmSavings'), fmtSats(savings) + ' sats', short ? 'err' : '')), + short ? h('div', { class: 'notice err small' }, t('arkExitFeeShort', { need: fmtSats(exitFee), have: fmtSats(savings) })) : null, + h('p', { class: 'small muted', style: 'margin:0' }, t('arkExitConfirmWait')), + h('button', { class: 'btn-primary btn-block', disabled: !!ui.arkBusy || short || !spend.length, onClick: () => { + ui.arkExitConfirm = null; + doArkExit(); + ui.arkCoinsSel = null; + back(); + ui.tab = 'history'; // the exit lives as a pending record there + render(); + } }, ui.arkBusy === 'exit' ? h('span', { class: 'spinner sm' }) : t('arkExitConfirmGo'))), + h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.arkExitConfirm = null; render(); } }, t('cancel'))); + } return h('div', { class: 'col', style: 'gap:16px' }, h('div', { class: 'card col', style: 'gap:8px' }, h('h3', { style: 'margin:0' }, t('arkCoinsTitle')), @@ -2175,7 +2202,8 @@ export function arkFeature(ctx) { h('div', { class: 'card col', style: 'gap:8px' }, h('h4', { style: 'margin:0' }, t('arkCoinsExitTitle')), h('p', { class: 'small muted', style: 'margin:0' }, - t('arkCoinsExitDesc', { fee: fmtSats(exitFee), after: fmtSats(afterFee) }))), + t('arkCoinsExitDesc', { fee: fmtSats(exitFee), after: fmtSats(afterFee) })), + h('button', { class: 'btn-ghost btn-block', disabled: !!ui.arkBusy || !spend.length, onClick: () => { ui.arkExitConfirm = true; render(); } }, t('arkCoinsExitBtn'))), h('div', { class: 'card col', style: 'gap:8px' }, h('h4', { style: 'margin:0' }, t('arkCoinsRenewTitle')), // the round cadence comes from the server (it has been an hour and -- cgit v1.2.3