diff --git a/apps/main/next-env.d.ts b/apps/main/next-env.d.ts index 52e831b43..a4a7b3f5c 100644 --- a/apps/main/next-env.d.ts +++ b/apps/main/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsFilters.tsx b/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsFilters.tsx index 5864157be..bb85a1acc 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsFilters.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsFilters.tsx @@ -18,13 +18,13 @@ const { Spacing } = SizesAndSpaces * This is used in the lending markets filters to display collateral and debt tokens. */ const Token = ({ symbol, data, field }: { symbol: string; data: LlamaMarket[]; field: 'collateral' | 'borrowed' }) => { - const { blockchainId, address } = useMemo( + const { chain, address } = useMemo( () => data.find((d) => d.assets[field].symbol === symbol)!.assets[field], [data, field, symbol], ) return ( <> - + {symbol} @@ -45,12 +45,12 @@ export const LendingMarketsFilters = ({ ( + field="chain" + renderItem={(chain) => ( <> - + - {capitalize(blockchainId)} + {capitalize(chain)} )} @@ -79,7 +79,7 @@ export const LendingMarketsFilters = ({ formatNumber(value, { currency: 'USD' })} {...props} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx b/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx index caaf0ace1..63b9bceab 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx @@ -2,130 +2,41 @@ import Stack from '@mui/material/Stack' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import { TableFilters, useColumnFilters } from '@ui-kit/shared/ui/TableFilters' import { t } from '@ui-kit/lib/i18n' -import { CompactUsdCell, LineGraphCell, PoolTitleCell, RateCell, UtilizationCell } from './cells' import { DataTable } from '@ui-kit/shared/ui/DataTable' import { LlamaMarket } from '@/loan/entities/llama-markets' -import { - ColumnDef, - createColumnHelper, - getCoreRowModel, - getFilteredRowModel, - getSortedRowModel, - useReactTable, -} from '@tanstack/react-table' +import { getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table' import { LendingMarketsFilters } from '@/loan/components/PageLlamaMarkets/LendingMarketsFilters' import { useSortFromQueryString } from '@ui-kit/hooks/useSortFromQueryString' -import { DeepKeys } from '@tanstack/table-core/build/lib/utils' -import { useVisibilitySettings, VisibilityGroup } from '@ui-kit/shared/ui/TableVisibilitySettingsPopover' +import { useVisibilitySettings } from '@ui-kit/shared/ui/TableVisibilitySettingsPopover' +import { MarketsFilterChips } from '@/loan/components/PageLlamaMarkets/MarketsFilterChips' +import { DEFAULT_SORT, DEFAULT_VISIBILITY, LLAMA_MARKET_COLUMNS } from '@/loan/components/PageLlamaMarkets/columns' -const { ColumnWidth, Spacing, MaxWidth } = SizesAndSpaces - -const columnHelper = createColumnHelper() - -/** Define a hidden column. */ -const hidden = (id: DeepKeys) => - columnHelper.accessor(id, { - filterFn: (row, columnId, filterValue) => !filterValue?.length || filterValue.includes(row.getValue(columnId)), - meta: { hidden: true }, - }) - -const [borrowChartId, lendChartId] = ['borrowChart', 'lendChart'] - -/** Columns for the lending markets table. */ -const columns = [ - columnHelper.accessor('assets', { - header: t`Collateral • Borrow`, - cell: PoolTitleCell, - size: ColumnWidth.lg, - }), - columnHelper.accessor('rates.borrow', { - header: t`7D Avg Borrow Rate`, - cell: (c) => , - meta: { type: 'numeric' }, - size: ColumnWidth.sm, - }), - columnHelper.accessor('rates.borrow', { - id: borrowChartId, - header: t`7D Borrow Rate Chart`, - cell: (c) => , - size: ColumnWidth.md, - enableSorting: false, - }), - columnHelper.accessor('rates.lend', { - header: t`7D Avg Supply Yield`, - cell: (c) => , - meta: { type: 'numeric' }, - size: ColumnWidth.sm, - sortUndefined: 'last', - }), - columnHelper.accessor('rates.lend', { - id: lendChartId, - header: t`7D Supply Yield Chart`, - cell: (c) => , - size: ColumnWidth.md, - sortUndefined: 'last', - enableSorting: false, - }), - columnHelper.accessor('utilizationPercent', { - header: t`Utilization`, - cell: UtilizationCell, - meta: { type: 'numeric' }, - size: ColumnWidth.sm, - }), - columnHelper.accessor('totalSupplied.usdTotal', { - header: () => t`Available Liquidity`, - cell: CompactUsdCell, - meta: { type: 'numeric' }, - size: ColumnWidth.sm, - }), - // following columns are used to configure and filter tanstack, but they are displayed together in PoolTitleCell - hidden('blockchainId'), - hidden('assets.collateral.symbol'), - hidden('assets.borrowed.symbol'), -] satisfies ColumnDef[] - -const DEFAULT_SORT = [{ id: 'totalSupplied.usdTotal', desc: true }] - -const DEFAULT_VISIBILITY: VisibilityGroup[] = [ - { - label: t`Markets`, - options: [ - { label: t`Available Liquidity`, id: 'totalSupplied.usdTotal', active: true }, - { label: t`Utilization`, id: 'utilizationPercent', active: true }, - ], - }, - { - label: t`Borrow`, - options: [{ label: t`Chart`, id: borrowChartId, active: true }], - }, - { - label: t`Lend`, - options: [{ label: t`Chart`, id: lendChartId, active: true }], - }, -] +const { Spacing, MaxWidth } = SizesAndSpaces export const LendingMarketsTable = ({ onReload, data, headerHeight, + isError, }: { onReload: () => void data: LlamaMarket[] headerHeight: string + isError: boolean }) => { - const [columnFilters, columnFiltersById, setColumnFilter] = useColumnFilters() + const [columnFilters, columnFiltersById, setColumnFilter, resetFilters] = useColumnFilters() const { columnSettings, columnVisibility, toggleVisibility } = useVisibilitySettings(DEFAULT_VISIBILITY) const [sorting, onSortingChange] = useSortFromQueryString(DEFAULT_SORT) const table = useReactTable({ - columns, + columns: LLAMA_MARKET_COLUMNS, data, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), state: { sorting, columnVisibility, columnFilters }, onSortingChange, - maxMultiSortColCount: 3, // allow 3 columns to be sorted at once + maxMultiSortColCount: 3, // allow 3 columns to be sorted at once while holding shift }) return ( @@ -136,7 +47,12 @@ export const LendingMarketsTable = ({ maxWidth: MaxWidth.table, }} > - + + } > - + diff --git a/apps/main/src/loan/components/PageLlamaMarkets/MarketsFilterChips.tsx b/apps/main/src/loan/components/PageLlamaMarkets/MarketsFilterChips.tsx new file mode 100644 index 000000000..62cc043d3 --- /dev/null +++ b/apps/main/src/loan/components/PageLlamaMarkets/MarketsFilterChips.tsx @@ -0,0 +1,99 @@ +import { t } from '@ui-kit/lib/i18n' +import { HeartIcon } from '@ui-kit/shared/icons/HeartIcon' +import { PointsIcon } from '@ui-kit/shared/icons/PointsIcon' +import { LlamaMarket, LlamaMarketType } from '@/loan/entities/llama-markets' +import Stack from '@mui/material/Stack' +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' +import { DeepKeys } from '@tanstack/table-core/build/lib/utils' +import { useCallback } from 'react' +import { SelectableChip } from '@ui-kit/shared/ui/SelectableChip' + +const { Spacing } = SizesAndSpaces + +type LlamaMarketKey = DeepKeys + +type ColumnFilterProps = { + columnFiltersById: Record + setColumnFilter: (id: LlamaMarketKey, value: unknown) => void +} + +/** Hook for managing a single boolean filter */ +function useToggleFilter(key: LlamaMarketKey, { columnFiltersById, setColumnFilter }: ColumnFilterProps) { + const isFiltered = !!columnFiltersById[key] + const toggle = useCallback( + () => setColumnFilter(key, isFiltered ? undefined : true), + [isFiltered, key, setColumnFilter], + ) + return [isFiltered, toggle] as const +} + +/** + * Hook for managing market type filter + * @returns marketTypes - object with keys for each market type and boolean values indicating if the type is selected + * @returns toggles - object with keys for each market type and functions to toggle the type + */ +function useMarketTypeFilter({ columnFiltersById, setColumnFilter }: ColumnFilterProps) { + const filter = columnFiltersById['type'] as LlamaMarketType[] | undefined + const toggleMarketType = useCallback( + (type: LlamaMarketType) => { + setColumnFilter( + 'type', + !filter || filter.includes(type) + ? (filter ?? Object.values(LlamaMarketType)).filter((f) => f !== type) + : [...(filter || []), type], + ) + }, + [filter, setColumnFilter], + ) + + const marketTypes = { + [LlamaMarketType.Mint]: !filter || filter.includes(LlamaMarketType.Mint), + [LlamaMarketType.Lend]: !filter || filter.includes(LlamaMarketType.Lend), + } + const toggles = { + [LlamaMarketType.Mint]: useCallback(() => toggleMarketType(LlamaMarketType.Mint), [toggleMarketType]), + [LlamaMarketType.Lend]: useCallback(() => toggleMarketType(LlamaMarketType.Lend), [toggleMarketType]), + } + return [marketTypes, toggles] as const +} + +export const MarketsFilterChips = (props: ColumnFilterProps) => { + const [favorites, toggleFavorites] = useToggleFilter('isFavorite', props) + const [rewards, toggleRewards] = useToggleFilter('rewards', props) + const [marketTypes, toggleMarkets] = useMarketTypeFilter(props) + + return ( + + + } + data-testid="chip-favorites" + /> + } + data-testid="chip-rewards" + /> + + + + + + + ) +} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/Page.tsx b/apps/main/src/loan/components/PageLlamaMarkets/Page.tsx index ba208db8e..eba5d612f 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/Page.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/Page.tsx @@ -10,8 +10,18 @@ import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import { useHeaderHeight } from '@ui-kit/widgets/Header' import useStore from '@/loan/store/useStore' import { useLlamaMarkets } from '@/loan/entities/llama-markets' +import usePageOnMount from '@/loan/hooks/usePageOnMount' +import { useLocation, useNavigate, useParams } from 'react-router-dom' +import { invalidateMintMarkets } from '@/loan/entities/mint-markets' -const onReload = () => invalidateLendingVaults({}) +/** + * Reloads the lending vaults and mint markets. + * Note: It does not reload the snapshots (for now). + */ +const onReload = () => { + invalidateLendingVaults({}) + invalidateMintMarkets({}) +} const { Spacing, MaxWidth, ModalHeight } = SizesAndSpaces @@ -19,16 +29,17 @@ const { Spacing, MaxWidth, ModalHeight } = SizesAndSpaces * Page for displaying the lending markets table. */ export const PageLlamaMarkets = () => { - const { data, isFetching, isError } = useLlamaMarkets() // todo: show errors and loading state + const { data, isLoading, isError } = useLlamaMarkets() // todo: show errors and loading state const bannerHeight = useStore((state) => state.layout.height.globalAlert) const headerHeight = useHeaderHeight(bannerHeight) + usePageOnMount(useParams(), useLocation(), useNavigate()) // required for connecting wallet return ( - {data ? ( - - ) : ( + {isLoading ? ( + ) : ( + )} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketBadges.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketBadges.tsx new file mode 100644 index 000000000..0687c0b06 --- /dev/null +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketBadges.tsx @@ -0,0 +1,71 @@ +import Stack from '@mui/material/Stack' +import Chip from '@mui/material/Chip' +import { t } from '@ui-kit/lib/i18n' +import React from 'react' +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' +import { LlamaMarket, LlamaMarketType } from '@/loan/entities/llama-markets' +import IconButton from '@mui/material/IconButton' +import { FavoriteHeartIcon } from '@ui-kit/shared/icons/HeartIcon' +import { PointsIcon } from '@ui-kit/shared/icons/PointsIcon' +import { useTheme } from '@mui/material/styles' +import Tooltip from '@mui/material/Tooltip' +import { DesktopOnlyHoverClass } from '@ui-kit/shared/ui/DataTable' +import { useFavoriteMarket } from '@/loan/entities/favorite-markets' +import { PoolRewards } from '@/loan/entities/campaigns' + +const { Spacing } = SizesAndSpaces + +const poolTypeNames: Record string> = { + [LlamaMarketType.Lend]: () => t`Lend`, + [LlamaMarketType.Mint]: () => t`Mint`, +} + +const poolTypeTooltips: Record string> = { + [LlamaMarketType.Lend]: () => t`Lend markets allow you to earn interest on your assets.`, + [LlamaMarketType.Mint]: () => t`Mint markets allow you to borrow assets against your collateral.`, +} + +const getRewardsDescription = ({ action, description, multiplier }: PoolRewards) => + `${multiplier}x: ${ + { + lp: description ?? t`Earn points by providing liquidity.`, + supply: t`Earn points by supplying liquidity.`, + borrow: t`Earn points by borrowing.`, + }[action] + }` + +/** Displays badges for a pool, such as the chain icon and the pool type. */ +export const MarketBadges = ({ market: { address, rewards, type, leverage } }: { market: LlamaMarket }) => { + const [isFavorite, toggleFavorite] = useFavoriteMarket(address) + const iconsColor = useTheme().design.Text.TextColors.Highlight + return ( + + + + + + {leverage > 0 && ( + + + + )} + + {rewards && ( + + + + )} + + + + + + + + ) +} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketTitleCell.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketTitleCell.tsx new file mode 100644 index 000000000..62d759700 --- /dev/null +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketTitleCell.tsx @@ -0,0 +1,45 @@ +import { LlamaMarket } from '@/loan/entities/llama-markets' +import Stack from '@mui/material/Stack' +import React from 'react' +import { CellContext } from '@tanstack/react-table' +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' +import Typography from '@mui/material/Typography' +import { MarketBadges } from '@/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketBadges' +import { MarketWarnings } from '@/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketWarnings' +import { TokenPair } from '@/loan/components/PageLlamaMarkets/cells/MarketTitleCell/TokenPair' +import { TransitionFunction } from '@ui-kit/themes/design/0_primitives' +import { t } from '@ui-kit/lib/i18n' +import { CopyIconButton } from '@ui-kit/shared/ui/CopyIconButton' +import { Link as RouterLink } from 'react-router-dom' +import MuiLink from '@mui/material/Link' + +const { Spacing } = SizesAndSpaces + +const showIconOnHover = { + '& .MuiIconButton-root': { opacity: 0, transition: `opacity ${TransitionFunction}` }, + [`&:hover .MuiIconButton-root`]: { opacity: 1 }, +} + +export const MarketTitleCell = ({ row: { original: market } }: CellContext) => ( + + + + + + + {market.assets.borrowed.symbol} - {market.assets.collateral.symbol} + + + + + + +) diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketWarnings.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketWarnings.tsx new file mode 100644 index 000000000..dabb8ee27 --- /dev/null +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/MarketWarnings.tsx @@ -0,0 +1,32 @@ +import Stack from '@mui/material/Stack' +import React from 'react' +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' +import { LlamaMarket } from '@/loan/entities/llama-markets' +import Chip from '@mui/material/Chip' +import { t } from '@ui-kit/lib/i18n' +import Tooltip from '@mui/material/Tooltip' +import Typography from '@mui/material/Typography' +import { ExclamationTriangleIcon } from '@ui-kit/shared/icons/ExclamationTriangleIcon' + +const { Spacing } = SizesAndSpaces + +/** + * Displays warnings for a pool, such as deprecated pools or pools with collateral corrosion. + */ +export const MarketWarnings = ({ market: { isCollateralEroded, deprecatedMessage } }: { market: LlamaMarket }) => ( + + {deprecatedMessage && ( + + + {t`Deprecated`} + + + + )} + {isCollateralEroded && ( + + + + )} + +) diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/TokenPair.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/TokenPair.tsx new file mode 100644 index 000000000..d0fdee365 --- /dev/null +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/TokenPair.tsx @@ -0,0 +1,37 @@ +import { AssetDetails, LlamaMarket } from '@/loan/entities/llama-markets' +import { getImageBaseUrl } from '@ui/utils' +import Box from '@mui/material/Box' +import Tooltip from '@mui/material/Tooltip' +import TokenIcon from '@/loan/components/TokenIcon' +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' +import { ChainIcon } from '@ui-kit/shared/icons/ChainIcon' +import { ReactNode } from 'react' +import type { SxProps, Theme } from '@mui/material/styles' + +type TokenPairProps = Pick + +const { IconSize } = SizesAndSpaces + +const TooltipBox = ({ title, children, sx }: { title: string; children: ReactNode; sx: SxProps }) => ( + + + {children} + + +) + +const TokenBox = ({ coin: { address, chain, symbol }, sx }: { coin: AssetDetails; sx: SxProps }) => ( + + + +) + +export const TokenPair = ({ chain, assets: { borrowed, collateral } }: TokenPairProps) => ( + + + + + + + +) diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/index.ts b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/index.ts new file mode 100644 index 000000000..93069c1b2 --- /dev/null +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/MarketTitleCell/index.ts @@ -0,0 +1 @@ +export { MarketTitleCell } from './MarketTitleCell' diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolBadges.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolBadges.tsx deleted file mode 100644 index 053706ba4..000000000 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolBadges.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import Stack from '@mui/material/Stack' -import { t } from '@ui-kit/lib/i18n' -import React, { ReactNode } from 'react' -import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' -import Typography from '@mui/material/Typography' -import { ChainIcon } from '@ui-kit/shared/icons/ChainIcon' -import { LlamaMarketType } from '@/loan/entities/llama-markets' - -const { Spacing } = SizesAndSpaces - -/** Display a single badge for a pool. */ -const Badge = ({ children, compact }: { children: ReactNode; compact?: boolean }) => ( - ({ - border: `1px solid ${t.design.Layer[1].Outline}`, - alignContent: 'center', - ...(compact - ? { - paddingInline: '1px', - height: 22, // not ideal to hardcode, but if left out the badge becomes 24px somehow - } - : { - paddingInline: '6px', // hardcoded from figma - paddingBlock: Spacing.xxs, // xs in figma but content is 12px there instead of 14px - }), - })} - > - {children} - -) - -const poolTypeNames: Record string> = { - [LlamaMarketType.Pool]: () => t`Pool`, - [LlamaMarketType.Mint]: () => t`Mint`, -} - -/** Displays badges for a pool, such as the chain icon and the pool type. */ -export const PoolBadges = ({ blockchainId, type }: { blockchainId: string; type: LlamaMarketType }) => ( - - - - - {poolTypeNames[type]()} - -) diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolTitleCell.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolTitleCell.tsx deleted file mode 100644 index 8652f1fb7..000000000 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolTitleCell.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { LlamaMarket } from '@/loan/entities/llama-markets' -import Stack from '@mui/material/Stack' -import TokenIcons from '@/loan/components/TokenIcons' -import React, { useMemo } from 'react' -import { CellContext } from '@tanstack/react-table' -import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' -import Typography from '@mui/material/Typography' -import { PoolBadges } from '@/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolBadges' -import { PoolWarnings } from '@/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolWarnings' -import { getImageBaseUrl } from '@ui/utils' -import { cleanColumnId } from '@ui-kit/shared/ui/TableVisibilitySettingsPopover' - -const { Spacing } = SizesAndSpaces - -export const PoolTitleCell = ({ getValue, row, table }: CellContext) => { - const showCollateral = table.getColumn(cleanColumnId('assets.collateral.symbol'))!.getIsVisible() - const coins = useMemo(() => { - const { borrowed, collateral } = getValue() - return showCollateral ? [collateral, borrowed] : [borrowed] - }, [getValue, showCollateral]) - const { blockchainId, type } = row.original - const imageBaseUrl = getImageBaseUrl(blockchainId) - return ( - - c.symbol)} - tokenAddresses={coins.map((c) => c.address)} - /> - - - {coins.map((coin) => coin.symbol).join(' - ')} - - - - ) -} diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolWarnings.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolWarnings.tsx deleted file mode 100644 index 546052b3e..000000000 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/PoolWarnings.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Stack from '@mui/material/Stack' -import React from 'react' -import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' - -const { Spacing } = SizesAndSpaces - -/** - * Displays warnings for a pool, such as deprecated pools or pools with collateral corrosion. - * Note: for now, this component is an empty placeholder to keep the design correct, it does not display any warnings. - */ -export const PoolWarnings = () => diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/index.ts b/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/index.ts deleted file mode 100644 index 726d40b57..000000000 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/PoolTitleCell/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PoolTitleCell } from './PoolTitleCell' diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx b/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx index c2504d827..e44cfa35e 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/RateCell.tsx @@ -4,5 +4,5 @@ import { GraphType } from '@/loan/components/PageLlamaMarkets/hooks/useSnapshots export const RateCell = ({ market, type }: { market: LlamaMarket; type: GraphType }) => { const { rate } = useSnapshots(market, type) - return rate == null ? '-' : `${rate.toPrecision(4)}%` + return rate == null ? '-' : `${(rate * 100).toPrecision(4)}%` } diff --git a/apps/main/src/loan/components/PageLlamaMarkets/cells/index.ts b/apps/main/src/loan/components/PageLlamaMarkets/cells/index.ts index 8c59115eb..f4569d391 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/cells/index.ts +++ b/apps/main/src/loan/components/PageLlamaMarkets/cells/index.ts @@ -1,5 +1,5 @@ export * from './CompactUsdCell' -export * from './PoolTitleCell' +export * from './MarketTitleCell' export * from './LineGraphCell' export * from './RateCell' export * from './UtilizationCell' diff --git a/apps/main/src/loan/components/PageLlamaMarkets/columns.tsx b/apps/main/src/loan/components/PageLlamaMarkets/columns.tsx new file mode 100644 index 000000000..38a2a56c7 --- /dev/null +++ b/apps/main/src/loan/components/PageLlamaMarkets/columns.tsx @@ -0,0 +1,107 @@ +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' +import { ColumnDef, createColumnHelper, FilterFnOption } from '@tanstack/react-table' +import { LlamaMarket } from '@/loan/entities/llama-markets' +import { DeepKeys } from '@tanstack/table-core/build/lib/utils' +import { t } from '@ui-kit/lib/i18n' +import { + CompactUsdCell, + LineGraphCell, + MarketTitleCell, + RateCell, + UtilizationCell, +} from '@/loan/components/PageLlamaMarkets/cells' +import { VisibilityGroup } from '@ui-kit/shared/ui/TableVisibilitySettingsPopover' + +const { ColumnWidth } = SizesAndSpaces + +const columnHelper = createColumnHelper() + +const multiFilterFn: FilterFnOption = (row, columnId, filterValue) => + !filterValue?.length || filterValue.includes(row.getValue(columnId)) +const boolFilterFn: FilterFnOption = (row, columnId, filterValue) => + filterValue === undefined || Boolean(row.getValue(columnId)) === Boolean(filterValue) + +/** Define a hidden column. */ +const hidden = (id: DeepKeys, filterFn: FilterFnOption) => + columnHelper.accessor(id, { + filterFn, + meta: { hidden: true }, + }) + +const [borrowChartId, lendChartId] = ['borrowChart', 'lendChart'] + +/** Columns for the lending markets table. */ +export const LLAMA_MARKET_COLUMNS = [ + columnHelper.accessor('assets', { + header: t`Collateral • Borrow`, + cell: MarketTitleCell, + size: ColumnWidth.lg, + }), + columnHelper.accessor('rates.borrow', { + header: t`7D Avg Borrow Rate`, + cell: (c) => , + meta: { type: 'numeric' }, + size: ColumnWidth.sm, + }), + columnHelper.accessor('rates.borrow', { + id: borrowChartId, + header: t`7D Borrow Rate Chart`, + cell: (c) => , + size: ColumnWidth.md, + enableSorting: false, + }), + columnHelper.accessor('rates.lend', { + header: t`7D Avg Supply Yield`, + cell: (c) => , + meta: { type: 'numeric' }, + size: ColumnWidth.sm, + sortUndefined: 'last', + }), + columnHelper.accessor('rates.lend', { + id: lendChartId, + header: t`7D Supply Yield Chart`, + cell: (c) => , + size: ColumnWidth.md, + sortUndefined: 'last', + enableSorting: false, + }), + columnHelper.accessor('utilizationPercent', { + header: t`Utilization`, + cell: UtilizationCell, + meta: { type: 'numeric' }, + size: ColumnWidth.sm, + }), + columnHelper.accessor('liquidityUsd', { + header: () => t`Available Liquidity`, + cell: CompactUsdCell, + meta: { type: 'numeric' }, + size: ColumnWidth.sm, + }), + // Following columns are used in tanstack filter, but they are displayed together in MarketTitleCell + hidden('chain', multiFilterFn), + hidden('assets.collateral.symbol', multiFilterFn), + hidden('assets.borrowed.symbol', multiFilterFn), + hidden('isFavorite', boolFilterFn), + hidden('rewards', boolFilterFn), + hidden('type', multiFilterFn), +] satisfies ColumnDef[] + +export const DEFAULT_SORT = [{ id: 'liquidityUsd', desc: true }] + +export const DEFAULT_VISIBILITY: VisibilityGroup[] = [ + { + label: t`Markets`, + options: [ + { label: t`Available Liquidity`, id: 'liquidityUsd', active: true }, + { label: t`Utilization`, id: 'utilizationPercent', active: true }, + ], + }, + { + label: t`Borrow`, + options: [{ label: t`Chart`, id: borrowChartId, active: true }], + }, + { + label: t`Lend`, + options: [{ label: t`Chart`, id: lendChartId, active: true }], + }, +] diff --git a/apps/main/src/loan/components/PageLlamaMarkets/hooks/useSnapshots.ts b/apps/main/src/loan/components/PageLlamaMarkets/hooks/useSnapshots.ts index c39f17f36..c430f3d61 100644 --- a/apps/main/src/loan/components/PageLlamaMarkets/hooks/useSnapshots.ts +++ b/apps/main/src/loan/components/PageLlamaMarkets/hooks/useSnapshots.ts @@ -1,6 +1,6 @@ import { LlamaMarket, LlamaMarketType } from '@/loan/entities/llama-markets' import { LendingSnapshot, useLendingSnapshots } from '@/loan/entities/lending-snapshots' -import { CrvUsdSnapshot, useCrvUsdSnapshots } from '@/loan/entities/crvusd' +import { CrvUsdSnapshot, useCrvUsdSnapshots } from '@/loan/entities/crvusd-snapshots' import { useMemo } from 'react' import { meanBy } from 'lodash' @@ -14,13 +14,13 @@ type UseSnapshotsResult = { } export function useSnapshots( - { address, blockchainId, controllerAddress, type: marketType, rates }: LlamaMarket, + { address, chain, controllerAddress, type: marketType, rates }: LlamaMarket, type: GraphType, ): UseSnapshotsResult { - const isPool = marketType == LlamaMarketType.Pool + const isPool = marketType == LlamaMarketType.Lend const showMintGraph = !isPool && type === 'borrow' const contractAddress = isPool ? controllerAddress : address - const params = { blockchainId: blockchainId, contractAddress } + const params = { blockchainId: chain, contractAddress } const { data: poolSnapshots, isLoading: poolIsLoading } = useLendingSnapshots(params, isPool) const { data: mintSnapshots, isLoading: mintIsLoading } = useCrvUsdSnapshots(params, showMintGraph) @@ -29,7 +29,7 @@ export function useSnapshots( ? { snapshots: poolSnapshots ?? null, isLoading: poolIsLoading, - snapshotKey: `${type}_apy` as const, + snapshotKey: `${type}Apy` as const, } : { snapshots: (showMintGraph && mintSnapshots) || null, diff --git a/apps/main/src/loan/entities/campaigns.ts b/apps/main/src/loan/entities/campaigns.ts new file mode 100644 index 000000000..92cbe58fb --- /dev/null +++ b/apps/main/src/loan/entities/campaigns.ts @@ -0,0 +1,32 @@ +import campaigns from '@external-rewards' +import { CampaignRewardsItem, RewardsAction, RewardsTags } from '@ui/CampaignRewards/types' +import { queryFactory } from '@ui-kit/lib/model' +import { EmptyValidationSuite } from '@ui-kit/lib' + +export type PoolRewards = { + action: RewardsAction + multiplier: number + tags: RewardsTags[] + description: string | null +} + +const REWARDS: Record = Object.fromEntries( + campaigns.flatMap(({ pools }: CampaignRewardsItem) => + pools.map(({ address, multiplier, tags, action, description }) => [ + address.toLowerCase(), + { + multiplier: parseFloat(multiplier), + tags, + action, + description: description === 'null' ? null : description, + }, + ]), + ), +) + +export const { getQueryOptions: getCampaignsOptions } = queryFactory({ + queryKey: () => ['external-rewards', 'v1'] as const, + queryFn: async () => REWARDS, + staleTime: '5m', + validationSuite: EmptyValidationSuite, +}) diff --git a/apps/main/src/loan/entities/chains.ts b/apps/main/src/loan/entities/chains.ts new file mode 100644 index 000000000..19e34c657 --- /dev/null +++ b/apps/main/src/loan/entities/chains.ts @@ -0,0 +1,18 @@ +import { queryFactory } from '@ui-kit/lib/model' +import { EmptyValidationSuite } from '@ui-kit/lib' +import { getChains } from '@curvefi/prices-api/llamalend' +import { getSupportedChains } from '@curvefi/prices-api/chains' + +export const { getQueryOptions: getSupportedChainOptions } = queryFactory({ + queryKey: () => ['prices-api', 'supported-chains'] as const, + queryFn: getSupportedChains, + staleTime: '1d', + validationSuite: EmptyValidationSuite, +}) + +export const { getQueryOptions: getSupportedLendingChainOptions } = queryFactory({ + queryKey: () => ['prices-api', 'supported-lending-chains'] as const, + queryFn: getChains, + staleTime: '1d', + validationSuite: EmptyValidationSuite, +}) diff --git a/apps/main/src/loan/entities/crvusd.ts b/apps/main/src/loan/entities/crvusd-snapshots.ts similarity index 63% rename from apps/main/src/loan/entities/crvusd.ts rename to apps/main/src/loan/entities/crvusd-snapshots.ts index aad860668..1e3c216b9 100644 --- a/apps/main/src/loan/entities/crvusd.ts +++ b/apps/main/src/loan/entities/crvusd-snapshots.ts @@ -8,13 +8,8 @@ export type CrvUsdSnapshot = Snapshot export const { useQuery: useCrvUsdSnapshots } = queryFactory({ queryKey: (params: ContractParams) => [...rootKeys.contract(params), 'crvUsd', 'snapshots'] as const, - queryFn: async ({ blockchainId, contractAddress }: ContractQuery): Promise => { - const snapshots = await getSnapshots(blockchainId as Chain, contractAddress) - return snapshots.map((snapshot) => ({ - ...snapshot, - rate: snapshot.rate * 100, // Convert to percentage for consistency with lending snapshots - })) - }, + queryFn: ({ blockchainId, contractAddress }: ContractQuery): Promise => + getSnapshots(blockchainId as Chain, contractAddress, { agg: 'none' }), staleTime: '10m', validationSuite: contractValidationSuite, }) diff --git a/apps/main/src/loan/entities/favorite-markets.ts b/apps/main/src/loan/entities/favorite-markets.ts new file mode 100644 index 000000000..ac663012c --- /dev/null +++ b/apps/main/src/loan/entities/favorite-markets.ts @@ -0,0 +1,23 @@ +import { queryFactory } from '@ui-kit/lib/model' +import { getFromLocalStorage, useLocalStorage } from '@ui-kit/hooks/useLocalStorage' +import { EmptyValidationSuite } from '@ui-kit/lib' +import { useCallback, useMemo } from 'react' + +const { getQueryOptions: getFavoriteMarketOptions, invalidate: invalidateFavoriteMarkets } = queryFactory({ + queryKey: () => ['favorite-markets'] as const, + queryFn: async () => getFromLocalStorage('favoriteMarkets') ?? [], + staleTime: '5m', + validationSuite: EmptyValidationSuite, +}) + +export function useFavoriteMarket(address: string) { + const [favorites, setFavorites] = useLocalStorage('favoriteMarkets', []) + const isFavorite = useMemo(() => favorites.includes(address), [favorites, address]) + const toggleFavorite = useCallback(() => { + isFavorite ? setFavorites(favorites.filter((id) => id !== address)) : setFavorites([...favorites, address]) + invalidateFavoriteMarkets({}) + }, [favorites, isFavorite, address, setFavorites]) + return [isFavorite, toggleFavorite] as const +} + +export { getFavoriteMarketOptions } diff --git a/apps/main/src/loan/entities/lending-snapshots.ts b/apps/main/src/loan/entities/lending-snapshots.ts index 4afbb29d4..4ac2ce057 100644 --- a/apps/main/src/loan/entities/lending-snapshots.ts +++ b/apps/main/src/loan/entities/lending-snapshots.ts @@ -1,68 +1,23 @@ import { ContractParams, ContractQuery, queryFactory, rootKeys } from '@ui-kit/lib/model/query' import { contractValidationSuite } from '@ui-kit/lib/model/query/contract-validation' import { queryClient } from '@ui-kit/lib/api/query-client' -import { EmptyValidationSuite } from '@ui-kit/lib' - -type LendingSnapshotFromApi = { - rate: number - borrow_apy: number - lend_apy: number - liquidation_discount: number - loan_discount: number - n_loans: number - price_oracle: number - amm_price: number - base_price: number - total_debt: number - total_assets: number - total_debt_usd: number - total_assets_usd: number - minted: number - redeemed: number - minted_usd: number - redeemed_usd: number - min_band: number - max_band: number - collateral_balance: number - borrowed_balance: number - collateral_balance_usd: number - borrowed_balance_usd: number - sum_debt_squared: number - timestamp: string -} +import { getSupportedLendingChainOptions } from '@/loan/entities/chains' +import { Chain } from '@curvefi/prices-api' +import { getSnapshots, Snapshot } from '@curvefi/prices-api/llamalend' +type LendingSnapshotFromApi = Snapshot export type LendingSnapshot = LendingSnapshotFromApi -type LendingSnapshotsFromApi = { - chain: string - market_id: number - data: LendingSnapshot[] -} - -export const { getQueryOptions: getSupportedChainOptions } = queryFactory({ - queryKey: () => ['lending-snapshots', 'supported-chains'] as const, - queryFn: async () => { - const response = await fetch(`https://prices.curve.fi/v1/lending/chains`) - const { data } = (await response.json()) as { data: string[] } - return data - }, - staleTime: '1d', - validationSuite: EmptyValidationSuite, -}) - export const { useQuery: useLendingSnapshots } = queryFactory({ - queryKey: (params: ContractParams) => [...rootKeys.contract(params), 'lendingSnapshots'] as const, + queryKey: (params: ContractParams) => [...rootKeys.contract(params), 'lendingSnapshots', 'v1'] as const, queryFn: async ({ blockchainId, contractAddress }: ContractQuery): Promise => { - const chains = await queryClient.fetchQuery(getSupportedChainOptions({})) - if (!chains.includes(blockchainId)) return [] // backend gives 404 for optimism + const chains = await queryClient.fetchQuery(getSupportedLendingChainOptions({})) + const chain = blockchainId as Chain + if (!chains.includes(chain)) return [] // backend gives 404 for optimism - const url = `https://prices.curve.fi/v1/lending/markets/${blockchainId}/${contractAddress}/snapshots?agg=none` - const response = await fetch(url) - const { data } = (await response.json()) as LendingSnapshotsFromApi - if (!data) { - throw new Error('Failed to fetch lending snapshots') - } - return data.reverse() // todo: pass &sort_by=DATE_ASC&start=${start} and remove reverse - backend is timing out + // todo: pass {sort_by: 'DATE_ASC, start: now-week} and remove reverse (backend is timing out) + const response = await getSnapshots(chain, contractAddress, { agg: 'none' }) + return response.reverse() }, staleTime: '1h', validationSuite: contractValidationSuite, diff --git a/apps/main/src/loan/entities/lending-vaults.ts b/apps/main/src/loan/entities/lending-vaults.ts index 710e5c629..aa2d6d8fb 100644 --- a/apps/main/src/loan/entities/lending-vaults.ts +++ b/apps/main/src/loan/entities/lending-vaults.ts @@ -1,113 +1,20 @@ import { queryFactory } from '@ui-kit/lib/model/query' import { EmptyValidationSuite } from '@ui-kit/lib/validation' +import { queryClient } from '@ui-kit/lib/api/query-client' +import { getMarkets, Market } from '@curvefi/prices-api/llamalend' +import { getSupportedLendingChainOptions } from '@/loan/entities/chains' +import { Chain } from '@curvefi/prices-api' -export type AmmBalances = { - ammBalanceBorrowed: number - ammBalanceBorrowedUsd: number - ammBalanceCollateral: number - ammBalanceCollateralUsd: number | null -} - -export type Assets = { - borrowed: AssetDetails - collateral: AssetDetails -} - -export type AssetDetails = { - symbol: string - decimals?: number - address: string - blockchainId: string - usdPrice: number | null -} - -export type CoinValue = { - total: number - usdTotal: number -} - -export type GaugeReward = { - gaugeAddress: string - tokenPrice: number - name: string - symbol: string - decimals: string - tokenAddress: string - apy: number - metaData?: GaugeRewardMetadata -} - -export type GaugeRewardMetadata = { - rate: string - periodFinish: number -} - -export type LendingVaultUrls = { - deposit: string - withdraw: string -} - -export type LendingRates = { - borrowApr: number - borrowApy: number - borrowApyPcent: number - lendApr: number - lendApy: number - lendApyPcent: number -} - -export type VaultShares = { - pricePerShare: number - totalShares: number -} - -export type LendingVaultFromApi = { - id: string - name: string - address: string - controllerAddress: string - ammAddress: string - monetaryPolicyAddress: string - rates: LendingRates - gaugeAddress?: string - gaugeRewards?: GaugeReward[] - assets: Assets - vaultShares: VaultShares - totalSupplied: CoinValue - borrowed: CoinValue - availableToBorrow: CoinValue - lendingVaultUrls: LendingVaultUrls - usdTotal: number - ammBalances: AmmBalances - blockchainId: string - registryId: 'oneway' -} - -type GetLendingVaultResponse = { - data?: { - lendingVaultData: LendingVaultFromApi[] - tvl: number - } - success: boolean -} +export type LendingVault = Market & { chain: Chain } export const { getQueryOptions: getLendingVaultOptions, invalidate: invalidateLendingVaults } = queryFactory({ - queryKey: () => ['lending-vaults-v3'] as const, - queryFn: async () => { - const response = await fetch('https://api.curve.fi/v1/getLendingVaults/all') - const { data, success } = (await response.json()) as GetLendingVaultResponse - if (!success || !data) { - throw new Error('Failed to fetch pools') - } - return { - ...data, - lendingVaultData: data.lendingVaultData - .filter((vault) => vault.totalSupplied.usdTotal) - .map((vault) => ({ - ...vault, - utilizationPercent: (100 * vault.borrowed.usdTotal) / vault.totalSupplied.usdTotal, - })), - } + queryKey: () => ['lending-vaults', 'v1'] as const, + queryFn: async (): Promise => { + const chains = await queryClient.fetchQuery(getSupportedLendingChainOptions({})) + const markets = await Promise.all( + chains.map(async (chain) => (await getMarkets(chain, {})).map((market) => ({ ...market, chain }))), + ) + return markets.flat() }, staleTime: '5m', validationSuite: EmptyValidationSuite, diff --git a/apps/main/src/loan/entities/llama-markets.ts b/apps/main/src/loan/entities/llama-markets.ts index 7e994e064..e3d63ce01 100644 --- a/apps/main/src/loan/entities/llama-markets.ts +++ b/apps/main/src/loan/entities/llama-markets.ts @@ -1,106 +1,167 @@ -import { Assets, getLendingVaultOptions, LendingVaultFromApi } from '@/loan/entities/lending-vaults' +import { getLendingVaultOptions, LendingVault } from '@/loan/entities/lending-vaults' import { useQueries } from '@tanstack/react-query' import { getMintMarketOptions, MintMarket } from '@/loan/entities/mint-markets' import { combineQueriesMeta, PartialQueryResult } from '@ui-kit/lib' +import { t } from '@ui-kit/lib/i18n' +import { APP_LINK, CRVUSD_ROUTES, LEND_ROUTES } from '@ui-kit/shared/routes' +import { Chain } from '@curvefi/prices-api' +import { getFavoriteMarketOptions } from '@/loan/entities/favorite-markets' +import { getCampaignsOptions, PoolRewards } from '@/loan/entities/campaigns' export enum LlamaMarketType { - Mint = 'mint', - Pool = 'pool', + Mint = 'Mint', + Lend = 'Lend', +} + +export type Assets = { + borrowed: AssetDetails + collateral: AssetDetails +} + +export type AssetDetails = { + symbol: string + address: string + chain: Chain + usdPrice: number | null } export type LlamaMarket = { - blockchainId: string + chain: Chain address: string controllerAddress: string assets: Assets utilizationPercent: number - totalSupplied: { - total: number - usdTotal: number - } + liquidityUsd: number rates: { - lend?: number // apy %, only for pools + lend: number | null // apy %, only for pools borrow: number // apy % } type: LlamaMarketType + url: string + rewards: PoolRewards | null + isCollateralEroded: boolean + isFavorite: boolean + leverage: number + deprecatedMessage?: string } -const convertLendingVault = ({ - controllerAddress, - blockchainId, - totalSupplied, - assets, - address, - rates, - borrowed, -}: LendingVaultFromApi): LlamaMarket => ({ - blockchainId: blockchainId, - address: address, - controllerAddress: controllerAddress, - assets: assets, - utilizationPercent: (100 * borrowed.usdTotal) / totalSupplied.usdTotal, - totalSupplied: totalSupplied, - rates: { - lend: rates.lendApyPcent, - borrow: rates.borrowApyPcent, +const DEPRECATED_LLAMAS: Record string> = { + '0x136e783846ef68C8Bd00a3369F787dF8d683a696': () => + t`Please note this market is being phased out. We recommend migrating to the sfrxETH v2 market which uses an updated oracle.`, +} + +const convertLendingVault = ( + { + controller, + chain, + totalAssetsUsd, + totalDebtUsd, + vault, + collateralToken, + collateralBalance, + collateralBalanceUsd, + borrowedToken, + borrowedBalance, + borrowedBalanceUsd, + apyBorrow, + apyLend, + leverage, + }: LendingVault, + favoriteMarkets: Set, + campaigns: Record, +): LlamaMarket => ({ + chain, + address: vault, + controllerAddress: controller, + assets: { + borrowed: { + ...borrowedToken, + usdPrice: borrowedBalanceUsd / borrowedBalance, + chain, + }, + collateral: { + ...collateralToken, + chain, + usdPrice: collateralBalanceUsd / collateralBalance, + }, }, - type: LlamaMarketType.Pool, + utilizationPercent: (100 * totalDebtUsd) / totalAssetsUsd, + liquidityUsd: collateralBalanceUsd + borrowedBalanceUsd, + rates: { lend: apyLend, borrow: apyBorrow }, + type: LlamaMarketType.Lend, + url: `${APP_LINK.lend.root}#/${chain}${LEND_ROUTES.PAGE_MARKETS}/${vault}/create`, + isFavorite: favoriteMarkets.has(vault), + rewards: campaigns[vault.toLowerCase()] ?? null, + leverage, + isCollateralEroded: false, // todo }) const convertMintMarket = ( { address, - collateral_token, - stablecoin_token, + collateralToken, + collateralAmount, + collateralAmountUsd, + stablecoinToken, llamma, rate, - total_debt, - debt_ceiling, - collateral_amount, - collateral_amount_usd, + borrowed, + debtCeiling, stablecoin_price, + chain, }: MintMarket, - blockchainId: string, + favoriteMarkets: Set, + campaigns: Record, ): LlamaMarket => ({ - blockchainId, + chain, address, controllerAddress: llamma, assets: { borrowed: { - symbol: stablecoin_token.symbol, - address: stablecoin_token.address, + symbol: stablecoinToken.symbol, + address: stablecoinToken.address, usdPrice: stablecoin_price, - blockchainId, + chain, }, collateral: { - symbol: collateral_token.symbol, - address: collateral_token.address, - usdPrice: collateral_amount_usd / collateral_amount, - blockchainId, + symbol: collateralToken.symbol, + address: collateralToken.address, + usdPrice: collateralAmountUsd / collateralAmount, + chain, }, }, - utilizationPercent: (100 * total_debt) / debt_ceiling, - totalSupplied: { - // todo: do we want to see collateral or borrowable? - total: collateral_amount, - usdTotal: collateral_amount_usd, - }, - rates: { - borrow: rate * 100, - }, + utilizationPercent: Math.min(100, (100 * borrowed) / debtCeiling), // debt ceiling may be lowered + // todo: do we want to see collateral or borrowable? + liquidityUsd: collateralAmountUsd, + rates: { borrow: rate, lend: null }, type: LlamaMarketType.Mint, + deprecatedMessage: DEPRECATED_LLAMAS[llamma]?.(), + url: `/${chain}${CRVUSD_ROUTES.PAGE_MARKETS}/${collateralToken.symbol}/create`, + isFavorite: favoriteMarkets.has(address), + rewards: campaigns[address.toLowerCase()] ?? null, + leverage: 0, + isCollateralEroded: false, // todo }) export const useLlamaMarkets = () => useQueries({ - queries: [getLendingVaultOptions({}), getMintMarketOptions({})], - combine: ([lendingVaults, mintMarkets]): PartialQueryResult => ({ - ...combineQueriesMeta([lendingVaults, mintMarkets]), - data: [ - ...(lendingVaults.data?.lendingVaultData ?? []) - .filter((vault) => vault.totalSupplied.usdTotal) - .map(convertLendingVault), - ...(mintMarkets.data ?? []).flatMap(({ chain, data }) => data.map((i) => convertMintMarket(i, chain))), - ], - }), + queries: [ + getLendingVaultOptions({}), + getMintMarketOptions({}), + getCampaignsOptions({}), + getFavoriteMarketOptions({}), + ], + combine: ([lendingVaults, mintMarkets, campaigns, favoriteMarkets]): PartialQueryResult => { + const favoriteMarketsSet = new Set(favoriteMarkets.data) + const campaignData = campaigns.data ?? {} + return { + ...combineQueriesMeta([lendingVaults, mintMarkets, favoriteMarkets]), + data: [ + ...(lendingVaults.data ?? []) + .filter((vault) => vault.totalAssetsUsd) + .map((vault) => convertLendingVault(vault, favoriteMarketsSet, campaignData)), + ...(mintMarkets.data ?? []).map((market) => convertMintMarket(market, favoriteMarketsSet, campaignData)), + ], + } + }, }) diff --git a/apps/main/src/loan/entities/mint-markets.ts b/apps/main/src/loan/entities/mint-markets.ts index f14e660f4..f6e61f299 100644 --- a/apps/main/src/loan/entities/mint-markets.ts +++ b/apps/main/src/loan/entities/mint-markets.ts @@ -3,74 +3,44 @@ import { EmptyValidationSuite } from '@ui-kit/lib/validation' import { queryClient } from '@ui-kit/lib/api/query-client' import uniq from 'lodash/uniq' import { getCoinPrices } from '@/loan/entities/usd-prices' +import { getMarkets, Market } from '@curvefi/prices-api/crvusd' +import { Chain } from '@curvefi/prices-api' +import { getSupportedChainOptions } from '@/loan/entities/chains' -type MintMarketFromApi = { - address: string - factory_address: string - llamma: string - rate: number - total_debt: number - n_loans: number - debt_ceiling: number - borrowable: number - pending_fees: number - collected_fees: number - collateral_amount: number - collateral_amount_usd: number - stablecoin_amount: number - collateral_token: { - symbol: string - address: string - } - stablecoin_token: { - symbol: string - address: string - } -} +type MintMarketFromApi = Market export type MintMarket = MintMarketFromApi & { stablecoin_price: number + chain: Chain } -export const { getQueryOptions: getSupportedChainOptions } = queryFactory({ - queryKey: () => ['mint-markets', 'supported-chains'] as const, - queryFn: async () => { - const response = await fetch(`https://prices.curve.fi/v1/chains`) - const { data } = (await response.json()) as { data: { name: string }[] } - return data.map((chain) => chain.name) - }, - staleTime: '1d', - validationSuite: EmptyValidationSuite, -}) - /** * Note: The API does not provide stablecoin prices, fetch them separately and add them to the data. * I requested benber86 to add stablecoin prices to the API, but it may take some time. */ -async function addStableCoinPrices({ chain, data }: { chain: string; data: MintMarketFromApi[] }) { - const stablecoinAddresses = uniq(data.map((market) => market.stablecoin_token.address)) +async function addStableCoinPrices({ chain, data }: { chain: Chain; data: MintMarketFromApi[] }) { + const stablecoinAddresses = uniq(data.map((market) => market.stablecoinToken.address)) const stablecoinPrices = await getCoinPrices(stablecoinAddresses, chain) - console.log({ stablecoinPrices, stablecoinAddresses }) - return { + return data.map((market) => ({ + ...market, chain, - data: data.map((market) => ({ - ...market, - stablecoin_price: stablecoinPrices[market.stablecoin_token.address], - })), - } + stablecoin_price: stablecoinPrices[market.stablecoinToken.address], + })) } -export const { getQueryOptions: getMintMarketOptions } = queryFactory({ - queryKey: () => ['mint-markets'] as const, +export const { getQueryOptions: getMintMarketOptions, invalidate: invalidateMintMarkets } = queryFactory({ + queryKey: () => ['mint-markets', 'v1'] as const, queryFn: async () => { const chains = await queryClient.fetchQuery(getSupportedChainOptions({})) - return await Promise.all( + const allMarkets = await Promise.all( + // todo: create separate query for the loop, so it can be cached separately chains.map(async (blockchainId) => { - const response = await fetch(`https://prices.curve.fi/v1/crvusd/markets/${blockchainId}`) - const data = (await response.json()) as { chain: string; data: MintMarketFromApi[] } - return await addStableCoinPrices(data) + const chain = blockchainId as Chain + const data = await getMarkets(chain, {}) + return await addStableCoinPrices({ chain, data }) }), ) + return allMarkets.flat() }, staleTime: '5m', validationSuite: EmptyValidationSuite, diff --git a/packages/curve-ui-kit/src/shared/icons/CancelIcon.tsx b/packages/curve-ui-kit/src/shared/icons/CancelIcon.tsx new file mode 100644 index 000000000..9feece6aa --- /dev/null +++ b/packages/curve-ui-kit/src/shared/icons/CancelIcon.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from '@mui/material/utils' + +export const CancelIcon = createSvgIcon( + + + , + 'Cancel', +) diff --git a/packages/curve-ui-kit/src/shared/icons/CopyIcon.tsx b/packages/curve-ui-kit/src/shared/icons/CopyIcon.tsx new file mode 100644 index 000000000..f5faa7102 --- /dev/null +++ b/packages/curve-ui-kit/src/shared/icons/CopyIcon.tsx @@ -0,0 +1,9 @@ +import { createSvgIcon } from '@mui/material/utils' + +export const CopyIcon = createSvgIcon( + + + + , + 'Copy', +) diff --git a/packages/curve-ui-kit/src/shared/icons/HeartIcon.tsx b/packages/curve-ui-kit/src/shared/icons/HeartIcon.tsx new file mode 100644 index 000000000..60dde126c --- /dev/null +++ b/packages/curve-ui-kit/src/shared/icons/HeartIcon.tsx @@ -0,0 +1,20 @@ +import { createSvgIcon } from '@mui/material/utils' + +export const HeartIcon = createSvgIcon( + + + , + 'Heart', +) + +export const FavoriteHeartIcon = ({ isFavorite, color = 'primary' }: { isFavorite: boolean; color?: string }) => ( + +) diff --git a/packages/curve-ui-kit/src/shared/icons/PointsIcon.tsx b/packages/curve-ui-kit/src/shared/icons/PointsIcon.tsx new file mode 100644 index 000000000..9e380c353 --- /dev/null +++ b/packages/curve-ui-kit/src/shared/icons/PointsIcon.tsx @@ -0,0 +1,13 @@ +import { createSvgIcon } from '@mui/material/utils' + +export const PointsIcon = createSvgIcon( + + + , + 'Points', +) diff --git a/packages/curve-ui-kit/src/shared/ui/CopyIconButton.tsx b/packages/curve-ui-kit/src/shared/ui/CopyIconButton.tsx new file mode 100644 index 000000000..67ea13a57 --- /dev/null +++ b/packages/curve-ui-kit/src/shared/ui/CopyIconButton.tsx @@ -0,0 +1,42 @@ +import { useSwitch } from '@ui-kit/hooks/useSwitch' +import Tooltip from '@mui/material/Tooltip' +import IconButton from '@mui/material/IconButton' +import { CopyIcon } from '@ui-kit/shared/icons/CopyIcon' +import Snackbar from '@mui/material/Snackbar' +import { Duration } from '@ui-kit/themes/design/0_primitives' +import Alert from '@mui/material/Alert' +import AlertTitle from '@mui/material/AlertTitle' + +export function CopyIconButton({ + copyText, + label, + confirmationText, +}: { + copyText: string + label: string + confirmationText: string +}) { + const [isCopied, showAlert, hideAlert] = useSwitch(false) + return ( + <> + + { + await navigator.clipboard.writeText(copyText) + showAlert() + }} + > + + + + + + + {confirmationText} + {copyText} + + + + ) +} diff --git a/packages/curve-ui-kit/src/shared/ui/DataTable.tsx b/packages/curve-ui-kit/src/shared/ui/DataTable.tsx index 58d0a5d92..f979ef374 100644 --- a/packages/curve-ui-kit/src/shared/ui/DataTable.tsx +++ b/packages/curve-ui-kit/src/shared/ui/DataTable.tsx @@ -13,6 +13,9 @@ import { TransitionFunction } from '@ui-kit/themes/design/0_primitives' const { Sizing, Spacing, MinWidth } = SizesAndSpaces +// css class to hide elements on desktop unless the row is hovered +export const DesktopOnlyHoverClass = 'desktop-only-on-hover' + const getAlignment = ({ columnDef }: Column) => columnDef.meta?.type == 'numeric' ? 'right' : 'left' @@ -43,12 +46,13 @@ const DataRow = ({ row, rowHeight }: { row: Row; rowHeight const entry = useIntersectionObserver(ref, { freezeOnceVisible: true }) // what about "TanStack Virtual"? return ( ({ marginBlock: 0, height: Sizing[rowHeight], - borderBottom: '1px solid', - borderColor: (t) => t.design.Layer[1].Outline, - }} + borderBottom: `1px solid${t.design.Layer[1].Outline}`, + [`& .${DesktopOnlyHoverClass}`]: { opacity: { desktop: 0 }, transition: `opacity ${TransitionFunction}` }, + [`&:hover .${DesktopOnlyHoverClass}`]: { opacity: { desktop: '100%' } }, + })} ref={ref} data-testid={`data-table-row-${row.id}`} > @@ -156,7 +160,7 @@ export const DataTable = ({ {table.getRowModel().rows.length === 0 && ( - + count + headers.length, 0)} diff --git a/packages/curve-ui-kit/src/shared/ui/SelectableChip.tsx b/packages/curve-ui-kit/src/shared/ui/SelectableChip.tsx new file mode 100644 index 000000000..bc1340672 --- /dev/null +++ b/packages/curve-ui-kit/src/shared/ui/SelectableChip.tsx @@ -0,0 +1,33 @@ +import Chip, { ChipProps } from '@mui/material/Chip' +import { CancelIcon } from '@ui-kit/shared/icons/CancelIcon' +import { TransitionFunction } from '@ui-kit/themes/design/0_primitives' + +/** + * Renders a chip that can be selected or deselected. + * This customizes the MUI Chip component to change color and icon based on selection state. + * The delete icon is always visible, but hidden when the chip is not selected via font-size animation. + */ +export const SelectableChip = ({ + selected, + toggle, + ...props +}: { + selected: boolean + toggle: () => void +} & ChipProps) => ( + + } + {...props} + /> +) diff --git a/packages/curve-ui-kit/src/shared/ui/TableFilters.tsx b/packages/curve-ui-kit/src/shared/ui/TableFilters.tsx index 0690b004b..6d0c1159d 100644 --- a/packages/curve-ui-kit/src/shared/ui/TableFilters.tsx +++ b/packages/curve-ui-kit/src/shared/ui/TableFilters.tsx @@ -16,11 +16,9 @@ import SvgIcon from '@mui/material/SvgIcon' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { TableVisibilitySettingsPopover, VisibilityGroup } from '@ui-kit/shared/ui/TableVisibilitySettingsPopover' import { ToolkitIcon } from '@ui-kit/shared/icons/ToolkitIcon' +import { t } from '@ui-kit/lib/i18n' -const { - Spacing, - Grid: { Column_Spacing }, -} = SizesAndSpaces +const { Spacing } = SizesAndSpaces /** * A button for controlling the DataTable. @@ -59,25 +57,29 @@ export const TableFilters = ({ title, subtitle, onReload, + onResetFilters, learnMoreUrl, visibilityGroups, toggleVisibility, + collapsible, children, }: { title: string subtitle: string learnMoreUrl: string onReload: () => void + onResetFilters: () => void visibilityGroups: VisibilityGroup[] toggleVisibility: (columnId: string) => void + collapsible: ReactNode children: ReactNode }) => { const [filterExpanded, setFilterExpanded] = useLocalStorage(`filter-expanded-${kebabCase(title)}`) const [visibilitySettingsOpen, openVisibilitySettings, closeVisibilitySettings] = useSwitch() const settingsRef = useRef(null) return ( - - + + {title} {subtitle} @@ -101,7 +103,19 @@ export const TableFilters = ({ - {filterExpanded != null && children} + + {children} + + + {filterExpanded != null && collapsible} {visibilitySettingsOpen != null && settingsRef.current && ( = columnFilters.reduce( (acc, filter) => ({ ...acc, [filter.id]: filter.value, @@ -140,5 +154,7 @@ export function useColumnFilters() { {}, ) - return [columnFilters, columnFiltersById, setColumnFilter] as const + const resetFilters = useCallback(() => setColumnFilters([]), [setColumnFilters]) + + return [columnFilters, columnFiltersById, setColumnFilter, resetFilters] as const } diff --git a/packages/curve-ui-kit/src/themes/button/mui-icon-button.ts b/packages/curve-ui-kit/src/themes/button/mui-icon-button.ts index 7531b3648..d9e55641b 100644 --- a/packages/curve-ui-kit/src/themes/button/mui-icon-button.ts +++ b/packages/curve-ui-kit/src/themes/button/mui-icon-button.ts @@ -21,6 +21,10 @@ export const defineMuiIconButton = ({ Button, Layer, Text }: DesignSystem): Comp '&:hover': { color: Button.Ghost.Hover.Label, backgroundColor: 'transparent', filter: 'saturate(2)' }, fontFamily: Fonts[Text.FontFamily.Button], }, + sizeExtraSmall: { + height: ButtonSize.xs, + minWidth: ButtonSize.xs, + }, sizeSmall: { height: ButtonSize.sm, minWidth: ButtonSize.sm, diff --git a/packages/curve-ui-kit/src/themes/button/override-buttons.d.ts b/packages/curve-ui-kit/src/themes/button/override-buttons.d.ts index b0962d87a..a9d151eff 100644 --- a/packages/curve-ui-kit/src/themes/button/override-buttons.d.ts +++ b/packages/curve-ui-kit/src/themes/button/override-buttons.d.ts @@ -1,5 +1,15 @@ import { DesignSystem } from './design' +declare module '@mui/material/IconButton' { + export interface IconButtonPropsSizeOverrides { + extraSmall: true + } + + export interface IconButtonClasses { + sizeExtraSmall: string + } +} + declare module '@mui/material/Button' { type Buttons = Omit type ButtonColors = { diff --git a/packages/curve-ui-kit/src/themes/chip/mui-chip.ts b/packages/curve-ui-kit/src/themes/chip/mui-chip.ts index 342964d69..33d396001 100644 --- a/packages/curve-ui-kit/src/themes/chip/mui-chip.ts +++ b/packages/curve-ui-kit/src/themes/chip/mui-chip.ts @@ -110,6 +110,7 @@ export const defineMuiChip = ( cursor: 'pointer', '&:has(.MuiChip-icon)': { ...handleBreakpoints({ paddingInline: Spacing.sm }) }, '&:hover': { + borderColor: 'transparent', backgroundColor: Chips.Hover.Fill, color: Chips.Hover.Label, '& .MuiChip-deleteIcon': { diff --git a/packages/curve-ui-kit/src/themes/design/1_sizes_spaces.ts b/packages/curve-ui-kit/src/themes/design/1_sizes_spaces.ts index 546022475..1b43a9fca 100644 --- a/packages/curve-ui-kit/src/themes/design/1_sizes_spaces.ts +++ b/packages/curve-ui-kit/src/themes/design/1_sizes_spaces.ts @@ -211,7 +211,7 @@ const MappedLineHeight = { lg: { mobile: '1.5rem', // 24px tablet: '1.5rem', // 24px - desktop: '1.75rem', // 28px + desktop: '2rem', // 32px }, xl: { mobile: '2rem', // 32px diff --git a/packages/external-rewards/src/index.ts b/packages/external-rewards/src/index.ts index df550abde..edd808e36 100644 --- a/packages/external-rewards/src/index.ts +++ b/packages/external-rewards/src/index.ts @@ -7,7 +7,7 @@ const campaigns = campaignList const campaignName = campaign.split('.')?.[0] if (!campaignName || !(campaignName in parsedCampaignsJsons)) return null - const dateFilteredCampaign = { + return { ...parsedCampaignsJsons[campaignName], pools: parsedCampaignsJsons[campaignName].pools.filter((pool: any) => { const currentTime = Date.now() / 1000 @@ -23,9 +23,7 @@ const campaigns = campaignList return currentTime >= startTime && currentTime <= endTime }), } - - return dateFilteredCampaign }) - .filter((campaign) => campaign !== null) + .filter(Boolean) export default campaigns diff --git a/packages/prices-api/src/crvusd/api.ts b/packages/prices-api/src/crvusd/api.ts index ba6bc81aa..3056671a7 100644 --- a/packages/prices-api/src/crvusd/api.ts +++ b/packages/prices-api/src/crvusd/api.ts @@ -1,23 +1,35 @@ import { getHost, type Options, type Chain } from '..' -import { fetchJson as fetch } from '../fetch' +import { fetchJson as fetch, addQueryString } from '../fetch' import type * as Responses from './responses' import * as Parsers from './parsers' -export async function getMarkets(chain: Chain, page: number, options?: Options) { +export async function getMarkets( + chain: Chain, + params: { + page?: number + per_page?: number + fetch_on_chain?: boolean + } = { fetch_on_chain: true }, + options?: Options, +) { const host = getHost(options) - const resp = await fetch( - `${host}/v1/crvusd/markets/${chain}?fetch_on_chain=true&page=${page}&per_page=10`, - ) - + const resp = await fetch(`${host}/v1/crvusd/markets/${chain}${addQueryString(params)}`) return resp.data.map(Parsers.parseMarket) } -export async function getSnapshots(chain: Chain, marketAddr: string, options?: Options) { +export async function getSnapshots( + chain: Chain, + marketAddr: string, + params: { + agg?: string + fetch_on_chain?: boolean + } = { fetch_on_chain: true, agg: 'day' }, + options?: Options, +) { const host = getHost(options) const resp = await fetch( - `${host}/v1/crvusd/markets/${chain}/${marketAddr}/snapshots?fetch_on_chain=true&agg=day`, + `${host}/v1/crvusd/markets/${chain}/${marketAddr}/snapshots${addQueryString(params)}`, ) - return resp.data.map(Parsers.parseSnapshot) } diff --git a/packages/prices-api/src/crvusd/models.ts b/packages/prices-api/src/crvusd/models.ts index ae1dab07d..8fe89c439 100644 --- a/packages/prices-api/src/crvusd/models.ts +++ b/packages/prices-api/src/crvusd/models.ts @@ -3,20 +3,20 @@ import type { Address } from '..' export type Market = { name: string address: Address - factory: Address + factoryAddress: Address llamma: Address rate: number borrowed: number borrowable: number - collateral: number - collateralUsd: number + collateralAmount: number + collateralAmountUsd: number debtCeiling: number loans: number - tokenCollateral: { + collateralToken: { symbol: string address: Address } - tokenStablecoin: { + stablecoinToken: { symbol: string address: Address } diff --git a/packages/prices-api/src/crvusd/parsers.ts b/packages/prices-api/src/crvusd/parsers.ts index e29a420e1..8bbab3358 100644 --- a/packages/prices-api/src/crvusd/parsers.ts +++ b/packages/prices-api/src/crvusd/parsers.ts @@ -5,20 +5,20 @@ import type * as Models from './models' export const parseMarket = (x: Responses.GetMarketsResponse['data'][number]): Models.Market => ({ name: x.collateral_token.symbol, address: x.address, - factory: x.factory_address, + factoryAddress: x.factory_address, llamma: x.llamma, rate: x.rate, borrowed: x.total_debt, borrowable: x.borrowable, - collateral: x.collateral_amount, - collateralUsd: x.collateral_amount_usd, + collateralAmount: x.collateral_amount, + collateralAmountUsd: x.collateral_amount_usd, debtCeiling: x.debt_ceiling, loans: x.n_loans, - tokenCollateral: { + collateralToken: { symbol: x.collateral_token.symbol, address: x.collateral_token.address, }, - tokenStablecoin: { + stablecoinToken: { symbol: x.stablecoin_token.symbol, address: x.stablecoin_token.address, }, diff --git a/packages/prices-api/src/fetch.ts b/packages/prices-api/src/fetch.ts index 1b54b8247..6612a32b1 100644 --- a/packages/prices-api/src/fetch.ts +++ b/packages/prices-api/src/fetch.ts @@ -1,3 +1,16 @@ +/** + * Converts a Record of string key-value pairs to a URL query string. + * Ignores keys with null or undefined values, automatically converting other values to strings. + */ +export const addQueryString = (params: Record) => { + const query = new URLSearchParams( + Object.entries(params) + .filter(([_, value]) => value != null) + .map(([key, value]) => [key, value!.toString()]), + ).toString() + return query && `?${query}` +} + export class FetchError extends Error { constructor( public status: number, @@ -32,9 +45,6 @@ export async function fetchJson(url: string, body?: Record, if (!resp.ok) { // Make the promise be rejected if we didn't get a 2xx response throw new FetchError(resp.status, `Fetch error ${resp.status} for URL: ${url}`) - } else { - const json = (await resp.json()) as T - - return json } + return resp.json() } diff --git a/packages/prices-api/src/llamalend/api.ts b/packages/prices-api/src/llamalend/api.ts index 40364c0b8..7a254ffb3 100644 --- a/packages/prices-api/src/llamalend/api.ts +++ b/packages/prices-api/src/llamalend/api.ts @@ -1,5 +1,5 @@ import { getHost, type Options, type Chain } from '..' -import { fetchJson as fetch } from '../fetch' +import { fetchJson as fetch, addQueryString } from '../fetch' import type * as Responses from './responses' import * as Parsers from './parsers' @@ -9,21 +9,33 @@ export async function getChains(options?: Options): Promise { return fetch(`${host}/v1/lending/chains`).then((resp) => resp.data) } -export async function getMarkets(chain: Chain, options?: Options) { +export async function getMarkets( + chain: Chain, + params: { + page?: number + per_page?: number + fetch_on_chain?: boolean + } = { fetch_on_chain: true }, + options?: Options, +) { const host = getHost(options) - const resp = await fetch( - `${host}/v1/lending/markets/${chain}?fetch_on_chain=true&page=1&per_page=100`, - ) - + const resp = await fetch(`${host}/v1/lending/markets/${chain}${addQueryString(params)}`) return resp.data.map(Parsers.parseMarket) } -export async function getSnapshots(chain: Chain, marketController: string, options?: Options) { +export async function getSnapshots( + chain: Chain, + marketController: string, + params: { + agg?: string + fetch_on_chain?: boolean + } = { fetch_on_chain: true, agg: 'day' }, + options?: Options, +) { const host = getHost(options) const resp = await fetch( - `${host}/v1/lending/markets/${chain}/${marketController}/snapshots?fetch_on_chain=true&agg=day`, + `${host}/v1/lending/markets/${chain}/${marketController}/snapshots${addQueryString(params)}`, ) - return resp.data.map(Parsers.parseSnapshot) } diff --git a/packages/prices-api/src/llamalend/models.ts b/packages/prices-api/src/llamalend/models.ts index 1fbe42935..9f06ad3de 100644 --- a/packages/prices-api/src/llamalend/models.ts +++ b/packages/prices-api/src/llamalend/models.ts @@ -5,18 +5,20 @@ import type { Address } from '..' * You can have a crvUSD borrow (partially) being collateralized by crvUSD. */ export type Market = { - name: Address + name: string controller: Address vault: Address llamma: Address policy: Address oracle: Address + oraclePools: Address[] rate: number apyBorrow: number apyLend: number nLoans: number priceOracle: number ammPrice: number + basePrice: number totalDebt: number // Borrowed totalAssets: number // Supplied totalDebtUsd: number @@ -25,18 +27,23 @@ export type Market = { mintedUsd: number redeemed: number redeemedUsd: number + loanDiscount: number + liquidationDiscount: number + minBand: number + maxBand: number collateralBalance: number // Collateral (like CRV) collateralBalanceUsd: number borrowedBalance: number // Collateral (like crvUSD) borrowedBalanceUsd: number - tokenCollateral: { + collateralToken: { symbol: string address: Address } - tokenBorrowed: { + borrowedToken: { symbol: string address: Address } + leverage: number } export type MarketPair = { long?: Market; short?: Market } diff --git a/packages/prices-api/src/llamalend/parsers.ts b/packages/prices-api/src/llamalend/parsers.ts index 3826541fe..fb832780a 100644 --- a/packages/prices-api/src/llamalend/parsers.ts +++ b/packages/prices-api/src/llamalend/parsers.ts @@ -9,32 +9,39 @@ export const parseMarket = (x: Responses.GetMarketsResponse['data'][number]): Mo llamma: x.llamma, policy: x.policy, oracle: x.oracle, - rate: parseFloat(x.rate), - apyBorrow: parseFloat(x.borrow_apy), - apyLend: parseFloat(x.lend_apy), + oraclePools: x.oracle_pools, + rate: x.rate, + apyBorrow: x.borrow_apy, + apyLend: x.lend_apy, nLoans: x.n_loans, - priceOracle: parseFloat(x.price_oracle), - ammPrice: parseFloat(x.amm_price), - totalDebt: parseFloat(x.total_debt), - totalAssets: parseFloat(x.total_assets), - totalDebtUsd: parseFloat(x.total_debt_usd), - totalAssetsUsd: parseFloat(x.total_assets_usd), - minted: parseFloat(x.minted), - redeemed: parseFloat(x.redeemed), - mintedUsd: parseFloat(x.minted_usd), - redeemedUsd: parseFloat(x.redeemed_usd), - collateralBalance: parseFloat(x.collateral_balance), - borrowedBalance: parseFloat(x.borrowed_balance), - collateralBalanceUsd: parseFloat(x.collateral_balance_usd), - borrowedBalanceUsd: parseFloat(x.borrowed_balance_usd), - tokenCollateral: { + priceOracle: x.price_oracle, + ammPrice: x.amm_price, + basePrice: x.base_price, + totalDebt: x.total_debt, + totalAssets: x.total_assets, + totalDebtUsd: x.total_debt_usd, + totalAssetsUsd: x.total_assets_usd, + minted: x.minted, + redeemed: x.redeemed, + mintedUsd: x.minted_usd, + redeemedUsd: x.redeemed_usd, + loanDiscount: x.loan_discount, + liquidationDiscount: x.liquidation_discount, + minBand: x.min_band, + maxBand: x.max_band, + collateralBalance: x.collateral_balance, + borrowedBalance: x.borrowed_balance, + collateralBalanceUsd: x.collateral_balance_usd, + borrowedBalanceUsd: x.borrowed_balance_usd, + collateralToken: { symbol: x.collateral_token.symbol, address: x.collateral_token.address, }, - tokenBorrowed: { + borrowedToken: { symbol: x.borrowed_token.symbol, address: x.borrowed_token.address, }, + leverage: x.leverage, }) export const parseSnapshot = (x: Responses.GetSnapshotsResponse['data'][number]): Models.Snapshot => ({ diff --git a/packages/prices-api/src/llamalend/responses.ts b/packages/prices-api/src/llamalend/responses.ts index 97a35cff2..f5833d7d8 100644 --- a/packages/prices-api/src/llamalend/responses.ts +++ b/packages/prices-api/src/llamalend/responses.ts @@ -6,30 +6,36 @@ export type GetChainsResponse = { export type GetMarketsResponse = { data: { - name: Address + name: string controller: Address vault: Address llamma: Address policy: Address oracle: Address - rate: string - borrow_apy: string - lend_apy: string - n_loans: 0 - price_oracle: string - amm_price: string - total_debt: string - total_assets: string - total_debt_usd: string - total_assets_usd: string - minted: string - redeemed: string - minted_usd: string - redeemed_usd: string - collateral_balance: string - borrowed_balance: string - collateral_balance_usd: string - borrowed_balance_usd: string + oracle_pools: Address[] + rate: number + borrow_apy: number + lend_apy: number + n_loans: number + price_oracle: number + amm_price: number + base_price: number + total_debt: number + total_assets: number + total_debt_usd: number + total_assets_usd: number + minted: number + redeemed: number + minted_usd: number + redeemed_usd: number + loan_discount: number + liquidation_discount: number + min_band: number + max_band: number + collateral_balance: number + borrowed_balance: number + collateral_balance_usd: number + borrowed_balance_usd: number collateral_token: { symbol: string address: Address @@ -38,6 +44,7 @@ export type GetMarketsResponse = { symbol: string address: Address } + leverage: number }[] } diff --git a/tests/cypress/e2e/loan/llamalend-markets.cy.ts b/tests/cypress/e2e/loan/llamalend-markets.cy.ts index 557c23da9..8af81bca5 100644 --- a/tests/cypress/e2e/loan/llamalend-markets.cy.ts +++ b/tests/cypress/e2e/loan/llamalend-markets.cy.ts @@ -1,17 +1,25 @@ -import { checkIsDarkMode, isInViewport, oneOf, oneViewport, TABLET_BREAKPOINT } from '@/support/ui' +import { type Breakpoint, checkIsDarkMode, isInViewport, oneViewport } from '@/support/ui' +import { mockLendingChains, mockLendingSnapshots, mockLendingVaults } from '@/support/helpers/lending-mocks' +import { mockChains, mockMintMarkets, mockMintSnapshots } from '@/support/helpers/minting-mocks' +import { oneOf, range, shuffle } from '@/support/generators' +import { mockTokenPrices } from '@/support/helpers/tokens' describe('LlamaLend Markets', () => { let isDarkMode: boolean - let viewport: readonly [number, number] + let breakpoint: Breakpoint beforeEach(() => { - cy.intercept('https://prices.curve.fi/v1/lending/chains', { body: { data: ['ethereum', 'fraxtal', 'arbitrum'] } }) - cy.intercept('https://api.curve.fi/v1/getLendingVaults/all', { fixture: 'llamalend-markets.json' }) - cy.intercept('https://prices.curve.fi/v1/lending/markets/*/*/snapshots?agg=none', { - fixture: 'lending-snapshots.json', - }).as('snapshots') - viewport = oneViewport() - cy.viewport(...viewport) + const [width, height, screen] = oneViewport() + breakpoint = screen + mockChains() + mockLendingChains() + mockTokenPrices() + mockLendingVaults() + mockLendingSnapshots().as('snapshots') + mockMintMarkets() + mockMintSnapshots() + + cy.viewport(width, height) cy.visit('/crvusd#/ethereum/beta-markets', { onBeforeLoad: (win) => { win.localStorage.clear() @@ -22,11 +30,18 @@ describe('LlamaLend Markets', () => { }) it('should have sticky headers', () => { + if (breakpoint === 'mobile') { + cy.viewport(400, 400) // fixed mobile viewport, filters wrap depending on the width + } + cy.get('[data-testid^="data-table-row"]').last().then(isInViewport).should('be.false') cy.get('[data-testid^="data-table-row"]').eq(10).scrollIntoView() cy.get('[data-testid="data-table-head"] th').eq(1).then(isInViewport).should('be.true') - const filterHeight = viewport[0] < TABLET_BREAKPOINT ? 48 : 64 + cy.get(`[data-testid^="pool-type-"]`).should('be.visible') // wait for the table to render + const filterHeight = { mobile: 202, tablet: 112, desktop: 120 }[breakpoint] + const rowHeight = { mobile: 77, tablet: 88, desktop: 88 }[breakpoint] cy.get('[data-testid="table-filters"]').invoke('outerHeight').should('equal', filterHeight) + cy.get('[data-testid^="data-table-row"]').eq(10).invoke('outerHeight').should('equal', rowHeight) }) it('should sort', () => { @@ -46,6 +61,7 @@ describe('LlamaLend Markets', () => { cy.get('[data-testid^="data-table-row"]').last().scrollIntoView() cy.wait('@snapshots') cy.get('[data-testid^="data-table-row"]').last().should('contain.html', 'path') // wait for the graph to render + cy.wait(range(calls1.length).map(() => '@snapshots')) cy.get(`@snapshots.all`).then((calls2) => { expect(calls2.length).to.be.greaterThan(calls1.length) }) @@ -53,63 +69,86 @@ describe('LlamaLend Markets', () => { }) it(`should allow filtering by using a slider`, () => { - const { columnId, expectedFilterText, expectedFirstCell } = oneOf( - { - columnId: 'totalSupplied_usdTotal', - expectedFilterText: 'Min Liquidity: $1,029,000', - expectedFirstCell: '$2.06M', - }, - { - columnId: 'utilizationPercent', - expectedFilterText: 'Min Utilization: 50.00%', - expectedFirstCell: '84.91%', - }, + const [columnId, initialFilterText] = oneOf( + ['liquidityUsd', 'Min Liquidity: $0'], + ['utilizationPercent', 'Min Utilization: 0.00%'], ) cy.viewport(1200, 800) // use fixed viewport to have consistent slider width - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.exist') - cy.get(`[data-testid="btn-expand-filters"]`).click() - cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).click(60, 20) - cy.get(`[data-testid="slider-${columnId}"]`).click() - cy.get(`[data-testid="slider-${columnId}"]`).should('not.be.visible') - cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).contains(expectedFilterText) - cy.get(`[data-testid="data-table-cell-${columnId}"]`).first().should('contain', expectedFirstCell) + cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.exist') + cy.get(`[data-testid="btn-expand-filters"]`).click() + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('contain', initialFilterText) + cy.get(`[data-testid="slider-${columnId}"]`).should('not.exist') + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).click(60, 20) + cy.get(`[data-testid="slider-${columnId}"]`).click() + cy.get(`[data-testid="slider-${columnId}"]`).should('not.be.visible') + cy.get(`[data-testid="minimum-slider-filter-${columnId}"]`).should('not.contain', initialFilterText) + cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) + }) }) it('should allow filtering by chain', () => { const chains = ['Ethereum', 'Fraxtal'] // only these chains are in the fixture const chain = oneOf(...chains) + cy.get('[data-testid="multi-select-filter-chain"]').should('not.exist') cy.get(`[data-testid="btn-expand-filters"]`).click() - cy.get('[data-testid="multi-select-filter-blockchainId"]').click() - cy.get(`#menu-blockchainId [data-value="${chain.toLowerCase()}"]`).click() + cy.get('[data-testid="multi-select-filter-chain"]').click() + cy.get(`#menu-chain [data-value="${chain.toLowerCase()}"]`).click() cy.get(`[data-testid="data-table-cell-assets"]:first [data-testid="chain-icon-${chain.toLowerCase()}"]`).should( 'be.visible', ) const otherChain = oneOf(...chains.filter((c) => c !== chain)) - cy.get(`#menu-blockchainId [data-value="${otherChain.toLowerCase()}"]`).click() + cy.get(`#menu-chain [data-value="${otherChain.toLowerCase()}"]`).click() ;[chain, otherChain].forEach((c) => cy.get(`[data-testid="chain-icon-${c.toLowerCase()}"]`).should('be.visible')) }) it(`should allow filtering by token`, () => { - const { columnId, iconIndex } = oneOf( - { iconIndex: 0, columnId: 'assets_collateral_symbol' }, - { iconIndex: 1, columnId: 'assets_borrowed_symbol' }, - ) + const columnId = oneOf('assets_collateral_symbol', 'assets_borrowed_symbol') cy.get(`[data-testid="btn-expand-filters"]`).click() cy.get(`[data-testid="multi-select-filter-${columnId}"]`).click() cy.get(`#menu-${columnId} [data-value="CRV"]`).click() - cy.get(`[data-testid="data-table-cell-assets"]:first [data-testid^="token-icon-"]`) - .eq(iconIndex) - .should('have.attr', 'data-testid', `token-icon-CRV`) + cy.get(`[data-testid="data-table-cell-assets"] [data-testid^="token-icon-CRV"]`).should('be.visible') cy.get(`#menu-${columnId} [data-value="crvUSD"]`).click() cy.get(`[data-testid="token-icon-crvUSD"]`).should('be.visible') }) + it(`should allow filtering favorites`, () => { + cy.get(`[data-testid="favorite-icon"]`).first().click() + cy.get(`[data-testid="chip-favorites"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) + cy.get(`[data-testid="favorite-icon"]`).should('not.exist') + cy.get(`[data-testid="favorite-icon-filled"]`).click() + cy.get(`[data-testid="table-empty-row"]`).should('exist') + cy.get(`[data-testid="reset-filter"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) + }) + + it(`should allow filtering by market type`, () => { + cy.get(`[data-testid^="data-table-row"]`).then(({ length }) => { + const [type, otherType] = shuffle('mint', 'lend') + cy.get(`[data-testid="chip-${type}"]`).click() + cy.get(`[data-testid^="pool-type-"]`).each(($el) => + expect($el.attr('data-testid')).equals(`pool-type-${otherType}`), + ) + cy.get(`[data-testid^="data-table-row"]`).should('have.length.below', length) + cy.get(`[data-testid="chip-${otherType}"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length', length) + }) + }) + + it(`should allow filtering by rewards`, () => { + cy.get(`[data-testid="chip-rewards"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length', 1) + cy.get(`[data-testid="rewards-lp"]`).should('be.visible') + cy.get(`[data-testid="chip-rewards"]`).click() + cy.get(`[data-testid^="data-table-row"]`).should('have.length.above', 1) + }) + it('should toggle columns', () => { const { toggle, element } = oneOf( // hide the whole column: - { toggle: 'totalSupplied_usdTotal', element: 'data-table-header-totalSupplied_usdTotal' }, + { toggle: 'liquidityUsd', element: 'data-table-header-liquidityUsd' }, { toggle: 'utilizationPercent', element: 'data-table-header-utilizationPercent' }, // hide the graph inside the cell: { toggle: 'borrowChart', element: 'line-graph-borrow' }, diff --git a/tests/cypress/fixtures/llamalend-markets.json b/tests/cypress/fixtures/llamalend-markets.json deleted file mode 100644 index 876e2fc56..000000000 --- a/tests/cypress/fixtures/llamalend-markets.json +++ /dev/null @@ -1,972 +0,0 @@ -{ - "success": true, - "data": { - "lendingVaultData": [ - { - "id": "oneway-0", - "name": "Borrow crvUSD (wstETH collateral)", - "address": "0x8cf1DE26729cfB7137AF1A6B2a665e099EC319b5", - "controllerAddress": "0x1E0165DbD2019441aB7927C018701f3138114D71", - "ammAddress": "0x847D7a5e4Aa4b380043B2908C29a92E2e5157E64", - "monetaryPolicyAddress": "0x319C06103bc51b3c01a1A121451Aa5E2A2a7778f", - "rates": { - "borrowApr": 0.1383, - "borrowApy": 0.1482, - "borrowApyPcent": 14.824, - "lendApr": 0.0938, - "lendApy": 0.0983, - "lendApyPcent": 9.8319 - }, - "gaugeAddress": "0x222d910ef37c06774e1edb9dc9459664f73776f0", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - }, - "collateral": { - "symbol": "wstETH", - "decimals": 18, - "address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", - "blockchainId": "ethereum", - "usdPrice": 3931.24 - } - }, - "vaultShares": { - "pricePerShare": 0.001054668769876685, - "totalShares": 136504296.78 - }, - "totalSupplied": { - "total": 143966.82, - "usdTotal": 143645.1 - }, - "borrowed": { - "total": 97666.52, - "usdTotal": 97448.27 - }, - "availableToBorrow": { - "total": 46300.3, - "usdTotal": 46196.84 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-0/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-0/vault/withdraw" - }, - "usdTotal": 143645.1, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 44.7, - "ammBalanceCollateralUsd": 175711.31 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-1", - "name": "Borrow crvUSD (WETH collateral)", - "address": "0x5AE28c9197a4a6570216fC7e53E7e0221D7A0FEF", - "controllerAddress": "0xaade9230AA9161880E13a38C83400d3D1995267b", - "ammAddress": "0xb46aDcd1eA7E35C4EB801406C3E76E76e9a46EdF", - "monetaryPolicyAddress": "0x1A783886F03710ABf4a6833F50D5e69047123be6", - "rates": { - "borrowApr": 0.3106, - "borrowApy": 0.3641, - "borrowApyPcent": 36.4116, - "lendApr": 0.305, - "lendApy": 0.3565, - "lendApyPcent": 35.6459 - }, - "gaugeAddress": "0x1cfabd1937e75e40fa06b650cb0c8cd233d65c20", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - }, - "collateral": { - "symbol": "WETH", - "decimals": 18, - "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "blockchainId": "ethereum", - "usdPrice": 3308.37 - } - }, - "vaultShares": { - "pricePerShare": 0.001059053019017878, - "totalShares": 272644464.56 - }, - "totalSupplied": { - "total": 288744.94, - "usdTotal": 288099.7 - }, - "borrowed": { - "total": 283508.2, - "usdTotal": 282874.66 - }, - "availableToBorrow": { - "total": 5236.74, - "usdTotal": 5225.04 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-1/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-1/vault/withdraw" - }, - "usdTotal": 288099.7, - "ammBalances": { - "ammBalanceBorrowed": 29.84, - "ammBalanceBorrowedUsd": 29.77, - "ammBalanceCollateral": 257.5, - "ammBalanceCollateralUsd": 851907.39 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-2", - "name": "Borrow crvUSD (tBTC collateral)", - "address": "0xb2b23C87a4B6d1b03Ba603F7C3EB9A81fDC0AAC9", - "controllerAddress": "0x413FD2511BAD510947a91f5c6c79EBD8138C29Fc", - "ammAddress": "0x5338B1bf469651a5951ef618Fb5DeFbffaed7BE9", - "monetaryPolicyAddress": "0x6Ddd163240c21189eD0c89D30f6681142bf05FFB", - "rates": { - "borrowApr": 0.1658, - "borrowApy": 0.1803, - "borrowApyPcent": 18.0271, - "lendApr": 0.1479, - "lendApy": 0.1593, - "lendApyPcent": 15.9338 - }, - "gaugeAddress": "0x41ebf0bec45642a675e8b7536a2ce9c078a814b4", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - }, - "collateral": { - "symbol": "tBTC", - "decimals": 18, - "address": "0x18084fba666a33d37592fa2633fd49a74dd93a88", - "blockchainId": "ethereum", - "usdPrice": 93750 - } - }, - "vaultShares": { - "pricePerShare": 0.001055557549368944, - "totalShares": 656245613.4 - }, - "totalSupplied": { - "total": 692705.01, - "usdTotal": 691157.06 - }, - "borrowed": { - "total": 617901.36, - "usdTotal": 616520.57 - }, - "availableToBorrow": { - "total": 74803.65, - "usdTotal": 74636.49 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-2/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-2/vault/withdraw" - }, - "usdTotal": 691157.06, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 14.58, - "ammBalanceCollateralUsd": 1367235.38 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-3", - "name": "Borrow crvUSD (CRV collateral)", - "address": "0xCeA18a8752bb7e7817F9AE7565328FE415C0f2cA", - "controllerAddress": "0xEdA215b7666936DEd834f76f3fBC6F323295110A", - "ammAddress": "0xafca625321Df8D6A068bDD8F1585d489D2acF11b", - "monetaryPolicyAddress": "0x8b6527063FbC9c30731D7E57F1DEf08edce57d07", - "rates": { - "borrowApr": 0.2827, - "borrowApy": 0.3266, - "borrowApyPcent": 32.6626, - "lendApr": 0.2156, - "lendApy": 0.2406, - "lendApyPcent": 24.058 - }, - "gaugeAddress": "0x49887df6fe905663cdb46c616bfbfbb50e85a265", - "gaugeRewards": [ - { - "gaugeAddress": "0x49887df6fe905663cdb46c616bfbfbb50e85a265", - "tokenPrice": 0.9977653510518104, - "name": "Curve.Fi USD Stablecoin", - "symbol": "crvUSD", - "decimals": "18", - "tokenAddress": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", - "apy": 0 - } - ], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - }, - "collateral": { - "symbol": "CRV", - "decimals": 18, - "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", - "blockchainId": "ethereum", - "usdPrice": 0.85 - } - }, - "vaultShares": { - "pricePerShare": 0.001114126801026674, - "totalShares": 1097660156.99 - }, - "totalSupplied": { - "total": 1222932.6, - "usdTotal": 1220199.77 - }, - "borrowed": { - "total": 932688.37, - "usdTotal": 930604.14 - }, - "availableToBorrow": { - "total": 290244.23, - "usdTotal": 289595.64 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-3/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-3/vault/withdraw" - }, - "usdTotal": 1220199.77, - "ammBalances": { - "ammBalanceBorrowed": 1.28, - "ammBalanceBorrowedUsd": 1.27, - "ammBalanceCollateral": 3205538.57, - "ammBalanceCollateralUsd": 2717344.66 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-4", - "name": "Borrow CRV (crvUSD collateral)", - "address": "0x4D2f44B0369f3C20c3d670D2C26b048985598450", - "controllerAddress": "0xC510d73Ad34BeDECa8978B6914461aA7b50CF3Fc", - "ammAddress": "0xe7B1c8cfC0Bc45957320895aA06884d516DAA8e6", - "monetaryPolicyAddress": "0x40A442F8CBFd125a762b55F76D9Dba66F84Dd6DD", - "rates": { - "borrowApr": 0.01, - "borrowApy": 0.0101, - "borrowApyPcent": 1.005, - "lendApr": 0, - "lendApy": 0, - "lendApyPcent": 0 - }, - "gaugeAddress": "0x99440e11485fc623c7a9f2064b97a961a440246b", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "CRV", - "decimals": 18, - "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", - "blockchainId": "ethereum", - "usdPrice": 0.85 - }, - "collateral": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - } - }, - "vaultShares": { - "pricePerShare": 0.001002211473462969, - "totalShares": 14871530.47 - }, - "totalSupplied": { - "total": 14904.42, - "usdTotal": 12634.52 - }, - "borrowed": { - "total": 0, - "usdTotal": 0 - }, - "availableToBorrow": { - "total": 14904.42, - "usdTotal": 12634.52 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-4/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-4/vault/withdraw" - }, - "usdTotal": 12634.52, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 0, - "ammBalanceCollateralUsd": 0 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-5", - "name": "Borrow WETH (crvUSD collateral)", - "address": "0x46196C004de85c7a75C8b1bB9d54Afb0f8654A45", - "controllerAddress": "0xa5D9137d2A1Ee912469d911A8E74B6c77503bac8", - "ammAddress": "0x08Ba6D7c10d1A7850aE938543bfbEA7C0240F9Cf", - "monetaryPolicyAddress": "0xbDb065458d34DB77d1fB2862D367edd8275f8352", - "rates": { - "borrowApr": 0, - "borrowApy": 0, - "borrowApyPcent": 0, - "lendApr": 0, - "lendApy": 0, - "lendApyPcent": 0 - }, - "gaugeAddress": "0x12b9db644ca8a8e27cd1770adb48513b5f8c5ae5", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "WETH", - "decimals": 18, - "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "blockchainId": "ethereum", - "usdPrice": 3308.37 - }, - "collateral": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - } - }, - "vaultShares": { - "pricePerShare": 0.001, - "totalShares": 0 - }, - "totalSupplied": { - "total": 0, - "usdTotal": 0 - }, - "borrowed": { - "total": 0, - "usdTotal": 0 - }, - "availableToBorrow": { - "total": 0, - "usdTotal": 0 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-5/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-5/vault/withdraw" - }, - "usdTotal": 0, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 0, - "ammBalanceCollateralUsd": 0 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-6", - "name": "Borrow tBTC (crvUSD collateral)", - "address": "0x99Cff9Dc26A44dc2496B4448ebE415b5E894bd30", - "controllerAddress": "0xe438658874b0acf4D81c24172E137F0eE00621b8", - "ammAddress": "0xfcb53ED72dAB68091aA6a2aB68b5116639ED8805", - "monetaryPolicyAddress": "0x62cD08caDABF473315D8953995DE0Dc0928b7D3C", - "rates": { - "borrowApr": 0.005, - "borrowApy": 0.005, - "borrowApyPcent": 0.5012, - "lendApr": 0, - "lendApy": 0, - "lendApyPcent": 0 - }, - "gaugeAddress": "0x2605d72e460feff15bf4fd728a5ea31928895c2a", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "tBTC", - "decimals": 18, - "address": "0x18084fba666a33d37592fa2633fd49a74dd93a88", - "blockchainId": "ethereum", - "usdPrice": 93750 - }, - "collateral": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - } - }, - "vaultShares": { - "pricePerShare": 0.001001265687185436, - "totalShares": 759.18 - }, - "totalSupplied": { - "total": 0.76, - "usdTotal": 71263.04 - }, - "borrowed": { - "total": 0, - "usdTotal": 0 - }, - "availableToBorrow": { - "total": 0.76, - "usdTotal": 71263.04 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-6/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-6/vault/withdraw" - }, - "usdTotal": 71263.04, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 0, - "ammBalanceCollateralUsd": 0 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-7", - "name": "Borrow crvUSD (sUSDe collateral)", - "address": "0x52096539ed1391CB50C6b9e4Fd18aFd2438ED23b", - "controllerAddress": "0x98Fc283d6636f6DCFf5a817A00Ac69A3ADd96907", - "ammAddress": "0x9bBdb1b160B48C48efCe260aaEa4505b1aDE8f4B", - "monetaryPolicyAddress": "0xF82A5a3c69cA11601C9aD4A351A75857bDd1365F", - "rates": { - "borrowApr": 0.005, - "borrowApy": 0.005, - "borrowApyPcent": 0.5012, - "lendApr": 0, - "lendApy": 0, - "lendApyPcent": 0 - }, - "gaugeAddress": "0x82195f78c313540e0363736b8320a256a019f7dd", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - }, - "collateral": { - "symbol": "sUSDe", - "decimals": 18, - "address": "0x9d39a5de30e57443bff2a8307a4256c8797a3497", - "blockchainId": "ethereum", - "usdPrice": 1.14 - } - }, - "vaultShares": { - "pricePerShare": 0.001073586612982044, - "totalShares": 179941.86 - }, - "totalSupplied": { - "total": 193.18, - "usdTotal": 192.75 - }, - "borrowed": { - "total": 0, - "usdTotal": 0 - }, - "availableToBorrow": { - "total": 193.18, - "usdTotal": 192.75 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-7/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-7/vault/withdraw" - }, - "usdTotal": 192.75, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 0, - "ammBalanceCollateralUsd": 0 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-8", - "name": "Borrow crvUSD (UwU collateral)", - "address": "0x7586C58bf6292B3C9DeFC8333fc757d6c5dA0f7E", - "controllerAddress": "0x09dBDEB3b301A4753589Ac6dF8A178C7716ce16B", - "ammAddress": "0x6BE658242b769500f27498Ba0637406E417507b1", - "monetaryPolicyAddress": "0x9058237c94551770BbB58b710E23e5277b6837da", - "rates": { - "borrowApr": 0.2058, - "borrowApy": 0.2285, - "borrowApyPcent": 22.8487, - "lendApr": 0.1599, - "lendApy": 0.1733, - "lendApyPcent": 17.33 - }, - "gaugeAddress": "0xad7b288315b0d71d62827338251a8d89a98132a0", - "gaugeRewards": [ - { - "gaugeAddress": "0xad7b288315b0d71d62827338251a8d89a98132a0", - "tokenPrice": 0.6121691699391112, - "name": "UwU Lend", - "symbol": "UwU", - "decimals": "18", - "tokenAddress": "0x55C08ca52497e2f1534B59E2917BF524D4765257", - "apy": 9.176178598472513 - } - ], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - }, - "collateral": { - "symbol": "UwU", - "decimals": 18, - "address": "0x55c08ca52497e2f1534b59e2917bf524d4765257", - "blockchainId": "ethereum", - "usdPrice": 0.61 - } - }, - "vaultShares": { - "pricePerShare": 0.001041908472975945, - "totalShares": 1394227869.77 - }, - "totalSupplied": { - "total": 1452657.83, - "usdTotal": 1449411.65 - }, - "borrowed": { - "total": 1128123.19, - "usdTotal": 1125602.23 - }, - "availableToBorrow": { - "total": 324534.65, - "usdTotal": 323809.42 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-8/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-8/vault/withdraw" - }, - "usdTotal": 1449411.65, - "ammBalances": { - "ammBalanceBorrowed": 1845.48, - "ammBalanceBorrowedUsd": 1841.36, - "ammBalanceCollateral": 4895240.06, - "ammBalanceCollateralUsd": 2996715.04 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-9", - "name": "Borrow crvUSD (WBTC collateral)", - "address": "0xccd37EB6374Ae5b1f0b85ac97eFf14770e0D0063", - "controllerAddress": "0xcaD85b7fe52B1939DCEebEe9bCf0b2a5Aa0cE617", - "ammAddress": "0x8eeDE294459EFaFf55d580bc95C98306Ab03F0C8", - "monetaryPolicyAddress": "0x188041aD83145351Ef45F4bb91D08886648aEaF8", - "rates": { - "borrowApr": 0.1376, - "borrowApy": 0.1475, - "borrowApyPcent": 14.7481, - "lendApr": 0.1168, - "lendApy": 0.1239, - "lendApyPcent": 12.3913 - }, - "gaugeAddress": "0x7dcb252f7ea2b8da6fa59c79edf63f793c8b63b6", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", - "blockchainId": "ethereum", - "usdPrice": 1 - }, - "collateral": { - "symbol": "WBTC", - "decimals": 8, - "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", - "blockchainId": "ethereum", - "usdPrice": 93974 - } - }, - "vaultShares": { - "pricePerShare": 0.001042193998272652, - "totalShares": 1977509017.06 - }, - "totalSupplied": { - "total": 2060948.03, - "usdTotal": 2056342.53 - }, - "borrowed": { - "total": 1749992.83, - "usdTotal": 1746082.21 - }, - "availableToBorrow": { - "total": 310955.2, - "usdTotal": 310260.32 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/ethereum/markets/one-way-market-9/vault/deposit", - "withdraw": "https://curve.fi/lend#/ethereum/markets/one-way-market-9/vault/withdraw" - }, - "usdTotal": 2056342.53, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 32.81, - "ammBalanceCollateralUsd": 3083584.97 - }, - "blockchainId": "ethereum", - "registryId": "oneway" - }, - { - "id": "oneway-0", - "name": "Borrow crvUSD (sfrxETH collateral)", - "address": "0x279A23349Fa48Ea5215D31666aFF359DBBec1404", - "controllerAddress": "0xc68f91FfA2B27147F9AB153267018f5Fe4b6850F", - "ammAddress": "0x8c6C08f76Eb895e318e800b232e412BBBA62eE86", - "monetaryPolicyAddress": "0xE51Aec0B19BF1A2040e8393a00FefabFA913B89a", - "rates": { - "borrowApr": 0.0689, - "borrowApy": 0.0713, - "borrowApyPcent": 7.1293, - "lendApr": 0.0314, - "lendApy": 0.0319, - "lendApyPcent": 3.1947 - }, - "gaugeAddress": "0x541f57ab2032a042ce6b02fd435347fbae1f6d0a", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xb102f7efa0d5de071a8d37b3548e1c7cb148caf3", - "blockchainId": "fraxtal", - "usdPrice": 1 - }, - "collateral": { - "symbol": "sfrxETH", - "decimals": 18, - "address": "0xfc00000000000000000000000000000000000005", - "blockchainId": "fraxtal", - "usdPrice": 3656.39 - } - }, - "vaultShares": { - "pricePerShare": 0.001011761683007702, - "totalShares": 1556106960.75 - }, - "totalSupplied": { - "total": 1574409.4, - "usdTotal": 1570881.99 - }, - "borrowed": { - "total": 718912.52, - "usdTotal": 717301.82 - }, - "availableToBorrow": { - "total": 855496.88, - "usdTotal": 853580.17 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/fraxtal/markets/one-way-market-0/vault/deposit", - "withdraw": "https://curve.fi/lend#/fraxtal/markets/one-way-market-0/vault/withdraw" - }, - "usdTotal": 1570881.99, - "ammBalances": { - "ammBalanceBorrowed": 1462.86, - "ammBalanceBorrowedUsd": 1459.59, - "ammBalanceCollateral": 368.86, - "ammBalanceCollateralUsd": 1348707.62 - }, - "blockchainId": "fraxtal", - "registryId": "oneway" - }, - { - "id": "oneway-1", - "name": "Borrow crvUSD (sFRAX collateral)", - "address": "0x0Edf4a3762Deb5329ECdbDEDA98d287aE41fbB7e", - "controllerAddress": "0xB4EbF87A474569d8eB7f7182B4beBD8aE79ae675", - "ammAddress": "0x1CEe2524008994f819aA45932b859982809F0940", - "monetaryPolicyAddress": "0xcd4fa32b388A2DC323b1c319649A2D046ebd7205", - "rates": { - "borrowApr": 0.046, - "borrowApy": 0.0471, - "borrowApyPcent": 4.7084, - "lendApr": 0.03, - "lendApy": 0.0305, - "lendApyPcent": 3.0479 - }, - "gaugeAddress": "0xf868b47717f4739ee142b3be6ee0f84da868e917", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xb102f7efa0d5de071a8d37b3548e1c7cb148caf3", - "blockchainId": "fraxtal", - "usdPrice": 1 - }, - "collateral": { - "symbol": "sFRAX", - "decimals": 18, - "address": "0xfc00000000000000000000000000000000000008", - "blockchainId": "fraxtal", - "usdPrice": 1.08 - } - }, - "vaultShares": { - "pricePerShare": 0.001005709253571587, - "totalShares": 1613115398.07 - }, - "totalSupplied": { - "total": 1622325.08, - "usdTotal": 1618690.32 - }, - "borrowed": { - "total": 1058653.44, - "usdTotal": 1056281.57 - }, - "availableToBorrow": { - "total": 563671.64, - "usdTotal": 562408.76 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/fraxtal/markets/one-way-market-1/vault/deposit", - "withdraw": "https://curve.fi/lend#/fraxtal/markets/one-way-market-1/vault/withdraw" - }, - "usdTotal": 1618690.32, - "ammBalances": { - "ammBalanceBorrowed": 0.04, - "ammBalanceBorrowedUsd": 0.04, - "ammBalanceCollateral": 1055591.17, - "ammBalanceCollateralUsd": 1137927.28 - }, - "blockchainId": "fraxtal", - "registryId": "oneway" - }, - { - "id": "oneway-2", - "name": "Borrow crvUSD (FXS collateral)", - "address": "0xa7573CBD8738Ed268B931B038079f993e78D4216", - "controllerAddress": "0xf0922934f16DbE5Df9f90F729b2023D5e1FC2F15", - "ammAddress": "0xe8d7Ce159EFB40dD332902B5B23400Cba5938445", - "monetaryPolicyAddress": "0xbb737078bd3e5169f5c991959E627719CD0D0d70", - "rates": { - "borrowApr": 0.0584, - "borrowApy": 0.0601, - "borrowApyPcent": 6.0105, - "lendApr": 0.0231, - "lendApy": 0.0234, - "lendApyPcent": 2.3357 - }, - "gaugeAddress": "0xfc6891c8482aef6aef71a09a09ce14432617a403", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xb102f7efa0d5de071a8d37b3548e1c7cb148caf3", - "blockchainId": "fraxtal", - "usdPrice": 1 - }, - "collateral": { - "symbol": "FXS", - "decimals": 18, - "address": "0xfc00000000000000000000000000000000000002", - "blockchainId": "fraxtal", - "usdPrice": 3.15 - } - }, - "vaultShares": { - "pricePerShare": 0.001011066074869517, - "totalShares": 874428273.52 - }, - "totalSupplied": { - "total": 884104.76, - "usdTotal": 882123.96 - }, - "borrowed": { - "total": 349697.01, - "usdTotal": 348913.52 - }, - "availableToBorrow": { - "total": 534407.76, - "usdTotal": 533210.43 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/fraxtal/markets/one-way-market-2/vault/deposit", - "withdraw": "https://curve.fi/lend#/fraxtal/markets/one-way-market-2/vault/withdraw" - }, - "usdTotal": 882123.96, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 236156.55, - "ammBalanceCollateralUsd": 743893.12 - }, - "blockchainId": "fraxtal", - "registryId": "oneway" - }, - { - "id": "oneway-3", - "name": "Borrow crvUSD (CRV collateral)", - "address": "0x040eFC9A141D7Fa47745751C253E02D065C90bDB", - "controllerAddress": "0x99d5b47D431f1963940F72ffa6F25bC0B9849CbF", - "ammAddress": "0x9090e71fC05DC66F67A086e0d69468F280BE98a1", - "monetaryPolicyAddress": "0x701eb05feba453b5Fb58E87158f8fc74eD3FE914", - "rates": { - "borrowApr": 0.0209, - "borrowApy": 0.0211, - "borrowApyPcent": 2.1118, - "lendApr": 0.0003, - "lendApy": 0.0003, - "lendApyPcent": 0.0339 - }, - "gaugeAddress": "0x2687a11cb2916bffb9406959c6d386c79c621f15", - "gaugeRewards": [], - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xb102f7efa0d5de071a8d37b3548e1c7cb148caf3", - "blockchainId": "fraxtal", - "usdPrice": 1 - }, - "collateral": { - "symbol": "CRV", - "decimals": 18, - "address": "0x331b9182088e2a7d6d3fe4742aba1fb231aecc56", - "blockchainId": "fraxtal", - "usdPrice": null - } - }, - "vaultShares": { - "pricePerShare": 0.00101945325787471, - "totalShares": 614772915.75 - }, - "totalSupplied": { - "total": 626732.25, - "usdTotal": 625328.08 - }, - "borrowed": { - "total": 10171.21, - "usdTotal": 10148.42 - }, - "availableToBorrow": { - "total": 616561.04, - "usdTotal": 615179.66 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/fraxtal/markets/one-way-market-3/vault/deposit", - "withdraw": "https://curve.fi/lend#/fraxtal/markets/one-way-market-3/vault/withdraw" - }, - "usdTotal": 625328.08, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 23376.27, - "ammBalanceCollateralUsd": null - }, - "blockchainId": "fraxtal", - "registryId": "oneway" - }, - { - "id": "oneway-4", - "name": "Borrow crvUSD (SQUID collateral)", - "address": "0x5071aE9579dB394f0A62E2Fd3CEfA6A1c434f61e", - "controllerAddress": "0xBF55Bb9463bBbB6aD724061910a450939E248eA6", - "ammAddress": "0x8feCf70B90ED40512C0608565EB9A53af0Ef42Ad", - "monetaryPolicyAddress": "0x5607772d573E09F69FDfC292b23B8E99918be0A3", - "rates": { - "borrowApr": 0.3, - "borrowApy": 0.3497, - "borrowApyPcent": 34.9692, - "lendApr": 0.3, - "lendApy": 0.3497, - "lendApyPcent": 34.9692 - }, - "assets": { - "borrowed": { - "symbol": "crvUSD", - "decimals": 18, - "address": "0xb102f7efa0d5de071a8d37b3548e1c7cb148caf3", - "blockchainId": "fraxtal", - "usdPrice": 1 - }, - "collateral": { - "symbol": "SQUID", - "decimals": 18, - "address": "0x6e58089d8e8f664823d26454f49a5a0f2ff697fe", - "blockchainId": "fraxtal", - "usdPrice": null - } - }, - "vaultShares": { - "pricePerShare": 0.001044456091682643, - "totalShares": 472758.39 - }, - "totalSupplied": { - "total": 493.78, - "usdTotal": 492.67 - }, - "borrowed": { - "total": 493.78, - "usdTotal": 492.67 - }, - "availableToBorrow": { - "total": 0, - "usdTotal": 0 - }, - "lendingVaultUrls": { - "deposit": "https://curve.fi/lend#/fraxtal/markets/one-way-market-4/vault/deposit", - "withdraw": "https://curve.fi/lend#/fraxtal/markets/one-way-market-4/vault/withdraw" - }, - "usdTotal": 492.67, - "ammBalances": { - "ammBalanceBorrowed": 0, - "ammBalanceBorrowedUsd": 0, - "ammBalanceCollateral": 1700844.13, - "ammBalanceCollateralUsd": null - }, - "blockchainId": "fraxtal", - "registryId": "oneway" - } - ], - "tvl": 29980665.480000008 - }, - "generatedTimeMs": 1734966397904 -} \ No newline at end of file diff --git a/tests/cypress/fixtures/minting-markets.json b/tests/cypress/fixtures/minting-markets.json new file mode 100644 index 000000000..f8f34d1be --- /dev/null +++ b/tests/cypress/fixtures/minting-markets.json @@ -0,0 +1,146 @@ +{ + "chain": "ethereum", + "page": 1, + "per_page": 10, + "count": 6, + "data": [ + { + "address": "0x8472A9A7632b173c8Cf3a86D3afec50c35548e76", + "factory_address": "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC", + "llamma": "0x136e783846ef68C8Bd00a3369F787dF8d683a696", + "rate": 0.1732388227788837, + "total_debt": 192623.07719378526, + "n_loans": 11, + "debt_ceiling": 174073.4413314184, + "borrowable": -18549.635862366835, + "pending_fees": 18549.635862366835, + "collected_fees": 172190.9307192066, + "collateral_amount": 158.69274312862743, + "collateral_amount_usd": 478089.21128159994, + "stablecoin_amount": 1.9563e-14, + "collateral_token": { + "symbol": "sfrxETH", + "address": "0xac3E018457B222d93114458476f3E3416Abbe38F" + }, + "stablecoin_token": { + "symbol": "crvUSD", + "address": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + } + }, + { + "address": "0x100dAa78fC509Db39Ef7D04DE0c1ABD299f4C6CE", + "factory_address": "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC", + "llamma": "0x37417B2238AA52D0DD2D6252d989E728e8f706e4", + "rate": 0.12383442746589446, + "total_debt": 4395955.560029227, + "n_loans": 91, + "debt_ceiling": 150000000.0, + "borrowable": 145604044.43997076, + "pending_fees": 1732.0738145295481, + "collected_fees": 6977379.984621132, + "collateral_amount": 3204.3026948619963, + "collateral_amount_usd": 10354724.950420054, + "stablecoin_amount": 62088.49949432901, + "collateral_token": { + "symbol": "wstETH", + "address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + "stablecoin_token": { + "symbol": "crvUSD", + "address": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + } + }, + { + "address": "0x4e59541306910aD6dC1daC0AC9dFB29bD9F15c67", + "factory_address": "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC", + "llamma": "0xE0438Eb3703bF871E31Ce639bd351109c88666ea", + "rate": 0.10501994534323122, + "total_debt": 29240131.542101294, + "n_loans": 167, + "debt_ceiling": 200000000.00000003, + "borrowable": 170759868.45789874, + "pending_fees": 9930.393149356307, + "collected_fees": 5968035.596323085, + "collateral_amount": 632.92716333, + "collateral_amount_usd": 62038766.10096086, + "stablecoin_amount": 159411.21751495206, + "collateral_token": { + "symbol": "WBTC", + "address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + "stablecoin_token": { + "symbol": "crvUSD", + "address": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + } + }, + { + "address": "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "factory_address": "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC", + "llamma": "0x1681195C176239ac5E72d9aeBaCf5b2492E0C4ee", + "rate": 0.10517678600372715, + "total_debt": 20869135.039368864, + "n_loans": 340, + "debt_ceiling": 200000000.00000003, + "borrowable": 179130864.96063116, + "pending_fees": 6989.453890899149, + "collected_fees": 4879978.852828198, + "collateral_amount": 11897.831740339434, + "collateral_amount_usd": 32239490.51137516, + "stablecoin_amount": 1037797.418483593, + "collateral_token": { + "symbol": "WETH", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "stablecoin_token": { + "symbol": "crvUSD", + "address": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + } + }, + { + "address": "0xEC0820EfafC41D8943EE8dE495fC9Ba8495B15cf", + "factory_address": "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC", + "llamma": "0xfA96ad0a9E64261dB86950e2dA362f5572c5c6fd", + "rate": 0.12816072453935456, + "total_debt": 8363583.4139668, + "n_loans": 21, + "debt_ceiling": 50000000.0, + "borrowable": 41636416.5860332, + "pending_fees": 3430.678776803918, + "collected_fees": 1708029.610412792, + "collateral_amount": 4419.64947425191, + "collateral_amount_usd": 13316024.544803528, + "stablecoin_amount": 0.0, + "collateral_token": { + "symbol": "sfrxETH", + "address": "0xac3E018457B222d93114458476f3E3416Abbe38F" + }, + "stablecoin_token": { + "symbol": "crvUSD", + "address": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + } + }, + { + "address": "0x1C91da0223c763d2e0173243eAdaA0A2ea47E704", + "factory_address": "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC", + "llamma": "0xf9bD9da2427a50908C4c6D1599D8e62837C2BCB0", + "rate": 0.11242232556486309, + "total_debt": 8037850.365583395, + "n_loans": 18, + "debt_ceiling": 50000000.0, + "borrowable": 41962149.6344166, + "pending_fees": 2808.8622262746676, + "collected_fees": 702963.8381835253, + "collateral_amount": 198.42359489039472, + "collateral_amount_usd": 19393288.39289104, + "stablecoin_amount": 0.0, + "collateral_token": { + "symbol": "tBTC", + "address": "0x18084fbA666a33d37592fA2633fD49a74DD93a88" + }, + "stablecoin_token": { + "symbol": "crvUSD", + "address": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + } + } + ] +} \ No newline at end of file diff --git a/tests/cypress/fixtures/minting-snapshots.json b/tests/cypress/fixtures/minting-snapshots.json new file mode 100644 index 000000000..181afa1cc --- /dev/null +++ b/tests/cypress/fixtures/minting-snapshots.json @@ -0,0 +1,246 @@ +{ + "chain": "ethereum", + "market": "0xEC0820EfafC41D8943EE8dE495fC9Ba8495B15cf", + "data": [ + { + "rate": 0.12816072453935456, + "minted": 57687520.68004391, + "redeemed": 49327367.94485391, + "total_collateral": 4419.64947425191, + "total_collateral_usd": 13338861.420634171, + "total_stablecoin": 0.0, + "total_debt": 8363348.969166238, + "n_loans": 21, + "amm_price": 2860.7977857863784, + "price_oracle": 3018.081297701096, + "base_price": 2169.712689383293, + "min_band": -75, + "max_band": 981, + "borrowable": 41639847.26481, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 37939132349027.805, + "dt": "2025-02-11T08:00:11" + }, + { + "rate": 0.12350547389952227, + "minted": 57546524.98534437, + "redeemed": 49327367.94485391, + "total_collateral": 4354.206965306379, + "total_collateral_usd": 12755078.486590678, + "total_stablecoin": 0.0, + "total_debt": 8222870.341977985, + "n_loans": 21, + "amm_price": 2620.01466720996, + "price_oracle": 2929.3689041933676, + "base_price": 2167.9968538263097, + "min_band": -75, + "max_band": 981, + "borrowable": 41780842.959509544, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 36677049030796.016, + "dt": "2025-02-09T00:00:11" + }, + { + "rate": 0.05986224417328345, + "minted": 57446524.985344365, + "redeemed": 49287319.616797455, + "total_collateral": 4296.106965306379, + "total_collateral_usd": 12815154.408939498, + "total_stablecoin": 0.0, + "total_debt": 8160052.720192771, + "n_loans": 20, + "amm_price": 2768.4203402621965, + "price_oracle": 2982.9691188858887, + "base_price": 2167.237608722246, + "min_band": -75, + "max_band": 981, + "borrowable": 41840794.63145308, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 36645687805726.6, + "dt": "2025-02-07T00:00:11" + }, + { + "rate": 0.05986224417328345, + "minted": 57446524.985344365, + "redeemed": 49287319.616797455, + "total_collateral": 4296.106965306379, + "total_collateral_usd": 12811851.052921671, + "total_stablecoin": 0.0, + "total_debt": 8160050.192922249, + "n_loans": 20, + "amm_price": 2766.281762534502, + "price_oracle": 2982.20020041051, + "base_price": 2167.236937501617, + "min_band": -75, + "max_band": 981, + "borrowable": 41840794.63145308, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 36645665106473.21, + "dt": "2025-02-06T23:57:23" + }, + { + "rate": 0.04974604614220168, + "minted": 57446524.985344365, + "redeemed": 49287319.616797455, + "total_collateral": 4291.119593224802, + "total_collateral_usd": 12857063.612428244, + "total_stablecoin": 0.0, + "total_debt": 8159837.675902452, + "n_loans": 20, + "amm_price": 2805.5768698999245, + "price_oracle": 2996.2025837564884, + "base_price": 2167.1804948665517, + "min_band": -75, + "max_band": 981, + "borrowable": 41840794.63145308, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 36643756361810.8, + "dt": "2025-02-06T20:00:11" + }, + { + "rate": 0.04974604614220168, + "minted": 57446524.985344365, + "redeemed": 49287319.616797455, + "total_collateral": 4291.119593224802, + "total_collateral_usd": 13520635.795283655, + "total_stablecoin": 0.0, + "total_debt": 8159293.5449004285, + "n_loans": 20, + "amm_price": 3263.2177945940916, + "price_oracle": 3150.841057105755, + "base_price": 2167.035978499796, + "min_band": -75, + "max_band": 981, + "borrowable": 41840794.63145308, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 36638869416787.555, + "dt": "2025-02-06T07:58:11" + }, + { + "rate": 0.047944530655772866, + "minted": 57414597.19452046, + "redeemed": 49287319.616797455, + "total_collateral": 4273.11828162783, + "total_collateral_usd": 13334133.961488768, + "total_stablecoin": 0.0, + "total_debt": 8129117.528835265, + "n_loans": 20, + "amm_price": 3169.896203246285, + "price_oracle": 3120.469194316141, + "base_price": 2166.9891491227695, + "min_band": -75, + "max_band": 981, + "borrowable": 41872722.422276996, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 36634585938627.53, + "dt": "2025-02-06T04:00:11" + }, + { + "rate": 0.03401332663166623, + "minted": 57384597.19452046, + "redeemed": 49287319.616797455, + "total_collateral": 4254.737408404868, + "total_collateral_usd": 13075338.227889553, + "total_stablecoin": 0.0, + "total_debt": 8098858.28407689, + "n_loans": 19, + "amm_price": 3027.98444905314, + "price_oracle": 3073.124607422387, + "base_price": 2166.9198265598043, + "min_band": -75, + "max_band": 981, + "borrowable": 41902722.422276996, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 36631342124169.234, + "dt": "2025-02-05T20:00:11" + }, + { + "rate": 0.0327819465080883, + "minted": 56748697.19452045, + "redeemed": 49282319.616797455, + "total_collateral": 3970.0424360484803, + "total_collateral_usd": 12282496.798412494, + "total_stablecoin": 0.0, + "total_debt": 7467145.53090825, + "n_loans": 19, + "amm_price": 3090.1481205081463, + "price_oracle": 3093.794838787085, + "base_price": 2166.6912308037968, + "min_band": -75, + "max_band": 981, + "borrowable": 42533622.422277, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 35794438630890.19, + "dt": "2025-02-04T15:57:59" + }, + { + "rate": 0.03293724986562663, + "minted": 56673697.19452046, + "redeemed": 49282319.616797455, + "total_collateral": 3949.3241355298783, + "total_collateral_usd": 12135875.862047454, + "total_stablecoin": 0.0, + "total_debt": 7392036.791826468, + "n_loans": 18, + "amm_price": 3028.0462320455194, + "price_oracle": 3072.899424199627, + "base_price": 2166.659555182087, + "min_band": -75, + "max_band": 981, + "borrowable": 42608622.422277, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 35768991389047.4, + "dt": "2025-02-04T12:00:11" + }, + { + "rate": 0.03293724986562663, + "minted": 56673697.19452046, + "redeemed": 49282319.616797455, + "total_collateral": 3949.3241355298783, + "total_collateral_usd": 12142681.054542972, + "total_stablecoin": 0.0, + "total_debt": 7392035.242251871, + "n_loans": 18, + "amm_price": 3033.1442915154225, + "price_oracle": 3074.6225525785558, + "base_price": 2166.6591009905424, + "min_band": -75, + "max_band": 981, + "borrowable": 42608622.422277, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 35768976392716.57, + "dt": "2025-02-04T11:56:47" + }, + { + "rate": 0.030969959883235365, + "minted": 56548986.8908721, + "redeemed": 49281345.44166028, + "total_collateral": 3873.5207656007356, + "total_collateral_usd": 10821539.32212743, + "total_stablecoin": 0.0, + "total_debt": 7271749.428271223, + "n_loans": 17, + "amm_price": 2275.888295743735, + "price_oracle": 2793.721778447506, + "base_price": 2166.4499837922435, + "min_band": -75, + "max_band": 981, + "borrowable": 42732358.55078819, + "loan_discount": 9e16, + "liquidation_discount": 6e16, + "sum_debt_squared": 35748305621806.73, + "dt": "2025-02-03T03:57:35" + } + ] +} \ No newline at end of file diff --git a/tests/cypress/support/generators.ts b/tests/cypress/support/generators.ts new file mode 100644 index 000000000..ec7d28d65 --- /dev/null +++ b/tests/cypress/support/generators.ts @@ -0,0 +1,29 @@ +import type { Address } from '@curvefi/prices-api' + +export const oneFloat = (minOrMax = 1, maxExclusive?: number): number => + maxExclusive === undefined ? Math.random() * minOrMax : minOrMax + Math.random() * (maxExclusive - minOrMax) + +export const oneInt = (minOrMax = 100, maxExclusive?: number): number => Math.floor(oneFloat(minOrMax, maxExclusive)) + +export const range = (lengthOrStart: number, length?: number) => + length === undefined + ? Array.from({ length: lengthOrStart }, (_, i) => i) + : Array.from({ length }, (_, i) => i + lengthOrStart) + +export const oneOf = (...options: T[]) => options[oneInt(0, options.length)] + +export const oneAddress = (): Address => + `0x${oneInt(0, 16 ** 40) + .toString(16) + .padStart(40, '0')}` + +export const onePrice = (max = 1e10) => oneFloat(max) + +export const shuffle = (...options: T[]): T[] => { + const result = [...options] + for (let i = result.length - 1; i > 0; i--) { + const j = oneInt(i + 1) + ;[result[i], result[j]] = [result[j], result[i]] + } + return result +} diff --git a/tests/cypress/support/helpers/lending-mocks.ts b/tests/cypress/support/helpers/lending-mocks.ts new file mode 100644 index 000000000..d281f6e31 --- /dev/null +++ b/tests/cypress/support/helpers/lending-mocks.ts @@ -0,0 +1,77 @@ +import { oneAddress, oneFloat, oneInt, oneOf, onePrice, range } from '@/support/generators' +import { oneToken } from '@/support/helpers/tokens' +import type { GetMarketsResponse } from '@curvefi/prices-api/src/llamalend/responses' + +const LendingChains = ['ethereum', 'fraxtal', 'arbitrum'] + +export const mockLendingChains = () => + cy.intercept('https://prices.curve.fi/v1/lending/chains', { body: { data: LendingChains } }) + +const oneLendingPool = (chain: string, utilization: number): GetMarketsResponse['data'][number] => { + const collateral = oneToken(chain) + const borrowed = oneToken(chain) + const collateralBalance = oneFloat() + const borrowedBalance = oneFloat() + const totalAssets = onePrice() + const borrowedPrice = borrowed.usdPrice ?? onePrice() + const collateralPrice = collateral.usdPrice ?? onePrice() + const totalAssetsUsd = totalAssets * collateralPrice + const totalDebtUsd = utilization * totalAssetsUsd + const minBand = oneInt() + const minted = onePrice() + const redeemed = onePrice(minted) + return { + name: [collateral.symbol, borrowed.symbol].join('-'), + controller: oneAddress(), + vault: oneAddress(), + llamma: oneAddress(), + policy: oneAddress(), + oracle: oneAddress(), + oracle_pools: oneOf([], [oneAddress()]), + rate: oneFloat(), + borrow_apy: oneFloat(), + lend_apy: oneFloat(), + n_loans: oneInt(), + price_oracle: onePrice(), + amm_price: onePrice(), + base_price: onePrice(), + total_debt: totalDebtUsd / borrowedPrice, + total_assets: totalAssets, + total_debt_usd: totalDebtUsd, + total_assets_usd: totalAssetsUsd, + minted: minted, + redeemed: redeemed, + minted_usd: minted * borrowedPrice, + redeemed_usd: redeemed * borrowedPrice, + loan_discount: oneFloat(1e18), + liquidation_discount: oneFloat(1e18), + leverage: oneFloat(), + min_band: minBand, + max_band: oneInt(minBand), + collateral_balance: collateralBalance, + borrowed_balance: borrowedBalance, + collateral_balance_usd: collateralBalance * collateralPrice, + borrowed_balance_usd: borrowedBalance * borrowedPrice, + collateral_token: { symbol: collateral.symbol, address: collateral.address }, + borrowed_token: { symbol: borrowed.symbol, address: borrowed.address }, + } +} + +export const mockLendingVaults = () => + cy.intercept('https://prices.curve.fi/v1/lending/markets/*', (req) => { + const chain = new URL(req.url).pathname.split('/').pop()! + const count = oneInt(2, 20) + const data = [ + ...range(count).map((index) => oneLendingPool(chain, index / (count - 1))), + // add a pool with a fixed vault address to test campaign rewards + ...(chain == 'ethereum' + ? [{ ...oneLendingPool(chain, oneFloat()), vault: '0x8c65cec3847ad99bdc02621bdbc89f2ace56934b' }] + : []), + ] + req.reply({ chain, count: data.length, data }) + }) + +export const mockLendingSnapshots = () => + cy.intercept('https://prices.curve.fi/v1/lending/markets/*/*/snapshots?agg=none', { + fixture: 'lending-snapshots.json', + }) diff --git a/tests/cypress/support/helpers/minting-mocks.ts b/tests/cypress/support/helpers/minting-mocks.ts new file mode 100644 index 000000000..a939a3c5a --- /dev/null +++ b/tests/cypress/support/helpers/minting-mocks.ts @@ -0,0 +1,17 @@ +export const mockChains = (chains = ['ethereum', 'arbitrum']) => + cy.intercept('https://prices.curve.fi/v1/chains/', { + body: { data: chains.map((name) => ({ name })) }, + }) + +export const mockMintMarkets = () => + cy.intercept('https://prices.curve.fi/v1/crvusd/markets/*', (req) => { + const chain = new URL(req.url).pathname.split('/').pop() + req.reply( + chain == 'ethereum' ? { fixture: 'minting-markets.json' } : { chain, page: 1, per_page: 10, count: 0, data: [] }, + ) + }) + +export const mockMintSnapshots = () => + cy.intercept('https://prices.curve.fi/v1/crvusd/markets/*/*/snapshots?agg=none', { + fixture: 'minting-snapshots.json', + }) diff --git a/tests/cypress/support/helpers/tokens.ts b/tests/cypress/support/helpers/tokens.ts new file mode 100644 index 000000000..ccbdf084c --- /dev/null +++ b/tests/cypress/support/helpers/tokens.ts @@ -0,0 +1,72 @@ +import { oneOf } from '@/support/generators' +import type { Address } from '@curvefi/prices-api' + +type Token = { + symbol: string + address: Address + chain: string + usdPrice: number | null +} + +// prettier-ignore +const TOKENS: Token[] = [ + { chain: 'arbitrum', symbol: 'ARB', address: '0x912CE59144191C1204E64559FE8253a0e49E6548', usdPrice: 0.4857460060008621 }, + { chain: 'arbitrum', symbol: 'CRV', address: '0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978', usdPrice: 0.5794560975151747 }, + { chain: 'arbitrum', symbol: 'FXN', address: '0x179F38f78346F5942E95C5C59CB1da7F55Cf7CAd', usdPrice: 45.47635754031463 }, + { chain: 'arbitrum', symbol: 'IBTC', address: '0x050C24dBf1eEc17babE5fc585F06116A259CC77A', usdPrice: 97557.79310687653 }, + { chain: 'arbitrum', symbol: 'WBTC', address: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f', usdPrice: 98208.09605114785 }, + { chain: 'arbitrum', symbol: 'WETH', address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', usdPrice: 2711.6599781943705 }, + { chain: 'arbitrum', symbol: 'asdCRV', address: '0x75289388d50364c3013583d97bd70cED0e183e32', usdPrice: 0.6427279359401797 }, + { chain: 'arbitrum', symbol: 'crvUSD', address: '0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5', usdPrice: null }, + { chain: 'arbitrum', symbol: 'gmUSDC', address: '0x5f851F67D24419982EcD7b7765deFD64fBb50a97', usdPrice: null }, + { chain: 'arbitrum', symbol: 'stXAI', address: '0xab5c23bdbE99d75A7Ae4756e7cCEfd0A97B37E78', usdPrice: 0.21829294913774286 }, + { chain: 'arbitrum', symbol: 'tBTC', address: '0x6c84a8f1c29108F47a79964b5Fe888D4f4D0dE40', usdPrice: 97751.80258242389 }, + { chain: 'ethereum', symbol: 'CRV', address: '0xD533a949740bb3306d119CC777fa900bA034cd52', usdPrice: null }, + { chain: 'ethereum', symbol: 'ETHFI', address: '0xFe0c30065B384F05761f15d0CC899D4F9F9Cc0eB', usdPrice: 1.1707246089415464 }, + { chain: 'ethereum', symbol: 'RCH', address: '0x57B96D4aF698605563A4653D882635da59Bf11AF', usdPrice: null }, + { chain: 'ethereum', symbol: 'USD0USD0++', address: '0x1d08E7adC263CfC70b1BaBe6dC5Bb339c16Eec52', usdPrice: 0.955776793088862 }, + { chain: 'ethereum', symbol: 'USDe', address: '0x4c9EDD5852cd905f086C759E8383e09bff1E68B3', usdPrice: 1.0004283272617351 }, + { chain: 'ethereum', symbol: 'UwU', address: '0x55C08ca52497e2f1534B59E2917BF524D4765257', usdPrice: 0.012731552600553007 }, + { chain: 'ethereum', symbol: 'WBTC', address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', usdPrice: 97887.09529556196 }, + { chain: 'ethereum', symbol: 'WETH', address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', usdPrice: 2703.2706511628267 }, + { chain: 'ethereum', symbol: 'XAUM', address: '0x2103E845C5E135493Bb6c2A4f0B8651956eA8682', usdPrice: 2902.343145 }, + { chain: 'ethereum', symbol: 'crvUSD', address: '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E', usdPrice: null }, + { chain: 'ethereum', symbol: 'ezETH', address: '0xbf5495Efe5DB9ce00f80364C8B423567e58d2110', usdPrice: null }, + { chain: 'ethereum', symbol: 'pufETH', address: '0xD9A442856C234a39a81a089C06451EBAa4306a72', usdPrice: 2803.417970729767 }, + { chain: 'ethereum', symbol: 'sDOLA', address: '0xb45ad160634c528Cc3D2926d9807104FA3157305', usdPrice: 1.0999216877241782 }, + { chain: 'ethereum', symbol: 'sFRAX', address: '0xA663B02CF0a4b149d2aD41910CB81e23e1c41c32', usdPrice: 1.1096446285557429 }, + { chain: 'ethereum', symbol: 'sUSDe', address: '0x9D39A5DE30e57443BfF2A8307A4256c8797A3497', usdPrice: 1.1513030611161528 }, + { chain: 'ethereum', symbol: 'sfrxETH', address: '0xac3E018457B222d93114458476f3E3416Abbe38F', usdPrice: 3005.634169914068 }, + { chain: 'ethereum', symbol: 'tBTC', address: '0x18084fbA666a33d37592fA2633fD49a74DD93a88', usdPrice: null }, + { chain: 'ethereum', symbol: 'wstETH', address: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', usdPrice: 3221.8239874724286 }, + { chain: 'ethereum', symbol: 'ynETH', address: '0x09db87A538BD693E9d08544577d5cCfAA6373A48', usdPrice: 2752.2723186980897 }, + { chain: 'fraxtal', symbol: 'CRV', address: '0x331B9182088e2A7d6D3Fe4742AbA1fB231aEcc56', usdPrice: 0.5777548800000001 }, + { chain: 'fraxtal', symbol: 'FXS', address: '0xFc00000000000000000000000000000000000002', usdPrice: 1.7102394 }, + { chain: 'fraxtal', symbol: 'SQUID', address: '0x6e58089d8E8f664823d26454f49A5A0f2fF697Fe', usdPrice: null }, + { chain: 'fraxtal', symbol: 'crvUSD', address: '0xB102f7Efa0d5dE071A8D37B3548e1C7CB148Caf3', usdPrice: null }, + { chain: 'fraxtal', symbol: 'sfrxETH', address: '0xFC00000000000000000000000000000000000005', usdPrice: 3015.8662585172283 }, + { chain: 'fraxtal', symbol: 'sfrxUSD', address: '0xfc00000000000000000000000000000000000008', usdPrice: 1.109793792183224 }, + { chain: 'optimism', symbol: 'CRV', address: '0x0994206dfE8De6Ec6920FF4D779B0d950605Fb53', usdPrice: null }, + { chain: 'optimism', symbol: 'OP', address: '0x4200000000000000000000000000000000000042', usdPrice: 1.4905563964960864 }, + { chain: 'optimism', symbol: 'WETH', address: '0x4200000000000000000000000000000000000006', usdPrice: 3262.5373942715196 }, + { chain: 'optimism', symbol: 'crvUSD', address: '0xC52D7F23a2e460248Db6eE192Cb23dD12bDDCbf6', usdPrice: null }, + { chain: 'optimism', symbol: 'wstETH', address: '0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb', usdPrice: null }, +] + +export const oneToken = (chain?: string) => + chain ? oneOf(...TOKENS.filter((t) => t.chain === chain)!) : oneOf(...TOKENS) + +export const mockTokenPrices = () => + cy.intercept('https://prices.curve.fi/v1/usd_price/*/*', (req) => { + const address = new URL(req.url).pathname.split('/').pop() + const token = TOKENS.find((t) => t.address === address) + if (!token) { + return req.reply(404, { error: `Token ${address} not in the mocked data` }) + } + const data = { + address: token.address, + usd_price: token.usdPrice, + last_updated: '2025-02-11T16:18:47', + } + req.reply({ data }) + }) diff --git a/tests/cypress/support/ui.ts b/tests/cypress/support/ui.ts index 056f291e3..ceb57aa19 100644 --- a/tests/cypress/support/ui.ts +++ b/tests/cypress/support/ui.ts @@ -1,22 +1,21 @@ +import { oneOf, oneInt } from '@/support/generators' + export const [MIN_WIDTH, TABLET_BREAKPOINT, DESKTOP_BREAKPOINT, MAX_WIDTH] = [320, 640, 1200, 2000] const [MIN_HEIGHT, MAX_HEIGHT] = [600, 1000] -const randomInt = (min: number, maxExclusive: number): number => Math.floor(Math.random() * (maxExclusive - min)) + min -export const oneOf = (...options: T[]) => options[randomInt(0, options.length)] - -export const oneDesktopViewport = () => - [randomInt(DESKTOP_BREAKPOINT, MAX_WIDTH), randomInt(MIN_HEIGHT, MAX_HEIGHT)] as const +export const oneDesktopViewport = () => [oneInt(DESKTOP_BREAKPOINT, MAX_WIDTH), oneInt(MIN_HEIGHT, MAX_HEIGHT)] as const -export const oneMobileViewport = () => - [randomInt(MIN_WIDTH, TABLET_BREAKPOINT), randomInt(MIN_HEIGHT, MAX_HEIGHT)] as const +export const oneMobileViewport = () => [oneInt(MIN_WIDTH, TABLET_BREAKPOINT), oneInt(MIN_HEIGHT, MAX_HEIGHT)] as const export const oneTabletViewport = () => - [randomInt(TABLET_BREAKPOINT, DESKTOP_BREAKPOINT), randomInt(MIN_HEIGHT, MAX_HEIGHT)] as const + [oneInt(TABLET_BREAKPOINT, DESKTOP_BREAKPOINT), oneInt(MIN_HEIGHT, MAX_HEIGHT)] as const export const oneMobileOrTabletViewport = () => - [randomInt(MIN_WIDTH, DESKTOP_BREAKPOINT), randomInt(MIN_HEIGHT, MAX_HEIGHT)] as const + [oneInt(MIN_WIDTH, DESKTOP_BREAKPOINT), oneInt(MIN_HEIGHT, MAX_HEIGHT)] as const -export const oneViewport = () => oneOf(oneDesktopViewport(), oneMobileViewport(), oneTabletViewport()) +export type Breakpoint = 'mobile' | 'tablet' | 'desktop' +export const oneViewport = (): [number, number, Breakpoint] => + oneOf([...oneDesktopViewport(), 'desktop'], [...oneMobileViewport(), 'mobile'], [...oneTabletViewport(), 'tablet']) export const isInViewport = ($el: JQuery) => { const height = Cypress.$(cy.state('window')).height()!