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()!