aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-21 14:38:53 -0700
committerAdam Soltys <[email protected]>2026-09-21 14:38:53 -0700
commit5603cc9c0d75690678cdb182d5c3e41962c1180b (patch)
treebf1e07c505cdc80fec2c7a993f3ffb2c1ec5b174 /src
parentf9bd28e2a03aa0874ea0416dfac9293bec4c4456 (diff)
downloadcoinosv3-5603cc9c0d75690678cdb182d5c3e41962c1180b.tar.gz
coinosv3-5603cc9c0d75690678cdb182d5c3e41962c1180b.zip
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
Diffstat (limited to 'src')
-rw-r--r--src/features/ark.js30
-rw-r--r--src/i18n.js15
2 files changed, 41 insertions, 4 deletions
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
diff --git a/src/i18n.js b/src/i18n.js
index 66ed1a8..62f29fe 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -367,7 +367,7 @@ const STR = {
onbBoardBody: 'Move some sats from Savings into Spending — payments there are instant, with no mining fees.',
topUpFromSavings: 'Top up from Savings',
arkRenewedHistory: 'Renewed',
- arkDepthNotice: 'Uncooperative exit costs about {fee} sats.',
+ arkDepthNotice: 'Unilateral exit costs about {fee} sats.',
arkDepthFree: 'free',
arkDepthBtn: 'Manage',
arkExpiredNotice: '{amount} has passed its expiry. The server can still renew it — until then it can\u2019t be spent or exited.',
@@ -387,10 +387,19 @@ const STR = {
arkFeeChain: 'On-chain fee',
arkFeeService: 'Service fee',
arkCoinsExpired: 'expired',
- arkCoinsExitTitle: 'Uncooperative exit',
+ arkCoinsExitTitle: 'Unilateral exit',
+ arkCoinsExitBtn: 'Exit now',
+ arkExitConfirmTitle: 'Start a unilateral exit?',
+ arkExitConfirmBody: 'This brings your Spending coins to Savings on-chain without the server, by publishing each coin’s transaction history. Nothing needs the server’s cooperation — that is the point — but it is slow and it costs mining fees.',
+ arkExitConfirmCoins: 'Coins',
+ arkExitConfirmAmount: 'Amount',
+ arkExitConfirmFee: 'Mining fees (about, paid from Savings)',
+ arkExitConfirmSavings: 'Savings available for fees',
+ arkExitConfirmWait: 'The funds arrive in Savings only once each coin’s timelock has run out — hours to days. An exit can’t be stopped once it has started.',
+ arkExitConfirmGo: 'Start the exit',
arkCoinsExitDesc: 'If the server vanished today, publishing every transaction in each coin’s history would cost about {fee} sats in mining fees. After a renewal it would be one fresh coin, one hop: about {after} sats.',
arkCoinsRenewTitle: 'Renewal',
- arkCoinsRenewDesc: 'Your wallet renews coins automatically once they are close to expiring and renewal is free. Renewing early is an option if you would rather pay a small fee now to keep your exit cost low in the uncooperative case — the coinos server not responding when you want to bring your funds on-chain. A renewal replaces your coins with a single fresh one in the next round ({every}), which flattens their history. The fee depends on how close the coin is to expiry — the closer, the cheaper:',
+ arkCoinsRenewDesc: 'Your wallet renews coins automatically once they are close to expiring and renewal is free. Renewing early is an option if you would rather pay a small fee now to keep your exit cost low should the coinos server ever stop responding when you want to bring your funds on-chain. A renewal replaces your coins with a single fresh one in the next round ({every}), which flattens their history. The fee depends on how close the coin is to expiry — the closer, the cheaper:',
arkCoinsRoundEvery: 'rounds run every {n} minutes',
arkCoinsRoundEveryOne: 'rounds run every minute',
arkCoinsRenewNowFee: 'Renew now for {fee}',