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
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
- 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.
- 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.
- 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.
- 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
Problem
getFactoryPools,getUserPosition, andcalculateUserCreditsinsrc/lib/soroban.tseach independently build a newTransactionBuilder, callsimulateTransaction, and parse the result — there is no in-flight-request de-duplication at theSorobanServicelayer itself. React Query's per-hook caching (usePools,useUserPosition,useUserCredits) prevents duplicate requests for the exact same query key, butuseAllUserPositions(src/hooks/useSorobanQuery.tslines 318-342) callssorobanService.getUserPosition(pool.id, publicKey)in aPromise.allSettledloop over every pool, anduseTotalUserCredits(lines 347-373) does the same forcalculateUserCredits— 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 freshgetAccountRPC call each time, rather than fetching it once per batch.Because
useAllUserPositionsanduseTotalUserCreditsboth run on every pools change and are both refetched independently on their ownrefetchIntervals (30s and 15s respectively), the number of redundantgetAccount+simulateTransactioncalls scales linearly with pool count and duplicates across the two hooks, with no batching, no shared account-sequence cache, and noPromise.all-based request coalescing at theSorobanServicelevel itself.Acceptance Criteria
getAccountresult insideSorobanService, so N concurrent calls togetUserPosition/calculateUserCreditswithin the same tick don't each re-fetch the identical account.getUserPositionandcalculateUserCreditscan 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 tripsuseAllUserPositions/useTotalUserCreditscollectively require per pool.getAccount/simulateTransactioncalls made for a Farm-page render with N pools stays roughly constant (or grows sub-linearly) rather than duplicating per-hook.Relevant Files
src/lib/soroban.ts— getFactoryPools/getUserPosition/calculateUserCredits (~L705-838) each independently call getAccount + simulateTransaction with no de-duplicationsrc/hooks/useSorobanQuery.ts— useAllUserPositions (~L318) and useTotalUserCredits (~L347) both fan out one RPC round trip per pool, on independent refetch intervalsFurther Investigation
Re-read
src/lib/soroban.tsandsrc/hooks/useSorobanQuery.tsdirectly. Confirmed the pattern exactly as described:server.getAccount(...)followed byserver.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 ownTransactionBuilderand 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 viaPromise.allSettledover every pool callinggetUserPositionindividually, anduseTotalUserCredits(~L347-373) does the equivalent forcalculateUserCredits— both on independentrefetchIntervals (30s/15s per the original body), so the redundant-getAccountproblem 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 — whethergetUserPositionandcalculateUserCreditscan 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 readingsoroban.tsalone; it requires investigation outside this repo's boundary, which is the hallmark of a genuine spike rather than a known fix.Tradeoff approaches
getAccountonly, scoped toSorobanService. 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 concurrentgetAccountcalls within one poll tick into one. Doesn't address thesimulateTransactionround-trip count at all, so RPC call volume drops but doesn't come close to halving.Map<key, Promise>in-flight tracker) rather than a time-based cache. Collapses truly-concurrent identical calls (e.g. two components both callinggetUserPositionfor 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 theuseAllUserPositions/useTotalUserCreditscross-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.spikelabel: the frontend team cannot unilaterally decide this approach is viable without first checking the contract's actual interface.simulateTransactioncalls into fewer RPC round trips viaPromise.allbatching at the transport layer (if the RPC endpoint or@stellar/stellar-sdksupports 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
SorobanService(e.g. wrapgetAccount/simulateTransactionin 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.getAccountRPC call.src/lib/soroban.auth.test.tsfor the existing pattern).Cross-references
useSorobanEventsstalls after a failedgetEventscall) and Farm page re-renders its entire pool list and deposit modal on every 5s event-poll tick with no row-level memoization for pools #88 (Farm page re-render storm) both drive how oftenuseAllUserPositions/useTotalUserCreditsrefetch — fixing useSorobanEvents permanently stalls real-time invalidation after a single failed getEvents call #72's backoff reduces spurious invalidation-triggered refetches, and fixing Farm page re-renders its entire pool list and deposit modal on every 5s event-poll tick with no row-level memoization for pools #88's memoization reduces wasted renders per refetch, but neither reduces the per-refetch RPC call count this issue targets; all three are complementary, not overlapping, and should be sequenced with awareness of each other (e.g. fixing useSorobanEvents permanently stalls real-time invalidation after a single failed getEvents call #72 first changes the traffic pattern this issue's before/after call-count test will observe).