Skip to content

feat: add SDK account sequence and concurrent submission safety system (#277) - #325

Merged
El-swaggerito merged 1 commit into
Axionvera:mainfrom
XxHugheadxX:feat/sequence-safety
Jul 27, 2026
Merged

feat: add SDK account sequence and concurrent submission safety system (#277)#325
El-swaggerito merged 1 commit into
Axionvera:mainfrom
XxHugheadxX:feat/sequence-safety

Conversation

@XxHugheadxX

Copy link
Copy Markdown
Contributor

Closes #277

Summary

Adds the sequence layer the SDK did not have, and gives stale-sequence failures
their own type so they can be told apart from failures that need different
handling.

The gap

Three build paths loaded the source account independently — sendXLM
(src/payments/index.ts:55), sendAsset (:340) and fetchNetworkState
(src/transactions/offline-preparation.ts:316) — with no cache, no freshness
marker and no invalidation. Concurrent intents on one account read the same
sequence and build conflicting envelopes.

Offline preparation makes that structural: it deliberately splits fetching state
from building, and NetworkState stored sequence with nothing recording when
it was read.

Root cause of the unclear failure

classifySubmitError (src/errors/index.ts) collapsed every
result_codes.transaction into TX_FAILED with retryable: false. So
tx_bad_seq — recoverable by rebuilding — was indistinguishable from
tx_insufficient_fee or tx_bad_auth, which are not. The real code survived
only inside the message string, which is the pattern the error standard exists
to remove. No sequence code existed in the registry.

Changes

  • src/account/sequence.ts (new) — SequenceProvider with get,
    refresh, invalidate, peek, loadAccount and withSequence; snapshots
    carry fetchedAt; plus validateSequenceValue and isSequenceStale.
  • src/errors/codes.tsTX_BAD_SEQUENCE added to the registry.
  • src/errors/index.tstx_bad_seq classified before the generic
    collapse; every other result code keeps its existing classification. Adds
    requiresRebuild().
  • src/transactions/offline-preparation.tsNetworkState.fetchedAt;
    manual sequences validated at preparation time; isPreparedSequenceStale();
    opt-in freshness enforcement in buildUnsignedTransaction.
  • docs/sequence-safety.md (new) — alongside retry-policy.md and
    idempotency.md.

Rebuild is not retry

retryable, isRetryableError() and isSafeToRetry() all mean "this same
signed envelope may be sent again". For tx_bad_seq that is false: the sequence
in the envelope is spent, so resubmitting can never succeed. Marking it retryable
would send consumers into a resubmit loop.

TX_BAD_SEQUENCE is therefore retryable: false, and recovery is expressed
through the separate requiresRebuild(error) helper. SubmissionOutcome is
unchanged, so no consumer's exhaustive switch breaks. A test asserts
isSafeToRetry still returns false for this case.

Scope of the concurrency guarantee

withSequence serializes intents for one account using an in-memory promise
chain — a per-process guarantee. It does not coordinate across workers,
containers or machines, and the documentation says so explicitly rather than
implying broader safety.

Sequences are deliberately not pre-allocated. Handing out sequence + 1,
sequence + 2, … would avoid serializing, but one failed submission leaves a gap
and every later transaction becomes permanently invalid. Re-reading after each
use is slower and correct.

Backwards compatibility

  • The provider is additive; no public signature changes.
  • buildUnsignedTransaction(prepared) behaves exactly as before. Freshness
    enforcement requires enforceSequenceFreshness: true, covered by a test.
  • NetworkState.fetchedAt is optional and absent for manual sequences.
  • Behaviour change: tx_bad_seq now reports TX_BAD_SEQUENCE instead of
    TX_FAILED. Consumers branching on TX_FAILED for this case should switch to
    requiresRebuild(); the message text is unchanged. MISSING_SEQUENCE, an
    unregistered string in updateWithNetworkState, also moves to
    TX_BAD_SEQUENCE; it had no test or documentation references.

Tests

31 new tests in tests/sequence-safety.test.ts: caching and freshness;
refresh/invalidate forcing re-reads; maxAgeMs: 0 disabling the cache;
concurrent reads sharing a sequence without serialization (documenting the
hazard) and diverging with it; serialized ordering; a failing intent not
cascading into queued ones; different accounts not blocking each other;
tx_bad_seq classification with every other result code staying on TX_FAILED;
isSafeToRetry staying false; and stale-snapshot detection with enforcement on
and off.

This also adds the first test coverage for offline-preparation.ts, which had
no test file.

Verification

lint, check:circular (41 modules, no cycles) and build all pass.

Full suite: 46 failed · 737 passed · 1 skipped. The 46 failures are
pre-existing on the base commit — verified by running the suite against a clean
checkout of ebd75d1, which gives 46 failed · 706 passed · 1 skipped: same
failures in the same five files. This branch adds exactly the 31 new passing
tests and introduces no regressions.

@El-swaggerito
El-swaggerito merged commit ef46a9a into Axionvera:main Jul 27, 2026
1 check passed
@XxHugheadxX
XxHugheadxX deleted the feat/sequence-safety branch July 28, 2026 00:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SDK account sequence and concurrent submission safety system

2 participants