aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/features/ark.js39
-rw-r--r--src/i18n.js4
2 files changed, 40 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) {
diff --git a/src/i18n.js b/src/i18n.js
index 2409c13..eec6eba 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -509,6 +509,10 @@ const STR = {
giftSourceArk: 'Spending',
giftArkNote: 'Sent from Spending — the link claims instantly, with no fee.',
arkRenewWarn: 'Your Spending balance ({amount}) is too small to renew itself and will expire around {date}. Receiving any amount — or spending it — keeps it alive.',
+ arkRenewForecast: 'Coins renew automatically {when} · fee: {fee}',
+ arkRenewInDays: 'in ~{n} day(s)',
+ arkRenewNow: 'at the next opportunity',
+ feeFree: 'free',
arkBoardStuck: 'Moving money hit a snag and will keep retrying: {error}',
arkRenewWarnPush: 'Your Spending balance is too small to renew itself and expires around {date}. Receive any amount or spend it to keep it alive.',
giftArkOutstanding: '{n} unclaimed instant gift(s)',