diff --git a/apps/main/next.config.js b/apps/main/next.config.js
index 9019efac4..25a489596 100644
--- a/apps/main/next.config.js
+++ b/apps/main/next.config.js
@@ -65,7 +65,7 @@ const nextConfiguration = {
permanent: false
}, {
source: '/llamalend/:network',
- destination: '/llamalend/ethereum/markets/',
+ destination: '/llamalend/:network/markets/',
permanent: false
}, {
source: '/crvusd/:network',
diff --git a/apps/main/src/lend/components/DetailsUser/components/DetailsUserLoan.tsx b/apps/main/src/lend/components/DetailsUser/components/DetailsUserLoan.tsx
index ecc22314a..43f14f6ea 100644
--- a/apps/main/src/lend/components/DetailsUser/components/DetailsUserLoan.tsx
+++ b/apps/main/src/lend/components/DetailsUser/components/DetailsUserLoan.tsx
@@ -16,6 +16,7 @@ import CellLoanState from '@/lend/components/SharedCellData/CellLoanState'
import CellLoss from '@/lend/components/SharedCellData/CellLoss'
import CellUserMain from '@/lend/components/SharedCellData/CellUserMain'
import { TITLE } from '@/lend/constants'
+import { useBorrowPositionDetails } from '@/lend/hooks/useBorrowPositionDetails'
import { useUserLoanStatus } from '@/lend/hooks/useUserLoanDetails'
import networks from '@/lend/networks'
import useStore from '@/lend/store/useStore'
@@ -25,15 +26,24 @@ import Box from '@ui/Box'
import ListInfoItem, { ListInfoItems, ListInfoItemsWrapper } from '@ui/ListInfo'
import { breakpoints } from '@ui/utils'
import { useUserProfileStore } from '@ui-kit/features/user-profile'
+import { useBetaFlag } from '@ui-kit/hooks/useLocalStorage'
+import { BorrowPositionDetails } from '@ui-kit/shared/ui/PositionDetails/BorrowPositionDetails'
const DetailsUserLoan = (pageProps: PageContentProps) => {
const { rChainId, rOwmId, api, market, titleMapper, userActiveKey } = pageProps
const loanExistsResp = useStore((state) => state.user.loansExistsMapper[userActiveKey])
const chartExpanded = useStore((state) => state.ohlcCharts.chartExpanded)
+ const borrowPositionDetailsProps = useBorrowPositionDetails({
+ chainId: rChainId,
+ market,
+ marketId: rOwmId,
+ userActiveKey,
+ })
const isAdvancedMode = useUserProfileStore((state) => state.isAdvancedMode)
const isSoftLiquidation = useUserLoanStatus(userActiveKey) === 'soft_liquidation'
+ const [isBeta] = useBetaFlag()
const { signerAddress } = api ?? {}
@@ -83,32 +93,36 @@ const DetailsUserLoan = (pageProps: PageContentProps) => {
) : foundLoan ? (
- {isSoftLiquidation && (
+ {!isBeta && isSoftLiquidation && (
)}
-
-
-
+ {isBeta && }
- {/* stats */}
-
- {contents.map((groupedContents, idx) => (
-
- {groupedContents
- .filter(({ show }) => _showContent(show))
- .map(({ titleKey, content }, idx) => (
-
- {content}
-
- ))}
-
- ))}
-
-
-
+ {!isBeta && (
+
+
+
+
+ {/* stats */}
+
+ {contents.map((groupedContents, idx) => (
+
+ {groupedContents
+ .filter(({ show }) => _showContent(show))
+ .map(({ titleKey, content }, idx) => (
+
+ {content}
+
+ ))}
+
+ ))}
+
+
+
+ )}
{/* CHARTS */}
diff --git a/apps/main/src/lend/components/PageVault/Page.tsx b/apps/main/src/lend/components/PageVault/Page.tsx
index 9f7932ff6..89128af89 100644
--- a/apps/main/src/lend/components/PageVault/Page.tsx
+++ b/apps/main/src/lend/components/PageVault/Page.tsx
@@ -8,11 +8,15 @@ import { _getSelectedTab } from '@/lend/components/PageLoanManage/utils'
import Vault from '@/lend/components/PageVault/index'
import PageTitleBorrowSupplyLinks from '@/lend/components/SharedPageStyles/PageTitleBorrowSupplyLinks'
import { useOneWayMarket } from '@/lend/entities/chain'
+import { useBorrowPositionDetails } from '@/lend/hooks/useBorrowPositionDetails'
+import { useLendPositionDetails } from '@/lend/hooks/useLendPositionDetails'
+import { useMarketDetails } from '@/lend/hooks/useMarketDetails'
import useTitleMapper from '@/lend/hooks/useTitleMapper'
import { helpers } from '@/lend/lib/apiLending'
import useStore from '@/lend/store/useStore'
import { Api, type MarketUrlParams, OneWayMarketTemplate, PageContentProps } from '@/lend/types/lend.types'
import { parseMarketParams } from '@/lend/utils/utilsRouter'
+import Stack from '@mui/material/Stack'
import {
AppPageFormContainer,
AppPageFormsWrapper,
@@ -26,8 +30,14 @@ import Tabs, { Tab } from '@ui/Tab'
import { ConnectWalletPrompt, isLoading, useConnection, useWallet } from '@ui-kit/features/connect-wallet'
import { useLayoutStore } from '@ui-kit/features/layout'
import { useUserProfileStore } from '@ui-kit/features/user-profile'
+import { useBetaFlag } from '@ui-kit/hooks/useLocalStorage'
import { t } from '@ui-kit/lib/i18n'
import { REFRESH_INTERVAL } from '@ui-kit/lib/model'
+import { MarketDetails } from '@ui-kit/shared/ui/MarketDetails'
+import { PositionDetails } from '@ui-kit/shared/ui/PositionDetails'
+import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
+
+const { Spacing } = SizesAndSpaces
const Page = (params: MarketUrlParams) => {
const { rMarket, rChainId, rFormType } = parseMarketParams(params)
@@ -48,8 +58,28 @@ const Page = (params: MarketUrlParams) => {
const isAdvancedMode = useUserProfileStore((state) => state.isAdvancedMode)
const rOwmId = market?.id ?? ''
+ const userActiveKey = helpers.getUserActiveKey(api, market!)
const { signerAddress } = api ?? {}
const [isLoaded, setLoaded] = useState(false)
+ const [isBeta] = useBetaFlag()
+
+ const borrowPositionDetails = useBorrowPositionDetails({
+ chainId: rChainId,
+ market: market ?? undefined,
+ marketId: market?.id ?? '',
+ userActiveKey: userActiveKey,
+ })
+ const lendPositionDetails = useLendPositionDetails({
+ chainId: rChainId,
+ market: market,
+ marketId: rOwmId,
+ userActiveKey: userActiveKey,
+ })
+ const marketDetails = useMarketDetails({
+ chainId: rChainId,
+ llamma: market,
+ llammaId: rOwmId,
+ })
// set tabs
const DETAIL_INFO_TYPES: { key: DetailInfoTypes; label: string }[] = [{ label: t`Lend Details`, key: 'market' }]
@@ -100,51 +130,84 @@ const Page = (params: MarketUrlParams) => {
isLoaded,
api,
market,
- userActiveKey: helpers.getUserActiveKey(api, market!),
+ userActiveKey: userActiveKey,
titleMapper,
}
return (
<>
{provider ? (
-
-
- {(!isMdUp || !isAdvancedMode) && }
- {rChainId && rOwmId && }
-
-
- {isAdvancedMode && rChainId && rOwmId && (
-
- {isMdUp && }
-
-
-
-
-
- {DETAIL_INFO_TYPES.map(({ key, label }) => (
- setMarketsStateKey('marketDetailsView', key)}
- >
- {label}
-
- ))}
-
-
-
-
- {selectedTab === 'market' && provider && }
- {selectedTab === 'user' && provider && }
-
-
- )}
-
+ !isBeta ? (
+
+
+ {(!isMdUp || !isAdvancedMode) && }
+ {rChainId && rOwmId && }
+
+
+ {isAdvancedMode && rChainId && rOwmId && (
+
+ {isMdUp && }
+
+
+
+ {isBeta && (
+
+ )}
+
+
+ {DETAIL_INFO_TYPES.map(({ key, label }) => (
+ setMarketsStateKey('marketDetailsView', key)}
+ >
+ {label}
+
+ ))}
+
+
+
+
+ {selectedTab === 'market' && provider && }
+ {selectedTab === 'user' && provider && }
+
+
+ )}
+
+ ) : (
+ // New design layout, only in beta for now
+
+ {rChainId && rOwmId && }
+
+
+
+
+
+
+ )
) : (
['prices-api', 'supported-chains'] as const,
+ queryFn: getSupportedChainsFromApi,
+ staleTime: '1d',
+ validationSuite: EmptyValidationSuite,
+})
+
+export const { fetchQuery: fetchSupportedLendingChains, setQueryData: setSupportedLendingChains } = queryFactory({
+ queryKey: () => ['prices-api', 'supported-lending-chains'] as const,
+ queryFn: getChains,
+ staleTime: '1d',
+ validationSuite: EmptyValidationSuite,
+})
diff --git a/apps/main/src/lend/entities/market-collateral-value.ts b/apps/main/src/lend/entities/market-collateral-value.ts
new file mode 100644
index 000000000..80d78e569
--- /dev/null
+++ b/apps/main/src/lend/entities/market-collateral-value.ts
@@ -0,0 +1,63 @@
+import { USE_API } from '@/lend/shared/config'
+import { ChainId } from '@/lend/types/lend.types'
+import { requireLib } from '@ui-kit/features/connect-wallet'
+import { FieldsOf } from '@ui-kit/lib'
+import { queryFactory } from '@ui-kit/lib/model/query'
+import type { ChainQuery } from '@ui-kit/lib/model/query'
+import { llamaApiValidationSuite } from '@ui-kit/lib/model/query/curve-api-validation'
+
+type MarketQuery = ChainQuery & { marketId: string }
+type MarketParams = FieldsOf
+
+type MarketCollateralValue = {
+ totalUsdValue: number
+ collateral: {
+ amount: number
+ usdRate: number
+ usdValue: number
+ }
+ borrowed: {
+ amount: number
+ usdRate: number
+ usdValue: number
+ }
+}
+
+/**
+ * The purpose of this query is to allow fetching market collateral values on chain
+ * in order to display the most current data when a wallet is connected.
+ * */
+const _getMarketCollateralValue = async ({ marketId }: MarketQuery): Promise => {
+ const api = requireLib('llamaApi')
+ const market = api.getLendMarket(marketId)
+ const { collateral_token, borrowed_token } = market
+ const [ammBalance, collateralUsdRate, borrowedUsdRate] = await Promise.all([
+ market.stats.ammBalances(false, USE_API),
+ api.getUsdRate(collateral_token.address),
+ api.getUsdRate(borrowed_token.address),
+ ])
+ const borrowedUsd = +ammBalance.borrowed * +borrowedUsdRate
+ const collateralUsd = +ammBalance.collateral * +collateralUsdRate
+ const total = +borrowedUsd + +collateralUsd
+ return {
+ totalUsdValue: total,
+ collateral: {
+ amount: +ammBalance.collateral,
+ usdRate: collateralUsdRate,
+ usdValue: collateralUsd,
+ },
+ borrowed: {
+ amount: +ammBalance.borrowed,
+ usdRate: borrowedUsdRate,
+ usdValue: borrowedUsd,
+ },
+ }
+}
+
+export const { useQuery: useMarketCollateralValue } = queryFactory({
+ queryKey: (params: MarketParams) =>
+ ['marketCollateralValue', { chainId: params.chainId }, { marketId: params.marketId }] as const,
+ queryFn: _getMarketCollateralValue,
+ refetchInterval: '1m',
+ validationSuite: llamaApiValidationSuite,
+})
diff --git a/apps/main/src/lend/entities/market-price-per-share.ts b/apps/main/src/lend/entities/market-price-per-share.ts
new file mode 100644
index 000000000..8df760575
--- /dev/null
+++ b/apps/main/src/lend/entities/market-price-per-share.ts
@@ -0,0 +1,26 @@
+import { ChainId } from '@/lend/types/lend.types'
+import { requireLib } from '@ui-kit/features/connect-wallet'
+import { FieldsOf } from '@ui-kit/lib'
+import type { ChainQuery } from '@ui-kit/lib/model/query'
+import { queryFactory } from '@ui-kit/lib/model/query'
+import { llamaApiValidationSuite } from '@ui-kit/lib/model/query/curve-api-validation'
+
+type MarketQuery = ChainQuery & { marketId: string }
+type MarketParams = FieldsOf
+
+const _fetchMarketPricePerShare = async ({ marketId }: MarketQuery) => {
+ const api = requireLib('llamaApi')
+ const market = api.getLendMarket(marketId)
+ return await market.vault.previewRedeem(1)
+}
+
+/**
+ * Fetches the price per share of a market on chain
+ */
+export const { useQuery: useMarketPricePerShare, invalidate: invalidateMarketPricePerShare } = queryFactory({
+ queryKey: (params: MarketParams) =>
+ ['marketPricePerShare', { chainId: params.chainId }, { marketId: params.marketId }] as const,
+ queryFn: _fetchMarketPricePerShare,
+ refetchInterval: '1m',
+ validationSuite: llamaApiValidationSuite,
+})
diff --git a/apps/main/src/lend/hooks/useBorrowPositionDetails.ts b/apps/main/src/lend/hooks/useBorrowPositionDetails.ts
new file mode 100644
index 000000000..95a23179a
--- /dev/null
+++ b/apps/main/src/lend/hooks/useBorrowPositionDetails.ts
@@ -0,0 +1,129 @@
+import meanBy from 'lodash/meanBy'
+import { useMemo } from 'react'
+import { useMarketOnChainRates } from '@/lend/entities/market-onchain-rate'
+import networks from '@/lend/networks'
+import useStore from '@/lend/store/useStore'
+import { ChainId, OneWayMarketTemplate } from '@/lend/types/lend.types'
+import type { Address, Chain } from '@curvefi/prices-api'
+import { useLendingSnapshots } from '@ui-kit/entities/lending-snapshots'
+import { useTokenUsdRate } from '@ui-kit/lib/model/entities/token-usd-rate'
+import { BorrowPositionDetailsProps } from '@ui-kit/shared/ui/PositionDetails/BorrowPositionDetails'
+
+type UseBorrowPositionDetailsProps = {
+ chainId: ChainId
+ market: OneWayMarketTemplate | null | undefined
+ marketId: string
+ userActiveKey: string
+}
+
+export const useBorrowPositionDetails = ({
+ chainId,
+ market,
+ marketId,
+ userActiveKey,
+}: UseBorrowPositionDetailsProps): BorrowPositionDetailsProps => {
+ const userLoanDetailsResp = useStore((state) => state.user.loansDetailsMapper[userActiveKey])
+ const isFetchingAll = useStore((state) => state.markets.isFetchingAll)
+ const marketRate = useStore((state) => state.markets.ratesMapper[chainId]?.[marketId])
+ const prices = useStore((state) => state.markets.pricesMapper[chainId]?.[marketId])
+
+ const { data: onChainRatesData, isLoading: isOnchainRatesLoading } = useMarketOnChainRates({
+ chainId: chainId,
+ marketId,
+ })
+ const { data: collateralUsdRate, isLoading: collateralUsdRateLoading } = useTokenUsdRate({
+ chainId: chainId,
+ tokenAddress: market?.addresses?.collateral_token,
+ })
+ const { data: borrowedUsdRate, isLoading: borrowedUsdRateLoading } = useTokenUsdRate({
+ chainId: chainId,
+ tokenAddress: market?.addresses?.borrowed_token,
+ })
+
+ const { details: userLoanDetails } = userLoanDetailsResp ?? {}
+
+ const { data: lendSnapshots, isLoading: isLendSnapshotsLoading } = useLendingSnapshots({
+ blockchainId: networks[chainId].id as Chain,
+ contractAddress: market?.addresses?.controller as Address,
+ })
+
+ const thirtyDayAvgRate = useMemo(() => {
+ if (!lendSnapshots) return null
+
+ const thirtyDaysAgo = new Date()
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
+
+ const recentSnapshots = lendSnapshots.filter((snapshot) => new Date(snapshot.timestamp) > thirtyDaysAgo)
+
+ if (recentSnapshots.length === 0) return null
+
+ return meanBy(recentSnapshots, ({ borrowApy }) => borrowApy) * 100
+ }, [lendSnapshots])
+
+ const borrowApy = onChainRatesData?.rates?.borrowApy ?? marketRate?.rates?.borrowApy
+
+ const collateralTotalValue = useMemo(() => {
+ if (!collateralUsdRate || !userLoanDetails?.state?.collateral) return null
+ return (
+ Number(userLoanDetails?.state?.collateral) * Number(collateralUsdRate) + Number(userLoanDetails?.state?.borrowed)
+ )
+ }, [userLoanDetails?.state?.collateral, userLoanDetails?.state?.borrowed, collateralUsdRate])
+
+ return {
+ isSoftLiquidation: userLoanDetails?.status?.colorKey === 'soft_liquidation',
+ health: {
+ value: Number(userLoanDetails?.healthFull),
+ loading: isFetchingAll ?? true,
+ },
+ borrowAPR: {
+ value: borrowApy != null ? Number(borrowApy) : null,
+ thirtyDayAvgRate: thirtyDayAvgRate,
+ loading: isOnchainRatesLoading || isLendSnapshotsLoading || !market?.addresses.controller,
+ },
+ liquidationRange: {
+ value: userLoanDetails?.prices ? userLoanDetails.prices.map(Number) : null,
+ rangeToLiquidation:
+ prices?.prices?.oraclePrice && userLoanDetails?.prices
+ ? (Number(userLoanDetails?.prices?.[1]) / Number(prices.prices.oraclePrice)) * 100
+ : null,
+ loading: isFetchingAll ?? true,
+ },
+ bandRange: {
+ value: userLoanDetails?.bands ? userLoanDetails.bands : null,
+ loading: isFetchingAll ?? true,
+ },
+ collateralValue: {
+ totalValue: collateralTotalValue,
+ collateral: {
+ value: userLoanDetails?.state?.collateral ? Number(userLoanDetails.state.collateral) : null,
+ usdRate: collateralUsdRate ?? null,
+ symbol: market?.collateral_token?.symbol,
+ },
+ borrow: {
+ value: userLoanDetails?.state?.borrowed ? Number(userLoanDetails.state.borrowed) : null,
+ usdRate: borrowedUsdRate ?? null,
+ symbol: market?.borrowed_token?.symbol,
+ },
+ loading: isFetchingAll || collateralUsdRateLoading || borrowedUsdRateLoading,
+ },
+ ltv: {
+ value: collateralTotalValue ? (Number(userLoanDetails?.state?.debt) / collateralTotalValue) * 100 : null,
+ loading: isFetchingAll ?? true,
+ },
+ pnl: {
+ currentProfit: userLoanDetails?.pnl?.currentProfit ? Number(userLoanDetails.pnl.currentProfit) : null,
+ currentPositionValue: userLoanDetails?.pnl?.currentPosition ? Number(userLoanDetails.pnl.currentPosition) : null,
+ depositedValue: userLoanDetails?.pnl?.deposited ? Number(userLoanDetails.pnl.deposited) : null,
+ percentageChange: userLoanDetails?.pnl?.percentage ? Number(userLoanDetails.pnl.percentage) : null,
+ loading: isFetchingAll ?? true,
+ },
+ leverage: {
+ value: userLoanDetails?.leverage ? Number(userLoanDetails.leverage) : null,
+ loading: isFetchingAll ?? true,
+ },
+ totalDebt: {
+ value: userLoanDetails?.state?.debt ? Number(userLoanDetails.state.debt) : null,
+ loading: isFetchingAll ?? true,
+ },
+ }
+}
diff --git a/apps/main/src/lend/hooks/useLendPositionDetails.ts b/apps/main/src/lend/hooks/useLendPositionDetails.ts
new file mode 100644
index 000000000..c18e6894e
--- /dev/null
+++ b/apps/main/src/lend/hooks/useLendPositionDetails.ts
@@ -0,0 +1,87 @@
+import { meanBy } from 'lodash'
+import { useMemo } from 'react'
+import { useMarketOnChainRates } from '@/lend/entities/market-onchain-rate'
+import { useMarketPricePerShare } from '@/lend/entities/market-price-per-share'
+import networks from '@/lend/networks'
+import useStore from '@/lend/store/useStore'
+import { ChainId, OneWayMarketTemplate } from '@/lend/types/lend.types'
+import type { Address, Chain } from '@curvefi/prices-api'
+import { useLendingSnapshots } from '@ui-kit/entities/lending-snapshots'
+import { useTokenUsdRate } from '@ui-kit/lib/model/entities/token-usd-rate'
+import { LendPositionDetailsProps } from '@ui-kit/shared/ui/PositionDetails/LendPositionDetails'
+
+type UseLendPositionDetailsProps = {
+ chainId: ChainId
+ market: OneWayMarketTemplate | null | undefined
+ marketId: string
+ userActiveKey: string
+}
+
+export const useLendPositionDetails = ({
+ chainId,
+ market,
+ marketId,
+ userActiveKey,
+}: UseLendPositionDetailsProps): LendPositionDetailsProps => {
+ const userBalancesResp = useStore((state) => state.user.marketsBalancesMapper[userActiveKey]) // TODO: add loading state
+ const { data: onChainRatesData, isLoading: isOnchainRatesLoading } = useMarketOnChainRates({
+ chainId: chainId,
+ marketId,
+ })
+ const { data: marketPricePerShare, isLoading: isMarketPricePerShareLoading } = useMarketPricePerShare({
+ chainId: chainId,
+ marketId,
+ })
+ const { data: lentAssetUsdRate, isLoading: lentAssetUsdRateLoading } = useTokenUsdRate({
+ chainId: chainId,
+ tokenAddress: market?.addresses?.borrowed_token,
+ })
+ const { data: lendSnapshots, isLoading: isLendSnapshotsLoading } = useLendingSnapshots({
+ blockchainId: networks[chainId].id as Chain,
+ contractAddress: market?.addresses?.controller as Address,
+ })
+
+ const thirtyDayAvgLendApr = useMemo(() => {
+ if (!lendSnapshots) return null
+
+ const thirtyDaysAgo = new Date()
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
+
+ const recentSnapshots = lendSnapshots.filter((snapshot) => new Date(snapshot.timestamp) > thirtyDaysAgo)
+
+ if (recentSnapshots.length === 0) return null
+
+ return meanBy(recentSnapshots, ({ lendApr }) => lendApr) * 100
+ }, [lendSnapshots])
+
+ return {
+ lendingAPY: {
+ value: onChainRatesData?.rates?.lendApy ? Number(onChainRatesData.rates.lendApy) : null,
+ thirtyDayAvgRate: thirtyDayAvgLendApr,
+ loading: isOnchainRatesLoading || isLendSnapshotsLoading,
+ },
+ shares: {
+ value: userBalancesResp?.vaultShares
+ ? Number(userBalancesResp.vaultShares) + Number(userBalancesResp.gauge)
+ : null,
+ staked: userBalancesResp?.gauge ? Number(userBalancesResp.gauge) : null,
+ loading: false,
+ },
+ lentAsset: {
+ symbol: market?.collateral_token.symbol,
+ address: market?.collateral_token.address,
+ usdRate: lentAssetUsdRate,
+ depositedAmount:
+ marketPricePerShare && userBalancesResp?.vaultShares && userBalancesResp?.gauge
+ ? +marketPricePerShare * (Number(userBalancesResp.vaultShares) + Number(userBalancesResp.gauge))
+ : null,
+ depositedUsdValue:
+ lentAssetUsdRate && marketPricePerShare && userBalancesResp?.vaultShares && userBalancesResp?.gauge
+ ? +marketPricePerShare *
+ (Number(userBalancesResp.vaultShares) + Number(userBalancesResp.gauge)) *
+ lentAssetUsdRate
+ : null,
+ loading: isMarketPricePerShareLoading || lentAssetUsdRateLoading,
+ },
+ }
+}
diff --git a/apps/main/src/lend/hooks/useMarketDetails.tsx b/apps/main/src/lend/hooks/useMarketDetails.tsx
new file mode 100644
index 000000000..ad281d3b4
--- /dev/null
+++ b/apps/main/src/lend/hooks/useMarketDetails.tsx
@@ -0,0 +1,83 @@
+import meanBy from 'lodash/meanBy'
+import { useMemo } from 'react'
+import { useMarketCollateralValue } from '@/lend/entities/market-collateral-value'
+import { useMarketOnChainRates } from '@/lend/entities/market-onchain-rate'
+import { networks } from '@/lend/networks'
+import useStore from '@/lend/store/useStore'
+import type { ChainId, OneWayMarketTemplate } from '@/lend/types/lend.types'
+import type { Chain, Address } from '@curvefi/prices-api'
+import { useLendingSnapshots } from '@ui-kit/entities/lending-snapshots'
+import { MarketDetailsProps } from '@ui-kit/shared/ui/MarketDetails'
+
+type UseMarketDetailsProps = {
+ chainId: ChainId
+ llamma: OneWayMarketTemplate | null | undefined
+ llammaId: string
+}
+
+export const useMarketDetails = ({ chainId, llamma, llammaId }: UseMarketDetailsProps): MarketDetailsProps => {
+ const capAndAvailable = useStore((state) => state.markets.statsCapAndAvailableMapper[chainId]?.[llammaId])
+ const maxLeverage = useStore((state) => state.markets.maxLeverageMapper[chainId]?.[llammaId])
+ const { data: collateralValue, isLoading: isCollateralValueLoading } = useMarketCollateralValue({
+ marketId: llammaId,
+ })
+ const { data: onChainRates, isLoading: isOnChainRatesLoading } = useMarketOnChainRates({ marketId: llammaId })
+ const { data: lendingSnapshots, isLoading: isSnapshotsLoading } = useLendingSnapshots({
+ blockchainId: networks[chainId as keyof typeof networks]?.id as Chain,
+ contractAddress: llamma?.addresses.controller as Address,
+ })
+
+ const thirtyDayAvgRates = useMemo(() => {
+ if (!lendingSnapshots) return null
+
+ const thirtyDaysAgo = new Date()
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
+
+ const recentSnapshots = lendingSnapshots.filter((snapshot) => new Date(snapshot.timestamp) > thirtyDaysAgo)
+
+ if (recentSnapshots.length === 0) return null
+
+ return {
+ borrowApyAvg: meanBy(recentSnapshots, ({ borrowApy }) => borrowApy) * 100,
+ lendApyAvg: meanBy(recentSnapshots, ({ lendApy }) => lendApy) * 100,
+ }
+ }, [lendingSnapshots])
+
+ return {
+ collateral: {
+ symbol: llamma?.collateral_token.symbol ?? null,
+ tokenAddress: llamma?.collateral_token.address,
+ total: collateralValue?.collateral.amount ?? null,
+ totalUsdValue: collateralValue?.totalUsdValue ?? null,
+ usdRate: collateralValue?.collateral.usdRate ?? null,
+ loading: isCollateralValueLoading,
+ },
+ borrowToken: {
+ symbol: llamma?.borrowed_token.symbol ?? null,
+ tokenAddress: llamma?.borrowed_token.address,
+ total: collateralValue?.borrowed.amount ?? null,
+ totalUsdValue: collateralValue?.borrowed.usdValue ?? null,
+ usdRate: collateralValue?.borrowed.usdRate ?? null,
+ loading: isCollateralValueLoading,
+ },
+ borrowAPR: {
+ value: onChainRates?.rates.borrowApr ? Number(onChainRates.rates.borrowApr) : null,
+ thirtyDayAvgRate: thirtyDayAvgRates?.borrowApyAvg ?? null,
+ loading: isSnapshotsLoading || (isOnChainRatesLoading ?? true),
+ },
+ lendingAPY: {
+ value: onChainRates?.rates.lendApy ? Number(onChainRates.rates.lendApy) : null,
+ thirtyDayAvgRate: thirtyDayAvgRates?.lendApyAvg ?? null,
+ loading: isSnapshotsLoading || (isOnChainRatesLoading ?? true),
+ },
+ availableLiquidity: {
+ value: capAndAvailable?.available ? Number(capAndAvailable.available) : null,
+ max: capAndAvailable?.cap ? Number(capAndAvailable.cap) : null,
+ loading: false, // to do: set up loading state
+ },
+ maxLeverage: {
+ value: maxLeverage ? Number(maxLeverage) : null,
+ loading: false, // to do: set up loading state
+ },
+ }
+}
diff --git a/apps/main/src/lend/store/createMarketsSlice.ts b/apps/main/src/lend/store/createMarketsSlice.ts
index 5126afe9b..e322b7e11 100644
--- a/apps/main/src/lend/store/createMarketsSlice.ts
+++ b/apps/main/src/lend/store/createMarketsSlice.ts
@@ -25,6 +25,7 @@ import { getLib } from '@ui-kit/features/connect-wallet'
type StateKey = keyof typeof DEFAULT_STATE
type SliceState = {
+ isFetchingAll: boolean
statsParametersMapper: { [chainId: string]: MarketsStatsParametersMapper }
statsBandsMapper: { [chainId: string]: MarketsStatsBandsMapper }
statsTotalsMapper: { [chainId: string]: MarketsStatsTotalsMapper }
@@ -62,6 +63,7 @@ export type MarketsSlice = {
}
const DEFAULT_STATE: SliceState = {
+ isFetchingAll: true,
statsParametersMapper: {},
statsBandsMapper: {},
statsTotalsMapper: {},
@@ -86,6 +88,8 @@ const createMarketsSlice = (set: SetState, get: GetState): Markets
const { chainId } = api
+ sliceState.setStateByKey('isFetchingAll', true)
+
const fnMapper = {
statsParametersMapper: apiLending.market.fetchStatsParameters,
statsBandsMapper: apiLending.market.fetchStatsBands,
@@ -116,6 +120,7 @@ const createMarketsSlice = (set: SetState, get: GetState): Markets
cMapper[owmId] = resp[owmId]
})
sliceState.setStateByActiveKey(k, chainId.toString(), cMapper)
+ sliceState.setStateByKey('isFetchingAll', false)
},
fetchAll: async (api, OneWayMarketTemplate, shouldRefetch) => {
diff --git a/apps/main/src/llamalend/PageLlamaMarkets/hooks/useSnapshots.ts b/apps/main/src/llamalend/PageLlamaMarkets/hooks/useSnapshots.ts
index e63d5f611..5aaa3de6f 100644
--- a/apps/main/src/llamalend/PageLlamaMarkets/hooks/useSnapshots.ts
+++ b/apps/main/src/llamalend/PageLlamaMarkets/hooks/useSnapshots.ts
@@ -1,8 +1,8 @@
import lodash from 'lodash'
import { useMemo } from 'react'
-import { CrvUsdSnapshot, useCrvUsdSnapshots } from '@/llamalend/entities/crvusd-snapshots'
-import { LendingSnapshot, useLendingSnapshots } from '@/llamalend/entities/lending-snapshots'
import { LlamaMarket, LlamaMarketType } from '@/llamalend/entities/llama-markets'
+import { CrvUsdSnapshot, useCrvUsdSnapshots } from '@ui-kit/entities/crvusd-snapshots'
+import { LendingSnapshot, useLendingSnapshots } from '@ui-kit/entities/lending-snapshots'
export type RateType = 'borrow' | 'lend'
diff --git a/apps/main/src/loan/components/LoanInfoLlamma/index.tsx b/apps/main/src/loan/components/LoanInfoLlamma/index.tsx
index 1653ef38f..52b0b0d07 100644
--- a/apps/main/src/loan/components/LoanInfoLlamma/index.tsx
+++ b/apps/main/src/loan/components/LoanInfoLlamma/index.tsx
@@ -9,6 +9,7 @@ import type { PageLoanManageProps } from '@/loan/components/PageLoanManage/types
import useStore from '@/loan/store/useStore'
import { breakpoints } from '@ui/utils/responsive'
import { useUserProfileStore } from '@ui-kit/features/user-profile'
+import { useBetaFlag } from '@ui-kit/hooks/useLocalStorage'
import { t } from '@ui-kit/lib/i18n'
interface Props extends Pick {
@@ -18,14 +19,16 @@ interface Props extends Pick {
const { rChainId, llamma, llammaId } = props
const chartExpanded = useStore((state) => state.ohlcCharts.chartExpanded)
-
const isAdvancedMode = useUserProfileStore((state) => state.isAdvancedMode)
+ const [isBeta] = useBetaFlag()
return (
-
-
-
+ {!isBeta && (
+
+
+
+ )}
{!chartExpanded && (
diff --git a/apps/main/src/loan/components/LoanInfoUser/index.tsx b/apps/main/src/loan/components/LoanInfoUser/index.tsx
index 52cc19dbf..4f1611fb1 100644
--- a/apps/main/src/loan/components/LoanInfoUser/index.tsx
+++ b/apps/main/src/loan/components/LoanInfoUser/index.tsx
@@ -9,13 +9,16 @@ import ChartUserLiquidationRange from '@/loan/components/LoanInfoUser/components
import UserInfos from '@/loan/components/LoanInfoUser/components/UserInfos'
import type { PageLoanManageProps } from '@/loan/components/PageLoanManage/types'
import { DEFAULT_HEALTH_MODE } from '@/loan/components/PageLoanManage/utils'
+import { useLoanPositionDetails } from '@/loan/hooks/useLoanPositionDetails'
import { useUserLoanDetails, useUserLoanStatus } from '@/loan/hooks/useUserLoanDetails'
import useStore from '@/loan/store/useStore'
import { ChainId } from '@/loan/types/loan.types'
import Box from '@ui/Box'
import { breakpoints } from '@ui/utils/responsive'
import { useUserProfileStore } from '@ui-kit/features/user-profile'
+import { useBetaFlag } from '@ui-kit/hooks/useLocalStorage'
import { t } from '@ui-kit/lib/i18n'
+import { BorrowPositionDetails } from '@ui-kit/shared/ui/PositionDetails/BorrowPositionDetails'
interface Props extends Pick
{
rChainId: ChainId
@@ -28,10 +31,17 @@ const LoanInfoUser = ({ llamma, llammaId, rChainId, titleMapper }: Props) => {
const isAdvancedMode = useUserProfileStore((state) => state.isAdvancedMode)
const isSoftLiquidation = useUserLoanStatus(llammaId) === 'soft_liquidation'
+ const [isBeta] = useBetaFlag()
const { oraclePriceBand } = loanDetails ?? {}
const [healthMode, setHealthMode] = useState(DEFAULT_HEALTH_MODE)
+ const positionDetailsProps = useLoanPositionDetails({
+ chainId: rChainId,
+ llamma,
+ llammaId,
+ health: healthMode.percent,
+ })
useEffect(() => {
if (!lodash.isUndefined(oraclePriceBand) && healthFull && healthNotFull && userBands) {
@@ -54,15 +64,18 @@ const LoanInfoUser = ({ llamma, llammaId, rChainId, titleMapper }: Props) => {
return (
-
-
-
+ {isBeta && }
+ {!isBeta && (
+
+
+
+ )}
{!chartExpanded && (
diff --git a/apps/main/src/loan/components/PageLoanCreate/Page.tsx b/apps/main/src/loan/components/PageLoanCreate/Page.tsx
index 6ff76a94d..4a201789a 100644
--- a/apps/main/src/loan/components/PageLoanCreate/Page.tsx
+++ b/apps/main/src/loan/components/PageLoanCreate/Page.tsx
@@ -5,6 +5,7 @@ import ChartOhlcWrapper from '@/loan/components/ChartOhlcWrapper'
import LoanInfoLlamma from '@/loan/components/LoanInfoLlamma'
import LoanCreate from '@/loan/components/PageLoanCreate/index'
import { hasLeverage } from '@/loan/components/PageLoanCreate/utils'
+import { useMarketDetails } from '@/loan/hooks/useMarketDetails'
import useTitleMapper from '@/loan/hooks/useTitleMapper'
import useStore from '@/loan/store/useStore'
import { type CollateralUrlParams, type LlamaApi, Llamma } from '@/loan/types/loan.types'
@@ -33,9 +34,11 @@ import { ConnectWalletPrompt, isLoading, useConnection, useWallet } from '@ui-ki
import { useLayoutStore } from '@ui-kit/features/layout'
import { useUserProfileStore } from '@ui-kit/features/user-profile'
import { useNavigate } from '@ui-kit/hooks/router'
+import { useBetaFlag } from '@ui-kit/hooks/useLocalStorage'
import usePageVisibleInterval from '@ui-kit/hooks/usePageVisibleInterval'
import { t } from '@ui-kit/lib/i18n'
import { REFRESH_INTERVAL } from '@ui-kit/lib/model'
+import { MarketDetails } from '@ui-kit/shared/ui/MarketDetails'
const Page = (params: CollateralUrlParams) => {
const { rFormType, rCollateralId } = parseCollateralParams(params)
@@ -68,6 +71,13 @@ const Page = (params: CollateralUrlParams) => {
const isValidRouterParams = !!rChainId && !!rCollateralId
const isLeverage = rFormType === 'leverage'
+ const [isBeta] = useBetaFlag()
+ const marketDetailsProps = useMarketDetails({
+ chainId: rChainId,
+ llamma,
+ llammaId,
+ })
+
const fetchInitial = useCallback(
(curve: LlamaApi, isLeverage: boolean, llamma: Llamma) => {
// reset createLoan estGas, detailInfo state
@@ -206,7 +216,8 @@ const Page = (params: CollateralUrlParams) => {
{isMdUp && !chartExpanded && }
- LLAMMA Details
+ {isBeta && }
+ {!isBeta && LLAMMA Details}
{isValidRouterParams && rChainId && (
)}
diff --git a/apps/main/src/loan/components/PagePegKeepers/components/PegKeeperStatistics.tsx b/apps/main/src/loan/components/PagePegKeepers/components/PegKeeperStatistics.tsx
index c221ef616..fcce1cd1f 100644
--- a/apps/main/src/loan/components/PagePegKeepers/components/PegKeeperStatistics.tsx
+++ b/apps/main/src/loan/components/PagePegKeepers/components/PegKeeperStatistics.tsx
@@ -2,6 +2,7 @@ import { useChainId } from 'wagmi'
import { useAppStatsTotalCrvusdSupply } from '@/loan/entities/appstats-total-crvusd-supply'
import type { ChainId } from '@/loan/types/loan.types'
import { CardHeader, Box } from '@mui/material'
+import { t } from '@ui-kit/lib/i18n'
import { Metric } from '@ui-kit/shared/ui/Metric'
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
@@ -18,17 +19,17 @@ export const PegKeeperStatistics = () => {
return (
-
+
diff --git a/apps/main/src/loan/hooks/useLoanPositionDetails.ts b/apps/main/src/loan/hooks/useLoanPositionDetails.ts
new file mode 100644
index 000000000..6bf5066b9
--- /dev/null
+++ b/apps/main/src/loan/hooks/useLoanPositionDetails.ts
@@ -0,0 +1,108 @@
+import meanBy from 'lodash/meanBy'
+import { useMemo } from 'react'
+import { CRVUSD_ADDRESS } from '@/loan/constants'
+import networks from '@/loan/networks'
+import useStore from '@/loan/store/useStore'
+import { ChainId, Llamma } from '@/loan/types/loan.types'
+import { Address } from '@curvefi/prices-api'
+import { useCrvUsdSnapshots } from '@ui-kit/entities/crvusd-snapshots'
+import { useTokenUsdRate } from '@ui-kit/lib/model/entities/token-usd-rate'
+import { BorrowPositionDetailsProps } from '@ui-kit/shared/ui/PositionDetails/BorrowPositionDetails'
+
+type UseLoanPositionDetailsProps = {
+ chainId: ChainId
+ llamma: Llamma | null | undefined
+ llammaId: string
+ health: string | undefined
+}
+
+export const useLoanPositionDetails = ({
+ chainId,
+ llamma,
+ llammaId,
+ health,
+}: UseLoanPositionDetailsProps): BorrowPositionDetailsProps => {
+ const userLoanDetails = useStore((state) => state.loans.userDetailsMapper[llammaId])
+ const loanDetails = useStore((state) => state.loans.detailsMapper[llammaId ?? ''])
+
+ const { data: collateralUsdRate, isLoading: collateralUsdRateLoading } = useTokenUsdRate({
+ chainId: chainId,
+ tokenAddress: llamma?.collateral,
+ })
+ const { data: borrowedUsdRate, isLoading: borrowedUsdRateLoading } = useTokenUsdRate({
+ chainId: chainId,
+ tokenAddress: CRVUSD_ADDRESS,
+ })
+ const { data: crvUsdSnapshots, isLoading: isSnapshotsLoading } = useCrvUsdSnapshots({
+ blockchainId: networks[chainId as keyof typeof networks].id,
+ contractAddress: llamma?.controller as Address,
+ })
+
+ const thirtyDayAvgRate = useMemo(() => {
+ if (!crvUsdSnapshots) return null
+
+ const thirtyDaysAgo = new Date()
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
+
+ const recentSnapshots = crvUsdSnapshots.filter((snapshot) => new Date(snapshot.timestamp) > thirtyDaysAgo)
+
+ if (recentSnapshots.length === 0) return null
+
+ return meanBy(recentSnapshots, ({ rate }) => rate) * 100
+ }, [crvUsdSnapshots])
+
+ const collateralTotalValue = useMemo(() => {
+ if (!collateralUsdRate || !userLoanDetails?.userState?.collateral) return null
+ return (
+ Number(userLoanDetails?.userState?.collateral) * Number(collateralUsdRate) +
+ Number(userLoanDetails?.userState?.stablecoin)
+ )
+ }, [userLoanDetails?.userState?.collateral, userLoanDetails?.userState?.stablecoin, collateralUsdRate])
+
+ return {
+ isSoftLiquidation: userLoanDetails?.userStatus?.colorKey === 'soft_liquidation',
+ health: {
+ value: Number(health),
+ loading: userLoanDetails?.loading ?? true,
+ },
+ borrowAPR: {
+ value: loanDetails?.parameters?.rate ? Number(loanDetails?.parameters?.rate) : null,
+ thirtyDayAvgRate: thirtyDayAvgRate,
+ loading: isSnapshotsLoading || (loanDetails?.loading ?? true),
+ },
+ liquidationRange: {
+ value: userLoanDetails?.userPrices ? userLoanDetails.userPrices.map(Number) : null,
+ rangeToLiquidation:
+ loanDetails?.priceInfo?.oraclePrice && userLoanDetails?.userPrices
+ ? (Number(userLoanDetails?.userPrices?.[1]) / Number(loanDetails.priceInfo.oraclePrice)) * 100
+ : null,
+ loading: userLoanDetails?.loading ?? true,
+ },
+ bandRange: {
+ value: userLoanDetails?.userBands ? userLoanDetails.userBands : null,
+ loading: userLoanDetails?.loading ?? true,
+ },
+ collateralValue: {
+ totalValue: collateralTotalValue,
+ collateral: {
+ value: userLoanDetails?.userState?.collateral ? Number(userLoanDetails.userState.collateral) : null,
+ usdRate: collateralUsdRate ? Number(collateralUsdRate) : null,
+ symbol: llamma?.collateralSymbol,
+ },
+ borrow: {
+ value: userLoanDetails?.userState?.stablecoin ? Number(userLoanDetails.userState.stablecoin) : null,
+ usdRate: borrowedUsdRate ? Number(borrowedUsdRate) : null,
+ symbol: 'crvUSD',
+ },
+ loading: (userLoanDetails?.loading ?? true) || collateralUsdRateLoading || borrowedUsdRateLoading,
+ },
+ ltv: {
+ value: collateralTotalValue ? (Number(userLoanDetails?.userState?.debt) / collateralTotalValue) * 100 : null,
+ loading: userLoanDetails?.loading ?? true,
+ },
+ totalDebt: {
+ value: userLoanDetails?.userState?.debt ? Number(userLoanDetails.userState.debt) : null,
+ loading: userLoanDetails?.loading ?? true,
+ },
+ }
+}
diff --git a/apps/main/src/loan/hooks/useMarketDetails.ts b/apps/main/src/loan/hooks/useMarketDetails.ts
new file mode 100644
index 000000000..46bb6e8d9
--- /dev/null
+++ b/apps/main/src/loan/hooks/useMarketDetails.ts
@@ -0,0 +1,68 @@
+import meanBy from 'lodash/meanBy'
+import { useMemo } from 'react'
+import { CRVUSD_ADDRESS } from '@/loan/constants'
+import networks from '@/loan/networks'
+import useStore from '@/loan/store/useStore'
+import { ChainId, Llamma } from '@/loan/types/loan.types'
+import { Address } from '@curvefi/prices-api'
+import { useCrvUsdSnapshots } from '@ui-kit/entities/crvusd-snapshots'
+import { MarketDetailsProps } from '@ui-kit/shared/ui/MarketDetails'
+
+type UseMarketDetailsProps = {
+ chainId: ChainId
+ llamma: Llamma | null | undefined
+ llammaId: string
+}
+
+export const useMarketDetails = ({ chainId, llamma, llammaId }: UseMarketDetailsProps): MarketDetailsProps => {
+ const loanDetails = useStore((state) => state.loans.detailsMapper[llammaId ?? ''])
+ const usdRatesLoading = useStore((state) => state.usdRates.loading)
+ const collateralUsdRate = useStore((state) => state.usdRates.tokens[llamma?.collateral ?? ''])
+ const borrowedUsdRate = useStore((state) => state.usdRates.tokens[CRVUSD_ADDRESS])
+ const { data: crvUsdSnapshots, isLoading: isSnapshotsLoading } = useCrvUsdSnapshots({
+ blockchainId: networks[chainId as keyof typeof networks]?.id,
+ contractAddress: llamma?.controller as Address,
+ })
+
+ const thirtyDayAvgBorrowAPR = useMemo(() => {
+ if (!crvUsdSnapshots) return null
+
+ const thirtyDaysAgo = new Date()
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
+
+ const recentSnapshots = crvUsdSnapshots.filter((snapshot) => new Date(snapshot.timestamp) > thirtyDaysAgo)
+
+ if (recentSnapshots.length === 0) return null
+
+ return meanBy(recentSnapshots, ({ rate }) => rate) * 100
+ }, [crvUsdSnapshots])
+
+ return {
+ collateral: {
+ symbol: llamma?.collateralSymbol ?? null,
+ tokenAddress: llamma?.collateral,
+ total: loanDetails?.totalCollateral ? Number(loanDetails.totalCollateral) : null,
+ totalUsdValue: loanDetails?.totalCollateral
+ ? Number(loanDetails.totalCollateral) * Number(collateralUsdRate)
+ : null,
+ usdRate: collateralUsdRate ? Number(collateralUsdRate) : null,
+ loading: usdRatesLoading || (loanDetails?.loading ?? true),
+ },
+ borrowToken: {
+ symbol: 'crvUSD',
+ tokenAddress: CRVUSD_ADDRESS,
+ usdRate: borrowedUsdRate ? Number(borrowedUsdRate) : null,
+ loading: usdRatesLoading || (loanDetails?.loading ?? true),
+ },
+ borrowAPR: {
+ value: loanDetails?.parameters?.rate ? Number(loanDetails?.parameters?.rate) : null,
+ thirtyDayAvgRate: thirtyDayAvgBorrowAPR,
+ loading: isSnapshotsLoading || (loanDetails?.loading ?? true),
+ },
+ availableLiquidity: {
+ value: loanDetails?.capAndAvailable?.available ? Number(loanDetails.capAndAvailable.available) : null,
+ max: loanDetails?.capAndAvailable?.cap ? Number(loanDetails.capAndAvailable.cap) : null,
+ loading: loanDetails?.loading ?? true,
+ },
+ }
+}
diff --git a/apps/main/src/loan/lib/apiCrvusd.ts b/apps/main/src/loan/lib/apiCrvusd.ts
index fef141587..27f36da87 100644
--- a/apps/main/src/loan/lib/apiCrvusd.ts
+++ b/apps/main/src/loan/lib/apiCrvusd.ts
@@ -204,6 +204,7 @@ const detailInfo = {
)
const fetchedUserDetails: UserLoanDetails = {
+ loading: false,
...fetchedPartialUserLoanInfo,
userBandsBalances: parsedBandsBalances,
userBandsRange,
diff --git a/apps/main/src/loan/store/createLoansSlice.ts b/apps/main/src/loan/store/createLoansSlice.ts
index e71a293bc..8658386d3 100644
--- a/apps/main/src/loan/store/createLoansSlice.ts
+++ b/apps/main/src/loan/store/createLoansSlice.ts
@@ -90,13 +90,19 @@ const createLoansSlice = (set: SetState, get: GetState) => ({
},
fetchLoanDetails: async (curve: LlamaApi, llamma: Llamma) => {
const chainId = curve.chainId as ChainId
+
+ get()[sliceKey].setStateByActiveKey('detailsMapper', llamma.id, {
+ ...get()[sliceKey].detailsMapper[llamma.id],
+ loading: true,
+ })
+
const [{ collateralId, ...loanDetails }, priceInfo, loanExists] = await Promise.all([
networks[chainId].api.detailInfo.loanInfo(llamma),
networks[chainId].api.detailInfo.priceInfo(llamma),
networks[chainId].api.loanCreate.exists(llamma, curve.signerAddress),
])
- const fetchedLoanDetails: LoanDetails = { ...loanDetails, priceInfo }
+ const fetchedLoanDetails: LoanDetails = { ...loanDetails, priceInfo, loading: false }
get()[sliceKey].setStateByActiveKey('detailsMapper', collateralId, fetchedLoanDetails)
get()[sliceKey].setStateByActiveKey('existsMapper', collateralId, loanExists)
@@ -123,10 +129,19 @@ const createLoansSlice = (set: SetState, get: GetState) => ({
fetchUserLoanDetails: async (curve: LlamaApi, llamma: Llamma) => {
const chainId = curve.chainId as ChainId
const userLoanDetailsFn = networks[chainId].api.detailInfo.userLoanInfo
+
+ get()[sliceKey].setStateByActiveKey('userDetailsMapper', llamma.id, {
+ ...get()[sliceKey].userDetailsMapper[llamma.id],
+ loading: true,
+ })
+
const resp = await userLoanDetailsFn(llamma, curve.signerAddress)
- get()[sliceKey].setStateByActiveKey('userDetailsMapper', llamma.id, resp)
- return resp
+ get()[sliceKey].setStateByActiveKey('userDetailsMapper', llamma.id, {
+ ...resp,
+ loading: false,
+ })
+ return { ...resp, loading: false }
},
fetchUserLoanPartialDetails: async (curve: LlamaApi, llamma: Llamma) => {
const chainId = curve.chainId as ChainId
diff --git a/apps/main/src/loan/types/loan.types.ts b/apps/main/src/loan/types/loan.types.ts
index a24a49a13..10fba87bd 100644
--- a/apps/main/src/loan/types/loan.types.ts
+++ b/apps/main/src/loan/types/loan.types.ts
@@ -82,6 +82,7 @@ export type LoanParameter = {
}
export type BandBalance = Record
export type LoanDetails = {
+ loading: boolean
oraclePriceBand: number | null
parameters: LoanParameter
balances: [string, string]
@@ -108,6 +109,7 @@ export type BandsBalancesData = {
collateralStablecoinUsd: number
}
export type UserLoanDetails = {
+ loading: boolean
healthFull: string
healthNotFull: string
userBands: number[]
diff --git a/apps/main/src/llamalend/entities/crvusd-snapshots.ts b/packages/curve-ui-kit/src/entities/crvusd-snapshots.ts
similarity index 100%
rename from apps/main/src/llamalend/entities/crvusd-snapshots.ts
rename to packages/curve-ui-kit/src/entities/crvusd-snapshots.ts
diff --git a/apps/main/src/llamalend/entities/lending-snapshots.ts b/packages/curve-ui-kit/src/entities/lending-snapshots.ts
similarity index 100%
rename from apps/main/src/llamalend/entities/lending-snapshots.ts
rename to packages/curve-ui-kit/src/entities/lending-snapshots.ts
diff --git a/packages/curve-ui-kit/src/shared/ui/MarketDetails/SymbolCell.tsx b/packages/curve-ui-kit/src/shared/ui/MarketDetails/SymbolCell.tsx
new file mode 100644
index 000000000..f004f48aa
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/MarketDetails/SymbolCell.tsx
@@ -0,0 +1,28 @@
+import { Stack, Typography } from '@mui/material'
+import { t } from '@ui-kit/lib/i18n'
+import { MetricSize } from '@ui-kit/shared/ui/Metric'
+import { TokenIcon } from '@ui-kit/shared/ui/TokenIcon'
+import { WithSkeleton } from '@ui-kit/shared/ui/WithSkeleton'
+
+type SymbolCellProps = {
+ label: string
+ symbol: string | undefined | null
+ tokenAddress: string | undefined | null
+ loading: boolean
+ size?: keyof Pick
+}
+
+/** Mimics the style of Metric but is used for cells that only have a symbol and a token icon. */
+export const SymbolCell = ({ label, symbol, tokenAddress, loading, size = 'medium' }: SymbolCellProps) => (
+
+
+ {label}
+
+
+
+ {symbol == null ? t`N/A` : symbol}
+
+
+
+
+)
diff --git a/packages/curve-ui-kit/src/shared/ui/MarketDetails/index.tsx b/packages/curve-ui-kit/src/shared/ui/MarketDetails/index.tsx
new file mode 100644
index 000000000..93e4d093f
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/MarketDetails/index.tsx
@@ -0,0 +1,175 @@
+import { CardHeader, Box } from '@mui/material'
+import { formatNumber, FORMAT_OPTIONS } from '@ui/utils/utilsFormat'
+import { t } from '@ui-kit/lib/i18n'
+import { SymbolCell } from '@ui-kit/shared/ui/MarketDetails/SymbolCell'
+import { Metric } from '@ui-kit/shared/ui/Metric'
+import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
+import { abbreviateNumber, scaleSuffix } from '@ui-kit/utils/number'
+
+const { Spacing } = SizesAndSpaces
+
+type Collateral = {
+ total: number | undefined | null
+ totalUsdValue: number | undefined | null
+ symbol: string | undefined | null
+ tokenAddress: string | undefined | null
+ usdRate: number | undefined | null
+ loading: boolean
+}
+type BorrowToken = {
+ total?: number | undefined | null
+ totalUsdValue?: number | undefined | null
+ symbol: string | undefined | null
+ tokenAddress: string | undefined | null
+ usdRate: number | undefined | null
+ loading: boolean
+}
+type BorrowAPR = {
+ value: number | undefined | null
+ thirtyDayAvgRate: number | undefined | null
+ loading: boolean
+}
+type LendingAPY = {
+ value: number | undefined | null
+ thirtyDayAvgRate: number | undefined | null
+ loading: boolean
+}
+type AvailableLiquidity = {
+ value: number | undefined | null
+ max: number | undefined | null
+ loading: boolean
+}
+type MaxLeverage = {
+ value: number | undefined | null
+ loading: boolean
+}
+
+export type MarketDetailsProps = {
+ /**
+ * Use true if Market Details appear on the top of the page, the upper row will have larger cells
+ */
+ marketPage: boolean
+ collateral: Collateral
+ borrowToken: BorrowToken
+ borrowAPR: BorrowAPR
+ lendingAPY?: LendingAPY
+ availableLiquidity: AvailableLiquidity
+ maxLeverage?: MaxLeverage
+}
+
+const formatLiquidity = (value: number) =>
+ `${formatNumber(abbreviateNumber(value), { ...FORMAT_OPTIONS.USD })}${scaleSuffix(value).toUpperCase()}`
+
+export const MarketDetails = ({
+ marketPage = false,
+ collateral,
+ borrowToken,
+ borrowAPR,
+ lendingAPY,
+ availableLiquidity,
+ maxLeverage,
+}: MarketDetailsProps) => {
+ const utilization =
+ availableLiquidity?.value && availableLiquidity.max
+ ? (availableLiquidity.value / availableLiquidity.max) * 100
+ : undefined
+ const utilizationBreakdown =
+ availableLiquidity?.value && availableLiquidity.max
+ ? `${formatLiquidity(availableLiquidity.value)}/${formatLiquidity(availableLiquidity.max)}`
+ : undefined
+
+ return (
+ t.design.Layer[1].Fill }}>
+
+
+
+ {lendingAPY && (
+
+ )}
+
+
+ {/* Insert empty box to maintain grid layout when there is no lending APY metric */}
+ {!lendingAPY && }
+
+
+
+ {maxLeverage && (
+
+ )}
+
+
+ )
+}
diff --git a/packages/curve-ui-kit/src/shared/ui/Metric.tsx b/packages/curve-ui-kit/src/shared/ui/Metric.tsx
index d5fb3b810..7f9759f33 100644
--- a/packages/curve-ui-kit/src/shared/ui/Metric.tsx
+++ b/packages/curve-ui-kit/src/shared/ui/Metric.tsx
@@ -20,14 +20,14 @@ const { Spacing, IconSize } = SizesAndSpaces
export const ALIGNMENTS = ['start', 'center', 'end'] as const
type Alignment = (typeof ALIGNMENTS)[number]
-const MetricSize = {
+export const MetricSize = {
small: 'highlightM',
medium: 'highlightL',
large: 'highlightXl',
extraLarge: 'highlightXxl',
} as const satisfies Record
-const MetricUnitSize = {
+export const MetricUnitSize = {
small: 'highlightXs',
medium: 'highlightS',
large: 'highlightM',
diff --git a/packages/curve-ui-kit/src/shared/ui/PositionDetails/BorrowInformation.tsx b/packages/curve-ui-kit/src/shared/ui/PositionDetails/BorrowInformation.tsx
new file mode 100644
index 000000000..ed052eb9d
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/PositionDetails/BorrowInformation.tsx
@@ -0,0 +1,146 @@
+import { CardHeader, Box } from '@mui/material'
+import { t } from '@ui-kit/lib/i18n'
+import { Metric } from '@ui-kit/shared/ui/Metric'
+import type {
+ Pnl,
+ BorrowAPR,
+ Leverage,
+ CollateralValue,
+ Ltv,
+ TotalDebt,
+ LiquidationRange,
+ BandRange,
+} from '@ui-kit/shared/ui/PositionDetails/BorrowPositionDetails'
+import { CollateralMetricTooltip } from '@ui-kit/shared/ui/PositionDetails/tooltips/CollateralMetricTooltip'
+import { LiquidityThresholdTooltip } from '@ui-kit/shared/ui/PositionDetails/tooltips/LiquidityThresholdMetricTooltip'
+import { PnlMetricTooltip } from '@ui-kit/shared/ui/PositionDetails/tooltips/PnlMetricTooltip'
+import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
+
+const { Spacing } = SizesAndSpaces
+
+const dollarUnitOptions = {
+ abbreviate: false,
+ unit: {
+ symbol: '$',
+ position: 'prefix' as const,
+ abbreviate: false,
+ },
+}
+
+type BorrowInformationProps = {
+ borrowAPR: BorrowAPR | undefined | null
+ pnl: Pnl | undefined | null
+ collateralValue: CollateralValue | undefined | null
+ ltv: Ltv | undefined | null
+ leverage: Leverage | undefined | null
+ liquidationRange: LiquidationRange | undefined | null
+ bandRange: BandRange | undefined | null
+ totalDebt: TotalDebt | undefined | null
+}
+
+export const BorrowInformation = ({
+ borrowAPR,
+ pnl,
+ collateralValue,
+ ltv,
+ leverage,
+ liquidationRange,
+ bandRange,
+ totalDebt,
+}: BorrowInformationProps) => (
+
+
+
+
+ {pnl && ( // PNL is only available on lend for now
+ ,
+ placement: 'top',
+ arrow: false,
+ }}
+ />
+ )}
+ ,
+ placement: 'top',
+ arrow: false,
+ }}
+ />
+
+ {leverage &&
+ leverage?.value &&
+ leverage?.value > 1 && ( // Leverage is only available on lend for now
+
+ )}
+ ,
+ placement: 'top',
+ arrow: false,
+ }}
+ notional={
+ liquidationRange?.rangeToLiquidation
+ ? {
+ value: liquidationRange.rangeToLiquidation,
+ unit: { symbol: '% Buffer to liquidation', position: 'suffix' },
+ }
+ : undefined
+ }
+ />
+
+
+
+)
diff --git a/packages/curve-ui-kit/src/shared/ui/PositionDetails/BorrowPositionDetails.tsx b/packages/curve-ui-kit/src/shared/ui/PositionDetails/BorrowPositionDetails.tsx
new file mode 100644
index 000000000..9efb68cd1
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/PositionDetails/BorrowPositionDetails.tsx
@@ -0,0 +1,103 @@
+import { Box, Typography } from '@mui/material'
+import { Alert } from '@mui/material'
+import { t } from '@ui-kit/lib/i18n'
+import { BorrowInformation } from '@ui-kit/shared/ui/PositionDetails/BorrowInformation'
+import { HealthDetails } from '@ui-kit/shared/ui/PositionDetails/HealthDetails'
+import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
+
+const { Spacing } = SizesAndSpaces
+
+export type Pnl = {
+ currentProfit: number | undefined | null
+ currentPositionValue: number | undefined | null
+ depositedValue: number | undefined | null
+ percentageChange: number | undefined | null
+ loading: boolean
+}
+export type Health = { value: number | undefined | null; loading: boolean }
+export type BorrowAPR = {
+ value: number | undefined | null
+ thirtyDayAvgRate: number | undefined | null
+ loading: boolean
+}
+export type LiquidationRange = {
+ value: number[] | undefined | null
+ rangeToLiquidation: number | undefined | null
+ loading: boolean
+}
+export type BandRange = { value: number[] | undefined | null; loading: boolean }
+export type Leverage = { value: number | undefined | null; loading: boolean }
+export type CollateralValue = {
+ totalValue: number | undefined | null
+ collateral: {
+ value: number | undefined | null
+ usdRate: number | undefined | null
+ symbol: string | undefined
+ }
+ borrow: {
+ value: number | undefined | null
+ usdRate: number | undefined | null
+ symbol: string | undefined
+ }
+ loading: boolean
+}
+export type Ltv = { value: number | undefined | null; loading: boolean }
+export type TotalDebt = { value: number | undefined | null; loading: boolean }
+
+export type BorrowPositionDetailsProps = {
+ isSoftLiquidation: boolean
+ health: Health
+ borrowAPR: BorrowAPR
+ pnl?: Pnl // doesn't exist yet for crvusd
+ liquidationRange: LiquidationRange
+ bandRange: BandRange
+ leverage?: Leverage // doesn't exist yet for crvusd
+ collateralValue: CollateralValue
+ ltv: Ltv
+ totalDebt: TotalDebt
+}
+
+export const BorrowPositionDetails = ({
+ isSoftLiquidation,
+ health,
+ borrowAPR,
+ pnl,
+ liquidationRange,
+ bandRange,
+ leverage,
+ collateralValue,
+ ltv,
+ totalDebt,
+}: BorrowPositionDetailsProps) => (
+
+ {isSoftLiquidation && (
+
+
+
+ {t`Soft-Liquidation active`}
+
+ {t`Price has entered the liquidation zone and your collateral is at risk. Manage your position to avoid full liquidation.`}
+
+
+
+
+ )}
+
+
+
+)
diff --git a/packages/curve-ui-kit/src/shared/ui/PositionDetails/HealthBar.tsx b/packages/curve-ui-kit/src/shared/ui/PositionDetails/HealthBar.tsx
new file mode 100644
index 000000000..bfa6e8294
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/PositionDetails/HealthBar.tsx
@@ -0,0 +1,98 @@
+import { Box, Typography, type Theme } from '@mui/material'
+import { t } from '@ui-kit/lib/i18n'
+import { Reds, Blues } from '@ui-kit/themes/design/0_primitives'
+
+type HealthBarProps = {
+ health: number | undefined | null
+}
+
+const BAR_HEIGHT = '1.4375rem' // 23px
+const LINE_WIDTH = '0.25rem' // 4px
+const LABEL_GAP = '0.125rem' // 2px
+
+type LineColor = 'red' | 'orange' | 'green' | 'dark-green'
+
+const getLineColor = (color: LineColor) => (t: Theme) =>
+ ({
+ red: t.design.Color.Tertiary[600],
+ orange: t.design.Color.Tertiary[400],
+ ['dark-green']: t.design.Color.Secondary[600],
+ green: t.design.Color.Secondary[500],
+ })[color]
+
+const Line = ({ first, position, color }: { first?: boolean; position: string; color: LineColor }) => (
+
+)
+
+const Label = ({
+ first,
+ last,
+ position,
+ text,
+}: {
+ first?: boolean
+ last?: boolean
+ position: string
+ text: string
+}) => (
+
+
+ {text}
+
+
+)
+
+export const HealthBar = ({ health }: HealthBarProps) => {
+ // Clamps health percentage between 0 and 100
+ const healthPercentage = Math.max(0, Math.min(health ?? 0, 100))
+ const trackColor = health != null && health < 5 ? Reds[500] : Blues[500]
+
+ return (
+
+
+
+
+
+
+
+ t.design.Color.Neutral[300],
+ transition: 'background-color 0.3s ease-in-out',
+ }}
+ >
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/curve-ui-kit/src/shared/ui/PositionDetails/HealthDetails.tsx b/packages/curve-ui-kit/src/shared/ui/PositionDetails/HealthDetails.tsx
new file mode 100644
index 000000000..5d3715280
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/PositionDetails/HealthDetails.tsx
@@ -0,0 +1,54 @@
+import { Box, Typography } from '@mui/material'
+import { t } from '@ui-kit/lib/i18n'
+import { Metric } from '@ui-kit/shared/ui/Metric'
+import type { Health } from '@ui-kit/shared/ui/PositionDetails/BorrowPositionDetails'
+import { HealthBar } from '@ui-kit/shared/ui/PositionDetails/HealthBar'
+import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
+
+const { Spacing } = SizesAndSpaces
+
+const getHealthValueColor = (value: number) => {
+ if (value < 5) return 'error'
+ if (value < 15) return 'warning'
+ return 'textPrimary'
+}
+
+export const HealthDetails = ({ health }: { health: Health }) => (
+
+ t.design.Layer.Highlight.Fill,
+ }}
+ >
+
+
+
+
+
+
+ {t`Health determines a position liquidation. It is not directly correlated to the price of the collateral. `}
+
+ t.typography.fontWeightBold }}>
+ {t`Liquidations occur when health reaches 0.`}
+
+
+
+
+)
diff --git a/packages/curve-ui-kit/src/shared/ui/PositionDetails/LendPositionDetails.tsx b/packages/curve-ui-kit/src/shared/ui/PositionDetails/LendPositionDetails.tsx
new file mode 100644
index 000000000..6568e8d1b
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/PositionDetails/LendPositionDetails.tsx
@@ -0,0 +1,89 @@
+import { CardHeader, Box } from '@mui/material'
+import { t } from '@ui-kit/lib/i18n'
+import { SymbolCell } from '@ui-kit/shared/ui/MarketDetails/SymbolCell'
+import { Metric } from '@ui-kit/shared/ui/Metric'
+import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
+
+const { Spacing } = SizesAndSpaces
+
+type LendingAPY = {
+ value: number | undefined | null
+ thirtyDayAvgRate: number | undefined | null
+ loading: boolean
+}
+type Shares = {
+ value: number | undefined | null
+ staked: number | undefined | null
+ loading: boolean
+}
+
+// TODO: figure out where to find boost data and add
+type Boost = {
+ value: number | undefined | null
+ loading: boolean
+}
+type LentAsset = {
+ symbol: string | undefined | null
+ address: string | undefined | null
+ usdRate: number | undefined | null
+ depositedAmount: number | undefined | null
+ depositedUsdValue: number | undefined | null
+ loading: boolean
+}
+
+export type LendPositionDetailsProps = {
+ lendingAPY: LendingAPY
+ shares: Shares
+ lentAsset: LentAsset
+}
+
+export const LendPositionDetails = ({ lendingAPY, shares, lentAsset }: LendPositionDetailsProps) => (
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/packages/curve-ui-kit/src/shared/ui/PositionDetails/index.tsx b/packages/curve-ui-kit/src/shared/ui/PositionDetails/index.tsx
new file mode 100644
index 000000000..2160acd5b
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/PositionDetails/index.tsx
@@ -0,0 +1,31 @@
+import { useState } from 'react'
+import { Box } from '@mui/material'
+import { t } from '@ui-kit/lib/i18n'
+import { TabsSwitcher } from '@ui-kit/shared/ui/TabsSwitcher'
+import { BorrowPositionDetails, type BorrowPositionDetailsProps } from './BorrowPositionDetails'
+import { LendPositionDetails, type LendPositionDetailsProps } from './LendPositionDetails'
+
+type PositionDetailsProps = {
+ borrowPositionDetails: BorrowPositionDetailsProps
+ lendPositionDetails?: LendPositionDetailsProps // lend is optional since it's not available for crvusd
+}
+
+const TABS = [
+ { value: 'borrow', label: t`Borrow`, href: '' },
+ { value: 'lend', label: t`Lend`, href: '' },
+]
+
+export const PositionDetails = ({ borrowPositionDetails, lendPositionDetails }: PositionDetailsProps) => {
+ const [tab, setTab] = useState<(typeof TABS)[number]['value']>(TABS[0].value)
+ return (
+
+ {lendPositionDetails && (
+
+ )}
+ t.design.Layer[1].Fill }}>
+ {tab === 'borrow' && }
+ {tab === 'lend' && lendPositionDetails && }
+
+
+ )
+}
diff --git a/packages/curve-ui-kit/src/shared/ui/PositionDetails/tooltips/CollateralMetricTooltip.tsx b/packages/curve-ui-kit/src/shared/ui/PositionDetails/tooltips/CollateralMetricTooltip.tsx
new file mode 100644
index 000000000..f4e54ab56
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/PositionDetails/tooltips/CollateralMetricTooltip.tsx
@@ -0,0 +1,82 @@
+import { Stack, Typography } from '@mui/material'
+import { FORMAT_OPTIONS, formatNumber } from '@ui/utils/utilsFormat'
+import { t } from '@ui-kit/lib/i18n'
+import { CollateralValue } from '@ui-kit/shared/ui/PositionDetails/BorrowPositionDetails'
+import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
+
+const { Spacing } = SizesAndSpaces
+
+type CollateralMetricTooltipProps = {
+ collateralValue: CollateralValue | undefined | null
+}
+
+const formatMetricValue = (value?: number | null) => {
+ if (value === 0) return '0'
+ if (value) return formatNumber(value, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+ return '-'
+}
+
+const formatPercentage = (
+ value: number | undefined | null,
+ totalValue: number | undefined | null,
+ usdRate: number | undefined | null,
+) => {
+ if (value === 0) return '0.00%'
+ if (value && totalValue && usdRate) {
+ return formatNumber(((value * usdRate) / totalValue) * 100, {
+ ...FORMAT_OPTIONS.PERCENT,
+ })
+ }
+ return null
+}
+
+export const CollateralMetricTooltip = ({ collateralValue }: CollateralMetricTooltipProps) => {
+ const collateralValueFormatted = formatMetricValue(collateralValue?.collateral?.value)
+ const collateralPercentage = formatPercentage(
+ collateralValue?.collateral?.value,
+ collateralValue?.totalValue,
+ collateralValue?.collateral?.usdRate,
+ )
+
+ const crvUSDValueFormatted = formatMetricValue(collateralValue?.borrow?.value)
+ const crvUSDPercentage = formatPercentage(
+ collateralValue?.borrow?.value,
+ collateralValue?.totalValue,
+ collateralValue?.borrow?.usdRate,
+ )
+
+ const totalValueFormatted = collateralValue?.totalValue
+ ? formatNumber(collateralValue.totalValue, { ...FORMAT_OPTIONS.USD })
+ : '-'
+
+ return (
+
+ {t`Collateral value is taken by multiplying tokens in collateral by the oracle price. In soft liquidation, it may include crvUSD due to liquidation protection.`}
+
+ t.design.Layer[2].Fill, padding: Spacing.sm }}>
+ {t`Breakdown`}
+
+
+ {t`Deposit token`}
+
+ {`${collateralValueFormatted} ${collateralValue?.collateral?.symbol ?? '-'}`}
+ {collateralPercentage && {`(${collateralPercentage})`}}
+
+
+
+
+ {collateralValue?.borrow?.symbol ?? '-'}
+
+ {`${crvUSDValueFormatted} ${collateralValue?.borrow?.symbol ?? '-'}`}
+ {`(${crvUSDPercentage})`}
+
+
+
+
+
+ {t`Total collateral value`}
+ {totalValueFormatted}
+
+
+ )
+}
diff --git a/packages/curve-ui-kit/src/shared/ui/PositionDetails/tooltips/LiquidityThresholdMetricTooltip.tsx b/packages/curve-ui-kit/src/shared/ui/PositionDetails/tooltips/LiquidityThresholdMetricTooltip.tsx
new file mode 100644
index 000000000..664f7de00
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/PositionDetails/tooltips/LiquidityThresholdMetricTooltip.tsx
@@ -0,0 +1,43 @@
+import { Stack, Typography } from '@mui/material'
+import { FORMAT_OPTIONS, formatNumber } from '@ui/utils/utilsFormat'
+import { t } from '@ui-kit/lib/i18n'
+import { BandRange, LiquidationRange } from '@ui-kit/shared/ui/PositionDetails/BorrowPositionDetails'
+import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
+
+const { Spacing } = SizesAndSpaces
+
+type LiquidityThresholdTooltipProps = {
+ liquidationRange: LiquidationRange | undefined | null
+ bandRange: BandRange | undefined | null
+}
+
+export const LiquidityThresholdTooltip = ({ liquidationRange, bandRange }: LiquidityThresholdTooltipProps) => (
+
+ {t`The price at which your position enters liquidation protection and your collateral starts to be eroded.`}
+
+ t.design.Layer[2].Fill, padding: Spacing.sm }}>
+ {t`Breakdown`}
+
+
+ {t`Liquidation threshold`}
+
+ {liquidationRange?.value?.[1] ? formatNumber(liquidationRange.value[1], { ...FORMAT_OPTIONS.USD }) : '-'}
+
+
+
+
+ {t`Liquidation lower bound`}
+
+ {liquidationRange?.value?.[0] ? formatNumber(liquidationRange.value[0], { ...FORMAT_OPTIONS.USD }) : '-'}
+
+
+
+
+ {t`Band range`}
+
+ {bandRange?.value ? `${bandRange.value[0]} - ${bandRange.value[1]}` : '-'}
+
+
+
+
+)
diff --git a/packages/curve-ui-kit/src/shared/ui/PositionDetails/tooltips/PnlMetricTooltip.tsx b/packages/curve-ui-kit/src/shared/ui/PositionDetails/tooltips/PnlMetricTooltip.tsx
new file mode 100644
index 000000000..5d7138e67
--- /dev/null
+++ b/packages/curve-ui-kit/src/shared/ui/PositionDetails/tooltips/PnlMetricTooltip.tsx
@@ -0,0 +1,47 @@
+import { Stack, Typography } from '@mui/material'
+import { FORMAT_OPTIONS, formatNumber } from '@ui/utils/utilsFormat'
+import { t } from '@ui-kit/lib/i18n'
+import { Pnl } from '@ui-kit/shared/ui/PositionDetails/BorrowPositionDetails'
+import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
+
+const { Spacing } = SizesAndSpaces
+
+type PnlMetricTooltipProps = {
+ pnl: Pnl | undefined | null
+}
+
+export const PnlMetricTooltip = ({ pnl }: PnlMetricTooltipProps) => (
+
+ {t`Profit and Loss (PnL) is calculated based on the value of the collateral at deposits minus the borrow costs and eventual losses if the position was in soft-liquidation.`}
+
+ t.design.Layer[2].Fill, padding: Spacing.sm }}>
+ {t`Breakdown`}
+
+
+ {t`Collateral value`}
+
+ {pnl?.currentPositionValue ? formatNumber(pnl.currentPositionValue, { ...FORMAT_OPTIONS.USD }) : '-'}
+
+
+
+
+ {t`Value at deposit`}
+
+ {pnl?.depositedValue ? formatNumber(pnl.depositedValue, { ...FORMAT_OPTIONS.USD }) : '-'}
+
+
+
+
+ {t`Profit/Loss`}
+
+
+ {pnl?.currentProfit ? formatNumber(pnl.currentProfit, { ...FORMAT_OPTIONS.USD }) : '-'}
+
+ {pnl?.currentPositionValue && (
+ {`(${formatNumber(pnl.currentPositionValue, { ...FORMAT_OPTIONS.PERCENT })})`}
+ )}
+
+
+
+
+)