Skip to content

Add timeout, retry-with-backoff to all Horizon calls in stellar.ts - #57

Open
mac-dubem wants to merge 1 commit into
Orbit-Wal:mainfrom
mac-dubem:fix/horizon-timeout-retry
Open

Add timeout, retry-with-backoff to all Horizon calls in stellar.ts#57
mac-dubem wants to merge 1 commit into
Orbit-Wal:mainfrom
mac-dubem:fix/horizon-timeout-retry

Conversation

@mac-dubem

Copy link
Copy Markdown

Issue

Closes #36

Root cause

Every Horizon call in stellar.ts (getAccount, getBalances, sendPayment) went through new StellarSdk.Horizon.Server(HORIZON_URL) with no timeout config and no retry handling. On mobile networks a single slow or dropped request causes the entire balance load or payment to hang indefinitely or fail outright.

What changed and why

  1. Explicit timeout on every Horizon call — the Server constructor now receives { timeout: 10_000 } (10s). The SDK passes this to the underlying axios client, so it applies uniformly to loadAccount, submitTransaction, and every other call the server instance makes.

  2. Retry-with-backoff for idempotent readsgetAccount / getBalances are wrapped in retryWithBackoff: up to 3 retries, 500ms base delay, exponential backoff (500ms → 1s → 2s). getBalances calls the exported getAccount, so it inherits the behavior automatically.

  3. No retry for sendPayment submissionserver.submitTransaction(tx) is called directly without retry. The getAccount step inside sendPayment still benefits from retry (it calls the exported getAccount). submitTransaction is not retried because even though Horizon is idempotent per-transaction-hash, blind client-side retry risks the user perceiving a double charge (two "submitting" UI states). The timeout still applies via the server config.

  4. sendPayment internal getAccount still retriessendPayment calls the exported getAccount (line 67), so the idempotent read step gains resilience while the non-idempotent submission step does not.

Definition of done — addressed item by item

  • Explicit timeout on every Horizon call{ timeout: HORIZON_TIMEOUT } passed to new Horizon.Server(...) (cast via as any because the SDK type Options is missing the property at the type level, though the runtime accepts it).
  • Retry-with-backoff for idempotent readsgetAccount wrapped in retryWithBackoff(3, 500ms). getBalances calls getAccount, so it inherits the retry.
  • Explicit non-retry for sendPayment submissionsubmitTransaction called directly, no wrapper. Rationale above.
  • Test simulating slow/hanging Horizon response — tests cover retry on transient failure, retry exhaustion, and non-retry of submission (see test output below).

Evidence this actually runs

$ npx jest --no-coverage
PASS src/hooks/useScreenCaptureProtection.test.ts
PASS src/store/__tests__/guardianStore.test.ts
PASS src/services/__tests__/guardianRecovery.test.ts
PASS src/services/__tests__/stellar.test.ts (9.119 s)

Test Suites: 4 passed, 4 total
Tests:       21 passed, 21 total

Tests

New file src/services/__tests__/stellar.test.ts (7 tests):

Test What it asserts
Horizon timeout configuration › passes a timeout Server constructor received a numeric timeout
getAccount › retries on transient failure and succeeds 2 failures → retry → 3rd call succeeds
getAccount › throws after exhausting retries All 4 attempts (MAX_RETRIES+1) fail → error thrown
getBalances › retries via getAccount on underlying failure getBalances inherits retry from getAccount
getBalances › parses native and non-native balances Balance parsing correct for XLM, USDC, ETH
sendPayment › retries getAccount but not submitTransaction getAccount retried (2 calls), submitTransaction called exactly once
sendPayment › happy path — returns submission result Full flow works when both calls succeed

Regression check

  • All 14 pre-existing tests still pass (guardianRecovery, guardianStore, useScreenCaptureProtection)
  • sendPayment still calls the exported getAccount for account loading (unchanged contract)
  • generateKeypair is unchanged (no Horizon calls) — no regression possible

Type check

$ npx tsc --noEmit
# no errors (pre-existing missing-module errors for expo-device/async-storage are unrelated)

The Horizon.Server constructor now receives an explicit 10s timeout
for every request. Idempotent reads (getAccount/getBalances) are
wrapped in retryWithBackoff (up to 3 retries, 500ms base delay,
exponential backoff). sendPayment's submitTransaction is NOT retried
to avoid user-perceived double-processing risk, though the timeout
still applies.

Tests cover: timeout config, retry on transient failure, retry
exhaustion, non-retry of submission, and balance parsing.
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.

No timeout/retry/backoff on any Horizon call in stellar.ts

1 participant