aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/ark.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-27 11:25:55 -0700
committerAdam Soltys <[email protected]>2026-08-27 11:25:55 -0700
commit838046f049cad53fd45ef435a6dafcb3efbda638 (patch)
treece86fc0de5a8300b68e9b83849552017712d9c1a /src/features/ark.js
parent340cb3704620ec4bf7ebafcd1ac1a1343251c14b (diff)
downloadcoinosv3-838046f049cad53fd45ef435a6dafcb3efbda638.tar.gz
coinosv3-838046f049cad53fd45ef435a6dafcb3efbda638.zip
Renewal becomes a promise you can read: coins now live sixty days (pool coins thirty, not three), the refresh base fee is gone and the brackets quartered, the app renews only the coins that need it inside the free final window — with free rescue even past expiry — and the wallet screen quietly states when the next renewal comes and what it costs, which is now almost always the word "free"
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.js39
1 files changed, 36 insertions, 3 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 59bdb10..77a6f5f 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -1838,6 +1838,11 @@ export function arkFeature(ctx) {
// exposing a refresh button, consolidate-and-renew automatically when a
// vtxo is inside the renewal window or fragmentation builds up. The round
// fee this spends is the cost of keeping the money spendable at all.
+ // The ASP's refresh fee brackets reward renewing LATE — under 96 blocks of
+ // remaining life a renewal is free, and this ASP keeps honoring (and freely
+ // rescuing) a coin even past its expiry, so aiming inside the free window
+ // risks nothing. ~16 hours of margin on mainnet.
+ const renewWindow = () => (getNetwork() === 'regtest' ? 24 : 96);
let arkAutoRefreshAt = 0;
// Set when the balance is too small to renew itself (total minus round fee
// under the server's 330-sat output minimum) AND a vtxo is inside the
@@ -1853,8 +1858,9 @@ export function arkFeature(ctx) {
arkAutoRefreshAt = Date.now();
try {
const tip = await mgr.chain.tipHeight();
- const RENEW_BLOCKS = getNetwork() === 'regtest' ? 24 : 1008; // ~1 week of margin on mainnet
- const expiring = spendables.some((v) => v.expiryHeight && v.expiryHeight - tip < RENEW_BLOCKS);
+ const RENEW_BLOCKS = renewWindow();
+ const expiringCoins = spendables.filter((v) => v.expiryHeight && v.expiryHeight - tip < RENEW_BLOCKS);
+ const expiring = expiringCoins.length > 0;
// Fragmentation alone is no longer worth paying for: multi-input sends
// handle up to 24 coins, and consolidating FRESH coins bills the round
// fee's top ppm bracket (a burst of refund coins after failed payments
@@ -1875,7 +1881,10 @@ export function arkFeature(ctx) {
}
arkRenewWarn = null;
if (!expiring && !fragmented) return;
- await mgr.refresh();
+ // Renew ONLY the coins that need it: dragging far-from-expiry coins
+ // into the round would bill their (higher) fee bracket for nothing.
+ // The rare fragmentation consolidation still sweeps everything.
+ await mgr.refresh(fragmented ? undefined : expiringCoins.map((v) => v.id));
render();
} catch {} // transient — the next throttled attempt retries
}
@@ -2335,6 +2344,30 @@ export function arkFeature(ctx) {
amount: fmtAmount(arkRenewWarn.sat) + ' ' + unitLabel(),
date: new Date(arkRenewWarn.deadlineMs).toLocaleDateString(),
})));
+ // The next automatic renewal — date and price — stated before it
+ // happens. Ark is new to nearly everyone, and a fee nobody announced
+ // reads as money gone missing (it did, once). Usually it says "free".
+ if (ark && ark.state && ark._tipH && !wallet.watchOnly) {
+ const spend = (ark.state.vtxos || []).filter((v) => v.state === 'spendable' && v.expiryHeight);
+ if (spend.length) {
+ const tip = ark._tipH;
+ const RENEW = renewWindow();
+ const minExpiry = Math.min(...spend.map((v) => v.expiryHeight));
+ const blocksUntil = Math.max(0, minExpiry - RENEW - tip);
+ // price it at the moment it will actually run: only the coins then
+ // inside the window ride along, billed at that moment's bracket
+ const simTip = minExpiry - RENEW + 1;
+ const batch = spend.filter((v) => v.expiryHeight - simTip < RENEW);
+ let feeSat = 0;
+ try { feeSat = ark.refreshFee(batch, simTip); } catch {}
+ const days = Math.max(1, Math.round(blocksUntil / 144));
+ out.push(h('div', { class: 'small faint', style: 'margin:10px 0 0;text-align:center' },
+ t('arkRenewForecast', {
+ when: blocksUntil <= 0 ? t('arkRenewNow') : t('arkRenewInDays', { n: days }),
+ fee: feeSat > 0 ? fmtAmount(feeSat) + ' ' + unitLabel() : t('feeFree'),
+ })));
+ }
+ }
return out;
},
decorateTxRow(tx) {