Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -5,12 +5,14 @@ import Button from '@mui/material/Button'
import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography'
import { useWallet } from '@ui-kit/features/connect-wallet'
import { useIsMobile } from '@ui-kit/hooks/useBreakpoints'
import { t } from '@ui-kit/lib/i18n'
import { EmptyStateCard } from '@ui-kit/shared/ui/EmptyStateCard'
import { TabsSwitcher, type TabOption } from '@ui-kit/shared/ui/TabsSwitcher'
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
import { MarketRateType } from '@ui-kit/types/market'
import { LlamaMonitorBotButton } from './LlamaMonitorBotButton'
import { UserPositionSummary } from './UserPositionsSummary'
import { UserPositionsTable, type UserPositionsTableProps } from './UserPositionsTable'

const { Spacing, Height } = SizesAndSpaces
Expand All @@ -19,6 +21,7 @@ export const UserPositionsTabs = (props: Omit<UserPositionsTableProps, 'tab' | '
const { connect } = useWallet()
const { address } = useConnection()
const { markets } = props.result ?? {}
const isMobile = useIsMobile()

// Calculate total positions number across all markets (independent of filters)
const openPositionsCount = useMemo(
Expand Down Expand Up @@ -80,6 +83,7 @@ export const UserPositionsTabs = (props: Omit<UserPositionsTableProps, 'tab' | '
</Stack>
{address ? (
<>
{!isMobile && <UserPositionSummary markets={markets} />}
<Stack
direction="row"
justifyContent="space-between"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { LlamaMarket } from '@/llamalend/queries/market-list/llama-markets'
import { TooltipDescription } from '@/llamalend/widgets/tooltips/TooltipComponents'
import CircularProgress from '@mui/material/CircularProgress'
import Grid from '@mui/material/Grid'
import { t } from '@ui-kit/lib/i18n'
import { ExclamationTriangleIcon } from '@ui-kit/shared/icons/ExclamationTriangleIcon'
import { Metric } from '@ui-kit/shared/ui/Metric'
import { Tooltip } from '@ui-kit/shared/ui/Tooltip'
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
import { UserPositionSummaryMetric, useUserPositionsSummary } from './hooks/useUserPositionsSummary'

const { Spacing, IconSize } = SizesAndSpaces

type UserPositionStatisticsProps = {
markets: LlamaMarket[] | undefined
}

export const UserPositionSummary = ({ markets }: UserPositionStatisticsProps) => {
const summary = useUserPositionsSummary({ markets })
return (
<Grid container spacing={Spacing.sm} sx={{ backgroundColor: (t) => t.design.Layer[1].Fill }}>
{summary.map((item) => (
<UserPositionStatisticItem key={item.label} {...item} />
))}
</Grid>
)
}

const UserPositionStatisticItem = ({ label, data, isLoading, isError }: UserPositionSummaryMetric) => (
<Grid size={3} padding={Spacing.md}>
<Metric
value={data}
size="large"
valueOptions={{
decimals: 2,
unit: 'dollar',
color: 'textPrimary',
}}
label={label}
rightAdornment={
<>
{isError && (
<Tooltip
arrow
placement="top"
title={t`Error fetching ${label}`}
body={<TooltipDescription text={t`Some positions may be missing.`} />}
>
<ExclamationTriangleIcon fontSize="small" color="error" />
</Tooltip>
)}
{isLoading && !isError && <CircularProgress size={IconSize.xs.desktop} />}
</>
}
/>
</Grid>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useMemo } from 'react'
import { useConnection } from 'wagmi'
import {
getUserLendingVaultEarningsOptions,
getUserLendingVaultStatsOptions,
} from '@/llamalend/queries/market-list/lending-vaults'
import { LlamaMarket } from '@/llamalend/queries/market-list/llama-markets'
import { getUserMintMarketsStatsOptions } from '@/llamalend/queries/market-list/mint-markets'
import { useQueries } from '@tanstack/react-query'
import { combineQueriesMeta } from '@ui-kit/lib/queries/combine'
import { LlamaMarketType } from '@ui-kit/types/market'

type StatsQueryOptions =
| ReturnType<typeof getUserLendingVaultStatsOptions>
| ReturnType<typeof getUserMintMarketsStatsOptions>
type EarningsQueryOptions = ReturnType<typeof getUserLendingVaultEarningsOptions>

export type UserPositionSummaryMetric = { label: string; data: number; isLoading: boolean; isError: boolean }

const createMetric = (
label: string,
data: number,
isLoading: boolean,
isError: boolean,
): UserPositionSummaryMetric => ({ label, data, isLoading, isError })

export const useUserPositionsSummary = ({
markets,
}: {
markets: LlamaMarket[] | undefined
}): UserPositionSummaryMetric[] => {
const { address: userAddress } = useConnection()

const userPositionOptions = useMemo(() => {
if (!markets) return { borrow: [], supply: [] }

return markets.reduce<{ borrow: StatsQueryOptions[]; supply: EarningsQueryOptions[] }>(
(options, market) => {
const isSupply = market.userHasPositions?.Supply
const isBorrow = market.userHasPositions?.Borrow
if (!market.userHasPositions || (isSupply && !market.vaultAddress)) return options

const params = {
userAddress,
blockchainId: market.chain,
}

if (isSupply)
options.supply.push(getUserLendingVaultEarningsOptions({ contractAddress: market.vaultAddress, ...params }))
if (isBorrow)
options.borrow.push(
market.type === LlamaMarketType.Lend
? getUserLendingVaultStatsOptions({ contractAddress: market.controllerAddress, ...params })
: getUserMintMarketsStatsOptions({ contractAddress: market.controllerAddress, ...params }),
)

return options
},
{ borrow: [], supply: [] },
)
}, [markets, userAddress])

const borrowSummary = useQueries({
queries: userPositionOptions.borrow,
combine: (results) => ({
data: results.reduce(
(acc, stat) => ({
totalCollateralValue: acc.totalCollateralValue + (stat.data?.collateral ?? 0) * (stat.data?.oraclePrice ?? 0),
// TODO: multiply by token price
totalBorrowedValue: acc.totalBorrowedValue + (stat.data?.debt ?? 0),
}),
{ totalCollateralValue: 0, totalBorrowedValue: 0 },
),
...combineQueriesMeta(results),
}),
})

const supplySummary = useQueries({
queries: userPositionOptions.supply,
combine: (results) => ({
data: results.reduce(
(acc, stat) => ({
// TODO: multiply by token price
totalSuppliedValue: acc.totalSuppliedValue + (stat.data?.totalCurrentAssets ?? 0),
}),
{ totalSuppliedValue: 0 },
),
...combineQueriesMeta(results),
}),
})

return [
createMetric(
'Total Collateral Value',
borrowSummary.data.totalCollateralValue,
borrowSummary.isLoading,
borrowSummary.isError,
),
createMetric(
'Total Borrowed',
borrowSummary.data.totalBorrowedValue,
borrowSummary.isLoading,
borrowSummary.isError,
),
createMetric(
'Total Supplied',
supplySummary.data.totalSuppliedValue,
supplySummary.isLoading,
supplySummary.isError,
),
]
}
14 changes: 12 additions & 2 deletions apps/main/src/llamalend/queries/market-list/lending-vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,23 @@ const {
validationSuite: userAddressValidationSuite,
})

const { useQuery: useUserLendingVaultStatsQuery, invalidate: invalidateUserLendingVaultStats } = queryFactory({
const {
getQueryOptions: getUserLendingVaultStatsQueryOptions,
useQuery: useUserLendingVaultStatsQuery,
invalidate: invalidateUserLendingVaultStats,
} = queryFactory({
queryKey: ({ userAddress, contractAddress, blockchainId }: UserContractParams) =>
['user-lending-vault', 'stats', { blockchainId }, { contractAddress }, { userAddress }, 'v1'] as const,
queryFn: async ({ userAddress, contractAddress, blockchainId }: UserContractQuery): Promise<UserMarketStats> =>
getUserMarketStats(userAddress, blockchainId, contractAddress),
validationSuite: userContractValidationSuite,
})

const { useQuery: useUserLendingVaultEarningsQuery, invalidate: invalidateUserLendingVaultEarnings } = queryFactory({
const {
useQuery: useUserLendingVaultEarningsQuery,
getQueryOptions: getUserLendingVaultEarningsQueryOptions,
invalidate: invalidateUserLendingVaultEarnings,
} = queryFactory({
queryKey: ({ userAddress, contractAddress, blockchainId }: UserContractParams) =>
['user-lending-vault', 'earnings', { blockchainId }, { contractAddress }, { userAddress }, 'v1'] as const,
queryFn: ({ userAddress, contractAddress, blockchainId }: UserContractQuery) =>
Expand Down Expand Up @@ -110,6 +118,8 @@ export function invalidateAllUserLendingSupplies(userAddress: Address | undefine
}

export const getUserLendingSuppliesOptions = getUserLendingSuppliesQueryOptions
export const getUserLendingVaultEarningsOptions = getUserLendingVaultEarningsQueryOptions
export const useUserLendingVaultEarnings = useUserLendingVaultEarningsQuery
export const getUserLendingVaultsOptions = getUserLendingVaultsQueryOptions
export const getUserLendingVaultStatsOptions = getUserLendingVaultStatsQueryOptions
export const useUserLendingVaultStats = useUserLendingVaultStatsQuery
7 changes: 6 additions & 1 deletion apps/main/src/llamalend/queries/market-list/mint-markets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ const {

export const getUserMintMarketsOptions = getUserMintMarketsQueryOptions

const { useQuery: useUserMintMarketStatsQuery, invalidate: invalidateUserMintMarketStats } = queryFactory({
const {
getQueryOptions: getUserMintMarketStatsQueryOptions,
useQuery: useUserMintMarketStatsQuery,
invalidate: invalidateUserMintMarketStats,
} = queryFactory({
queryKey: ({ userAddress, blockchainId, contractAddress }: UserContractParams) =>
['user-mint-markets', 'stats', { blockchainId }, { contractAddress }, { userAddress }, 'v1'] as const,
queryFn: ({ userAddress, blockchainId, contractAddress }: UserContractQuery) =>
Expand All @@ -62,3 +66,4 @@ export const invalidateAllUserMintMarkets = (userAddress: Address | undefined) =
}

export const useUserMintMarketStats = useUserMintMarketStatsQuery
export const getUserMintMarketsStatsOptions = getUserMintMarketStatsQueryOptions
28 changes: 18 additions & 10 deletions packages/curve-ui-kit/src/shared/ui/Metric.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react'
import { ReactNode, useCallback, useMemo } from 'react'
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
import Alert from '@mui/material/Alert'
import AlertTitle from '@mui/material/AlertTitle'
Expand Down Expand Up @@ -175,6 +175,9 @@ export type MetricProps = {
/** Notional values give extra context to the metric, like underlying value */
notional?: number | string | Notional | Notional[]

/** Optional content to display to the right of the value */
rightAdornment?: ReactNode

size?: keyof typeof MetricSize
alignment?: Alignment
loading?: boolean
Expand All @@ -194,6 +197,8 @@ export const Metric = ({

notional,

rightAdornment,

size = 'medium',
alignment = 'start',
loading = false,
Expand Down Expand Up @@ -222,15 +227,18 @@ export const Metric = ({
</Typography>

<WithSkeleton loading={loading}>
<MetricValue
value={value}
valueOptions={valueOptions}
change={change}
size={size}
copyValue={value || value === 0 ? copyValue : undefined}
tooltip={valueTooltip}
testId={testId}
/>
<Stack direction="row" alignItems="baseline">
<MetricValue
value={value}
valueOptions={valueOptions}
change={change}
size={size}
copyValue={value || value === 0 ? copyValue : undefined}
tooltip={valueTooltip}
testId={testId}
/>
{rightAdornment}
</Stack>
</WithSkeleton>

{notionals && (
Expand Down
15 changes: 15 additions & 0 deletions packages/curve-ui-kit/src/shared/ui/stories/Metric.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Typography from '@mui/material/Typography'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { FireIcon } from '@ui-kit/shared/icons/FireIcon'
import { Metric, SIZES, ALIGNMENTS } from '../Metric'

const meta: Meta<typeof Metric> = {
Expand Down Expand Up @@ -185,4 +186,18 @@ export const NotAvailable: Story = {
},
}

export const RightAdornment: Story = {
args: {
size: 'large',
rightAdornment: <FireIcon fontSize="small" color="error" />,
},
parameters: {
docs: {
description: {
story: 'Demonstrates the Metric component with a right adornment',
},
},
},
}

export default meta