Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
export type BorrowerTableColumnVisibility = {
healthScore: boolean;
daysToLiquidation: boolean;
liquidationPrice: boolean;
};

export const BORROWER_TABLE_COLUMN_LABELS: Record<keyof BorrowerTableColumnVisibility, string> = {
healthScore: 'Health Score',
daysToLiquidation: 'Days to Liquidation',
liquidationPrice: 'Liquidation Price',
};

export const BORROWER_TABLE_COLUMN_DESCRIPTIONS: Record<keyof BorrowerTableColumnVisibility, string> = {
healthScore: 'Distance to liquidation threshold. 1.00 is the liquidation boundary.',
daysToLiquidation: 'Estimated days until position reaches liquidation threshold.',
liquidationPrice: 'Price where position becomes liquidatable, plus move from current oracle price.',
};

export const DEFAULT_BORROWER_TABLE_COLUMN_VISIBILITY: BorrowerTableColumnVisibility = {
healthScore: true,
daysToLiquidation: true,
liquidationPrice: true,
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ReactNode } from 'react';
import { FiSliders } from 'react-icons/fi';
import { IconSwitch } from '@/components/ui/icon-switch';
import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal';
Expand All @@ -18,24 +17,6 @@ type BorrowerTableSettingsModalProps = {
) => void;
};

type SettingItemProps = {
title: string;
description: string;
children: ReactNode;
};

function SettingItem({ title, description, children }: SettingItemProps) {
return (
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 flex-1 flex flex-col gap-1">
<h4 className="text-sm font-medium text-primary">{title}</h4>
<p className="text-xs text-secondary">{description}</p>
</div>
<div className="shrink-0">{children}</div>
</div>
);
}

export function BorrowerTableSettingsModal({
isOpen,
onOpenChange,
Expand Down Expand Up @@ -65,11 +46,17 @@ export function BorrowerTableSettingsModal({
<h3 className="text-xs uppercase text-secondary">Visible Columns</h3>
<div className="flex flex-col gap-1">
{columnKeys.map((key) => (
<SettingItem
<div
key={key}
title={BORROWER_TABLE_COLUMN_LABELS[key]}
description={BORROWER_TABLE_COLUMN_DESCRIPTIONS[key]}
className="flex items-center justify-between gap-4 py-1"
>
<label
htmlFor={`borrower-col-${key}`}
className="flex-grow cursor-pointer"
>
<p className="text-sm font-medium text-primary">{BORROWER_TABLE_COLUMN_LABELS[key]}</p>
<p className="text-xs text-secondary">{BORROWER_TABLE_COLUMN_DESCRIPTIONS[key]}</p>
</label>
<IconSwitch
id={`borrower-col-${key}`}
selected={columnVisibility[key] ?? DEFAULT_BORROWER_TABLE_COLUMN_VISIBILITY[key]}
Expand All @@ -78,7 +65,7 @@ export function BorrowerTableSettingsModal({
color="primary"
aria-label={`Toggle ${BORROWER_TABLE_COLUMN_LABELS[key]} column`}
/>
</SettingItem>
</div>
))}
</div>
</div>
Expand Down
29 changes: 28 additions & 1 deletion src/features/market-detail/components/borrowers-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import { formatSimple } from '@/utils/balance';
import type { Market } from '@/utils/types';
import { LiquidateModal } from '@/modals/liquidate/liquidate-modal';
import {
computeHealthScoreFromLtv,
computeLiquidationOraclePrice,
computeLtv,
computeOraclePriceChangePercent,
formatHealthScore,
formatMarketOraclePriceWithSymbol,
formatRelativeLiquidationPriceMove,
isInfiniteLtv,
Expand All @@ -39,6 +41,7 @@ type BorrowersTableProps = {

type BorrowerRowMetric = {
ltvPercent: number | null;
healthScore: string;
daysToLiquidation: number | null;
liquidationPrice: string;
liquidationPriceMove: string;
Expand All @@ -64,6 +67,7 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen

const hasActiveFilter = minShares !== '0';
const tableKey = `borrowers-table-${currentPage}`;
const showHealthScore = borrowerTableColumnVisibility.healthScore ?? DEFAULT_BORROWER_TABLE_COLUMN_VISIBILITY.healthScore;
const showDaysToLiquidation =
borrowerTableColumnVisibility.daysToLiquidation ?? DEFAULT_BORROWER_TABLE_COLUMN_VISIBILITY.daysToLiquidation;
const showLiquidationPrice = borrowerTableColumnVisibility.liquidationPrice ?? DEFAULT_BORROWER_TABLE_COLUMN_VISIBILITY.liquidationPrice;
Expand All @@ -79,6 +83,12 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen
const collateralAssets = BigInt(borrower.collateral);
const ltvWad = computeLtv({ borrowAssets, collateralAssets, oraclePrice });
const ltvPercent = isInfiniteLtv(ltvWad) ? null : Number(ltvWad) / 1e16;
const healthScore = formatHealthScore(
computeHealthScoreFromLtv({
ltv: ltvWad,
lltv,
}),
);

let daysToLiquidation: number | null = null;
if (!isInfiniteLtv(ltvWad) && ltvWad > 0n && borrowApy > 0 && lltv > ltvWad) {
Expand Down Expand Up @@ -117,6 +127,7 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen

const metrics: BorrowerRowMetric = {
ltvPercent,
healthScore,
daysToLiquidation,
liquidationPrice,
liquidationPriceMove,
Expand All @@ -129,7 +140,8 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen
});
}, [borrowers, oraclePrice, market]);

const emptyStateColSpan = 5 + (showDaysToLiquidation ? 1 : 0) + (showLiquidationPrice ? 1 : 0) + (showDeveloperOptions ? 1 : 0);
const emptyStateColSpan =
5 + (showHealthScore ? 1 : 0) + (showDaysToLiquidation ? 1 : 0) + (showLiquidationPrice ? 1 : 0) + (showDeveloperOptions ? 1 : 0);

return (
<div>
Expand Down Expand Up @@ -198,6 +210,20 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen
<TableHead className="text-right">BORROWED</TableHead>
<TableHead className="text-right">COLLATERAL</TableHead>
<TableHead className="text-right">LTV</TableHead>
{showHealthScore && (
<TableHead className="text-right">
<Tooltip
content={
<TooltipContent
title="Health Score"
detail="Liquidation threshold divided by current LTV. 1.00 is the liquidation boundary."
/>
}
>
<span className="cursor-help border-b border-dashed border-secondary/50">HEALTH SCORE</span>
</Tooltip>
</TableHead>
)}
{showDaysToLiquidation && (
<TableHead className="text-right">
<Tooltip
Expand Down Expand Up @@ -289,6 +315,7 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen
</div>
</TableCell>
<TableCell className="text-right text-sm">{ltvDisplay}</TableCell>
{showHealthScore && <TableCell className="text-right text-sm tabular-nums">{borrower.healthScore}</TableCell>}
{showDaysToLiquidation && <TableCell className="text-right text-sm">{daysDisplay}</TableCell>}
{showLiquidationPrice && (
<TableCell className="text-right text-sm">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { type ReactNode } from 'react';
import type { ReactNode } from 'react';
import { motion } from 'framer-motion';
import { RateFormatted } from '@/components/shared/rate-formatted';
import { TooltipContent } from '@/components/shared/tooltip-content';
Expand Down Expand Up @@ -121,8 +121,7 @@ function MetricRow({
}

export function BorrowedMorphoBlueRowDetail({ row }: BorrowedMorphoBlueRowDetailProps) {
const { currentLtvLabel, displayLtv, liquidationOraclePrice, lltv, lltvLabel, ltvWidth, oraclePrice } =
deriveBorrowPositionMetrics(row);
const { currentLtvLabel, displayLtv, liquidationOraclePrice, lltv, lltvLabel, ltvWidth, oraclePrice } = deriveBorrowPositionMetrics(row);
const currentPrice = formatBorrowPositionPrice(row, oraclePrice);
const liquidationPrice = liquidationOraclePrice == null ? '—' : formatBorrowPositionPrice(row, liquidationOraclePrice);
const priceMove =
Expand All @@ -138,26 +137,26 @@ export function BorrowedMorphoBlueRowDetail({ row }: BorrowedMorphoBlueRowDetail
: `font-zen text-sm tabular-nums text-right ${getLTVColor(displayLtv, lltv)}`;
const ltvBarClassName = displayLtv == null ? 'bg-gray-500/50' : getLTVProgressColor(displayLtv, lltv);
const liquidationTooltip =
liquidationPrice === '—'
? null
: (
<TooltipContent
title="Liquidation Price"
detail={
<div className="space-y-1">
<div className="flex items-center justify-between gap-4">
<span className="text-secondary">Current Price</span>
<span className="tabular-nums">{currentPrice}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-secondary">Liquidation Price</span>
<span className="tabular-nums">{liquidationPrice}</span>
</div>
</div>
}
secondaryDetail={priceMove == null ? undefined : `Relative to current: ${formatRelativeLiquidationPriceMove({ percentChange: priceMove })}`}
/>
);
liquidationPrice === '—' ? null : (
<TooltipContent
title="Liquidation Price"
detail={
<div className="space-y-1">
<div className="flex items-center justify-between gap-4">
<span className="text-secondary">Current Price</span>
<span className="tabular-nums">{currentPrice}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-secondary">Liquidation Price</span>
<span className="tabular-nums">{liquidationPrice}</span>
</div>
</div>
}
secondaryDetail={
priceMove == null ? undefined : `Relative to current: ${formatRelativeLiquidationPriceMove({ percentChange: priceMove })}`
}
/>
);

return (
<motion.div
Expand Down Expand Up @@ -215,14 +214,14 @@ export function BorrowedMorphoBlueRowDetail({ row }: BorrowedMorphoBlueRowDetail
tooltipDetail="Borrow divided by collateral value."
tooltipSecondaryDetail={`Liquidation starts at ${lltvLabel}.`}
value={
currentLtvLabel == null
? '—'
: (
<span>
<span>{currentLtvLabel}</span>
<span className="ml-1 text-xs text-secondary">/ {lltvLabel}</span>
</span>
)
currentLtvLabel == null ? (
'—'
) : (
<span>
<span>{currentLtvLabel}</span>
<span className="ml-1 text-xs text-secondary">/ {lltvLabel}</span>
</span>
)
}
valueClassName={ltvValueClassName}
/>
Expand Down
Loading