diff --git a/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx b/apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx index ad14ab4aa..0d5c697e3 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 000000000..ccca9c40f --- /dev/null +++ b/apps/main/src/llamalend/features/market-list/hooks/useUserPositionsSummary.tsx @@ -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 + | ReturnType +type EarningsQueryOptions = ReturnType + +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, + ), + ] +} 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 05475034a..a36e1e579 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 => @@ -54,7 +58,11 @@ const { useQuery: useUserLendingVaultStatsQuery, invalidate: invalidateUserLendi 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) => @@ -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 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 ad2ce23fb..dcce65a8b 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 diff --git a/packages/curve-ui-kit/src/shared/ui/Metric.tsx b/packages/curve-ui-kit/src/shared/ui/Metric.tsx index b3b8a096f..660008e77 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 b02623bc1..95aad3fa2 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