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
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"Bash(tee:*)",
"WebFetch(domain:medium.com)",
"Bash(pnpm info:*)",
"Bash(for file in )"
"Bash(for file in )",
"Bash(timeout 30 npx tsc:*)"
],
"deny": []
}
Expand Down
2 changes: 1 addition & 1 deletion app/market/[chainId]/[marketid]/RateChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function RateChart({ historicalData, market, isLoading, selectedTimeframe, selec
];

return (
<Card className="bg-surface my-4 rounded p-4 shadow-sm">
<Card className="bg-surface rounded p-4 shadow-sm">
<CardHeader className="flex items-center justify-between px-6 py-4 text-xl">
<span />
<ButtonGroup
Expand Down
2 changes: 1 addition & 1 deletion app/market/[chainId]/[marketid]/VolumeChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ function VolumeChart({
});

return (
<Card className="bg-surface my-4 rounded p-4 shadow-sm">
<Card className="bg-surface rounded p-4 shadow-sm">
<CardHeader className="flex items-center justify-between px-6 py-4 text-xl">
<span />
<div className="flex gap-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Input } from '@heroui/react';
import { FiSliders } from 'react-icons/fi';
import { Button } from '@/components/ui/button';
import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal';
import { SettingItem, createNumericInputHandler } from './shared-filter-utils';

type BorrowerFiltersModalProps = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
minShares: string;
onMinSharesChange: (value: string) => void;
loanAssetSymbol: string;
};

export default function BorrowerFiltersModal({
isOpen,
onOpenChange,
minShares,
onMinSharesChange,
loanAssetSymbol,
}: BorrowerFiltersModalProps) {
const handleSharesChange = createNumericInputHandler(onMinSharesChange);

return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
backdrop="blur"
size="xl"
zIndex="settings"
>
{(onClose) => (
<>
<ModalHeader
title="Borrower Filters"
description="Filter borrowers by minimum borrow amount"
mainIcon={<FiSliders className="h-5 w-5" />}
onClose={onClose}
/>
<ModalBody className="flex flex-col gap-5">
<div className="bg-surface-soft flex flex-col gap-4 rounded p-4">
<h3 className="text-xs uppercase text-secondary">Minimum Amount</h3>
<p className="-mt-3 mb-1 text-xs text-secondary">Filter borrowers to show only those above the specified minimum amount.</p>
<SettingItem
title={`Min Borrow Amount (${loanAssetSymbol})`}
description="Only show borrowers with borrow amounts above this value."
>
<Input
aria-label="Minimum borrow amount"
placeholder="0"
value={minShares}
onChange={handleSharesChange}
size="sm"
type="text"
inputMode="decimal"
className="w-28"
classNames={{ input: 'text-right' }}
/>
</SettingItem>
</div>
</ModalBody>
<ModalFooter>
<Button
variant="ghost"
onClick={onClose}
>
Close
</Button>
</ModalFooter>
</>
)}
</Modal>
);
}
195 changes: 195 additions & 0 deletions app/market/[chainId]/[marketid]/components/BorrowersTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { useState, useMemo } from 'react';
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Tooltip } from '@heroui/react';
import { FiFilter } from 'react-icons/fi';
import type { Address } from 'viem';
import { formatUnits } from 'viem';
import { Button } from '@/components/ui/button';
import { AccountIdentity } from '@/components/common/AccountIdentity';
import { Spinner } from '@/components/common/Spinner';
import { TablePagination } from '@/components/common/TablePagination';
import { TokenIcon } from '@/components/TokenIcon';
import { TooltipContent } from '@/components/TooltipContent';
import { MONARCH_PRIMARY } from '@/constants/chartColors';
import { useMarketBorrowers } from '@/hooks/useMarketBorrowers';
import { formatSimple } from '@/utils/balance';
import type { Market } from '@/utils/types';

type BorrowersTableProps = {
chainId: number;
market: Market;
minShares: string;
oraclePrice: bigint;
onOpenFiltersModal: () => void;
};

export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpenFiltersModal }: BorrowersTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;

const { data: paginatedData, isLoading, isFetching } = useMarketBorrowers(market?.uniqueKey, chainId, minShares, currentPage, pageSize);

const borrowers = paginatedData?.items ?? [];
const totalCount = paginatedData?.totalCount ?? 0;
const totalPages = Math.ceil(totalCount / pageSize);

const handlePageChange = (page: number) => {
setCurrentPage(page);
};

const hasActiveFilter = minShares !== '0';
const tableKey = `borrowers-table-${currentPage}`;

// Calculate LTV for each borrower
// LTV = borrowAssets / (collateral * oraclePrice)
const borrowersWithLTV = useMemo(() => {
if (!oraclePrice || oraclePrice === 0n) return [];

return borrowers.map((borrower) => {
const borrowAssets = BigInt(borrower.borrowAssets);
const collateral = BigInt(borrower.collateral);

// Calculate collateral value in loan asset terms
// oraclePrice is scaled by 10^36, need to adjust for token decimals
const collateralValueInLoan = (collateral * oraclePrice) / BigInt(10 ** 36);

// Calculate LTV as a percentage
// LTV = (borrowAssets / collateralValue) * 100
let ltv = 0;
if (collateralValueInLoan > 0n) {
ltv = Number((borrowAssets * 10000n) / collateralValueInLoan) / 100;
}

return {
...borrower,
ltv,
};
});
}, [borrowers, oraclePrice]);

return (
<div>
<div className="mb-4 flex items-center justify-between">
<h4 className="text-lg text-secondary">Top Borrowers</h4>
<div className="flex items-center gap-2">
<Tooltip
classNames={{
base: 'p-0 m-0 bg-transparent shadow-sm border-none',
content: 'p-0 m-0 bg-transparent shadow-sm border-none',
}}
content={
<TooltipContent
title="Filters"
detail="Filter borrowers by minimum borrow amount"
icon={<FiFilter size={14} />}
/>
}
>
<Button
variant="ghost"
size="sm"
className="min-w-0 px-2 text-secondary"
aria-label="Borrower filters"
onClick={onOpenFiltersModal}
>
<FiFilter
size={14}
style={{ color: hasActiveFilter ? MONARCH_PRIMARY : undefined }}
/>
</Button>
</Tooltip>
</div>
</div>

<div className="relative">
{/* Loading overlay */}
{isFetching && (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded bg-surface/80 backdrop-blur-sm">
<Spinner size={24} />
</div>
)}

<Table
key={tableKey}
classNames={{
wrapper: 'bg-surface shadow-sm rounded',
table: 'bg-surface',
}}
aria-label="Market borrowers"
>
<TableHeader>
<TableColumn>ACCOUNT</TableColumn>
<TableColumn align="end">BORROWED</TableColumn>
<TableColumn align="end">COLLATERAL</TableColumn>
<TableColumn align="end">LTV</TableColumn>
<TableColumn align="end">% OF BORROW</TableColumn>
</TableHeader>
<TableBody
className="font-zen"
emptyContent={isLoading ? 'Loading...' : 'No borrowers found for this market'}
isLoading={isLoading}
>
{borrowersWithLTV.map((borrower) => {
const totalBorrow = BigInt(market.state.borrowAssets);
const borrowerAssets = BigInt(borrower.borrowAssets);
const percentOfBorrow = totalBorrow > 0n ? (Number(borrowerAssets) / Number(totalBorrow)) * 100 : 0;
const percentDisplay = percentOfBorrow < 0.01 && percentOfBorrow > 0 ? '<0.01%' : `${percentOfBorrow.toFixed(2)}%`;

return (
<TableRow key={`borrower-${borrower.userAddress}`}>
<TableCell>
<AccountIdentity
address={borrower.userAddress as Address}
variant="compact"
linkTo="profile"
/>
</TableCell>
<TableCell className="text-right">
{formatSimple(Number(formatUnits(BigInt(borrower.borrowAssets), market.loanAsset.decimals)))}
{market?.loanAsset?.symbol && (
<span className="ml-1 inline-flex items-center">
<TokenIcon
address={market.loanAsset.address}
chainId={market.morphoBlue.chain.id}
symbol={market.loanAsset.symbol}
width={16}
height={16}
/>
</span>
)}
</TableCell>
<TableCell className="text-right">
{formatSimple(Number(formatUnits(BigInt(borrower.collateral), market.collateralAsset.decimals)))}
{market?.collateralAsset?.symbol && (
<span className="ml-1 inline-flex items-center">
<TokenIcon
address={market.collateralAsset.address}
chainId={market.morphoBlue.chain.id}
symbol={market.collateralAsset.symbol}
width={16}
height={16}
/>
</span>
)}
</TableCell>
<TableCell className="text-right">{borrower.ltv.toFixed(2)}%</TableCell>
<TableCell className="text-right">{percentDisplay}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>

{totalCount > 0 && (
<TablePagination
currentPage={currentPage}
totalPages={totalPages}
totalEntries={totalCount}
pageSize={pageSize}
onPageChange={handlePageChange}
isLoading={isFetching}
/>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function BorrowsTable({ chainId, market, minAssets, onOpenFiltersModal }:
}

return (
<div className="mt-8">
<div>
<div className="mb-4 flex items-center justify-between">
<h4 className="text-lg text-secondary">Borrow & Repay</h4>
<div className="flex items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function LiquidationsTable({ chainId, market }: LiquidationsTableProps) {
}

return (
<div className="mt-8">
<div>
<h4 className="mb-4 text-lg text-secondary">Liquidations</h4>

<div className="relative">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Input } from '@heroui/react';
import { FiSliders } from 'react-icons/fi';
import { Button } from '@/components/ui/button';
import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal';
import { SettingItem, createNumericInputHandler } from './shared-filter-utils';

type SupplierFiltersModalProps = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
minShares: string;
onMinSharesChange: (value: string) => void;
loanAssetSymbol: string;
};

export default function SupplierFiltersModal({
isOpen,
onOpenChange,
minShares,
onMinSharesChange,
loanAssetSymbol,
}: SupplierFiltersModalProps) {
const handleSharesChange = createNumericInputHandler(onMinSharesChange);

return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
backdrop="blur"
size="xl"
zIndex="settings"
>
{(onClose) => (
<>
<ModalHeader
title="Supplier Filters"
description="Filter suppliers by minimum supply amount"
mainIcon={<FiSliders className="h-5 w-5" />}
onClose={onClose}
/>
<ModalBody className="flex flex-col gap-5">
<div className="bg-surface-soft flex flex-col gap-4 rounded p-4">
<h3 className="text-xs uppercase text-secondary">Minimum Amount</h3>
<p className="-mt-3 mb-1 text-xs text-secondary">Filter suppliers to show only those above the specified minimum amount.</p>
<SettingItem
title={`Min Supply Amount (${loanAssetSymbol})`}
description="Only show suppliers with supply amounts above this value."
>
<Input
aria-label="Minimum supply amount"
placeholder="0"
value={minShares}
onChange={handleSharesChange}
size="sm"
type="text"
inputMode="decimal"
className="w-28"
classNames={{ input: 'text-right' }}
/>
</SettingItem>
</div>
</ModalBody>
<ModalFooter>
<Button
variant="ghost"
onClick={onClose}
>
Close
</Button>
</ModalFooter>
</>
)}
</Modal>
);
}
Loading