aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/ark.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-23 07:59:40 -0700
committerAdam Soltys <[email protected]>2026-10-23 07:59:40 -0700
commit6c3b49a559c90954bf5ae6dbfada73ec5b3409cc (patch)
treef0b6acf7e2ddbf22be00002b9c0b86e23c3f64a3 /src/features/ark.js
parentb3cee21d7f6614c4ec0cf462ba4cfe3a3d4e308c (diff)
downloadcoinosv3-6c3b49a559c90954bf5ae6dbfada73ec5b3409cc.tar.gz
coinosv3-6c3b49a559c90954bf5ae6dbfada73ec5b3409cc.zip
Deep coins are renewed under every tier, free of the base fee, before the server's depth cap strands them
A coin's exit depth grows by one per off-chain hop; the server refuses to cosign a spend of a coin at max_vtxo_exit_depth (100), so a wallet paying hourly from one change chain has an unspendable coin within days. Survey of the ASP's 2,279 unspent coins: median depth 7, 154 coins past 40 holding 7.7k sats, five already stuck at the cap. The server now waives the refresh base fee when every input is at its deep_waiver_depth (60; ArkInfo.fees.refresh carries it). The client mirrors that in refreshFee, never selects a capped coin as an input (the shortfall message says why), and flattens deep coins whatever the custody tier: the open app in a round of their own, the worker with a standing order two blocks out per coin. The default tier's copy names the one exception. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src/features/ark.js')
-rw-r--r--src/features/ark.js22
1 files changed, 22 insertions, 0 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 5a8dc61..b155d17 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -3431,10 +3431,32 @@ export function arkFeature(ctx) {
return true;
}
+ // Whatever the tier, a coin whose history has run very deep is renewed:
+ // the server stops cosigning spends of a coin at its depth cap, so this is
+ // what keeps such a coin spendable, and it waives the base fee for a
+ // refresh of deep coins alone — so they go in a round of their own, never
+ // mixed with coins that would make it pay. A wallet paying hourly from one
+ // change chain (a subscription) is the usual way to get here.
+ async function flattenDeepCoins(mgr, tip) {
+ const deep = mgr.deepCoins ? mgr.deepCoins() : [];
+ if (!deep.length) return false;
+ if ((mgr.state.actions || []).some((a) => !['done', 'failed'].includes(a.step))) return false;
+ const feeSat = mgr.refreshFee(deep, tip);
+ const totalSat = deep.reduce((n, v) => n + v.amountSat, 0);
+ if (totalSat - feeSat < 330 || feeSat * 10 > totalSat) return false; // dust, or a server charging full price: left alone
+ await mgr.refresh(deep.map((v) => v.id));
+ render();
+ return true;
+ }
+
async function maybeAutoRefresh(mgr) {
if (wallet.watchOnly || !mgr || !mgr.state) return;
if (Date.now() - arkAutoRefreshAt < 30 * 60_000) return;
const policy = custodyPolicy();
+ if (mgr.deepCoins && mgr.deepCoins().length) {
+ arkAutoRefreshAt = Date.now();
+ try { if (await flattenDeepCoins(mgr, await mgr.chain.tipHeight())) return; } catch {}
+ }
// "coinos holds my coins": nothing renews on its own and nothing warns.
// The server honors a coin past its timelock, and the holder chose this.
if (!policy) { arkRenewWarn = null; return; }