diff options
| author | Adam Soltys <[email protected]> | 2026-10-08 13:41:24 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-08 13:41:24 -0700 |
| commit | fcd023fb4b33404eb62afceba0c0232f8b25f200 (patch) | |
| tree | 217d18025a37035865025a5c88122ccd76c16df9 /src | |
| parent | a2d9b4aaa2b118e4114e794ddd802c5f12ab02d6 (diff) | |
| download | coinosv3-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')
| -rw-r--r-- | src/ark/manager.js | 34 |
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; } |
