Skip to content

fix(contract): bound withStateRestore retries and handle re-archival - #631

Merged
nonsobethel0-dev merged 2 commits into
Parashield-Protocol:mainfrom
Smoothjane:fix/536-state-restore-retry-limit
Sep 23, 2026
Merged

nonsobethel0-dev merged 2 commits into
Parashield-Protocol:mainfrom
Smoothjane:fix/536-state-restore-retry-limit

Conversation

@Smoothjane

@Smoothjane Smoothjane commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

withStateRestore in src/lib/contract.ts caught a StateArchivedError, restored the footprint, and then called run() a second time outside any try:

await restoreArchivedState(err);
return run(); // unguarded

If the entries were archived again before the retry finished (a race under load), the second StateArchivedError escaped with no handling. Callers (invokeBuyPolicy, invokeSubmitClaim, DepositModal, WithdrawModal) then got a raw archival error carrying a stale restoreXdr, and there was no explicit limit on how many restore rounds could happen.

Changes

src/lib/contract.ts

  • The single unguarded retry is replaced by a bounded restore/retry loop.
  • New optional StateRestoreOptions argument:
    • maxRestores (default 1, the same number of restores as before): the most restore rounds, and so the most extra signature prompts, the user will see.
    • restore: the restore implementation, defaulting to restoreArchivedState. It is injectable so the retry logic can be unit-tested without a live RPC.
  • Once the limit is reached, a re-archived call throws a plain ContractError: "Contract data was archived again after being restored. Please try again in a moment." The last StateArchivedError is kept in details for debugging, and toUserMessage already logs details without showing it to the user.
  • Non-archival errors and restore failures (e.g. the user rejecting the restore signature) are passed through exactly as before, without a retry.
  • Existing call sites need no changes: the new parameter is optional.

src/lib/__tests__/contract.test.ts

New withStateRestore() retry limit (#536) suite:

  • a restore followed by a successful retry returns the result (1 restore, 2 runs);
  • re-archival after the restore gives a ContractError, not a StateArchivedError, with the original error in details;
  • maxRestores: 2 caps the restores at exactly 2 (3 runs);
  • a failed restore is propagated and the operation is not retried.

Verification

Replace the unguarded second run() with a bounded restore/retry loop
(maxRestores, default 1). If state is archived again after a restore,
surface a ContractError with the original error in details instead of
leaking a raw StateArchivedError. Restore is injectable for testing.
Verify successful retry after restore, ContractError on re-archival,
maxRestores cap, and that a failed restore is not retried.
@drips-wave

drips-wave Bot commented Sep 23, 2026

Copy link
Copy Markdown

@Smoothjane Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@netlify

netlify Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

❌ Deploy Preview for boisterous-sunshine-dd4c4c failed.

Name Link
🔨 Latest commit 80eb171
🔍 Latest deploy log https://app.netlify.com/projects/boisterous-sunshine-dd4c4c/deploys/6ab42414f16dd800088c8a70

@nonsobethel0-dev
nonsobethel0-dev merged commit ea6a8a6 into Parashield-Protocol:main Sep 23, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants