feat: LP position management UI for liquidity providers (#1030) - #1049
Merged
Chucks1093 merged 1 commit intoSep 28, 2026
Merged
Chucks1093 merged 1 commit into
Chucks1093 merged 1 commit into
Conversation
…g#1030) Adds a Liquidity tab to /profile (read-only on /profile/:wallet) showing active LP positions, total LP earnings, and add / claim / remove controls. - Reads positions and pool APR from the LP API specified in accesslayer-server accesslayerorg#980 (GET /lp/positions, GET /lp/pool/:keyId). - Submits add_liquidity / claim_lp_rewards / remove_liquidity on the Creator Keys contract (accesslayer-contracts accesslayerorg#1002, PR accesslayerorg#1004) through the existing Soroban Client + Signer pattern used by governance votes. - Validates add-liquidity amounts against spendable XLM read from the account ledger entry (net of reserve and selling liabilities), and re-validates against a fresh balance right before signing. - Pool share uses exact integer math, floored to 0.01%. All amounts are bigint stroops end to end. - Claim is simulated first and never prompts the wallet for a zero payout. Removal is blocked with a live countdown while locked, and the lock is re-checked from fresh data before signing. - Success is only reported once getTransaction returns SUCCESS; an on-chain FAILED is surfaced as an error. - Earnings total is de-duplicated by lpId and withheld when any position's rewards are unavailable. Documented in docs/LpPositionManagement.md, including the expected API shape and the known limitations (no lock in the current contract, XLM assumed as the pool asset). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@zainabwahab-eth Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1030
Summary
Adds a Liquidity tab to the Portfolio page (
/profile, and read-only on/profile/:wallet) where liquidity providers can see their active LP positions, total LP earnings, and add liquidity / claim rewards / remove liquidity.No LP code existed in the client, and the backend pieces are still open (contract: accesslayer-contracts #1002 / PR #1004; API: accesslayer-server #980). So this binds to those specified interfaces and reuses the client's existing infrastructure. It does not invent new ones:
GET /lp/positions?wallet=andGET /lp/pool/:keyIdfrom server #980, via aBaseApiServiceclientadd_liquidity,claim_lp_rewards,remove_liquidityon the Creator Keys contract (PR #1004), via the same SorobanClient+Signerpattern asgovernanceContract.serviceuseStellarWallet/useSigner(Freighter or Ledger)showToast.transactionSuccess(tx hash + Stellar Expert link) andshowToast.errorqueryKeys(lp.positions,lp.pool,wallet.xlmBalance)formatXlm,bpsToPercentWhat's included
LP positions list (
LpPositionsList): key name, contributed amount, pool share %, accrued rewards, lock status. Pool share uses exact integer math (contribution / poolTotalLiquidity), floored to 0.01%, so 99.996% never reads 100.00%. Tiny shares read<0.01%. It falls back to the contract's basis points when the pool total is missing.Add liquidity modal (
AddLiquidityDialog): amount input withMax, validation (required, numeric, > 0, ≤ 7 decimals, ≤ spendable XLM), and a reward-rate preview. The preview shows the API's APR estimate and the pool share the deposit would get, using the contract's ownamount / (total + amount)formula. Spendable XLM is read from the account ledger entry over Soroban RPC, net of the account reserve and selling liabilities. The mutation reads the balance again right before signing, so a balance that dropped after the modal opened is caught before the wallet prompt.Claim rewards: simulates
claim_lp_rewardsfirst. If the contract would pay out 0, the wallet is never prompted (the contract returnsOk(0)rather than erroring). On confirmation, positions/pool/balance queries are invalidated and the toast shows the claimed amount returned by the contract. Nothing is subtracted optimistically.Lock-aware removal: Remove is disabled while
unlocksAtis in the future, with a live2d 14h 32mcountdown from one shared, cleaned-up interval (useNowMs). An unparseable lock timestamp fails closed. When a countdown reaches zero, positions are refetched. Before signing, the mutation fetches fresh positions and re-checks the lock, and the contract simulation has the final say. A confirm dialog shows the principal and rewards being returned.Earnings summary (
LpEarningsSummaryCard): unclaimed + claimed across positions, de-duplicated bylpId. The total is withheld ("Unavailable") if any position's rewards couldn't be read, instead of showing a partial sum.Transaction correctness: success is reported only when
getTransactionreturnsSUCCESS. The SDK'ssignAndSenddoes not throw for an on-chainFAILED, so this is checked explicitly. Contract error codes (Error(Contract, #N)) map to readable messages. Wallet rejections show "Transaction cancelled in your wallet."Stale-data recovery: if the contract rejects a claim or removal (nothing to claim, position already closed), the positions query is re-synced immediately instead of waiting for the 30s poll. The removal pre-check fetches through
queryClient.fetchQuery, so a newly discovered lock also updates the list.States: loading skeleton, empty state, error with Retry, a stale-data banner when a refresh fails after a successful load, and a "Connect a Stellar wallet" prompt.
All amounts are
bigintstroops end to end. No floating point is used for money.Tests
utils/lpPositions.utils.test.ts: parsing, lock state, countdown, share (incl. fast-check property: displayed share is the exact floor and never exceeds 100%), amount validation, earnings aggregation (incl. property: total equals sum over distinct positions), contract error mapping.services/lpPositions.service.test.ts: endpoints/params, i128 normalisation, closed/malformed records dropped, API errors.services/lpContract.service.test.ts: exact contract args (Address,u64/i128bigints), simulation errors never reach the wallet, zero-claim never prompts,FAILED/ missing hash never reported as success.services/stellarAccount.service.test.ts: reserve math and real XDRAccountEntryv0 / v1+v2 decoding.hooks/useLpPositions.test.tsx: fresh-balance re-validation, invalid amounts never submitted, invalidation after confirmation, failures don't refresh as success, stale-lock guard on removal.LpPositionsList,AddLiquidityDialog,LiquidityPositionsSectioncomponent tests and aProfilePageLiquidity-tab integration test.Verification (local)
tsc -b: pass.pnpm lint: pass.pnpm build: pass.ProfilePage.integrationtest).pnpm test: 50 files / 131 tests fail, all pre-existing onupstream/dev(78ee248). The baseline run before this change also had 50 failing files.LandingPage.debouncedSearchClearshows up only because it failed to start a worker in the baseline run; it fails 2/2 on a pristineupstream/devcheckout. None of the failing files touch LP code.Limitations / assumptions (also in
docs/LpPositionManagement.md)docs/LpPositionManagement.md. The server PR should match them, or this client normaliser needs a small follow-up.unlocksAtwhen the API provides it. Otherwise removal is allowed and the contract decides.🤖 Generated with Claude Code