Problem Statement
Multi-second blockchain finality lag makes stake modification actions feel sluggish. The current implementation waits for the Soroban transaction to be confirmed (6-30 seconds) before updating the UI, leaving the user staring at a loading spinner with no feedback about the expected outcome. This creates user anxiety and increases the likelihood of duplicate submissions from impatience clicks.
Technical Bounds & Invariants
- Soroban transaction finality: 6-30 seconds (1-5 ledger closes)
- Optimistic update must be revertible if transaction fails
- Staking actions: stake, unstake, restake, delegate, undelegate
- UX rule: user must see the result of their action within 500ms of clicking
- Optimistic state must persist across component remounts (tab navigation)
Codebase Navigation Guide
- Primary target:
/src/hooks/useSorobanStaking.ts
- Current (synchronous) staking hook:
/src/hooks/useSorobanStaking.old.ts (to be replaced)
- Staking UI components:
/src/components/staking/StakingPanel.tsx, /src/components/staking/StakingHistory.tsx
- Transaction builder:
/src/lib/stellar/transaction.ts
Step-by-Step Resolution Blueprint
- Restructure
useSorobanStaking to use a state machine with four states per action: idle -> pending (optimistic) -> confirmed | failed (rollback). Store the optimistic state in a Zustand slice stakingStore that persists to sessionStorage so it survives tab switches
- On
stake(amount): (a) immediately compute the new balance as currentBalance - amount and update the Zustand store, (b) generate a temporary optimisticTxId (UUID v4) to track the operation, (c) call api.staking.submitStake(amount) which returns a real transactionHash, (d) store a mapping optimisticTxId -> realTxHash in the store
- On successful confirmation (listening for
transactionHash via Horizon SSE): mark the optimistic update as confirmed, remove the temporary ID, and show a success toast with a link to the Stellar explorer
- On transaction failure (timeout after 30s or
HostError): (a) revert the balance by adding back the amount, (b) set error: { txId, reason } in the store, (c) show an error toast with the decoded error message, (d) log the event to Sentry with the optimisticTxId for traceability
- Add a
pendingTransactions array to the store exposed as useSorobanStaking().pending; the UI reads this to show a "transaction in flight" indicator (animated pulse) next to each pending action in the staking history table
- Implement a
retry(txId) method that re-submits a failed transaction with the same parameters; on re-submission, re-enter the optimistic state and repeat the lifecycle
- Write a test using
vitest + msw that: (a) calls stake(100), (b) asserts the balance updates immediately (optimistic), (c) simulates a failed on-chain execution, (d) asserts the balance reverts to the original value, (e) calls retry() and asserts the balance updates optimistically again
Problem Statement
Multi-second blockchain finality lag makes stake modification actions feel sluggish. The current implementation waits for the Soroban transaction to be confirmed (6-30 seconds) before updating the UI, leaving the user staring at a loading spinner with no feedback about the expected outcome. This creates user anxiety and increases the likelihood of duplicate submissions from impatience clicks.
Technical Bounds & Invariants
Codebase Navigation Guide
/src/hooks/useSorobanStaking.ts/src/hooks/useSorobanStaking.old.ts(to be replaced)/src/components/staking/StakingPanel.tsx,/src/components/staking/StakingHistory.tsx/src/lib/stellar/transaction.tsStep-by-Step Resolution Blueprint
useSorobanStakingto use a state machine with four states per action:idle -> pending (optimistic) -> confirmed | failed (rollback). Store the optimistic state in a Zustand slicestakingStorethat persists to sessionStorage so it survives tab switchesstake(amount): (a) immediately compute the new balance ascurrentBalance - amountand update the Zustand store, (b) generate a temporaryoptimisticTxId(UUID v4) to track the operation, (c) callapi.staking.submitStake(amount)which returns a realtransactionHash, (d) store a mappingoptimisticTxId -> realTxHashin the storetransactionHashvia Horizon SSE): mark the optimistic update asconfirmed, remove the temporary ID, and show a success toast with a link to the Stellar explorerHostError): (a) revert the balance by adding back the amount, (b) seterror: { txId, reason }in the store, (c) show an error toast with the decoded error message, (d) log the event to Sentry with the optimisticTxId for traceabilitypendingTransactionsarray to the store exposed asuseSorobanStaking().pending; the UI reads this to show a "transaction in flight" indicator (animated pulse) next to each pending action in the staking history tableretry(txId)method that re-submits a failed transaction with the same parameters; on re-submission, re-enter the optimistic state and repeat the lifecyclevitest+mswthat: (a) callsstake(100), (b) asserts the balance updates immediately (optimistic), (c) simulates a failed on-chain execution, (d) asserts the balance reverts to the original value, (e) callsretry()and asserts the balance updates optimistically again