Skip to content

fix: validate withdrawal against share balance and available liquidity #89

Description

@EmeditWeb

Problem

The withdrawal form on src/pages/Sponsors.tsx validates only that the share amount is a positive number. handleWithdraw() (line 72) guards with if (!shares || shares <= 0) return, and the submit button (line 351) is disabled only on !withdrawShares || Number(withdrawShares) <= 0. Nothing checks the amount against the sponsor's actual share balance or against the pool's available liquidity.

The page already has half the data it needs and ignores it. It fetches poolInfo and renders poolInfo.availableLiquidity twice, at lines 158 and 225, purely as display. It never compares the requested withdrawal against it.

The sponsor's own share balance is not fetched at all. src/services/sponsors.service.ts calls only /liquidity/overview, /liquidity/deposit, and /liquidity/withdraw. The API exposes GET /liquidity/my-summary, which carries the caller's position, and nothing in the web app calls it.

What a sponsor experiences today: they type any number, see a confident "Preview Value" computed as shares * sharePrice with no upper bound, click Withdraw, approve a transaction in Freighter, and then watch it fail on-chain. They have signed and submitted a transaction that could never have succeeded. Worse, the preview will happily quote a payout for shares they do not own.

Ground Rules

This issue must be solved according to StepFi's established engineering standards. Before writing any code:

  1. Read context/architecture-context.md in full
  2. Read context/code-standards.md in full
  3. Read context/progress-tracker.md to understand what has already been built and why, so you do not duplicate or contradict recent work
  4. Read src/pages/Sponsors.tsx in full, including the deposit path, so validation is consistent across both tabs
  5. Read src/services/sponsors.service.ts and src/hooks/useTransaction.ts to follow the existing service and transaction patterns. Do not put API calls in the page file
  6. Read src/types/index.ts for PoolInfo and the existing share-related types before adding new ones

Note the repo rules that apply here: no API calls in page files, services only. No hardcoded hex colors, use constants/colors.ts. Every page needs loading, error, and empty states.

Your PR will be rejected regardless of whether CI passes if it conflicts with anything in these files, introduces patterns inconsistent with what already exists, or duplicates functionality that was solved differently elsewhere in the codebase.

What To Build

  1. Add a getMySummary() method to src/services/sponsors.service.ts calling GET /liquidity/my-summary. Type the response in src/types/index.ts. Do not call the endpoint from the page.
  2. Fetch the sponsor's position in Sponsors.tsx with TanStack Query, matching the existing poolInfo query, including enabled: isConnected.
  3. Validate the requested shares against the sponsor's own share balance. Block submission and show an inline field-level message when the request exceeds what they hold.
  4. Validate the estimated payout against poolInfo.availableLiquidity. A withdrawal larger than available liquidity cannot settle even if the sponsor owns the shares. This is a separate check from the balance check and needs its own message, because the two failures need different explanations.
  5. Extend the disabled condition on the submit button to cover both new cases, keeping the existing positive-number guard.
  6. Add a Max button that fills the field with the sponsor's full share balance. Most over-withdrawal attempts are someone trying to exit their whole position and guessing at the number.
  7. Suppress the "Preview Value" block when the input is invalid. Quoting a payout for shares the sponsor does not own is the most misleading part of the current behavior.
  8. Keep validation in sync with the deposit tab so both behave consistently.
  9. Handle loading, error, and empty states for the new query. While the position is loading, the withdraw button must be disabled rather than optimistically enabled. Treat a sponsor with zero shares as an empty state with an explanatory message, not a broken form.
  10. This is client-side guarding for user experience only. It is not a security boundary. The server and the contract remain authoritative, and this PR must not weaken any server-side check on the assumption the client now validates.

Files To Touch

  • src/pages/Sponsors.tsx
  • src/services/sponsors.service.ts
  • src/types/index.ts
  • src/pages/__tests__/Sponsors.test.tsx

Acceptance Criteria

  • Requesting more shares than the sponsor holds blocks submission and shows an inline message
  • A payout exceeding availableLiquidity blocks submission with its own distinct message
  • The submit button is disabled while the position query is loading
  • A Max button fills the sponsor's full share balance
  • The Preview Value block does not render for an invalid amount
  • A sponsor with zero shares sees an empty state, not a usable form
  • No API call is made from the page file
  • No hardcoded hex colors are introduced
  • Loading, error, and empty states handled
  • Tests cover: over-balance, over-liquidity, exact-balance boundary, and zero-share sponsor

Mandatory Checks Before Opening PR

  • All context/ files read and understood
  • Code follows context/code-standards.md exactly
  • context/progress-tracker.md updated with an accurate entry describing the change and why
  • Build passes with zero errors
  • All existing tests still pass, test count has not decreased
  • New tests written covering the new functionality
  • No new any types
  • PR template filled out completely, including the "problem this solves" and "how it was tested" sections
  • PR references this issue number exactly

PRs that fail any check above will be closed without review, regardless of how much work went into them. No exceptions. This is a Grantfox-funded, quality-first project. Code that does not meet these standards will not be merged and will not be paid.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions