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:
- Read context/architecture-context.md in full
- Read context/code-standards.md in full
- Read context/progress-tracker.md to understand what has already been built and why, so you do not duplicate or contradict recent work
- Read
src/pages/Sponsors.tsx in full, including the deposit path, so validation is consistent across both tabs
- 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
- 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
- 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.
- Fetch the sponsor's position in
Sponsors.tsx with TanStack Query, matching the existing poolInfo query, including enabled: isConnected.
- 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.
- 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.
- Extend the disabled condition on the submit button to cover both new cases, keeping the existing positive-number guard.
- 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.
- 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.
- Keep validation in sync with the deposit tab so both behave consistently.
- 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.
- 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
Mandatory Checks Before Opening PR
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.
Problem
The withdrawal form on
src/pages/Sponsors.tsxvalidates only that the share amount is a positive number.handleWithdraw()(line 72) guards withif (!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
poolInfoand renderspoolInfo.availableLiquiditytwice, 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.tscalls only/liquidity/overview,/liquidity/deposit, and/liquidity/withdraw. The API exposesGET /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 * sharePricewith 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:
src/pages/Sponsors.tsxin full, including the deposit path, so validation is consistent across both tabssrc/services/sponsors.service.tsandsrc/hooks/useTransaction.tsto follow the existing service and transaction patterns. Do not put API calls in the page filesrc/types/index.tsforPoolInfoand the existing share-related types before adding new onesNote 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
getMySummary()method tosrc/services/sponsors.service.tscallingGET /liquidity/my-summary. Type the response insrc/types/index.ts. Do not call the endpoint from the page.Sponsors.tsxwith TanStack Query, matching the existingpoolInfoquery, includingenabled: isConnected.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.Files To Touch
src/pages/Sponsors.tsxsrc/services/sponsors.service.tssrc/types/index.tssrc/pages/__tests__/Sponsors.test.tsxAcceptance Criteria
availableLiquidityblocks submission with its own distinct messageMandatory Checks Before Opening PR
anytypesPRs 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.