Overview
src/hooks/useSorobanQuery.ts defines two structurally parallel mutation hooks — useLockAssets and useUnlockAssets — and their success handlers invalidate different sets of queries. useLockAssets:
onSuccess: (result, variables) => {
if (result.success) {
...
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.POOLS] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.USER_POSITION] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.USER_POSITION, variables.poolId] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.USER_POSITION, 'all', publicKey] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.USER_CREDITS, variables.poolId] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.PLATFORM_STATS] });
queryClient.invalidateQueries({ queryKey: ['stellarBalance', publicKey] }); // <-- balance refreshed
}
...
useUnlockAssets:
onSuccess: (result, variables) => {
if (result.success) {
...
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.USER_POSITION, variables.poolId] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.USER_CREDITS, variables.poolId] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.PLATFORM_STATS] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.POOLS] });
// no 'stellarBalance' invalidation anywhere
}
...
Unlocking assets returns principal (and, per computePartialUnlockPreview's premise, the underlying asset) to the user's own Stellar account, directly increasing their spendable balance — exactly the value useStellarBalance (useSorobanQuery.ts:72-80, staleTime: 15000, no refetchInterval) surfaces as "Available balance" in the Deposit modal. Because useUnlockAssets never invalidates ['stellarBalance', publicKey], a user who unlocks assets from Pool A and then immediately opens the Deposit modal for Pool B sees a stale, too-low "Available balance" — reflecting their balance from up to 15 seconds (or longer, until the next unrelated remount/refocus) before the unlock — with no indication anything is out of date. Depending on the stale figure, this can incorrectly trigger exceedsBalance/disable canSubmit (farm/page.tsx:118-121,139-152) for an amount the user can actually now afford, or incorrectly show/hide the isFeeSponsored warning banner (farm/page.tsx:132-136, which is also balance-gated), both directly following from unlockAssets succeeding but the same cache key lockAssets correctly refreshes never being touched.
Requirements
- Add
queryClient.invalidateQueries({ queryKey: ['stellarBalance', publicKey] }) to useUnlockAssets's onSuccess handler, matching useLockAssets.
- Audit
useSetBoost's onSuccess handler for the same gap, since boost changes could plausibly also affect balance-adjacent state depending on contract semantics, and it's the third mutation in this same file with its own independently-maintained invalidation list.
- Consider extracting the common "assets moved, refresh balance + position + credits + platform stats" invalidation set into one shared helper so
useLockAssets/useUnlockAssets/useSetBoost can't independently drift again.
Acceptance Criteria
Additional Notes
More precise references
src/hooks/useSorobanQuery.ts:146-172 (useLockAssets's onSuccess), specifically line 163 (queryClient.invalidateQueries({ queryKey: ['stellarBalance', publicKey] })).
src/hooks/useSorobanQuery.ts:207-239 (useUnlockAssets's onSuccess) — confirmed via full read that no 'stellarBalance' invalidation exists anywhere in this block or the rest of the file's useUnlockAssets definition.
src/hooks/useSorobanQuery.ts:72-80 (useStellarBalance) — confirmed staleTime: 15000 with no refetchInterval, meaning without an explicit invalidation, the balance can only become fresh again via a 15-second staleness window lapsing plus a remount/refocus-triggered refetch, or an unrelated invalidation elsewhere that happens to also touch this key (none do, per the file's full contents).
src/app/farm/page.tsx:104,117-121,132-136,139-152 — confirmed balanceQuery/availableBalance/exceedsBalance/isFeeSponsored/canSubmit in DepositModal all derive from useStellarBalance, i.e. this is the exact, real, user-facing surface affected by the stale value.
Additional edge cases
Implementation sketch
export const useUnlockAssets = () => {
const { walletApi, publicKey } = useStellarWallet();
const queryClient = useQueryClient();
const toast = useToast();
return useMutation({
mutationFn: async ({ poolId, amount }: { poolId: string; amount: string }) => {
if (!walletApi || !publicKey) throw new Error('Wallet not connected');
return sorobanService.unlockAssets(poolId, publicKey, amount, walletApi);
},
onSuccess: (result, variables) => {
if (result.success) {
...
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.POOLS] });
queryClient.invalidateQueries({ queryKey: ['stellarBalance', publicKey] }); // added
}
...
Test/reproduction plan
- Mock
sorobanService.unlockAssets to resolve { success: true, hash: '...' }; trigger useUnlockAssets().mutate(...); spy on queryClient.invalidateQueries; assert it was called with { queryKey: ['stellarBalance', publicKey] } — currently fails (red), passes after the fix.
- Integration test: seed
useStellarBalance's cache with a stale value; perform a mocked successful unlock; assert useStellarBalance triggers a genuine refetch afterward rather than continuing to serve the pre-unlock cached value for the remainder of staleTime.
Cross-references
- No existing issue in the repo's 75-issue history covers this specific cache-invalidation asymmetry between
useLockAssets and useUnlockAssets.
Overview
src/hooks/useSorobanQuery.tsdefines two structurally parallel mutation hooks —useLockAssetsanduseUnlockAssets— and their success handlers invalidate different sets of queries.useLockAssets:useUnlockAssets:Unlocking assets returns principal (and, per
computePartialUnlockPreview's premise, the underlying asset) to the user's own Stellar account, directly increasing their spendable balance — exactly the valueuseStellarBalance(useSorobanQuery.ts:72-80,staleTime: 15000, norefetchInterval) surfaces as "Available balance" in the Deposit modal. BecauseuseUnlockAssetsnever invalidates['stellarBalance', publicKey], a user who unlocks assets from Pool A and then immediately opens the Deposit modal for Pool B sees a stale, too-low "Available balance" — reflecting their balance from up to 15 seconds (or longer, until the next unrelated remount/refocus) before the unlock — with no indication anything is out of date. Depending on the stale figure, this can incorrectly triggerexceedsBalance/disablecanSubmit(farm/page.tsx:118-121,139-152) for an amount the user can actually now afford, or incorrectly show/hide theisFeeSponsoredwarning banner (farm/page.tsx:132-136, which is also balance-gated), both directly following fromunlockAssetssucceeding but the same cache keylockAssetscorrectly refreshes never being touched.Requirements
queryClient.invalidateQueries({ queryKey: ['stellarBalance', publicKey] })touseUnlockAssets'sonSuccesshandler, matchinguseLockAssets.useSetBoost'sonSuccesshandler for the same gap, since boost changes could plausibly also affect balance-adjacent state depending on contract semantics, and it's the third mutation in this same file with its own independently-maintained invalidation list.useLockAssets/useUnlockAssets/useSetBoostcan't independently drift again.Acceptance Criteria
useUnlockAssetsmutation invalidates['stellarBalance', publicKey], verified via a spy onqueryClient.invalidateQueries.useStellarBalance's next read reflects a refetch rather than serving 15-second-stale cached data.useLockAssetsanduseUnlockAssetsnow invalidate an equivalent set of balance-adjacent queries (either identical, or documented if intentionally different).Additional Notes
More precise references
src/hooks/useSorobanQuery.ts:146-172(useLockAssets'sonSuccess), specifically line 163 (queryClient.invalidateQueries({ queryKey: ['stellarBalance', publicKey] })).src/hooks/useSorobanQuery.ts:207-239(useUnlockAssets'sonSuccess) — confirmed via full read that no'stellarBalance'invalidation exists anywhere in this block or the rest of the file'suseUnlockAssetsdefinition.src/hooks/useSorobanQuery.ts:72-80(useStellarBalance) — confirmedstaleTime: 15000with norefetchInterval, meaning without an explicit invalidation, the balance can only become fresh again via a 15-second staleness window lapsing plus a remount/refocus-triggered refetch, or an unrelated invalidation elsewhere that happens to also touch this key (none do, per the file's full contents).src/app/farm/page.tsx:104,117-121,132-136,139-152— confirmedbalanceQuery/availableBalance/exceedsBalance/isFeeSponsored/canSubmitinDepositModalall derive fromuseStellarBalance, i.e. this is the exact, real, user-facing surface affected by the stale value.Additional edge cases
useSetBoost'sonSuccess(useSorobanQuery.ts:273-301) invalidatesUSER_POSITION,USER_CREDITS, andBOOST_CONFIGfor the pool, but — likeuseUnlockAssets— has no'stellarBalance'invalidation. Boost allocation is currently dead/unreachable UI (per Boost allocation is fully wired in the data layer but has no UI — the Boost button is permanently disabled dead code #81/Boost allocation is fully wired in the data layer but has no UI — the Boost button is permanently disabled dead code #111), so this specific instance is lower-priority today, but should be included in whatever shared-invalidation refactor addresses theuseUnlockAssetsgap, so it isn't independently rediscovered once boost UI ships.PoolDetailClient.tsxalso readsuseStellarBalance(:111-112) for its own deposit modal's fee-sponsorship check — the stale-balance consequence of this bug applies there too, not just the Farm page'sDepositModal.Implementation sketch
Test/reproduction plan
sorobanService.unlockAssetsto resolve{ success: true, hash: '...' }; triggeruseUnlockAssets().mutate(...); spy onqueryClient.invalidateQueries; assert it was called with{ queryKey: ['stellarBalance', publicKey] }— currently fails (red), passes after the fix.useStellarBalance's cache with a stale value; perform a mocked successful unlock; assertuseStellarBalancetriggers a genuine refetch afterward rather than continuing to serve the pre-unlock cached value for the remainder ofstaleTime.Cross-references
useLockAssetsanduseUnlockAssets.