Skip to content

feat: LP position management UI for liquidity providers (#1030) - #1049

Merged
Chucks1093 merged 1 commit into
accesslayerorg:devfrom
zainabwahab-eth:feat/1030-lp-position-management
Sep 28, 2026
Merged

Chucks1093 merged 1 commit into
accesslayerorg:devfrom
zainabwahab-eth:feat/1030-lp-position-management

Conversation

@zainabwahab-eth

Copy link
Copy Markdown
Contributor

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:

Concern Built on
Positions, rewards, lock, pool APR GET /lp/positions?wallet= and GET /lp/pool/:keyId from server #980, via a BaseApiService client
Add / claim / remove add_liquidity, claim_lp_rewards, remove_liquidity on the Creator Keys contract (PR #1004), via the same Soroban Client + Signer pattern as governanceContract.service
Wallet / signing Existing useStellarWallet / useSigner (Freighter or Ledger)
Notifications Existing showToast.transactionSuccess (tx hash + Stellar Expert link) and showToast.error
Cache React Query keys added to queryKeys (lp.positions, lp.pool, wallet.xlmBalance)
Formatting Existing bigint-safe formatXlm, bpsToPercent

What'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 with Max, 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 own amount / (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_rewards first. If the contract would pay out 0, the wallet is never prompted (the contract returns Ok(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 unlocksAt is in the future, with a live 2d 14h 32m countdown 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 by lpId. 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 getTransaction returns SUCCESS. The SDK's signAndSend does not throw for an on-chain FAILED, 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 bigint stroops 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/i128 bigints), 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 XDR AccountEntry v0 / 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, LiquidityPositionsSection component tests and a ProfilePage Liquidity-tab integration test.

Verification (local)

  • tsc -b: pass. pnpm lint: pass. pnpm build: pass.
  • All 10 LP-related test files: 134/134 pass (including the existing ProfilePage.integration test).
  • Full pnpm test: 50 files / 131 tests fail, all pre-existing on upstream/dev (78ee248). The baseline run before this change also had 50 failing files. LandingPage.debouncedSearchClear shows up only because it failed to start a worker in the baseline run; it fails 2/2 on a pristine upstream/dev checkout. None of the failing files touch LP code.

Limitations / assumptions (also in docs/LpPositionManagement.md)

  • API shape: server Add performance bond status panel on creator key detail page #980 isn't merged, so the expected response fields are documented in docs/LpPositionManagement.md. The server PR should match them, or this client normaliser needs a small follow-up.
  • Lock period: the LP contract in PR feat(#978): add key subscription status and content access gate #1004 has no lock. The UI honours unlocksAt when the API provides it. Otherwise removal is allowed and the contract decides.
  • Asset: the contract doesn't name the deposited token. XLM (7 decimals) is assumed, like the rest of the app.
  • Entry point: Add liquidity is per existing pool. Opening a first position in a new pool needs an entry point on the creator page (separate issue).

🤖 Generated with Claude Code

…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>
@drips-wave

drips-wave Bot commented Sep 28, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@Chucks1093
Chucks1093 merged commit e970127 into accesslayerorg:dev Sep 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement LP position management UI for liquidity providers

2 participants