diff options
| author | Adam Soltys <[email protected]> | 2026-08-29 07:33:26 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-29 07:33:26 -0700 |
| commit | 0cb46b772c543240cf303197247039a8d875acb2 (patch) | |
| tree | b5be73e7e01b8f7ca374ddaeb7ee827c7056e2a2 /src | |
| parent | b61198fc49e55213ee29a545f7bab3691f43d6b5 (diff) | |
| download | coinosv3-0cb46b772c543240cf303197247039a8d875acb2.tar.gz coinosv3-0cb46b772c543240cf303197247039a8d875acb2.zip | |
The coins screen answers the question it was built for: each vtxo shows what renewing it TODAY costs beside its expiry and depth, the renewal card lays out the server's whole bracket schedule (read live from ArkInfo, never hardcoded) — dearer the more lifetime a coin still carries, free in the final stretch where the wallet renews on its own — and says plainly that renewing sooner only buys the cheaper exit sooner
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/ark.js | 41 | ||||
| -rw-r--r-- | src/i18n.js | 12 |
2 files changed, 47 insertions, 6 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index 6d122a0..b49e95e 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1568,6 +1568,29 @@ export function arkFeature(ctx) { const depthOf = (v) => { try { return mgr._decoded(v).genesis.length; } catch { return null; } }; const expiresOf = (v) => (v.expiryHeight && tip ? new Date(Date.now() + (v.expiryHeight - tip) * 600_000).toLocaleDateString() : '—'); + // The server prices a renewal by how much lifetime each coin still + // carries (its ppm bracket) — waiting IS cheaper, and inside the final + // bracket it's free, where the wallet renews on its own anyway. + const table = ((mgr.info || {}).refreshFees || {}).ppmExpiryTable || []; + const ppmFor = (blocks) => (table.filter((e) => e.thresholdBlocks <= blocks).pop()?.ppm) ?? 0; + const feeNowOf = (v) => Math.ceil((v.amountSat * ppmFor((v.expiryHeight || 0) - tip)) / 1_000_000); + const span = (blocks) => { + if (blocks >= 144) { + const d = Math.round(blocks / 144); + return d === 1 ? t('arkCoinsSpanDay') : t('arkCoinsSpanDays', { n: d }); + } + const hs = Math.max(1, Math.round(blocks / 6)); + return hs === 1 ? t('arkCoinsSpanHour') : t('arkCoinsSpanHours', { n: hs }); + }; + const schedule = [...table].sort((a, b) => b.thresholdBlocks - a.thresholdBlocks).map((e, i, arr) => ({ + label: e.ppm === 0 + ? t('arkCoinsSchedFinal', { span: span(arr[i - 1] ? arr[i - 1].thresholdBlocks : 144) }) + : i === 0 + ? t('arkCoinsSchedOver', { span: span(e.thresholdBlocks) }) + : t('arkCoinsSchedBetween', { a: span(e.thresholdBlocks), b: span(arr[i - 1].thresholdBlocks) }), + cost: e.ppm === 0 ? t('arkDepthFree') : (e.ppm / 10000).toLocaleString(undefined, { maximumFractionDigits: 3 }) + '%', + free: e.ppm === 0, + })); const renew = () => { // fire and let the round machinery carry it — rounds can be an hour out mgr.refresh(spend.map((v) => v.id)).catch((e) => toast(e.message)); @@ -1583,13 +1606,16 @@ export function arkFeature(ctx) { t('arkCoinsIntro', { n: spend.length, total: fmtAmount(totalSat) + ' ' + unitLabel() })), h('div', { class: 'col', style: 'gap:4px;margin-top:4px' }, h('div', { class: 'row gap6' }, - head(t('arkCoinsColAmount')), head(t('arkCoinsColExpires'), 'text-align:center'), head(t('arkCoinsColDepth'), 'text-align:right')), + head(t('arkCoinsColAmount')), head(t('arkCoinsColExpires'), 'text-align:center'), + head(t('arkCoinsColDepth'), 'text-align:center'), head(t('arkCoinsColFee'), 'text-align:right')), ...spend.map((v) => { const d = depthOf(v); + const f = feeNowOf(v); return h('div', { class: 'row gap6' }, cell(fmtAmount(v.amountSat) + ' ' + unitLabel()), cell(expiresOf(v), 'text-align:center'), - cell(d == null ? '—' : d === 1 ? t('arkCoinsHop') : t('arkCoinsHops', { n: d }), 'text-align:right')); + cell(d == null ? '—' : d === 1 ? t('arkCoinsHop') : t('arkCoinsHops', { n: d }), 'text-align:center'), + cell(f > 0 ? fmtAmount(f) : t('arkDepthFree'), 'text-align:right')); })), h('p', { class: 'small faint', style: 'margin:4px 0 0' }, t('arkCoinsExpiryNote'))), h('div', { class: 'card col', style: 'gap:8px' }, @@ -1598,9 +1624,14 @@ export function arkFeature(ctx) { t('arkCoinsExitDesc', { fee: fmtAmount(exitFee), after: fmtAmount(afterFee) }))), h('div', { class: 'card col', style: 'gap:8px' }, h('h4', { style: 'margin:0' }, t('arkCoinsRenewTitle')), - h('p', { class: 'small muted', style: 'margin:0' }, - t('arkCoinsRenewDesc', { fee: renewFee > 0 ? fmtAmount(renewFee) + ' ' + unitLabel() : t('arkDepthFree') })), - h('button', { class: 'btn-primary btn-block', disabled: !!ui.arkBusy, onClick: renew }, t('arkDepthRenewBtn'))), + h('p', { class: 'small muted', style: 'margin:0' }, t('arkCoinsRenewDesc')), + schedule.length ? h('div', { class: 'col', style: 'gap:2px;margin:2px 0' }, + ...schedule.map((r) => h('div', { class: 'row between' }, + h('span', { class: 'small muted' }, r.label), + h('span', { class: 'small' + (r.free ? ' faint' : '') }, r.cost)))) : null, + h('p', { class: 'small faint', style: 'margin:0' }, t('arkCoinsRenewAuto')), + h('button', { class: 'btn-primary btn-block', disabled: !!ui.arkBusy, onClick: renew }, + renewFee > 0 ? t('arkCoinsRenewNowFee', { fee: fmtAmount(renewFee) + ' ' + unitLabel() }) : t('arkDepthRenewBtn'))), h('button', { class: 'btn-ghost btn-block', onClick: back }, t('back'))); } diff --git a/src/i18n.js b/src/i18n.js index ab52fcd..4b5d8eb 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -219,7 +219,17 @@ const STR = { arkCoinsExitTitle: 'Uncooperative 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: 'Renewing asks the server to replace your coins with a single fresh one in its next round (which can be up to an hour out). That flattens the history, restarts the expiry clock, and costs {fee}.', + arkCoinsRenewDesc: 'Renewing asks the server to replace your coins with a single fresh one in its next round (which can be up to an hour out). That flattens the history and restarts the expiry clock. The fee depends on how much lifetime a coin still carries — waiting is cheaper:', + arkCoinsRenewAuto: 'In the final stretch renewal is free, and the wallet does it there automatically. Renewing sooner only buys the cheaper self-custody exit sooner.', + arkCoinsRenewNowFee: 'Renew now for {fee}', + arkCoinsColFee: 'Renew fee', + arkCoinsSpanDay: '1 day', + arkCoinsSpanDays: '{n} days', + arkCoinsSpanHour: '1 hour', + arkCoinsSpanHours: '{n} hours', + arkCoinsSchedOver: 'more than {span} left', + arkCoinsSchedBetween: '{a} to {b} left', + arkCoinsSchedFinal: 'less than {span} left', arkUniUneconomical: 'A trustless on-chain exit would cost about {fee} sats in mining fees — more than the {n} sats it would move, so it isn’t offered. For a balance this size, send it out over Lightning instead: no mining fees at all.', arkUniFallbackBody: 'The server didn’t cooperate, but your money can still move without it: your whole Spending balance exits to Savings on-chain. It takes about a day, and roughly {fee} sats of mining fees will be paid from Savings. Funds land in Savings — send them on from there once it completes.', arkUniFallbackShort: 'Savings holds {have} sats — about {need} sats are needed for the mining fees. Top up Savings first.', |
