Skip to content

SorobanService issues a fresh simulateTransaction round trip per call with no de-duplication across concurrently-mounted components #90

Description

@prodbycorne

Problem

getFactoryPools, getUserPosition, and calculateUserCredits in src/lib/soroban.ts each independently build a new TransactionBuilder, call simulateTransaction, and parse the result — there is no in-flight-request de-duplication at the SorobanService layer itself. React Query's per-hook caching (usePools, useUserPosition, useUserCredits) prevents duplicate requests for the exact same query key, but useAllUserPositions (src/hooks/useSorobanQuery.ts lines 318-342) calls sorobanService.getUserPosition(pool.id, publicKey) in a Promise.allSettled loop over every pool, and useTotalUserCredits (lines 347-373) does the same for calculateUserCredits — meaning a user with N pools triggers N sequential/parallel full simulate-transaction RPC round trips just to render the Farm page's 'My Earnings' section, none of which share the account-fetch (getAccount) step even though every one of those N calls fetches the same hardcoded simulation account (see the related issue about that hardcoded account) via a fresh getAccount RPC call each time, rather than fetching it once per batch.

Because useAllUserPositions and useTotalUserCredits both run on every pools change and are both refetched independently on their own refetchIntervals (30s and 15s respectively), the number of redundant getAccount + simulateTransaction calls scales linearly with pool count and duplicates across the two hooks, with no batching, no shared account-sequence cache, and no Promise.all-based request coalescing at the SorobanService level itself.

Acceptance Criteria

  • Add a short-lived (e.g. request-scoped or few-second TTL) cache for the simulation account's getAccount result inside SorobanService, so N concurrent calls to getUserPosition/calculateUserCredits within the same tick don't each re-fetch the identical account.
  • Investigate whether getUserPosition and calculateUserCredits can be coalesced into a single contract call (if the pool contract's ABI supports returning both position and credits together) to halve the RPC round trips useAllUserPositions/useTotalUserCredits collectively require per pool.
  • Add a test/metric asserting the number of getAccount/simulateTransaction calls made for a Farm-page render with N pools stays roughly constant (or grows sub-linearly) rather than duplicating per-hook.
  • Document the RPC call budget this produces so future contributors don't accidentally regress it further.

Relevant Files

  • src/lib/soroban.ts — getFactoryPools/getUserPosition/calculateUserCredits (~L705-838) each independently call getAccount + simulateTransaction with no de-duplication
  • src/hooks/useSorobanQuery.ts — useAllUserPositions (~L318) and useTotalUserCredits (~L347) both fan out one RPC round trip per pool, on independent refetch intervals

Further Investigation

Re-read src/lib/soroban.ts and src/hooks/useSorobanQuery.ts directly. Confirmed the pattern exactly as described: server.getAccount(...) followed by server.simulateTransaction(...) appears as an independent pair at (at least) lines ~264/287, ~714/725, ~762/773, ~810/821, ~1051/1061, and ~1181/1191 — six separate call sites, each building its own TransactionBuilder and hitting the RPC fresh, with no shared cache or in-flight-request coalescing anywhere in the file. useAllUserPositions (useSorobanQuery.ts ~L318-342) confirmed to fan out via Promise.allSettled over every pool calling getUserPosition individually, and useTotalUserCredits (~L347-373) does the equivalent for calculateUserCredits — both on independent refetchIntervals (30s/15s per the original body), so the redundant-getAccount problem is real and compounds across both hooks simultaneously, not just within one.

This is correctly labeled spike: the "add a cache" half of the fix is mechanical, but the higher-value question — whether getUserPosition and calculateUserCredits can be coalesced into a single contract call — depends entirely on the deployed contract's ABI/interface, which requires reading the actual Soroban contract source (not part of this frontend repo) or querying its spec on-chain to determine feasibility. That can't be resolved by reading soroban.ts alone; it requires investigation outside this repo's boundary, which is the hallmark of a genuine spike rather than a known fix.

Tradeoff approaches

  1. In-memory TTL cache for getAccount only, scoped to SorobanService. Lowest-risk, addresses the clearly-duplicated, definitely-safe-to-cache half of the problem (the simulation account's sequence number is read-only for simulation purposes and doesn't need to be fresh to the millisecond). A few-second TTL (e.g. keyed by account ID, invalidated after ~2-3s) would collapse N concurrent getAccount calls within one poll tick into one. Doesn't address the simulateTransaction round-trip count at all, so RPC call volume drops but doesn't come close to halving.
  2. Request-scoped promise de-duplication (a Map<key, Promise> in-flight tracker) rather than a time-based cache. Collapses truly-concurrent identical calls (e.g. two components both calling getUserPosition for the same pool in the same tick) into one shared promise, with no staleness risk since it only dedupes calls that are already in flight together, not calls that are merely close in time. Simpler correctness story than a TTL cache, but doesn't help the useAllUserPositions/useTotalUserCredits cross-hook duplication as much as the ABI-coalescing option would, since those two hooks fetch related-but-different data (position vs. credits), not identical calls.
  3. Contract-side coalescing (single call returning both position and credits). Highest potential payoff — literally halves the per-pool RPC round trips this issue describes — but entirely gated on whether the deployed contract's ABI already exposes such a combined view function, or would require a contract change (out of this frontend repo's control, likely a separate team/repo, possibly a multi-week turnaround). This is the piece that most justifies the spike label: the frontend team cannot unilaterally decide this approach is viable without first checking the contract's actual interface.
  4. Batch multiple pools' simulateTransaction calls into fewer RPC round trips via Promise.all batching at the transport layer (if the RPC endpoint or @stellar/stellar-sdk supports batched JSON-RPC requests) rather than changing the contract — worth investigating whether the underlying RPC transport (or a different SDK method) supports request batching independent of the contract-level question in option 3; this could be pursued in parallel since it doesn't require a contract change.

Recommended spike output: a short written finding on (a) whether the contract ABI supports a combined position+credits call [option 3], and (b) whether the RPC transport supports request batching [option 4], each independent of whichever caching layer (option 1 or 2) is implemented as the near-term win.

Test plan

  • Instrument SorobanService (e.g. wrap getAccount/simulateTransaction in a call-counter for tests) and write a test asserting a Farm-page render with N=20 mocked pools produces a call count that stays roughly constant (post-cache) rather than 20x, per the existing acceptance criteria.
  • Unit test for whichever de-duplication mechanism is chosen (TTL cache or in-flight-map): assert two concurrent calls for the same account within the window produce exactly one underlying getAccount RPC call.
  • If contract-side coalescing (option 3) turns out feasible: an integration/contract test verifying the combined call returns data equivalent to the two separate calls it replaces, run against whatever local/test Soroban network this repo already uses for contract-facing tests (check src/lib/soroban.auth.test.ts for the existing pattern).
  • Document (per the existing acceptance criteria) the resulting RPC call budget per N pools, so a future regression is measurable against a concrete number, not just "vibes."

Cross-references

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignOfficial Campaign | FWC26Campaign: Official Campaign | FWC26performanceRendering performance, caching, or bundle sizesorobanSoroban smart-contract integration (XDR, RPC, transaction building)very hardExtremely hard — deep expertise, careful design, and significant time required

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions