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/user-pnl.query.ts b/apps/main/src/llamalend/queries/user-pnl.query.ts new file mode 100644 index 000000000..b5fbefacf --- /dev/null +++ b/apps/main/src/llamalend/queries/user-pnl.query.ts @@ -0,0 +1,42 @@ +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' + +/** + * Query for fetching user PNL data in lend and mint markets. + * 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 + +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..cb3002da2 100644 --- a/apps/main/src/loan/hooks/useLoanPositionDetails.ts +++ b/apps/main/src/loan/hooks/useLoanPositionDetails.ts @@ -1,15 +1,19 @@ 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 { 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' @@ -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,20 @@ 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: userPnl, isLoading: isUserPnlLoading } = useUserPnl({ + chainId, + marketId: llammaId, + userAddress, + loanExists, + hasV2Leverage: v2LeverageEnabled, + }) const { oraclePriceBand } = loanDetails ?? {} const [healthMode, setHealthMode] = useState(DEFAULT_HEALTH_MODE) @@ -155,6 +174,15 @@ export const useLoanPositionDetails = ({ : null, loading: userLoanDetailsLoading ?? true, }, + pnl: v2LeverageEnabled + ? { + 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,