aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/ark.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-28 18:52:05 -0700
committerAdam Soltys <[email protected]>2026-08-28 18:52:05 -0700
commite082396d99c69adb580ea9cbf5a1be856f1f0d2f (patch)
treec19571b1797eff6e3515e6003a7b0dfbf7c6d4cc /src/features/ark.js
parent33a9e4ce070236403218b20985d548a675751fc8 (diff)
downloadcoinosv3-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/ark.js')
-rw-r--r--src/features/ark.js51
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) {