diff options
| author | Adam Soltys <[email protected]> | 2026-08-28 18:52:05 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-28 18:52:05 -0700 |
| commit | e082396d99c69adb580ea9cbf5a1be856f1f0d2f (patch) | |
| tree | c19571b1797eff6e3515e6003a7b0dfbf7c6d4cc /src/features | |
| parent | 33a9e4ce070236403218b20985d548a675751fc8 (diff) | |
| download | coinosv3-e082396d99c69adb580ea9cbf5a1be856f1f0d2f.tar.gz coinosv3-e082396d99c69adb580ea9cbf5a1be856f1f0d2f.zip | |
The send flow chooses the road itself: cooperative always goes first, and only when the server actually refuses does the trustless fallback appear — stating up front that the whole balance exits to Savings, how long it takes, and the mining-fee reserve Savings will pay, with the button disabled until Savings can cover it
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/ark.js | 51 |
1 files changed, 39 insertions, 12 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index 0a41abc..953de5a 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1571,6 +1571,22 @@ export function arkFeature(ctx) { .sort((a, b) => a.value - b.value)[0] || null; } + // What a unilateral exit will need from Savings, BEFORE it starts: every + // chain hop is a zero-fee tx bumped by a ~130 vB CPFP child, plus the + // final claim. Priced at today's feerate — the driver reprices per hop. + function estimateExitFeeSat(mgr) { + const feeRate = Math.max(1, (wallet.feeRates && wallet.feeRates.halfHourFee) || 2); + let total = 0; + for (const v of mgr.vtxos().filter((x) => x.state === 'spendable')) { + try { + for (const txi of signedExitTxs(mgr._decoded(v), mgr.serverPub)) + total += Math.ceil((txi.vsize + 130) * feeRate); + total += Math.ceil(150 * feeRate); // the claim tx + } catch {} + } + return total; + } + async function doArkExit() { ui.arkBusy = 'exit'; ui.arkError = ''; render(); try { @@ -1839,18 +1855,29 @@ export function arkFeature(ctx) { if (!sats || sats <= 0 || sats > spendable) { ui.arkError = t('enterValidAmtForN', { n: 1 }); render(); return; } doArkOffboard(sats >= spendable ? 0 : sats, o.address); } }, t('send'))), - // This send IS the cooperative exit. The trustless door stays visible — - // quiet while things work, promoted to a button the moment the server - // fails to cooperate (that being the whole point of a unilateral exit). - ui.arkError && ui.arkBusy !== 'offboard' - ? h('button', { - class: 'btn-block', - onClick: () => { ui.arkOffboardSend = null; ui.arkError = ''; ui.arkExitPage = true; render(); }, - }, t('arkUniOffer')) - : h('button', { - class: 'linklike small', style: 'align-self:center', - onClick: () => { ui.arkOffboardSend = null; ui.arkError = ''; ui.arkExitPage = true; render(); }, - }, t('arkUniOfferQuiet'))); + // This send IS the cooperative exit. The trustless fallback only + // appears when cooperation actually failed — and it states, before + // anything is confirmed, exactly what it does and what the mining + // fees from Savings will be. + (() => { + if (!ui.arkError || ui.arkBusy === 'offboard' || !ark) return null; + let feeEst = 0; + try { feeEst = estimateExitFeeSat(ark); } catch {} + const savings = wallet.spendable || 0; + const short = savings < feeEst; + return h('div', { class: 'col', style: 'gap:8px;border-top:1px solid var(--border,rgba(128,128,128,.2));padding-top:10px' }, + h('div', { class: 'small muted' }, t('arkUniFallbackBody', { fee: fmtAmount(feeEst) })), + short ? h('div', { class: 'small err' }, t('arkUniFallbackShort', { need: fmtAmount(feeEst), have: fmtAmount(savings) })) : null, + h('button', { + class: 'btn-block', disabled: !!ui.arkBusy || short, + onClick: () => { + ui.arkOffboardSend = null; ui.arkError = ''; ui.send = blankSend(); + doArkExit(); + ui.tab = 'history'; // the exit lives as a pending record there + render(); + }, + }, t('arkUniFallbackBtn', { fee: fmtAmount(feeEst) }))); + })()); } async function doArkOffboard(amountSat, destAddress = null) { |
