Skip to content

useUnlockAssets never invalidates the stellarBalance query cache on success, unlike useLockAssets — stale Available balance shown right after unlocking #138

Description

@prodbycorne

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

  • A successful useUnlockAssets mutation invalidates ['stellarBalance', publicKey], verified via a spy on queryClient.invalidateQueries.
  • After an unlock, useStellarBalance's next read reflects a refetch rather than serving 15-second-stale cached data.
  • A regression test confirms useLockAssets and useUnlockAssets now 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'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.

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 | FWC26Third CampaignCampaign: Third CampaignbugSomething isn't workingfarmFarming/staking flow — deposit, lock, unlock, creditsvery hardExtremely hard — deep expertise, careful design, and significant time required

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions