aboutsummaryrefslogtreecommitdiffstats
path: root/src/ark/manager.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-08 13:41:24 -0700
committerAdam Soltys <[email protected]>2026-10-08 13:41:24 -0700
commitfcd023fb4b33404eb62afceba0c0232f8b25f200 (patch)
tree217d18025a37035865025a5c88122ccd76c16df9 /src/ark/manager.js
parenta2d9b4aaa2b118e4114e794ddd802c5f12ab02d6 (diff)
downloadcoinosv3-fcd023fb4b33404eb62afceba0c0232f8b25f200.tar.gz
coinosv3-fcd023fb4b33404eb62afceba0c0232f8b25f200.zip
A renewal refused for 'insufficient refresh fee' re-reads the server's fee schedule and re-prices instead of resubmitting the same underpaying set on every sync; if the new fee leaves under 330 sats (or the price didn't change) it fails and frees the coins. Backstop registration re-reads the schedule on the same refusal
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ark/manager.js')
-rw-r--r--src/ark/manager.js34
1 files changed, 33 insertions, 1 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js
index ac63457..7861b43 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -2037,6 +2037,27 @@ export class ArkManager {
return [{ amountSat: action.outAmountSat, userPubkey: this._key(action.outKeyIndex).pubkey }];
}
+ // Re-read the server's fee schedule and re-price a refused refresh at it:
+ // still 'created' (the next sync resubmits) while the output clears the
+ // 330-sat floor, otherwise failed with its inputs released.
+ async _repriceRefresh(action, inputRecs) {
+ this.info = await getArkInfo(this.arkUrl);
+ const tip = await this.chain.tipHeight();
+ const totalSat = inputRecs.reduce((n, v) => n + v.amountSat, 0);
+ const feeSat = this.refreshFee(inputRecs, tip);
+ if (totalSat - feeSat >= 330 && feeSat !== action.feeSat) {
+ action.feeSat = feeSat;
+ action.outAmountSat = totalSat - feeSat;
+ delete action.lastError;
+ } else {
+ for (const v of inputRecs) if (v.state === 'pending') v.state = 'spendable';
+ action.step = 'failed';
+ action.superseded = true; // never began: no history row
+ action.lastError = 'the renewal fee leaves less than the 330-sat minimum';
+ }
+ this._save();
+ }
+
// Ask the server which of a rejected participation's inputs are dead, mark
// those spent, and rebuild the action around the survivors — or fail it and
// release them if nothing changed (the cause isn't a spent input) or what
@@ -2152,6 +2173,13 @@ export class ArkManager {
// was spent elsewhere and this state didn't know. Repair instead of
// retrying the same doomed set on every sync forever (which also
// holds the inputs pending, blocking every future auto-refresh).
+ // The server raised its renewal fee since this wallet read it (an app
+ // left open across a fee change): re-read the schedule and re-price,
+ // or the same underpaying set is refused on every sync forever.
+ if (/insufficient refresh fee/i.test(e.message || '')) {
+ await this._repriceRefresh(action, inputRecs);
+ return;
+ }
if (!/unusable/i.test(e.message || '')) throw e; // transient: retry on next sync
await this._repairRefresh(action, inputRecs);
return;
@@ -2309,7 +2337,11 @@ export class ArkManager {
// One participation per coin. A shared one dies whole the moment any
// single input is spent, taking the untouched coins' cover with it.
try { await this.scheduleRefresh([v.id], v.expiryHeight - marginBlocks); n++; }
- catch (e) { console.warn('ark: no backstop refresh for', v.id, '—', e.message); }
+ catch (e) {
+ // priced from a stale schedule: re-read it so the next pass registers
+ if (/insufficient refresh fee/i.test(e.message || '')) this.info = await getArkInfo(this.arkUrl).catch(() => this.info);
+ console.warn('ark: no backstop refresh for', v.id, '—', e.message);
+ }
}
return n;
}