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
168 changes: 168 additions & 0 deletions app/market/[chainId]/[marketid]/components/LiquidationsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { useMemo, useState } from 'react';
import { Link, Pagination } from '@nextui-org/react';
import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import moment from 'moment';
import Image from 'next/image';
import { Address, formatUnits } from 'viem';
import AccountWithAvatar from '@/components/Account/AccountWithAvatar';
import { MarketLiquidationTransaction } from '@/hooks/useMarketLiquidations';
import { getExplorerTxURL, getExplorerURL } from '@/utils/external';
import { findToken } from '@/utils/tokens';
import { Market } from '@/utils/types';

// Helper functions to format data
const formatAddress = (address: string) => {
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`;
};

type LiquidationsTableProps = {
chainId: number;
liquidations: MarketLiquidationTransaction[];
loading: boolean;
error: string | null;
market: Market;
};

export function LiquidationsTable({
chainId,
liquidations,
loading,
error,
market,
}: LiquidationsTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 8;
const totalPages = Math.ceil(liquidations.length / pageSize);

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

const paginatedLiquidations = useMemo(() => {
const sliced = liquidations.slice((currentPage - 1) * pageSize, currentPage * pageSize);
return sliced;
}, [currentPage, liquidations, pageSize]);

const tableKey = `liquidations-table-${currentPage}`;

const collateralToken = useMemo(() => {
if (!market) return null;
return findToken(market.collateralAsset.address, chainId);
}, [market, chainId]);

const loanToken = useMemo(() => {
if (!market) return null;
return findToken(market.loanAsset.address, chainId);
}, [market, chainId]);

if (error) {
return <p className="text-danger">Error loading liquidations: {error}</p>;
}

return (
<div className="mt-8">
<h4 className="mb-4 text-xl font-semibold">Liquidations</h4>

<Table
key={tableKey}
aria-label="Liquidations history"
bottomContent={
totalPages > 1 ? (
<div className="flex w-full justify-center">
<Pagination
isCompact
showControls
color="primary"
page={currentPage}
total={totalPages}
onChange={handlePageChange}
/>
</div>
) : null
}
>
<TableHeader>
<TableColumn>Liquidator</TableColumn>
<TableColumn align="end">Repaid ({market?.loanAsset?.symbol ?? 'USDC'})</TableColumn>
<TableColumn align="end">
Seized{' '}
{market?.collateralAsset?.symbol && (
<span className="inline-flex items-center">{market.collateralAsset.symbol}</span>
)}
</TableColumn>
<TableColumn>Time</TableColumn>
<TableColumn className="font-mono">Transaction</TableColumn>
</TableHeader>
<TableBody
className="font-zen"
emptyContent={loading ? 'Loading...' : 'No liquidations found for this market'}
isLoading={loading}
>
{paginatedLiquidations.map((liquidation) => (
<TableRow key={liquidation.hash}>
<TableCell>
<Link
href={getExplorerURL(liquidation.data.liquidator, chainId)}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-primary"
>
<AccountWithAvatar address={liquidation.data.liquidator as Address} />
<ExternalLinkIcon className="ml-1" />
</Link>
</TableCell>
<TableCell className="text-right">
{formatUnits(BigInt(liquidation.data.repaidAssets), loanToken?.decimals ?? 6)}
{market?.loanAsset?.symbol && (
<span className="ml-1 inline-flex items-center">
{loanToken?.img && (
<Image
src={loanToken.img}
alt={market.loanAsset.symbol || ''}
width={16}
height={16}
className="rounded-full"
/>
)}
</span>
)}
</TableCell>
<TableCell className="text-right">
{formatUnits(
BigInt(liquidation.data.seizedAssets),
collateralToken?.decimals ?? 18,
)}
{market?.collateralAsset?.symbol && (
<span className="ml-1 inline-flex items-center">
{collateralToken?.img && (
<Image
src={collateralToken.img}
alt={market.collateralAsset.symbol}
width={16}
height={16}
className="rounded-full"
/>
)}
</span>
)}
</TableCell>
<TableCell>{moment.unix(liquidation.timestamp).fromNow()}</TableCell>
<TableCell className="font-zen ">
<Link
href={getExplorerTxURL(liquidation.hash, chainId)}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-sm text-secondary"
>
{formatAddress(liquidation.hash)}
<ExternalLinkIcon className="ml-1" />
</Link>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
17 changes: 17 additions & 0 deletions app/market/[chainId]/[marketid]/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import Header from '@/components/layout/header/Header';
import OracleVendorBadge from '@/components/OracleVendorBadge';
import { SupplyModal } from '@/components/supplyModal';
import { useMarket, useMarketHistoricalData } from '@/hooks/useMarket';
import useMarketLiquidations from '@/hooks/useMarketLiquidations';
import MORPHO_LOGO from '@/imgs/tokens/morpho.svg';
import { getExplorerURL, getMarketURL } from '@/utils/external';
import { getIRMTitle } from '@/utils/morpho';
import { getNetworkImg, getNetworkName, SupportedNetworks } from '@/utils/networks';
import { findToken } from '@/utils/tokens';
import { TimeseriesOptions } from '@/utils/types';
import { LiquidationsTable } from './components/LiquidationsTable';
import RateChart from './RateChart';
import VolumeChart from './VolumeChart';

Expand Down Expand Up @@ -55,12 +57,19 @@ function MarketContent() {
isLoading: isMarketLoading,
error: marketError,
} = useMarket(marketid as string, network);

const {
data: historicalData,
isLoading: isHistoricalLoading,
refetch: refetchHistoricalData,
} = useMarketHistoricalData(marketid as string, network, rateTimeRange, volumeTimeRange);

const {
liquidations,
loading: liquidationsLoading,
error: liquidationsError,
} = useMarketLiquidations(market?.uniqueKey);

const setTimeRangeAndRefetch = useCallback(
(days: number, type: 'rate' | 'volume') => {
const endTimestamp = Math.floor(Date.now() / 1000);
Expand Down Expand Up @@ -321,6 +330,14 @@ function MarketContent() {
setApyTimeframe={setApyTimeframe}
setTimeRangeAndRefetch={setTimeRangeAndRefetch}
/>

<LiquidationsTable
chainId={network}
liquidations={liquidations || []}
loading={liquidationsLoading}
error={liquidationsError}
market={market}
/>
</div>
</>
);
Expand Down
1 change: 0 additions & 1 deletion docs/Styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Use these shared components instead of raw HTML elements:
- `Button`: Import from `@/components/common/Button` for all clickable actions
- `Modal`: For all modal dialogs
- `Card`: For contained content sections
- `Typography`: For text elements

## Component Guidelines

Expand Down
20 changes: 20 additions & 0 deletions src/components/Account/AccountWithAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Address } from 'viem';
import { Avatar } from '@/components/Avatar/Avatar';
import { getSlicedAddress } from '@/utils/address';

type AccountWithAvatarProps = {
address: Address;
};

function AccountWithSmallAvatar({ address }: AccountWithAvatarProps) {
return (
<div className="inline-flex items-center gap-2">
<Avatar address={address as `0x${string}`} size={16} />
<span className="font-inter text-sm font-medium text-primary">
{getSlicedAddress(address as `0x${string}`)}
</span>
</div>
);
}

export default AccountWithSmallAvatar;
26 changes: 26 additions & 0 deletions src/components/Account/AccountWithENS.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Address } from 'viem';
import { Avatar } from '@/components/Avatar/Avatar';
import { getSlicedAddress } from '@/utils/address';
import { Name } from '../common/Name';

type AccountWithENSProps = {
address: Address;
};

function AccountWithENS({ address }: AccountWithENSProps) {
return (
<div className="inline-flex items-center justify-start gap-2">
<Avatar address={address} />
<div className="inline-flex flex-col items-start justify-center gap-1">
<div className="font-inter text-sm font-medium text-primary">
<Name address={address} />
</div>
<span className="font-inter text-xs font-medium text-zinc-400">
{getSlicedAddress(address)}
</span>
</div>
</div>
);
}

export default AccountWithENS;
15 changes: 2 additions & 13 deletions src/components/layout/header/AccountDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@nextui-o
import { ExitIcon, ExternalLinkIcon, CopyIcon } from '@radix-ui/react-icons';
import { clsx } from 'clsx';
import { useAccount, useDisconnect } from 'wagmi';
import AccountWithENS from '@/components/Account/AccountWithENS';
import { Avatar } from '@/components/Avatar/Avatar';
import { Name } from '@/components/common/Name';
import { useStyledToast } from '@/hooks/useStyledToast';
import { getSlicedAddress } from '@/utils/address';
import { getExplorerURL } from '@/utils/external';

export function AccountDropdown() {
Expand Down Expand Up @@ -56,17 +55,7 @@ export function AccountDropdown() {
>
<DropdownItem className="border-b border-primary/10 pb-4" isReadOnly showDivider={false}>
<div className="flex w-full flex-col gap-2">
<div className="inline-flex items-center justify-start gap-2">
<Avatar address={address} />
<div className="inline-flex flex-col items-start justify-center gap-1">
<div className="font-inter text-sm font-medium text-primary">
<Name address={address} />
</div>
<span className="font-inter text-xs font-medium text-zinc-400">
{getSlicedAddress(address)}
</span>
</div>
</div>
<AccountWithENS address={address} />
</div>
</DropdownItem>

Expand Down
32 changes: 32 additions & 0 deletions src/graphql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,35 @@ export const userTransactionsQuery = `
}
}
`;

export const marketLiquidationsQuery = `
query getMarketLiquidations($uniqueKey: String!, $first: Int, $skip: Int) {
transactions (where: {
marketUniqueKey_in: [$uniqueKey],
type_in: [MarketLiquidation]
},
first: $first,
skip: $skip
) {
items {
hash
timestamp
type
data {
... on MarketLiquidationTransactionData {
repaidAssets
seizedAssets
liquidator
}
}
}
pageInfo {
countTotal
count
limit
skip
}
}
}

`;
Loading