From 68bb1f5a20f2e16f9088eeba1a23b48bd68b159e Mon Sep 17 00:00:00 2001 From: JustJousting Date: Thu, 27 Nov 2025 15:08:32 +0200 Subject: [PATCH 1/3] feat: add pnl metric to /loan markets that has leverageV2 --- .../src/lend/entities/user-loan-details.ts | 2 - .../lend/hooks/useBorrowPositionDetails.ts | 26 ++++++++---- apps/main/src/lend/lib/apiLending.ts | 4 +- apps/main/src/lend/types/lend.types.ts | 1 - .../BorrowInformation.tsx | 6 ++- .../BorrowPositionDetails.tsx | 10 ++--- .../queries/market-has-v2-leverage.ts | 22 ++++++++++ .../src/llamalend/queries/user-pnl.query.ts | 41 +++++++++++++++++++ .../src/loan/hooks/useLoanPositionDetails.ts | 30 ++++++++++++++ 9 files changed, 122 insertions(+), 20 deletions(-) create mode 100644 apps/main/src/llamalend/queries/market-has-v2-leverage.ts create mode 100644 apps/main/src/llamalend/queries/user-pnl.query.ts diff --git a/apps/main/src/lend/entities/user-loan-details.ts b/apps/main/src/lend/entities/user-loan-details.ts index a86233c25..d377cafee 100644 --- a/apps/main/src/lend/entities/user-loan-details.ts +++ b/apps/main/src/lend/entities/user-loan-details.ts @@ -26,7 +26,6 @@ type UserLoanDetails = { state: { collateral: string; borrowed: string; debt: string; N: string } status: { label: string; colorKey: HealthColorKey; tooltip: string } leverage: string - pnl: Record } const _getUserLoanDetails = async ({ marketId, userAddress }: UserLoanDetailsQuery): Promise => { @@ -79,7 +78,6 @@ const _getUserLoanDetails = async ({ marketId, userAddress }: UserLoanDetailsQue prices, loss, leverage, - pnl, status: getLiquidationStatus(healthNotFull, isCloseToLiquidation, state.borrowed), } } diff --git a/apps/main/src/lend/hooks/useBorrowPositionDetails.ts b/apps/main/src/lend/hooks/useBorrowPositionDetails.ts index 06e7ac56f..b465193a9 100644 --- a/apps/main/src/lend/hooks/useBorrowPositionDetails.ts +++ b/apps/main/src/lend/hooks/useBorrowPositionDetails.ts @@ -8,6 +8,8 @@ import { ChainId, OneWayMarketTemplate } from '@/lend/types/lend.types' import type { BorrowPositionDetailsProps } from '@/llamalend/features/market-position-details' import { calculateRangeToLiquidation } from '@/llamalend/features/market-position-details/utils' import { calculateLtv } from '@/llamalend/llama.utils' +import { useLoanExists } from '@/llamalend/queries/loan-exists' +import { useUserPnl } from '@/llamalend/queries/user-pnl.query' import type { Address, Chain } from '@curvefi/prices-api' import { useCampaignsByAddress } from '@ui-kit/entities/campaigns' import { useLendingSnapshots } from '@ui-kit/entities/lending-snapshots' @@ -41,14 +43,24 @@ export const useBorrowPositionDetails = ({ bands, health, leverage, - pnl, loss, prices: liquidationPrices, status, state: { collateral, borrowed, debt } = {}, } = userLoanDetails ?? {} const prices = useStore((state) => state.markets.pricesMapper[chainId]?.[marketId]) - + const { data: loanExists } = useLoanExists({ + chainId, + marketId, + userAddress, + }) + const { data: userPnl, isLoading: isUserPnlLoading } = useUserPnl({ + chainId, + marketId, + userAddress, + loanExists, + hasV2Leverage: true, + }) const blockchainId = networks[chainId].id as Chain const { data: campaigns } = useCampaignsByAddress({ blockchainId, address: controller as Address }) const { data: onChainRatesData, isLoading: isOnchainRatesLoading } = useMarketOnChainRates({ @@ -141,11 +153,11 @@ export const useBorrowPositionDetails = ({ loading: !market || isUserLoanDetailsLoading, }, pnl: { - currentProfit: pnl?.currentProfit ? Number(pnl.currentProfit) : null, - currentPositionValue: pnl?.currentPosition ? Number(pnl.currentPosition) : null, - depositedValue: pnl?.deposited ? Number(pnl.deposited) : null, - percentageChange: pnl?.percentage ? Number(pnl.percentage) : null, - loading: !market || isUserLoanDetailsLoading, + currentProfit: userPnl?.currentProfit, + currentPositionValue: userPnl?.currentPosition, + depositedValue: userPnl?.deposited, + percentageChange: userPnl?.percentage, + loading: !market || isUserPnlLoading, }, leverage: { value: leverage ? Number(leverage) : null, diff --git a/apps/main/src/lend/lib/apiLending.ts b/apps/main/src/lend/lib/apiLending.ts index f7136df21..90e3bb55d 100644 --- a/apps/main/src/lend/lib/apiLending.ts +++ b/apps/main/src/lend/lib/apiLending.ts @@ -263,7 +263,7 @@ const user = { .process(async (market) => { const userActiveKey = helpers.getUserActiveKey(api, market) - const [state, healthFull, healthNotFull, range, bands, prices, bandsBalances, oraclePriceBand, leverage, pnl] = + const [state, healthFull, healthNotFull, range, bands, prices, bandsBalances, oraclePriceBand, leverage] = await Promise.all([ market.userState(), market.userHealth(), @@ -274,7 +274,6 @@ const user = { market.userBandsBalances(), market.oraclePriceBand(), market.currentLeverage(signerAddress), - market.currentPnL(signerAddress), ]) // Fetch user loss separately to prevent prices-api dependency from blocking contract read data @@ -311,7 +310,6 @@ const user = { prices, loss, leverage, - pnl, status: getLiquidationStatus(healthNotFull, isCloseToLiquidation, state.borrowed), }, error: '', diff --git a/apps/main/src/lend/types/lend.types.ts b/apps/main/src/lend/types/lend.types.ts index b9ab38c28..67e82c40a 100644 --- a/apps/main/src/lend/types/lend.types.ts +++ b/apps/main/src/lend/types/lend.types.ts @@ -210,7 +210,6 @@ export type UserLoanDetails = { state: { collateral: string; borrowed: string; debt: string; N: string } status: { label: string; colorKey: HealthColorKey; tooltip: string } leverage: string - pnl: Record } | null error: string } diff --git a/apps/main/src/llamalend/features/market-position-details/BorrowInformation.tsx b/apps/main/src/llamalend/features/market-position-details/BorrowInformation.tsx index aaf49002a..02dc5a8bd 100644 --- a/apps/main/src/llamalend/features/market-position-details/BorrowInformation.tsx +++ b/apps/main/src/llamalend/features/market-position-details/BorrowInformation.tsx @@ -164,11 +164,13 @@ export const BorrowInformation = ({ label={t`PNL`} valueOptions={{ unit: 'dollar' }} value={ - pnl?.currentPositionValue && pnl?.currentProfit && pnl?.depositedValue ? pnl?.currentProfit : undefined + pnl?.currentPositionValue && pnl?.currentProfit && pnl?.depositedValue + ? Number(pnl?.currentProfit) + : undefined } change={ pnl?.currentPositionValue && pnl?.percentageChange && pnl?.depositedValue - ? pnl?.percentageChange + ? Number(pnl?.percentageChange) : undefined } loading={pnl?.currentProfit == null && pnl?.loading} diff --git a/apps/main/src/llamalend/features/market-position-details/BorrowPositionDetails.tsx b/apps/main/src/llamalend/features/market-position-details/BorrowPositionDetails.tsx index a38d6452f..8c352686e 100644 --- a/apps/main/src/llamalend/features/market-position-details/BorrowPositionDetails.tsx +++ b/apps/main/src/llamalend/features/market-position-details/BorrowPositionDetails.tsx @@ -14,10 +14,10 @@ export type LiquidationAlert = { hardLiquidation: boolean } export type Pnl = { - currentProfit: number | undefined | null - currentPositionValue: number | undefined | null - depositedValue: number | undefined | null - percentageChange: number | undefined | null + currentProfit: Decimal | undefined + currentPositionValue: Decimal | undefined + depositedValue: Decimal | undefined + percentageChange: Decimal | undefined loading: boolean } export type Health = { value: number | undefined | null; loading: boolean } @@ -69,7 +69,7 @@ export type BorrowPositionDetailsProps = { liquidationAlert: LiquidationAlert health: Health borrowAPY: BorrowAPY - pnl?: Pnl // doesn't exist yet for crvusd + pnl?: Pnl // not all mint markets has PNL data (requires v2 leverage support) liquidationRange: LiquidationRange bandRange: BandRange leverage?: Leverage // doesn't exist yet for crvusd diff --git a/apps/main/src/llamalend/queries/market-has-v2-leverage.ts b/apps/main/src/llamalend/queries/market-has-v2-leverage.ts new file mode 100644 index 000000000..b5cb8d593 --- /dev/null +++ b/apps/main/src/llamalend/queries/market-has-v2-leverage.ts @@ -0,0 +1,22 @@ +import { getLlamaMarket } from '@/llamalend/llama.utils' +import { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets' +import { queryFactory, rootKeys, type MarketParams, type MarketQuery } from '@ui-kit/lib/model' +import { marketIdValidationSuite } from '@ui-kit/lib/model/query/market-id-validation' + +/** + * This query exists to check if v2 leverage exists in a mint market. + * PNL data from llamalend-js for mint markets is currently only avaiable when v2 leverage is enabled. + */ +export const { + useQuery: useHasV2Leverage, + refetchQuery: refetchHasV2Leverage, + getQueryData: getHasV2Leverage, +} = queryFactory({ + queryKey: (params: MarketParams) => [...rootKeys.market(params), 'market-has-v2-leverage'] as const, + queryFn: async ({ marketId }: MarketQuery) => { + const market = getLlamaMarket(marketId) + return market instanceof MintMarketTemplate ? market.leverageV2.hasLeverage() : false + }, + staleTime: '1m', + validationSuite: marketIdValidationSuite, +}) diff --git a/apps/main/src/llamalend/queries/user-pnl.query.ts b/apps/main/src/llamalend/queries/user-pnl.query.ts new file mode 100644 index 000000000..64acdc1ef --- /dev/null +++ b/apps/main/src/llamalend/queries/user-pnl.query.ts @@ -0,0 +1,41 @@ +import { enforce, group, test } from 'vest' +import { getLlamaMarket } from '@/llamalend/llama.utils' +import type { IChainId } from '@curvefi/api/lib/interfaces' +import { type FieldsOf } from '@ui-kit/lib' +import { type MarketQuery, queryFactory, rootKeys, type UserQuery } from '@ui-kit/lib/model' +import { loanExistsValidationGroup } from '@ui-kit/lib/model/query/loan-exists-validation' +import { marketIdValidationSuite } from '@ui-kit/lib/model/query/market-id-validation' +import { createValidationSuite } from '@ui-kit/lib/validation' +import { decimal } from '@ui-kit/utils' + +/** + * PNL data from llamalend-js for mint markets is currently only avaiable when v2 leverage is enabled. + */ +type UserPnlQuery = UserQuery & MarketQuery & { loanExists: boolean; hasV2Leverage: boolean } +type UserPnlParams = FieldsOf + +export const { useQuery: useUserPnl, invalidate: invalidateUserPnl } = queryFactory({ + queryKey: ({ chainId, marketId, userAddress }: UserPnlParams) => + [...rootKeys.userMarket({ chainId, marketId, userAddress }), 'user-pnl'] as const, + queryFn: async ({ marketId, userAddress }: UserPnlQuery) => { + const market = getLlamaMarket(marketId) + + const pnl = await market.currentPnL(userAddress) + return { + currentPosition: decimal(pnl.currentPosition), + deposited: decimal(pnl.deposited), + currentProfit: decimal(pnl.currentProfit), + percentage: decimal(pnl.percentage), + } + }, + staleTime: '1m', + validationSuite: createValidationSuite((params: UserPnlParams) => { + marketIdValidationSuite(params) + loanExistsValidationGroup(params) + group('hasV2LeverageValidation', () => { + test('hasV2Leverage', () => { + enforce(params.hasV2Leverage).isBoolean().equals(true) + }) + }) + }), +}) diff --git a/apps/main/src/loan/hooks/useLoanPositionDetails.ts b/apps/main/src/loan/hooks/useLoanPositionDetails.ts index fc0111f71..375fe4ddc 100644 --- a/apps/main/src/loan/hooks/useLoanPositionDetails.ts +++ b/apps/main/src/loan/hooks/useLoanPositionDetails.ts @@ -1,10 +1,14 @@ import lodash from 'lodash' import { useEffect, useMemo, useState } from 'react' +import { useAccount } from 'wagmi' import { DEFAULT_HEALTH_MODE } from '@/llamalend/constants' import type { BorrowPositionDetailsProps } from '@/llamalend/features/market-position-details' import { calculateRangeToLiquidation } from '@/llamalend/features/market-position-details/utils' import { DEFAULT_BORROW_TOKEN_SYMBOL, getHealthMode } from '@/llamalend/health.util' import { calculateLtv } from '@/llamalend/llama.utils' +import { useLoanExists } from '@/llamalend/queries/loan-exists' +import { useHasV2Leverage } from '@/llamalend/queries/market-has-v2-leverage' +import { useUserPnl } from '@/llamalend/queries/user-pnl.query' import { CRVUSD_ADDRESS } from '@/loan/constants' import { useUserLoanDetails } from '@/loan/hooks/useUserLoanDetails' import networks from '@/loan/networks' @@ -33,6 +37,7 @@ export const useLoanPositionDetails = ({ llammaId, }: UseLoanPositionDetailsProps): BorrowPositionDetailsProps => { const blockchainId = networks[chainId]?.id + const { address: userAddress } = useAccount() const { data: campaigns } = useCampaignsByAddress({ blockchainId, address: llamma?.controller?.toLocaleLowerCase() as Address, @@ -45,6 +50,22 @@ export const useLoanPositionDetails = ({ const userLoanDetailsLoading = useStore((state) => state.loans.userDetailsMapper[llammaId]?.loading) const loanDetails = useStore((state) => state.loans.detailsMapper[llammaId ?? '']) const { healthFull, healthNotFull } = useUserLoanDetails(llammaId) ?? {} + const { data: loanExists } = useLoanExists({ + chainId, + marketId: llammaId, + userAddress, + }) + const { data: hasV2Leverage } = useHasV2Leverage({ + chainId, + marketId: llammaId, + }) + const { data: userPnl, isLoading: isUserPnlLoading } = useUserPnl({ + chainId, + marketId: llammaId, + userAddress, + loanExists, + hasV2Leverage, + }) const { oraclePriceBand } = loanDetails ?? {} const [healthMode, setHealthMode] = useState(DEFAULT_HEALTH_MODE) @@ -155,6 +176,15 @@ export const useLoanPositionDetails = ({ : null, loading: userLoanDetailsLoading ?? true, }, + pnl: hasV2Leverage + ? { + currentProfit: userPnl?.currentProfit, + currentPositionValue: userPnl?.currentPosition, + depositedValue: userPnl?.deposited, + percentageChange: userPnl?.percentage, + loading: isUserPnlLoading ?? true, + } + : undefined, totalDebt: { value: debt ? Number(debt) : null, loading: userLoanDetailsLoading ?? true, From fecc528122ccfdc4525aea712e33d1af14370aa3 Mon Sep 17 00:00:00 2001 From: JustJousting Date: Thu, 27 Nov 2025 15:12:34 +0200 Subject: [PATCH 2/3] refactor: update query comment --- apps/main/src/llamalend/queries/user-pnl.query.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/main/src/llamalend/queries/user-pnl.query.ts b/apps/main/src/llamalend/queries/user-pnl.query.ts index 64acdc1ef..16b09ce6e 100644 --- a/apps/main/src/llamalend/queries/user-pnl.query.ts +++ b/apps/main/src/llamalend/queries/user-pnl.query.ts @@ -9,6 +9,7 @@ import { createValidationSuite } from '@ui-kit/lib/validation' import { decimal } from '@ui-kit/utils' /** + * Query for fetching user PNL data in lend and mint markets. * PNL data from llamalend-js for mint markets is currently only avaiable when v2 leverage is enabled. */ type UserPnlQuery = UserQuery & MarketQuery & { loanExists: boolean; hasV2Leverage: boolean } From e189bab47e5eacb0cc2cc1efa976d52d1512fb30 Mon Sep 17 00:00:00 2001 From: JustJousting Date: Thu, 27 Nov 2025 19:34:27 +0200 Subject: [PATCH 3/3] refactor: review comments --- .../queries/market-has-v2-leverage.ts | 22 ------------------- .../src/llamalend/queries/user-pnl.query.ts | 10 ++++----- .../src/loan/hooks/useLoanPositionDetails.ts | 12 +++++----- 3 files changed, 10 insertions(+), 34 deletions(-) delete mode 100644 apps/main/src/llamalend/queries/market-has-v2-leverage.ts diff --git a/apps/main/src/llamalend/queries/market-has-v2-leverage.ts b/apps/main/src/llamalend/queries/market-has-v2-leverage.ts deleted file mode 100644 index b5cb8d593..000000000 --- a/apps/main/src/llamalend/queries/market-has-v2-leverage.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' -import { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets' -import { queryFactory, rootKeys, type MarketParams, type MarketQuery } from '@ui-kit/lib/model' -import { marketIdValidationSuite } from '@ui-kit/lib/model/query/market-id-validation' - -/** - * This query exists to check if v2 leverage exists in a mint market. - * PNL data from llamalend-js for mint markets is currently only avaiable when v2 leverage is enabled. - */ -export const { - useQuery: useHasV2Leverage, - refetchQuery: refetchHasV2Leverage, - getQueryData: getHasV2Leverage, -} = queryFactory({ - queryKey: (params: MarketParams) => [...rootKeys.market(params), 'market-has-v2-leverage'] as const, - queryFn: async ({ marketId }: MarketQuery) => { - const market = getLlamaMarket(marketId) - return market instanceof MintMarketTemplate ? market.leverageV2.hasLeverage() : false - }, - staleTime: '1m', - validationSuite: marketIdValidationSuite, -}) diff --git a/apps/main/src/llamalend/queries/user-pnl.query.ts b/apps/main/src/llamalend/queries/user-pnl.query.ts index 16b09ce6e..b5fbefacf 100644 --- a/apps/main/src/llamalend/queries/user-pnl.query.ts +++ b/apps/main/src/llamalend/queries/user-pnl.query.ts @@ -10,7 +10,7 @@ import { decimal } from '@ui-kit/utils' /** * Query for fetching user PNL data in lend and mint markets. - * PNL data from llamalend-js for mint markets is currently only avaiable when v2 leverage is enabled. + * PNL data from llamalend-js for mint markets is currently only available when v2 leverage is enabled. */ type UserPnlQuery = UserQuery & MarketQuery & { loanExists: boolean; hasV2Leverage: boolean } type UserPnlParams = FieldsOf @@ -23,10 +23,10 @@ export const { useQuery: useUserPnl, invalidate: invalidateUserPnl } = queryFact const pnl = await market.currentPnL(userAddress) return { - currentPosition: decimal(pnl.currentPosition), - deposited: decimal(pnl.deposited), - currentProfit: decimal(pnl.currentProfit), - percentage: decimal(pnl.percentage), + currentPosition: decimal(pnl?.currentPosition), + deposited: decimal(pnl?.deposited), + currentProfit: decimal(pnl?.currentProfit), + percentage: decimal(pnl?.percentage), } }, staleTime: '1m', diff --git a/apps/main/src/loan/hooks/useLoanPositionDetails.ts b/apps/main/src/loan/hooks/useLoanPositionDetails.ts index 375fe4ddc..cb3002da2 100644 --- a/apps/main/src/loan/hooks/useLoanPositionDetails.ts +++ b/apps/main/src/loan/hooks/useLoanPositionDetails.ts @@ -7,13 +7,13 @@ import { calculateRangeToLiquidation } from '@/llamalend/features/market-positio import { DEFAULT_BORROW_TOKEN_SYMBOL, getHealthMode } from '@/llamalend/health.util' import { calculateLtv } from '@/llamalend/llama.utils' import { useLoanExists } from '@/llamalend/queries/loan-exists' -import { useHasV2Leverage } from '@/llamalend/queries/market-has-v2-leverage' import { useUserPnl } from '@/llamalend/queries/user-pnl.query' import { CRVUSD_ADDRESS } from '@/loan/constants' import { useUserLoanDetails } from '@/loan/hooks/useUserLoanDetails' import networks from '@/loan/networks' import useStore from '@/loan/store/useStore' import { ChainId, Llamma } from '@/loan/types/loan.types' +import { hasV2Leverage } from '@/loan/utils/leverage' import { Address } from '@curvefi/prices-api' import { useCampaignsByAddress } from '@ui-kit/entities/campaigns' import { useCrvUsdSnapshots } from '@ui-kit/entities/crvusd-snapshots' @@ -50,21 +50,19 @@ export const useLoanPositionDetails = ({ const userLoanDetailsLoading = useStore((state) => state.loans.userDetailsMapper[llammaId]?.loading) const loanDetails = useStore((state) => state.loans.detailsMapper[llammaId ?? '']) const { healthFull, healthNotFull } = useUserLoanDetails(llammaId) ?? {} + const v2LeverageEnabled = useMemo(() => hasV2Leverage(llamma ?? null), [llamma]) + const { data: loanExists } = useLoanExists({ chainId, marketId: llammaId, userAddress, }) - const { data: hasV2Leverage } = useHasV2Leverage({ - chainId, - marketId: llammaId, - }) const { data: userPnl, isLoading: isUserPnlLoading } = useUserPnl({ chainId, marketId: llammaId, userAddress, loanExists, - hasV2Leverage, + hasV2Leverage: v2LeverageEnabled, }) const { oraclePriceBand } = loanDetails ?? {} @@ -176,7 +174,7 @@ export const useLoanPositionDetails = ({ : null, loading: userLoanDetailsLoading ?? true, }, - pnl: hasV2Leverage + pnl: v2LeverageEnabled ? { currentProfit: userPnl?.currentProfit, currentPositionValue: userPnl?.currentPosition,