From 7a0fd6204ffbde6b8249ce18b5b0b16b0cacbadd Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 17 Dec 2025 16:48:43 +0100 Subject: [PATCH 1/4] feat: add rightAdornment prop to Metric component --- .../curve-ui-kit/src/shared/ui/Metric.tsx | 28 ++++++++++++------- .../src/shared/ui/stories/Metric.stories.tsx | 15 ++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/curve-ui-kit/src/shared/ui/Metric.tsx b/packages/curve-ui-kit/src/shared/ui/Metric.tsx index b3b8a096f9..660008e770 100644 --- a/packages/curve-ui-kit/src/shared/ui/Metric.tsx +++ b/packages/curve-ui-kit/src/shared/ui/Metric.tsx @@ -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' @@ -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 @@ -194,6 +197,8 @@ export const Metric = ({ notional, + rightAdornment, + size = 'medium', alignment = 'start', loading = false, @@ -222,15 +227,18 @@ export const Metric = ({ - + + + {rightAdornment} + {notionals && ( diff --git a/packages/curve-ui-kit/src/shared/ui/stories/Metric.stories.tsx b/packages/curve-ui-kit/src/shared/ui/stories/Metric.stories.tsx index b02623bc19..95aad3fa2f 100644 --- a/packages/curve-ui-kit/src/shared/ui/stories/Metric.stories.tsx +++ b/packages/curve-ui-kit/src/shared/ui/stories/Metric.stories.tsx @@ -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 = { @@ -185,4 +186,18 @@ export const NotAvailable: Story = { }, } +export const RightAdornment: Story = { + args: { + size: 'large', + rightAdornment: , + }, + parameters: { + docs: { + description: { + story: 'Demonstrates the Metric component with a right adornment', + }, + }, + }, +} + export default meta From 38411dc7949ccbe218a1cf5c780d1647e5792292 Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 17 Dec 2025 21:39:38 +0100 Subject: [PATCH 2/4] feat: user position summary with total collateral value --- .../features/market-list/UserPositionTabs.tsx | 4 ++ .../market-list/UserPositionsSummary.tsx | 58 +++++++++++++++++ .../hooks/useUserPositionsSummary.tsx | 64 +++++++++++++++++++ .../queries/market-list/lending-vaults.ts | 7 +- .../queries/market-list/mint-markets.ts | 7 +- 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 apps/main/src/llamalend/features/market-list/UserPositionsSummary.tsx create mode 100644 apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx diff --git a/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx b/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx index ad14ab4aab..5f55de8268 100644 --- a/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx +++ b/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx @@ -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 @@ -19,6 +21,7 @@ export const UserPositionsTabs = (props: Omit {address ? ( <> + {!isMobile && } { + const summary = useUserPositionsSummary({ markets }) + return ( + t.design.Layer[1].Fill }}> + {summary.map((item) => ( + + ))} + + ) +} + +const UserPositionStatisticItem = ({ label, data, isLoading, isError }: UserPositionSummaryMetric) => ( + + + {isError && ( + } + > + + + )} + {isLoading && !isError && } + + } + /> + +) diff --git a/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx b/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx new file mode 100644 index 0000000000..72163bdb94 --- /dev/null +++ b/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx @@ -0,0 +1,64 @@ +import { useMemo } from 'react' +import { useConnection } from 'wagmi' +import { 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 QueryOptions = + | ReturnType + | ReturnType + +export type UserPositionSummaryMetric = { label: string; data: number; isLoading: boolean; isError: boolean } + +const sumCollateralValue = (acc: number, stat: { data?: { collateral?: number; oraclePrice?: number } }) => + acc + (stat.data?.collateral ?? 0) * (stat.data?.oraclePrice ?? 0) + +export const useUserPositionsSummary = ({ + markets, +}: { + markets: LlamaMarket[] | undefined +}): UserPositionSummaryMetric[] => { + const { address: userAddress } = useConnection() + + const userPositionStatsOptions = useMemo(() => { + if (!markets) return [] + + return markets.reduce((options, market) => { + if (!market.userHasPositions?.Borrow) return options + + const params = { + userAddress, + contractAddress: market.controllerAddress, + blockchainId: market.chain, + } + + options.push( + market.type === LlamaMarketType.Lend + ? getUserLendingVaultStatsOptions(params) + : getUserMintMarketsStatsOptions(params), + ) + + return options + }, []) + }, [markets, userAddress]) + + const totalCollateralValue = useQueries({ + queries: userPositionStatsOptions, + combine: (results) => ({ + data: results.reduce(sumCollateralValue, 0), + ...combineQueriesMeta(results), + }), + }) + + return [ + { + label: 'Total Collateral Value', + data: totalCollateralValue.data, + isLoading: totalCollateralValue.isLoading, + isError: totalCollateralValue.isError, + }, + ] +} diff --git a/apps/main/src/llamalend/queries/market-list/lending-vaults.ts b/apps/main/src/llamalend/queries/market-list/lending-vaults.ts index 05475034a3..ddd4eb67fe 100644 --- a/apps/main/src/llamalend/queries/market-list/lending-vaults.ts +++ b/apps/main/src/llamalend/queries/market-list/lending-vaults.ts @@ -46,7 +46,11 @@ 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 => @@ -112,4 +116,5 @@ export function invalidateAllUserLendingSupplies(userAddress: Address | undefine export const getUserLendingSuppliesOptions = getUserLendingSuppliesQueryOptions export const useUserLendingVaultEarnings = useUserLendingVaultEarningsQuery export const getUserLendingVaultsOptions = getUserLendingVaultsQueryOptions +export const getUserLendingVaultStatsOptions = getUserLendingVaultStatsQueryOptions export const useUserLendingVaultStats = useUserLendingVaultStatsQuery diff --git a/apps/main/src/llamalend/queries/market-list/mint-markets.ts b/apps/main/src/llamalend/queries/market-list/mint-markets.ts index ad2ce23fb3..dcce65a8b7 100644 --- a/apps/main/src/llamalend/queries/market-list/mint-markets.ts +++ b/apps/main/src/llamalend/queries/market-list/mint-markets.ts @@ -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) => @@ -62,3 +66,4 @@ export const invalidateAllUserMintMarkets = (userAddress: Address | undefined) = } export const useUserMintMarketStats = useUserMintMarketStatsQuery +export const getUserMintMarketsStatsOptions = getUserMintMarketStatsQueryOptions From 8d2e3f358ee52d2fdde414f8c0f9e8956034388c Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 17 Dec 2025 22:16:57 +0100 Subject: [PATCH 3/4] feat: added total debt to user's positions summary --- .../features/market-list/UserPositionTabs.tsx | 2 +- .../market-list/UserPositionsSummary.tsx | 3 +-- .../hooks/useUserPositionsSummary.tsx | 26 ++++++++++++------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx b/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx index 5f55de8268..0d5c697e30 100644 --- a/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx +++ b/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx @@ -83,7 +83,7 @@ export const UserPositionsTabs = (props: Omit {address ? ( <> - {!isMobile && } + {!isMobile && } { +export const UserPositionSummary = ({ markets }: UserPositionStatisticsProps) => { const summary = useUserPositionsSummary({ markets }) return ( t.design.Layer[1].Fill }}> diff --git a/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx b/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx index 72163bdb94..ec5da87969 100644 --- a/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx +++ b/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx @@ -13,8 +13,12 @@ type QueryOptions = export type UserPositionSummaryMetric = { label: string; data: number; isLoading: boolean; isError: boolean } -const sumCollateralValue = (acc: number, stat: { data?: { collateral?: number; oraclePrice?: number } }) => - acc + (stat.data?.collateral ?? 0) * (stat.data?.oraclePrice ?? 0) +const createMetric = ( + label: string, + data: number, + isLoading: boolean, + isError: boolean, +): UserPositionSummaryMetric => ({ label, data, isLoading, isError }) export const useUserPositionsSummary = ({ markets, @@ -45,20 +49,22 @@ export const useUserPositionsSummary = ({ }, []) }, [markets, userAddress]) - const totalCollateralValue = useQueries({ + const summary = useQueries({ queries: userPositionStatsOptions, combine: (results) => ({ - data: results.reduce(sumCollateralValue, 0), + data: results.reduce( + (acc, stat) => ({ + totalCollateralValue: acc.totalCollateralValue + (stat.data?.collateral ?? 0) * (stat.data?.oraclePrice ?? 0), + totalBorrowedValue: acc.totalBorrowedValue + (stat.data?.debt ?? 0), + }), + { totalCollateralValue: 0, totalBorrowedValue: 0 }, + ), ...combineQueriesMeta(results), }), }) return [ - { - label: 'Total Collateral Value', - data: totalCollateralValue.data, - isLoading: totalCollateralValue.isLoading, - isError: totalCollateralValue.isError, - }, + createMetric('Total Collateral Value', summary.data.totalCollateralValue, summary.isLoading, summary.isError), + createMetric('Total Borrowed', summary.data.totalBorrowedValue, summary.isLoading, summary.isError), ] } From 15dd1e053f1072ee878f00b947801ea7d841c0f1 Mon Sep 17 00:00:00 2001 From: Pearce Date: Thu, 18 Dec 2025 09:04:12 +0100 Subject: [PATCH 4/4] feat: added total supply to user's positions summary --- .../hooks/useUserPositionsSummary.tsx | 86 ++++++++++++++----- .../queries/market-list/lending-vaults.ts | 7 +- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx b/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx index ec5da87969..ccca9c40fd 100644 --- a/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx +++ b/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx @@ -1,15 +1,19 @@ import { useMemo } from 'react' import { useConnection } from 'wagmi' -import { getUserLendingVaultStatsOptions } from '@/llamalend/queries/market-list/lending-vaults' +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 QueryOptions = +type StatsQueryOptions = | ReturnType | ReturnType +type EarningsQueryOptions = ReturnType export type UserPositionSummaryMetric = { label: string; data: number; isLoading: boolean; isError: boolean } @@ -27,34 +31,42 @@ export const useUserPositionsSummary = ({ }): UserPositionSummaryMetric[] => { const { address: userAddress } = useConnection() - const userPositionStatsOptions = useMemo(() => { - if (!markets) return [] + const userPositionOptions = useMemo(() => { + if (!markets) return { borrow: [], supply: [] } - return markets.reduce((options, market) => { - if (!market.userHasPositions?.Borrow) return options + 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, - contractAddress: market.controllerAddress, - blockchainId: market.chain, - } + const params = { + userAddress, + blockchainId: market.chain, + } - options.push( - market.type === LlamaMarketType.Lend - ? getUserLendingVaultStatsOptions(params) - : getUserMintMarketsStatsOptions(params), - ) + 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 - }, []) + return options + }, + { borrow: [], supply: [] }, + ) }, [markets, userAddress]) - const summary = useQueries({ - queries: userPositionStatsOptions, + 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 }, @@ -63,8 +75,38 @@ export const useUserPositionsSummary = ({ }), }) + 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', summary.data.totalCollateralValue, summary.isLoading, summary.isError), - createMetric('Total Borrowed', summary.data.totalBorrowedValue, summary.isLoading, summary.isError), + 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, + ), ] } diff --git a/apps/main/src/llamalend/queries/market-list/lending-vaults.ts b/apps/main/src/llamalend/queries/market-list/lending-vaults.ts index ddd4eb67fe..a36e1e5799 100644 --- a/apps/main/src/llamalend/queries/market-list/lending-vaults.ts +++ b/apps/main/src/llamalend/queries/market-list/lending-vaults.ts @@ -58,7 +58,11 @@ const { 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) => @@ -114,6 +118,7 @@ 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