aboutsummaryrefslogtreecommitdiffstats
path: root/src/ark/manager.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-26 23:46:07 -0700
committerAdam Soltys <[email protected]>2026-09-26 23:46:07 -0700
commit6c4e3d1fe2bac42f8497488335e808b5b097061a (patch)
treee6b2c52788947a99da3a08831f856d0411b24b1b /src/ark/manager.js
parent93a95d1ce45d09cb833120cd61573fd88eb8ed26 (diff)
downloadcoinosv3-6c4e3d1fe2bac42f8497488335e808b5b097061a.tar.gz
coinosv3-6c4e3d1fe2bac42f8497488335e808b5b097061a.zip
Renewal: a submitted round the server has forgotten is repaired, not asked about forever.
Adam's wallet carried a refresh action from August 31 in step 'submitted' whose unlock hash the server never ran: the round went through under a later submission (another hash), the inputs were spent by it and the 5,233-sat output reached the wallet through the mailbox — but the original action kept asking RoundParticipationStatus on every sync for four weeks, and since this week's coins page shows renewals in flight, it showed up as "in progress" with the raw gRPC error underneath. On "not found" the manager now checks the inputs: all spent means the round ran under another submission and the record is retired (failed, superseded — no history row, no card); none spent means the server dropped it before a round ran, so it is submitted again; a mix goes through the existing input repair. The coins page never prints a server error string; a failed check reads as "the wallet keeps trying". Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
Diffstat (limited to 'src/ark/manager.js')
-rw-r--r--src/ark/manager.js35
1 files changed, 34 insertions, 1 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js
index 7f24b7d..4a0a9c6 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -1920,6 +1920,33 @@ export class ArkManager {
this._save();
}
+ // The server no longer knows the participation we submitted. Either the
+ // round ran under a DIFFERENT submission — a retry from this or another
+ // device minted a new unlock hash, the inputs are spent, and the output
+ // reached the wallet through the mailbox — or it was dropped before any
+ // round ran, and the inputs are still ours to submit again. Left alone,
+ // the action sat "in progress" for weeks, asking on every sync.
+ async _repairSubmitted(action, inputRecs) {
+ const states = await Promise.all(inputRecs.map((v) =>
+ getVtxoStatus(this.arkUrl, this._decoded(v).point.raw, this._keyForVtxo(v).privkey).catch(() => null)));
+ if (states.some((st) => st == null)) return; // couldn't tell today; ask again next sync
+ const spent = inputRecs.filter((v, i) => states[i] === VTXO_STATE_SPENT);
+ if (spent.length === inputRecs.length) {
+ for (const v of inputRecs) v.state = 'spent';
+ action.step = 'failed';
+ action.superseded = true; // not a failure of the renewal, only of this record of it
+ action.lastError = 'superseded: the round ran under another submission';
+ } else if (!spent.length) {
+ action.step = 'created';
+ delete action.unlockHash;
+ delete action.lastError;
+ } else {
+ await this._repairRefresh(action, inputRecs);
+ return;
+ }
+ this._save();
+ }
+
// A refresh claim can be reached from two directions at once — the sync
// loop resuming the action, and a mailbox breadcrumb claiming the moment
// the round is announced. Both are safe server-side (cosign and forfeit are
@@ -1962,7 +1989,13 @@ export class ArkManager {
this._save();
}
if (action.step === 'submitted') {
- const status = await roundParticipationStatus(this.arkUrl, hex.decode(action.unlockHash));
+ let status;
+ try { status = await roundParticipationStatus(this.arkUrl, hex.decode(action.unlockHash)); }
+ catch (e) {
+ if (!/not found/i.test(e.message || '')) throw e; // transient: retry on next sync
+ await this._repairSubmitted(action, inputRecs);
+ return;
+ }
if (status.status === 0 || !status.fundingTx) return; // round pending; retry on next sync
const fundingTx = parseTx(status.fundingTx);
const confirmed = await this.chain.getTxStatus(fundingTx.txid);