Skip to content

feat: new position details on crvusd/lend #1097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 46 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
62ea26d
refactor: integrate i18n for PegKeeperStatistics labels
OnlyJousting Jun 9, 2025
49a1627
Merge branch 'main' into feat/new-lending-position-details
OnlyJousting Jun 9, 2025
e4b7725
Merge branch 'main' into feat/new-lending-position-details
OnlyJousting Jun 10, 2025
a7efca0
feat: init position details
OnlyJousting Jun 11, 2025
eb972d7
feat: add support for number[] in Metric
OnlyJousting Jun 12, 2025
cf8bed1
feat: set up borrow information for crvUSD
OnlyJousting Jun 12, 2025
b9055d0
refactor: assemble position details in own wrapper
OnlyJousting Jun 12, 2025
d5bdbd7
feat: init position details on lend
OnlyJousting Jun 16, 2025
8962392
Merge branch 'main' into feat/new-lending-position-details
OnlyJousting Jun 16, 2025
965d509
styling: tweak healthbar
OnlyJousting Jun 17, 2025
6595129
fix: lend average rate calculation
OnlyJousting Jun 17, 2025
9bc2e3f
feat: add soft-liquidation warning
OnlyJousting Jun 17, 2025
bfc190d
feat: add the option to hide the metric components tooltip
OnlyJousting Jun 18, 2025
e33bdb9
feat: init collateral metric tooltip
OnlyJousting Jun 18, 2025
4c7e9ac
feat: collateral metric tooltip
OnlyJousting Jun 19, 2025
7817109
refactor: remove 'liquidation-range' metric
OnlyJousting Jun 23, 2025
f96f0f8
refactor: revert changes to Metric component to support an array of v…
OnlyJousting Jun 23, 2025
a5b14ba
Merge branch 'main' into feat/new-lending-position-details
OnlyJousting Jun 23, 2025
a23db23
refactor: update metric component usage according to Metric changes
OnlyJousting Jun 23, 2025
d402ab0
fix: enable copy on click when hideTooltip is true
OnlyJousting Jun 23, 2025
1ac700e
refactor: handle custom metric tooltips
OnlyJousting Jun 23, 2025
866a1cf
feat: added liquidation threshold metric tooltip
OnlyJousting Jun 23, 2025
0bc6a82
feat: add pnl tooltip
OnlyJousting Jun 24, 2025
c9a4feb
feat: set up rate
OnlyJousting Jun 24, 2025
c72eb46
Merge branch 'main' into feat/new-lending-position-details
OnlyJousting Jun 24, 2025
3054c48
feat: set crvusd rate loading state
OnlyJousting Jun 24, 2025
554d954
Merge branch 'main' into feat/new-lending-position-details
OnlyJousting Jun 25, 2025
70fa6d6
Merge branch 'main' into feat/new-lending-position-details
OnlyJousting Jun 25, 2025
46fc2ec
feat: add range to liquidation
OnlyJousting Jun 25, 2025
7e8f442
feat: only show new position details on beta
OnlyJousting Jun 25, 2025
d181804
refactor: set PositionDetails props in hooks
OnlyJousting Jun 25, 2025
d449413
refactor: move lending snapshots hook to ui-kit
OnlyJousting Jun 25, 2025
52d3c33
fix: remove unintended changes to Metric
OnlyJousting Jun 25, 2025
f23a6a0
refactor: apply review comments
OnlyJousting Jun 25, 2025
0e9758e
fix: update market and llamma props to allow null values
OnlyJousting Jun 25, 2025
aa6b83b
fix: update import path for getSnapshots in useLendingSnapshots and a…
OnlyJousting Jun 25, 2025
da58a5a
refactor: remove app switches
OnlyJousting Jun 25, 2025
49885ff
Merge branch 'main' into feat/new-lending-position-details
OnlyJousting Jun 26, 2025
02dcede
refactor: adopt changes to metric component
OnlyJousting Jun 26, 2025
a61d4f2
feat: set health value color based on current health
OnlyJousting Jun 26, 2025
ba61d43
fix: remove redundant abbreviation setting in BorrowInformation compo…
OnlyJousting Jun 26, 2025
48f40ca
refactor: rename borrowRate to borrowAPR for consistency across compo…
OnlyJousting Jun 26, 2025
a4b6049
refactor: update BorrowInformation Metric labels capitalization
OnlyJousting Jun 26, 2025
6b45b1c
feat: new market details for loan
OnlyJousting Jun 27, 2025
136b85e
Merge branch 'main' into feat/new-lending-position-details
OnlyJousting Jul 14, 2025
3e3a6c5
refactor: use lending-snapshots from llamalend directory
OnlyJousting Jul 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ 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 { PositionDetailsWrapper } from './PositionDetailsWrapper'

const DetailsUserLoan = (pageProps: PageContentProps) => {
const { rChainId, rOwmId, api, market, titleMapper, userActiveKey } = pageProps
Expand All @@ -34,6 +36,7 @@ const DetailsUserLoan = (pageProps: PageContentProps) => {

const isAdvancedMode = useUserProfileStore((state) => state.isAdvancedMode)
const isSoftLiquidation = useUserLoanStatus(userActiveKey) === 'soft_liquidation'
const [isBeta] = useBetaFlag()

const { signerAddress } = api ?? {}

Expand Down Expand Up @@ -83,32 +86,43 @@ const DetailsUserLoan = (pageProps: PageContentProps) => {
<DetailsConnectWallet />
) : foundLoan ? (
<div>
{isSoftLiquidation && (
{!isBeta && isSoftLiquidation && (
<AlertContent>
<DetailsUserLoanAlertSoftLiquidation {...pageProps} />
</AlertContent>
)}

<ContentWrapper paddingTop isBorderBottom>
<StatsWrapper>
<CellUserMain {...pageProps} market={cellProps.market} type="borrow" />
{isBeta && (
<PositionDetailsWrapper
rChainId={rChainId}
market={market}
marketId={rOwmId}
userActiveKey={userActiveKey}
/>
)}

{/* stats */}
<ListInfoItemsWrapper>
{contents.map((groupedContents, idx) => (
<ListInfoItems key={`contents${idx}`}>
{groupedContents
.filter(({ show }) => _showContent(show))
.map(({ titleKey, content }, idx) => (
<ListInfoItem key={`content${idx}`} {...titleMapper[titleKey]}>
{content}
</ListInfoItem>
))}
</ListInfoItems>
))}
</ListInfoItemsWrapper>
</StatsWrapper>
</ContentWrapper>
{!isBeta && (
<ContentWrapper paddingTop isBorderBottom>
<StatsWrapper>
<CellUserMain {...pageProps} market={cellProps.market} type="borrow" />

{/* stats */}
<ListInfoItemsWrapper>
{contents.map((groupedContents, idx) => (
<ListInfoItems key={`contents${idx}`}>
{groupedContents
.filter(({ show }) => _showContent(show))
.map(({ titleKey, content }, idx) => (
<ListInfoItem key={`content${idx}`} {...titleMapper[titleKey]}>
{content}
</ListInfoItem>
))}
</ListInfoItems>
))}
</ListInfoItemsWrapper>
</StatsWrapper>
</ContentWrapper>
)}

{/* CHARTS */}
<ContentWrapper>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { usePositionDetails } from '@/lend/hooks/usePositionDetails'
import { ChainId, OneWayMarketTemplate } from '@/lend/types/lend.types'
import { PositionDetails } from '@ui-kit/shared/ui/PositionDetails'

type PositionDetailsWrapperProps = {
rChainId: ChainId
market: OneWayMarketTemplate | null | undefined
marketId: string
userActiveKey: string
}

export const PositionDetailsWrapper = ({ rChainId, market, marketId, userActiveKey }: PositionDetailsWrapperProps) => {
const positionDetailsProps = usePositionDetails({
rChainId,
market,
marketId,
userActiveKey,
})

return <PositionDetails {...positionDetailsProps} />
}
18 changes: 18 additions & 0 deletions apps/main/src/lend/entities/chains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getSupportedChains as getSupportedChainsFromApi } from '@curvefi/prices-api/chains'
import { getChains } from '@curvefi/prices-api/llamalend'
import { EmptyValidationSuite } from '@ui-kit/lib'
import { queryFactory } from '@ui-kit/lib/model'

export const { fetchQuery: fetchSupportedChains, setQueryData: setSupportedChains } = queryFactory({
queryKey: () => ['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,
})
126 changes: 126 additions & 0 deletions apps/main/src/lend/hooks/usePositionDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import meanBy from 'lodash/meanBy'
import { useMemo } from 'react'
import { useMarketOnChainRates } from '@/lend/entities/market-onchain-rate'
import { useTokenUsdRate } from '@/lend/entities/token'
import networks from '@/lend/networks'
import useStore from '@/lend/store/useStore'
import { ChainId, OneWayMarketTemplate } from '@/lend/types/lend.types'
import { useLendingSnapshots } from '@/llamalend/entities/lending-snapshots'
import type { Address, Chain } from '@curvefi/prices-api'
import { PositionDetailsProps } from '@ui-kit/shared/ui/PositionDetails'

type UsePositionDetailsProps = {
rChainId: ChainId
market: OneWayMarketTemplate | null | undefined
marketId: string
userActiveKey: string
}

export const usePositionDetails = ({
rChainId,
market,
marketId,
userActiveKey,
}: UsePositionDetailsProps): PositionDetailsProps => {
const userLoanDetailsResp = useStore((state) => state.user.loansDetailsMapper[userActiveKey])
const isFetchingAll = useStore((state) => state.markets.isFetchingAll)
const marketRate = useStore((state) => state.markets.ratesMapper[rChainId]?.[marketId])
const prices = useStore((state) => state.markets.pricesMapper[rChainId]?.[marketId])

const { data: onchainData, isLoading: isOnchainRatesLoading } = useMarketOnChainRates({ chainId: rChainId, marketId })
const { data: collateralUsdRate, isLoading: collateralUsdRateLoading } = useTokenUsdRate({
chainId: rChainId,
tokenAddress: market?.addresses?.collateral_token,
})
const { data: borrowedUsdRate, isLoading: borrowedUsdRateLoading } = useTokenUsdRate({
chainId: rChainId,
tokenAddress: market?.addresses?.borrowed_token,
})

const { details: userLoanDetails } = userLoanDetailsResp ?? {}

const { data: crvUsdSnapshots, isLoading: isSnapshotsLoading } = useLendingSnapshots({
blockchainId: networks[rChainId].id as Chain,
contractAddress: market?.addresses?.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, ({ borrowApy }) => borrowApy) * 100
}, [crvUsdSnapshots])

const borrowApy = onchainData?.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 || isSnapshotsLoading || !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,
},
}
}
5 changes: 5 additions & 0 deletions apps/main/src/lend/store/createMarketsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -62,6 +63,7 @@ export type MarketsSlice = {
}

const DEFAULT_STATE: SliceState = {
isFetchingAll: true,
statsParametersMapper: {},
statsBandsMapper: {},
statsTotalsMapper: {},
Expand All @@ -86,6 +88,8 @@ const createMarketsSlice = (set: SetState<State>, get: GetState<State>): Markets

const { chainId } = api

sliceState.setStateByKey('isFetchingAll', true)

const fnMapper = {
statsParametersMapper: apiLending.market.fetchStatsParameters,
statsBandsMapper: apiLending.market.fetchStatsBands,
Expand Down Expand Up @@ -116,6 +120,7 @@ const createMarketsSlice = (set: SetState<State>, get: GetState<State>): Markets
cMapper[owmId] = resp[owmId]
})
sliceState.setStateByActiveKey(k, chainId.toString(), cMapper)
sliceState.setStateByKey('isFetchingAll', false)
},

fetchAll: async (api, OneWayMarketTemplate, shouldRefetch) => {
Expand Down
19 changes: 15 additions & 4 deletions apps/main/src/loan/components/LoanInfoLlamma/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import DetailsInfo from '@/loan/components/LoanInfoLlamma/components/DetailsInfo
import LoanInfoParameters from '@/loan/components/LoanInfoLlamma/LoanInfoParameters'
import { SubTitle } from '@/loan/components/LoanInfoLlamma/styles'
import type { PageLoanManageProps } from '@/loan/components/PageLoanManage/types'
import { useMarketDetails } from '@/loan/hooks/useMarketDetails'
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'
import { MarketDetails } from '@ui-kit/shared/ui/MarketDetails'

interface Props extends Pick<PageLoanManageProps, 'llamma' | 'llammaId' | 'rChainId' | 'titleMapper'> {
className?: string
Expand All @@ -18,14 +21,22 @@ interface Props extends Pick<PageLoanManageProps, 'llamma' | 'llammaId' | 'rChai
const LoanInfoLlamma = (props: Props) => {
const { rChainId, llamma, llammaId } = props
const chartExpanded = useStore((state) => state.ohlcCharts.chartExpanded)

const isAdvancedMode = useUserProfileStore((state) => state.isAdvancedMode)
const [isBeta] = useBetaFlag()
const marketDetailsProps = useMarketDetails({
rChainId,
llamma,
llammaId,
})

return (
<Wrapper>
<div className="wrapper">
<DetailsInfo {...props} collateralId={llammaId} />
</div>
{!isBeta && (
<div className="wrapper">
<DetailsInfo {...props} collateralId={llammaId} />
</div>
)}
{isBeta && <MarketDetails {...marketDetailsProps} />}

{!chartExpanded && (
<div className={isAdvancedMode ? 'wrapper' : ''}>
Expand Down
31 changes: 22 additions & 9 deletions apps/main/src/loan/components/LoanInfoUser/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { PositionDetails } from '@ui-kit/shared/ui/PositionDetails'

interface Props extends Pick<PageLoanManageProps, 'llamma' | 'llammaId' | 'titleMapper'> {
rChainId: ChainId
Expand All @@ -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({
rChainId,
llamma,
llammaId,
health: healthMode.percent,
})

useEffect(() => {
if (!isUndefined(oraclePriceBand) && healthFull && healthNotFull && userBands) {
Expand All @@ -54,15 +64,18 @@ const LoanInfoUser = ({ llamma, llammaId, rChainId, titleMapper }: Props) => {

return (
<Wrapper>
<StatsWrapper className={`wrapper ${isSoftLiquidation ? 'alert' : 'first'}`}>
<UserInfos
llammaId={llammaId}
llamma={llamma}
isSoftLiquidation={isSoftLiquidation}
healthMode={healthMode}
titleMapper={titleMapper}
/>
</StatsWrapper>
{isBeta && <PositionDetails {...positionDetailsProps} />}
{!isBeta && (
<StatsWrapper className={`wrapper ${isSoftLiquidation ? 'alert' : 'first'}`}>
<UserInfos
llammaId={llammaId}
llamma={llamma}
isSoftLiquidation={isSoftLiquidation}
healthMode={healthMode}
titleMapper={titleMapper}
/>
</StatsWrapper>
)}

{!chartExpanded && (
<div className="wrapper">
Expand Down
Loading
Loading