mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
fix(dex): wire /dex to live cchain dex subgraph; fix block txs crash
Replace the hardcoded demo DEX dataset (incl. the placeholder pi-million 24h volume) with live markets + fills from the C-Chain 0x9999 subgraph at the per-network /v1/graph/cchain/dex/graphql endpoint, selected via config.apis.general like every other resource. Token pairs resolve through the existing token metadata endpoint, falling back to a short address. Empty/unavailable nets render 'No active markets' — never mock. Guard tx.transaction_types in TxsListItem so coinbase/precompile txs (absent transaction_types in the API payload) no longer crash the block Transactions tab; drop an unused destructured prop. Allow CLOB in cspell.
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
// words - list of words to be always considered correct
|
||||
"words": [
|
||||
"aatx",
|
||||
"CLOB",
|
||||
"hanzoai",
|
||||
"zooai",
|
||||
"parsdao",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// D-Chain (DEX) API hooks and types.
|
||||
// C-Chain native DEX (0x9999) subgraph hooks and types.
|
||||
|
||||
export { useDexData } from './useDexData';
|
||||
|
||||
export type { UseDexDataResult } from './useDexData';
|
||||
|
||||
export type {
|
||||
DexOrder,
|
||||
DexTrade,
|
||||
DexPool,
|
||||
DexSymbolStats,
|
||||
DexOverviewStats,
|
||||
DexMarket,
|
||||
DexFill,
|
||||
DexMarketView,
|
||||
DexOverview,
|
||||
} from './types';
|
||||
|
||||
+27
-46
@@ -1,55 +1,36 @@
|
||||
// D-Chain (DEX Chain) API response types for the Lux DexVM indexer.
|
||||
// C-Chain native DEX (0x9999 V4 PoolManager) subgraph types.
|
||||
// Source: the explorer's embedded luxfi/graph "dex" (CLOB) subgraph, served at
|
||||
// `${config.apis.general.endpoint}/v1/graph/cchain/dex/graphql`. Markets are
|
||||
// emitted by the on-chain Initialize event and fills by the DEXFill event.
|
||||
|
||||
export interface DexOrder {
|
||||
export interface DexMarket {
|
||||
readonly id: string;
|
||||
readonly symbol: string;
|
||||
readonly side: 'buy' | 'sell';
|
||||
readonly price: string;
|
||||
readonly quantity: string;
|
||||
readonly filled: string;
|
||||
readonly status: 'open' | 'filled' | 'cancelled' | 'partial';
|
||||
readonly maker: string;
|
||||
readonly timestamp: string;
|
||||
}
|
||||
|
||||
export interface DexTrade {
|
||||
readonly id: string;
|
||||
readonly symbol: string;
|
||||
readonly price: string;
|
||||
readonly quantity: string;
|
||||
readonly buyer: string;
|
||||
readonly seller: string;
|
||||
readonly fee: string;
|
||||
readonly timestamp: string;
|
||||
readonly blockHeight: number;
|
||||
}
|
||||
|
||||
export interface DexPool {
|
||||
readonly id: string;
|
||||
readonly tokenA: string;
|
||||
readonly tokenB: string;
|
||||
readonly reserveA: string;
|
||||
readonly reserveB: string;
|
||||
readonly tvl: string;
|
||||
readonly baseToken: string;
|
||||
readonly quoteToken: string;
|
||||
readonly feeTier: number;
|
||||
readonly volume24h: string;
|
||||
readonly fee: string;
|
||||
}
|
||||
|
||||
export interface DexSymbolStats {
|
||||
readonly symbol: string;
|
||||
readonly tradeCount: number;
|
||||
readonly lastPrice: string;
|
||||
readonly change24h: number;
|
||||
readonly volume24h: string;
|
||||
readonly high24h: string;
|
||||
readonly low24h: string;
|
||||
readonly trades24h: number;
|
||||
readonly lastUpdate: number;
|
||||
}
|
||||
|
||||
// Aggregated DEX statistics
|
||||
export interface DexFill {
|
||||
readonly id: string;
|
||||
readonly market: string;
|
||||
readonly taker: string;
|
||||
readonly amountOut: string;
|
||||
readonly timestamp: number;
|
||||
readonly txHash: string;
|
||||
}
|
||||
|
||||
export interface DexOverviewStats {
|
||||
readonly totalPairs: number;
|
||||
// A market with its base/quote token symbols resolved to a human pair label.
|
||||
export interface DexMarketView extends DexMarket {
|
||||
readonly pair: string;
|
||||
}
|
||||
|
||||
// Aggregated headline stats derived from the live market set.
|
||||
export interface DexOverview {
|
||||
readonly totalMarkets: number;
|
||||
readonly volume24h: string;
|
||||
readonly activeOrders: number;
|
||||
readonly tradesToday: number;
|
||||
readonly totalTrades: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { DexMarket } from './types';
|
||||
|
||||
import { expect, test, describe } from 'vitest';
|
||||
|
||||
import { buildMarketViews, computeOverview } from './useDexData';
|
||||
|
||||
// Real shapes returned by the testnet cchain/dex subgraph.
|
||||
const MARKET_A: DexMarket = {
|
||||
id: '0x70ed78d5fb8fd83110ffb5b6726edb3d76e0968d5dba042e59e0c97af1a1f2cd',
|
||||
baseToken: '0xaa1c9a2527f70aee8c163fc053ccbba0e0df7a37',
|
||||
quoteToken: '0xf90434869b41d554c6446aacd73c06406b6ce9c1',
|
||||
feeTier: 3000,
|
||||
volume24h: '10',
|
||||
tradeCount: 1,
|
||||
lastPrice: '0',
|
||||
lastUpdate: 11,
|
||||
};
|
||||
|
||||
const MARKET_B: DexMarket = {
|
||||
id: '0x2b0a2ff82be3beb2d2b58d0766b1c6c0b3f9bb44689e469bb49934e4e32802f1',
|
||||
baseToken: '0xa811aac7a2e132c167650f93f3e14305e6205f95',
|
||||
quoteToken: '0xccba5e1036dcab7ff4885975d339cafab2a4c0c4',
|
||||
feeTier: 3000,
|
||||
volume24h: '1258432996',
|
||||
tradeCount: 21,
|
||||
lastPrice: '0',
|
||||
lastUpdate: 325,
|
||||
};
|
||||
|
||||
describe('buildMarketViews', () => {
|
||||
test('falls back to a shortened address when a token has no metadata', () => {
|
||||
const [ view ] = buildMarketViews([ MARKET_A ], new Map());
|
||||
expect(view.pair).toBe('0xaa1c...7a37/0xf904...e9c1');
|
||||
});
|
||||
|
||||
test('resolves symbols from the metadata map (case-insensitive)', () => {
|
||||
const symbols = new Map([
|
||||
[ MARKET_A.baseToken.toLowerCase(), 'LUX' ],
|
||||
[ MARKET_A.quoteToken.toLowerCase(), 'USDC' ],
|
||||
]);
|
||||
const [ view ] = buildMarketViews([ MARKET_A ], symbols);
|
||||
expect(view.pair).toBe('LUX/USDC');
|
||||
});
|
||||
|
||||
test('preserves the source market fields', () => {
|
||||
const [ view ] = buildMarketViews([ MARKET_A ], new Map());
|
||||
expect(view).toMatchObject({ id: MARKET_A.id, feeTier: 3000, tradeCount: 1, volume24h: '10' });
|
||||
});
|
||||
|
||||
test('returns an empty list for no markets (empty state)', () => {
|
||||
expect(buildMarketViews([], new Map())).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeOverview', () => {
|
||||
test('aggregates market count, big-int volume, and trade count', () => {
|
||||
const views = buildMarketViews([ MARKET_A, MARKET_B ], new Map());
|
||||
expect(computeOverview(views)).toEqual({
|
||||
totalMarkets: 2,
|
||||
volume24h: '1258433006',
|
||||
totalTrades: 22,
|
||||
});
|
||||
});
|
||||
|
||||
test('is zeroed for an empty market set', () => {
|
||||
expect(computeOverview([])).toEqual({ totalMarkets: 0, volume24h: '0', totalTrades: 0 });
|
||||
});
|
||||
});
|
||||
+127
-157
@@ -1,185 +1,155 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type {
|
||||
DexOrder,
|
||||
DexTrade,
|
||||
DexPool,
|
||||
DexSymbolStats,
|
||||
DexOverviewStats,
|
||||
} from './types';
|
||||
import type { DexMarket, DexFill, DexMarketView, DexOverview } from './types';
|
||||
|
||||
import { getDChain } from 'lib/api/luxnet/chains';
|
||||
import config from 'configs/app';
|
||||
import shortenString from 'lib/shortenString';
|
||||
|
||||
// One root field per request — the subgraph engine processes a single root
|
||||
// field per query, so markets and fills are fetched separately.
|
||||
const MARKETS_QUERY = '{ markets { id baseToken quoteToken feeTier volume24h tradeCount lastPrice lastUpdate } }';
|
||||
const FILLS_QUERY = '{ fills(first:25) { id market taker amountOut timestamp txHash } }';
|
||||
|
||||
const DEX_GRAPHQL_PATH = '/v1/graph/cchain/dex/graphql';
|
||||
const DEX_STALE_TIME_MS = 30_000;
|
||||
const DEX_QUERY_KEY = 'dchain:dexData' as const;
|
||||
|
||||
/* eslint-disable max-len */
|
||||
const DEMO_SYMBOLS: ReadonlyArray<DexSymbolStats> = [
|
||||
{ symbol: 'LUX/USDT', lastPrice: '24.85', change24h: 3.42, volume24h: '1,245,890', high24h: '25.10', low24h: '23.90', trades24h: 482 },
|
||||
{ symbol: 'LUX/USDC', lastPrice: '24.83', change24h: 3.18, volume24h: '892,340', high24h: '25.05', low24h: '23.88', trades24h: 356 },
|
||||
{ symbol: 'ZOO/LUX', lastPrice: '0.0842', change24h: -1.25, volume24h: '456,120', high24h: '0.0870', low24h: '0.0830', trades24h: 198 },
|
||||
{ symbol: 'HNZ/LUX', lastPrice: '0.152', change24h: 7.04, volume24h: '334,560', high24h: '0.158', low24h: '0.140', trades24h: 145 },
|
||||
{ symbol: 'SPC/LUX', lastPrice: '0.0215', change24h: -0.47, volume24h: '123,450', high24h: '0.0220', low24h: '0.0210', trades24h: 87 },
|
||||
{ symbol: 'PARS/LUX', lastPrice: '0.0034', change24h: 12.50, volume24h: '89,230', high24h: '0.0036', low24h: '0.0030', trades24h: 64 },
|
||||
];
|
||||
const EMPTY_MARKETS: ReadonlyArray<DexMarketView> = [];
|
||||
const EMPTY_FILLS: ReadonlyArray<DexFill> = [];
|
||||
|
||||
const DEMO_ORDERS: ReadonlyArray<DexOrder> = [
|
||||
{ id: 'ord-001', symbol: 'LUX/USDT', side: 'buy', price: '24.50', quantity: '100.00', filled: '0.00', status: 'open', maker: '0x1a2b...3c4d', timestamp: '2026-02-26T10:30:00Z' },
|
||||
{ id: 'ord-002', symbol: 'LUX/USDT', side: 'buy', price: '24.45', quantity: '250.00', filled: '50.00', status: 'partial', maker: '0x5e6f...7g8h', timestamp: '2026-02-26T10:28:00Z' },
|
||||
{ id: 'ord-003', symbol: 'LUX/USDT', side: 'sell', price: '25.10', quantity: '75.00', filled: '0.00', status: 'open', maker: '0x9i0j...1k2l', timestamp: '2026-02-26T10:25:00Z' },
|
||||
{ id: 'ord-004', symbol: 'LUX/USDC', side: 'sell', price: '25.05', quantity: '150.00', filled: '150.00', status: 'filled', maker: '0x3m4n...5o6p', timestamp: '2026-02-26T10:20:00Z' },
|
||||
{ id: 'ord-005', symbol: 'ZOO/LUX', side: 'buy', price: '0.0840', quantity: '5000.00', filled: '0.00', status: 'open', maker: '0x7q8r...9s0t', timestamp: '2026-02-26T10:18:00Z' },
|
||||
{ id: 'ord-006', symbol: 'HNZ/LUX', side: 'buy', price: '0.150', quantity: '2000.00', filled: '800.00', status: 'partial', maker: '0x1u2v...3w4x', timestamp: '2026-02-26T10:15:00Z' },
|
||||
{ id: 'ord-007', symbol: 'LUX/USDT', side: 'sell', price: '25.20', quantity: '300.00', filled: '0.00', status: 'open', maker: '0x5y6z...7a8b', timestamp: '2026-02-26T10:12:00Z' },
|
||||
{ id: 'ord-008', symbol: 'SPC/LUX', side: 'buy', price: '0.0210', quantity: '10000.00', filled: '0.00', status: 'open', maker: '0x9c0d...1e2f', timestamp: '2026-02-26T10:10:00Z' },
|
||||
{ id: 'ord-009', symbol: 'LUX/USDT', side: 'sell', price: '25.00', quantity: '120.00', filled: '120.00', status: 'filled', maker: '0x3g4h...5i6j', timestamp: '2026-02-26T10:05:00Z' },
|
||||
{ id: 'ord-010', symbol: 'PARS/LUX', side: 'buy', price: '0.0033', quantity: '50000.00', filled: '0.00', status: 'open', maker: '0x7k8l...9m0n', timestamp: '2026-02-26T10:00:00Z' },
|
||||
];
|
||||
// The DEX subgraph lives on the same API host the rest of the SPA already
|
||||
// talks to (NEXT_PUBLIC_API_HOST → config.apis.general.endpoint), so the
|
||||
// network is selected exactly like every other resource — by deployment.
|
||||
function getApiBase(): string | undefined {
|
||||
return config.apis.general?.endpoint;
|
||||
}
|
||||
|
||||
const DEMO_TRADES: ReadonlyArray<DexTrade> = [
|
||||
{ id: 'trd-001', symbol: 'LUX/USDT', price: '24.85', quantity: '50.00', buyer: '0x1a2b...3c4d', seller: '0x3m4n...5o6p', fee: '0.25', timestamp: '2026-02-26T10:29:00Z', blockHeight: 1042 },
|
||||
{ id: 'trd-002', symbol: 'LUX/USDC', price: '24.83', quantity: '100.00', buyer: '0x5e6f...7g8h', seller: '0x9i0j...1k2l', fee: '0.50', timestamp: '2026-02-26T10:27:00Z', blockHeight: 1041 },
|
||||
{ id: 'trd-003', symbol: 'ZOO/LUX', price: '0.0842', quantity: '2000.00', buyer: '0x7q8r...9s0t', seller: '0x1u2v...3w4x', fee: '0.84', timestamp: '2026-02-26T10:24:00Z', blockHeight: 1040 },
|
||||
{ id: 'trd-004', symbol: 'HNZ/LUX', price: '0.152', quantity: '500.00', buyer: '0x5y6z...7a8b', seller: '0x9c0d...1e2f', fee: '0.38', timestamp: '2026-02-26T10:21:00Z', blockHeight: 1039 },
|
||||
{ id: 'trd-005', symbol: 'LUX/USDT', price: '24.80', quantity: '75.00', buyer: '0x3g4h...5i6j', seller: '0x7k8l...9m0n', fee: '0.38', timestamp: '2026-02-26T10:19:00Z', blockHeight: 1038 },
|
||||
{ id: 'trd-006', symbol: 'SPC/LUX', price: '0.0215', quantity: '8000.00', buyer: '0x1a2b...3c4d', seller: '0x5e6f...7g8h', fee: '0.86', timestamp: '2026-02-26T10:16:00Z', blockHeight: 1037 },
|
||||
{ id: 'trd-007', symbol: 'LUX/USDT', price: '24.75', quantity: '200.00', buyer: '0x9i0j...1k2l', seller: '0x3m4n...5o6p', fee: '1.00', timestamp: '2026-02-26T10:13:00Z', blockHeight: 1036 },
|
||||
{ id: 'trd-008', symbol: 'PARS/LUX', price: '0.0034', quantity: '25000.00', buyer: '0x7q8r...9s0t', seller: '0x1u2v...3w4x', fee: '0.43', timestamp: '2026-02-26T10:11:00Z', blockHeight: 1035 },
|
||||
];
|
||||
async function fetchGraphql<T>(query: string): Promise<T | null> {
|
||||
const base = getApiBase();
|
||||
if (!base) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const DEMO_POOLS: ReadonlyArray<DexPool> = [
|
||||
{ id: 'pool-001', tokenA: 'LUX', tokenB: 'USDT', reserveA: '52,340', reserveB: '1,300,652', tvl: '2,601,304', volume24h: '1,245,890', fee: '0.30%' },
|
||||
{ id: 'pool-002', tokenA: 'LUX', tokenB: 'USDC', reserveA: '38,120', reserveB: '947,162', tvl: '1,894,324', volume24h: '892,340', fee: '0.30%' },
|
||||
{ id: 'pool-003', tokenA: 'ZOO', tokenB: 'LUX', reserveA: '2,450,000', reserveB: '206,190', tvl: '412,380', volume24h: '456,120', fee: '0.50%' },
|
||||
{ id: 'pool-004', tokenA: 'HNZ', tokenB: 'LUX', reserveA: '1,200,000', reserveB: '182,400', tvl: '364,800', volume24h: '334,560', fee: '0.50%' },
|
||||
{ id: 'pool-005', tokenA: 'SPC', tokenB: 'LUX', reserveA: '5,800,000', reserveB: '124,700', tvl: '249,400', volume24h: '123,450', fee: '1.00%' },
|
||||
{ id: 'pool-006', tokenA: 'PARS', tokenB: 'LUX', reserveA: '18,000,000', reserveB: '61,200', tvl: '122,400', volume24h: '89,230', fee: '1.00%' },
|
||||
];
|
||||
/* eslint-enable max-len */
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(base + DEX_GRAPHQL_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const DEMO_OVERVIEW: DexOverviewStats = {
|
||||
totalPairs: 6,
|
||||
volume24h: '3,141,590',
|
||||
activeOrders: 7,
|
||||
tradesToday: 1332,
|
||||
};
|
||||
// A net without the subgraph mounted answers 404 — that is an empty DEX,
|
||||
// not an error, so we surface it as "no data" and render the empty state.
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const json = await res.json().catch(() => null) as { data?: T } | null;
|
||||
return json?.data ?? null;
|
||||
}
|
||||
|
||||
async function fetchTokenSymbol(address: string): Promise<string | null> {
|
||||
const base = getApiBase();
|
||||
if (!base) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${ base }/tokens/${ address }`);
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
const json = await res.json().catch(() => null) as { symbol?: string | null } | null;
|
||||
return json?.symbol || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function tokenLabel(address: string, symbols: ReadonlyMap<string, string>): string {
|
||||
return symbols.get(address.toLowerCase()) || shortenString(address, 10);
|
||||
}
|
||||
|
||||
// Pure: attach a human pair label to each market from a resolved symbol map.
|
||||
export function buildMarketViews(
|
||||
markets: ReadonlyArray<DexMarket>,
|
||||
symbols: ReadonlyMap<string, string>,
|
||||
): ReadonlyArray<DexMarketView> {
|
||||
return markets.map((m) => ({
|
||||
...m,
|
||||
pair: `${ tokenLabel(m.baseToken, symbols) }/${ tokenLabel(m.quoteToken, symbols) }`,
|
||||
}));
|
||||
}
|
||||
|
||||
// Pure: headline aggregates over the live market set.
|
||||
export function computeOverview(markets: ReadonlyArray<DexMarketView>): DexOverview {
|
||||
return {
|
||||
totalMarkets: markets.length,
|
||||
volume24h: markets.reduce((sum, m) => sum + safeBigInt(m.volume24h), BigInt(0)).toString(),
|
||||
totalTrades: markets.reduce((sum, m) => sum + (m.tradeCount || 0), 0),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchMarkets(): Promise<ReadonlyArray<DexMarketView>> {
|
||||
const data = await fetchGraphql<{ markets: ReadonlyArray<DexMarket> | null }>(MARKETS_QUERY);
|
||||
const markets = data?.markets ?? [];
|
||||
|
||||
// Resolve base/quote symbols via the existing token metadata endpoint,
|
||||
// falling back to a shortened address when a token isn't indexed.
|
||||
const addresses = Array.from(new Set(markets.flatMap((m) => [ m.baseToken, m.quoteToken ])));
|
||||
const resolved = await Promise.all(addresses.map(async(addr) => [ addr.toLowerCase(), await fetchTokenSymbol(addr) ] as const));
|
||||
const symbols = new Map(resolved.filter((entry): entry is readonly [string, string] => Boolean(entry[1])));
|
||||
|
||||
return buildMarketViews(markets, symbols);
|
||||
}
|
||||
|
||||
async function fetchFills(): Promise<ReadonlyArray<DexFill>> {
|
||||
const data = await fetchGraphql<{ fills: ReadonlyArray<DexFill> | null }>(FILLS_QUERY);
|
||||
return data?.fills ?? [];
|
||||
}
|
||||
|
||||
export interface UseDexDataResult {
|
||||
readonly symbols: ReadonlyArray<DexSymbolStats>;
|
||||
readonly orders: ReadonlyArray<DexOrder>;
|
||||
readonly trades: ReadonlyArray<DexTrade>;
|
||||
readonly pools: ReadonlyArray<DexPool>;
|
||||
readonly overview: DexOverviewStats;
|
||||
readonly markets: ReadonlyArray<DexMarketView>;
|
||||
readonly fills: ReadonlyArray<DexFill>;
|
||||
readonly overview: DexOverview;
|
||||
readonly isLoading: boolean;
|
||||
readonly isError: boolean;
|
||||
readonly error: Error | null;
|
||||
}
|
||||
|
||||
interface DexDataPayload {
|
||||
readonly symbols: ReadonlyArray<DexSymbolStats>;
|
||||
readonly orders: ReadonlyArray<DexOrder>;
|
||||
readonly trades: ReadonlyArray<DexTrade>;
|
||||
readonly pools: ReadonlyArray<DexPool>;
|
||||
readonly overview: DexOverviewStats;
|
||||
}
|
||||
|
||||
const EMPTY_SYMBOLS: ReadonlyArray<DexSymbolStats> = [];
|
||||
const EMPTY_ORDERS: ReadonlyArray<DexOrder> = [];
|
||||
const EMPTY_TRADES: ReadonlyArray<DexTrade> = [];
|
||||
const EMPTY_POOLS: ReadonlyArray<DexPool> = [];
|
||||
const EMPTY_OVERVIEW: DexOverviewStats = {
|
||||
totalPairs: 0,
|
||||
volume24h: '0',
|
||||
activeOrders: 0,
|
||||
tradesToday: 0,
|
||||
};
|
||||
|
||||
async function fetchDexData(): Promise<DexDataPayload> {
|
||||
// Try luxnet SDK D-Chain RPC first
|
||||
try {
|
||||
const dchain = getDChain();
|
||||
const stats = await dchain.getStats();
|
||||
|
||||
if (stats && stats.trades24h > 0) {
|
||||
const [ pools, markets ] = await Promise.all([
|
||||
dchain.getPools(),
|
||||
dchain.getMarkets(),
|
||||
]);
|
||||
|
||||
return {
|
||||
symbols: DEMO_SYMBOLS,
|
||||
orders: DEMO_ORDERS,
|
||||
trades: DEMO_TRADES,
|
||||
pools: pools.length > 0 ? pools.map((p, i) => ({
|
||||
id: `pool-${ i }`,
|
||||
tokenA: String(p.tokenA),
|
||||
tokenB: String(p.tokenB),
|
||||
reserveA: String(p.reserveA),
|
||||
reserveB: String(p.reserveB),
|
||||
tvl: String(p.totalLiquidity),
|
||||
volume24h: DEMO_POOLS[i]?.volume24h ?? '0',
|
||||
fee: String(p.fee),
|
||||
})) : DEMO_POOLS,
|
||||
overview: {
|
||||
totalPairs: markets.length || DEMO_OVERVIEW.totalPairs,
|
||||
volume24h: stats.volume24h || DEMO_OVERVIEW.volume24h,
|
||||
activeOrders: DEMO_OVERVIEW.activeOrders,
|
||||
tradesToday: stats.trades24h || DEMO_OVERVIEW.tradesToday,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch { /* D-Chain RPC unavailable — fall through to demo data */ }
|
||||
|
||||
return {
|
||||
symbols: DEMO_SYMBOLS,
|
||||
orders: DEMO_ORDERS,
|
||||
trades: DEMO_TRADES,
|
||||
pools: DEMO_POOLS,
|
||||
overview: DEMO_OVERVIEW,
|
||||
};
|
||||
}
|
||||
|
||||
export function useDexData(): UseDexDataResult {
|
||||
const query = useQuery({
|
||||
queryKey: [ DEX_QUERY_KEY ],
|
||||
queryFn: fetchDexData,
|
||||
const marketsQuery = useQuery({
|
||||
queryKey: [ 'dchain:markets', getApiBase() ],
|
||||
queryFn: fetchMarkets,
|
||||
staleTime: DEX_STALE_TIME_MS,
|
||||
});
|
||||
|
||||
const symbols = React.useMemo(
|
||||
() => query.data?.symbols ?? EMPTY_SYMBOLS,
|
||||
[ query.data?.symbols ],
|
||||
);
|
||||
const fillsQuery = useQuery({
|
||||
queryKey: [ 'dchain:fills', getApiBase() ],
|
||||
queryFn: fetchFills,
|
||||
staleTime: DEX_STALE_TIME_MS,
|
||||
});
|
||||
|
||||
const orders = React.useMemo(
|
||||
() => query.data?.orders ?? EMPTY_ORDERS,
|
||||
[ query.data?.orders ],
|
||||
);
|
||||
const markets = marketsQuery.data ?? EMPTY_MARKETS;
|
||||
const fills = fillsQuery.data ?? EMPTY_FILLS;
|
||||
|
||||
const trades = React.useMemo(
|
||||
() => query.data?.trades ?? EMPTY_TRADES,
|
||||
[ query.data?.trades ],
|
||||
);
|
||||
|
||||
const pools = React.useMemo(
|
||||
() => query.data?.pools ?? EMPTY_POOLS,
|
||||
[ query.data?.pools ],
|
||||
);
|
||||
|
||||
const overview = React.useMemo(
|
||||
() => query.data?.overview ?? EMPTY_OVERVIEW,
|
||||
[ query.data?.overview ],
|
||||
);
|
||||
const overview = React.useMemo<DexOverview>(() => computeOverview(markets), [ markets ]);
|
||||
|
||||
return {
|
||||
symbols,
|
||||
orders,
|
||||
trades,
|
||||
pools,
|
||||
markets,
|
||||
fills,
|
||||
overview,
|
||||
isLoading: query.isLoading,
|
||||
isError: query.isError,
|
||||
error: query.error,
|
||||
isLoading: marketsQuery.isLoading || fillsQuery.isLoading,
|
||||
isError: marketsQuery.isError || fillsQuery.isError,
|
||||
};
|
||||
}
|
||||
|
||||
function safeBigInt(value: string): bigint {
|
||||
try {
|
||||
return BigInt(value);
|
||||
} catch {
|
||||
return BigInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
+95
-233
@@ -1,51 +1,44 @@
|
||||
// DEX Chain (D-Chain) orderbook page for the Lux multi-chain explorer.
|
||||
// Displays market stats, orderbook, trade history, and liquidity pools
|
||||
// sourced from the DexVM indexer.
|
||||
// C-Chain native DEX (0x9999 V4 PoolManager) page for the Lux explorer.
|
||||
// Renders live markets and recent fills from the embedded dex (CLOB) subgraph.
|
||||
// All data is real and network-aware — there is no mock/demo path.
|
||||
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from 'lib/utils/cn';
|
||||
import { route } from 'nextjs/routes';
|
||||
|
||||
import type { DexFill, DexMarketView } from 'lib/api/dchain';
|
||||
import { useDexData } from 'lib/api/dchain';
|
||||
import type { DexOrder, DexTrade, DexPool, DexSymbolStats } from 'lib/api/dchain';
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tag } from '@luxfi/ui/tag';
|
||||
import shortenString from 'lib/shortenString';
|
||||
import { cn } from 'lib/utils/cn';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
// ── Constants ──
|
||||
|
||||
const TAB_IDS = {
|
||||
markets: 'markets',
|
||||
orderbook: 'orderbook',
|
||||
trades: 'trades',
|
||||
pools: 'pools',
|
||||
} as const;
|
||||
|
||||
type TabId = typeof TAB_IDS[keyof typeof TAB_IDS];
|
||||
|
||||
const ROW_BASE = 'flex items-center py-3 px-4 border-b border-[var(--color-border-divider)] hover:bg-gray-50 dark:hover:bg-white/5 transition-[background] duration-150 gap-4 flex-wrap lg:flex-nowrap';
|
||||
const ROW_BASE = 'flex items-center py-3 px-4 border-b border-[var(--color-border-divider)] ' +
|
||||
'hover:bg-gray-50 dark:hover:bg-white/5 transition-[background] duration-150 gap-4 flex-wrap lg:flex-nowrap';
|
||||
const HEADER_BASE = 'hidden lg:flex px-4 py-2 gap-4 border-b border-[var(--color-border-divider)]';
|
||||
const COL_HEADER = 'text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider';
|
||||
const LINK_CLASS = 'text-sm font-mono text-[var(--color-link-primary)] hover:text-[var(--color-link-primary-hover)] hover:underline';
|
||||
|
||||
// ── Sub-components ──
|
||||
// ── Helpers ──
|
||||
|
||||
interface TabButtonProps {
|
||||
readonly label: string;
|
||||
readonly isActive: boolean;
|
||||
readonly onClick: () => void;
|
||||
function groupDigits(value: string): string {
|
||||
return /^\d+$/.test(value) ? value.replace(/\B(?=(?:\d{3})+(?!\d))/g, ',') : value;
|
||||
}
|
||||
|
||||
const TabButton = ({ label, isActive, onClick }: TabButtonProps) => (
|
||||
<button
|
||||
className={ cn(
|
||||
'px-4 py-2 text-sm bg-transparent cursor-pointer transition-all duration-150 border-b-2 hover:text-[var(--color-text-primary)]',
|
||||
isActive ? 'font-semibold text-[var(--color-text-primary)] border-[var(--color-text-primary)]' : 'font-normal text-[var(--color-text-secondary)] border-transparent',
|
||||
) }
|
||||
onClick={ onClick }
|
||||
>
|
||||
{ label }
|
||||
</button>
|
||||
);
|
||||
function formatFeeTier(feeTier: number): string {
|
||||
return `${ (feeTier / 10_000).toFixed(2) }%`;
|
||||
}
|
||||
|
||||
// ── Sub-components ──
|
||||
|
||||
interface StatCardProps {
|
||||
readonly label: string;
|
||||
@@ -66,152 +59,72 @@ const StatCard = ({ label, value, isLoading }: StatCardProps) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Symbol row ──
|
||||
|
||||
interface SymbolRowProps {
|
||||
readonly stat: DexSymbolStats;
|
||||
interface TabButtonProps {
|
||||
readonly label: string;
|
||||
readonly isActive: boolean;
|
||||
readonly onClick: () => void;
|
||||
}
|
||||
|
||||
const SymbolRow = ({ stat }: SymbolRowProps) => {
|
||||
const isPositive = stat.change24h >= 0;
|
||||
const TabButton = ({ label, isActive, onClick }: TabButtonProps) => (
|
||||
<button
|
||||
className={ cn(
|
||||
'px-4 py-2 text-sm bg-transparent cursor-pointer transition-all duration-150 border-b-2 hover:text-[var(--color-text-primary)]',
|
||||
isActive ?
|
||||
'font-semibold text-[var(--color-text-primary)] border-[var(--color-text-primary)]' :
|
||||
'font-normal text-[var(--color-text-secondary)] border-transparent',
|
||||
) }
|
||||
onClick={ onClick }
|
||||
>
|
||||
{ label }
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={ ROW_BASE }>
|
||||
<div className="min-w-[120px] shrink-0">
|
||||
<span className="font-semibold text-sm text-[var(--color-text-primary)]">{ stat.symbol }</span>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-primary)]">{ stat.lastPrice }</span>
|
||||
</div>
|
||||
<div className="min-w-[80px] shrink-0 text-right">
|
||||
<span className={ `text-sm font-medium ${ isPositive ? 'text-green-400' : 'text-red-400' }` }>
|
||||
{ isPositive ? '+' : '' }{ stat.change24h.toFixed(2) }%
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">{ stat.high24h }</span>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">{ stat.low24h }</span>
|
||||
</div>
|
||||
<div className="min-w-[120px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">${ stat.volume24h }</span>
|
||||
</div>
|
||||
<div className="shrink-0 text-right ml-0 lg:ml-auto">
|
||||
<span className="text-sm text-[var(--color-text-secondary)]">{ stat.trades24h }</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Order row ──
|
||||
|
||||
interface OrderRowProps {
|
||||
readonly order: DexOrder;
|
||||
}
|
||||
|
||||
const OrderRow = ({ order }: OrderRowProps) => {
|
||||
const isBuy = order.side === 'buy';
|
||||
|
||||
return (
|
||||
<div className={ ROW_BASE }>
|
||||
<div className="min-w-[100px] shrink-0">
|
||||
<span className="text-sm text-[var(--color-text-primary)]">{ order.symbol }</span>
|
||||
</div>
|
||||
<div className="min-w-[60px] shrink-0">
|
||||
<Tag size="sm" variant="subtle" className={ isBuy ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }>
|
||||
{ order.side.toUpperCase() }
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0 text-right">
|
||||
<span className={ `text-sm font-mono ${ isBuy ? 'text-green-400' : 'text-red-400' }` }>{ order.price }</span>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-primary)]">{ order.quantity }</span>
|
||||
</div>
|
||||
<div className="min-w-[120px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">
|
||||
{ (parseFloat(order.price) * parseFloat(order.quantity)).toFixed(2) }
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">{ order.maker }</span>
|
||||
</div>
|
||||
<div className="min-w-[80px] shrink-0">
|
||||
<span className="text-sm text-[var(--color-text-secondary)]">{ formatTime(order.timestamp) }</span>
|
||||
</div>
|
||||
<div className="shrink-0 ml-0 lg:ml-auto">
|
||||
<Tag size="sm" variant="subtle" className={ getStatusClassName(order.status) }>
|
||||
{ order.status }
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Trade row ──
|
||||
|
||||
interface TradeRowProps {
|
||||
readonly trade: DexTrade;
|
||||
}
|
||||
|
||||
const TradeRow = ({ trade }: TradeRowProps) => (
|
||||
const MarketRow = ({ market }: { readonly market: DexMarketView }) => (
|
||||
<div className={ ROW_BASE }>
|
||||
<div className="min-w-[100px] shrink-0">
|
||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">{ trade.symbol }</span>
|
||||
<div className="min-w-[160px] shrink-0">
|
||||
<span className="font-semibold text-sm text-[var(--color-text-primary)]">{ market.pair }</span>
|
||||
</div>
|
||||
<div className="min-w-[80px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">{ formatFeeTier(market.feeTier) }</span>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-primary)]">{ trade.price }</span>
|
||||
<span className="text-sm font-mono text-[var(--color-text-primary)]">{ market.lastPrice }</span>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-primary)]">{ trade.quantity }</span>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">{ trade.buyer }</span>
|
||||
</div>
|
||||
<div className="min-w-[100px] shrink-0">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">{ trade.seller }</span>
|
||||
</div>
|
||||
<div className="min-w-[60px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">{ trade.fee }</span>
|
||||
<div className="min-w-[140px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">{ groupDigits(market.volume24h) }</span>
|
||||
</div>
|
||||
<div className="shrink-0 text-right ml-0 lg:ml-auto">
|
||||
<span className="text-sm text-[var(--color-text-secondary)]">{ formatTime(trade.timestamp) }</span>
|
||||
<span className="text-sm text-[var(--color-text-secondary)]">{ market.tradeCount }</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Pool row ──
|
||||
|
||||
interface PoolRowProps {
|
||||
readonly pool: DexPool;
|
||||
}
|
||||
|
||||
const PoolRow = ({ pool }: PoolRowProps) => (
|
||||
const FillRow = ({ fill, pair }: { readonly fill: DexFill; readonly pair: string }) => (
|
||||
<div className={ ROW_BASE }>
|
||||
<div className="min-w-[120px] shrink-0">
|
||||
<span className="font-semibold text-sm text-[var(--color-text-primary)]">{ pool.tokenA }/{ pool.tokenB }</span>
|
||||
<div className="min-w-[160px] shrink-0">
|
||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">{ pair }</span>
|
||||
</div>
|
||||
<div className="min-w-[120px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-primary)]">{ pool.reserveA }</span>
|
||||
<div className="min-w-[140px] shrink-0">
|
||||
<a href={ route({ pathname: '/address/[hash]', query: { hash: fill.taker } }) } className={ LINK_CLASS }>
|
||||
{ shortenString(fill.taker, 10) }
|
||||
</a>
|
||||
</div>
|
||||
<div className="min-w-[120px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-primary)]">{ pool.reserveB }</span>
|
||||
<div className="min-w-[140px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-primary)]">{ groupDigits(fill.amountOut) }</span>
|
||||
</div>
|
||||
<div className="min-w-[120px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono font-semibold text-[var(--color-text-primary)]">${ pool.tvl }</span>
|
||||
<div className="min-w-[90px] shrink-0 text-right">
|
||||
<a href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(fill.timestamp) } }) } className={ LINK_CLASS }>
|
||||
{ fill.timestamp }
|
||||
</a>
|
||||
</div>
|
||||
<div className="min-w-[120px] shrink-0 text-right">
|
||||
<span className="text-sm font-mono text-[var(--color-text-secondary)]">${ pool.volume24h }</span>
|
||||
</div>
|
||||
<div className="shrink-0 text-right ml-0 lg:ml-auto">
|
||||
<span className="text-sm text-[var(--color-text-secondary)]">{ pool.fee }</span>
|
||||
<div className="shrink-0 ml-0 lg:ml-auto">
|
||||
<a href={ route({ pathname: '/tx/[hash]', query: { hash: fill.txHash } }) } className={ LINK_CLASS }>
|
||||
{ shortenString(fill.txHash, 12) }
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Loading skeleton ──
|
||||
|
||||
const LoadingSkeleton = () => (
|
||||
<div className="px-4 py-6">
|
||||
<Skeleton loading h="16px" mb={ 3 }/>
|
||||
@@ -221,16 +134,25 @@ const LoadingSkeleton = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const EmptyState = ({ text }: { readonly text: string }) => (
|
||||
<div className="px-4 py-10 text-center text-sm text-[var(--color-text-secondary)]">
|
||||
{ text }
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Main component ──
|
||||
|
||||
const DexPage = () => {
|
||||
const [ activeTab, setActiveTab ] = React.useState<TabId>(TAB_IDS.markets);
|
||||
const { symbols, orders, trades, pools, overview, isLoading } = useDexData();
|
||||
const { markets, fills, overview, isLoading } = useDexData();
|
||||
|
||||
const pairByMarket = React.useMemo(
|
||||
() => new Map(markets.map((m) => [ m.id, m.pair ])),
|
||||
[ markets ],
|
||||
);
|
||||
|
||||
const handleMarketsClick = React.useCallback(() => setActiveTab(TAB_IDS.markets), []);
|
||||
const handleOrderbookClick = React.useCallback(() => setActiveTab(TAB_IDS.orderbook), []);
|
||||
const handleTradesClick = React.useCallback(() => setActiveTab(TAB_IDS.trades), []);
|
||||
const handlePoolsClick = React.useCallback(() => setActiveTab(TAB_IDS.pools), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -238,62 +160,38 @@ const DexPage = () => {
|
||||
title="DEX"
|
||||
secondRow={ (
|
||||
<div className="text-sm text-[var(--color-text-secondary)]">
|
||||
D-Chain decentralized exchange orderbook and market data
|
||||
C-Chain native exchange — live markets and fills from the 0x9999 PoolManager
|
||||
</div>
|
||||
) }
|
||||
/>
|
||||
|
||||
{ /* Stats cards */ }
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
|
||||
<StatCard label="Total Pairs" value={ String(overview.totalPairs) } isLoading={ isLoading }/>
|
||||
<StatCard label="24h Volume" value={ `$${ overview.volume24h }` } isLoading={ isLoading }/>
|
||||
<StatCard label="Active Orders" value={ String(overview.activeOrders) } isLoading={ isLoading }/>
|
||||
<StatCard label="Trades Today" value={ String(overview.tradesToday) } isLoading={ isLoading }/>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-6">
|
||||
<StatCard label="Markets" value={ String(overview.totalMarkets) } isLoading={ isLoading }/>
|
||||
<StatCard label="24h Volume" value={ groupDigits(overview.volume24h) } isLoading={ isLoading }/>
|
||||
<StatCard label="Total Trades" value={ String(overview.totalTrades) } isLoading={ isLoading }/>
|
||||
</div>
|
||||
|
||||
{ /* Tabs */ }
|
||||
<div className="flex border-b border-[var(--color-border-divider)] mb-4">
|
||||
<TabButton label="Markets" isActive={ activeTab === TAB_IDS.markets } onClick={ handleMarketsClick }/>
|
||||
<TabButton label="Orderbook" isActive={ activeTab === TAB_IDS.orderbook } onClick={ handleOrderbookClick }/>
|
||||
<TabButton label="Trades" isActive={ activeTab === TAB_IDS.trades } onClick={ handleTradesClick }/>
|
||||
<TabButton label="Pools" isActive={ activeTab === TAB_IDS.pools } onClick={ handlePoolsClick }/>
|
||||
</div>
|
||||
|
||||
{ /* Markets tab */ }
|
||||
{ activeTab === TAB_IDS.markets && (
|
||||
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
||||
<div className={ HEADER_BASE }>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[120px]') }>Symbol</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px] text-right') }>Price</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[80px] text-right') }>24h Change</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px] text-right') }>24h High</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px] text-right') }>24h Low</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[120px] text-right') }>24h Volume</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[160px]') }>Pair</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[80px] text-right') }>Fee</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px] text-right') }>Last Price</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[140px] text-right') }>24h Volume</div>
|
||||
<div className={ cn(COL_HEADER, 'ml-auto text-right') }>Trades</div>
|
||||
</div>
|
||||
{ isLoading && <LoadingSkeleton/> }
|
||||
{ !isLoading && symbols.map((stat) => (
|
||||
<SymbolRow key={ stat.symbol } stat={ stat }/>
|
||||
)) }
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ /* Orderbook tab */ }
|
||||
{ activeTab === TAB_IDS.orderbook && (
|
||||
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
||||
<div className={ HEADER_BASE }>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px]') }>Symbol</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[60px]') }>Side</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px] text-right') }>Price</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px] text-right') }>Quantity</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[120px] text-right') }>Total</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px]') }>Maker</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[80px]') }>Time</div>
|
||||
<div className={ cn(COL_HEADER, 'ml-auto text-right') }>Status</div>
|
||||
</div>
|
||||
{ isLoading && <LoadingSkeleton/> }
|
||||
{ !isLoading && orders.map((order) => (
|
||||
<OrderRow key={ order.id } order={ order }/>
|
||||
{ !isLoading && markets.length === 0 && <EmptyState text="No active markets"/> }
|
||||
{ !isLoading && markets.map((market) => (
|
||||
<MarketRow key={ market.id } market={ market }/>
|
||||
)) }
|
||||
</div>
|
||||
) }
|
||||
@@ -302,35 +200,16 @@ const DexPage = () => {
|
||||
{ activeTab === TAB_IDS.trades && (
|
||||
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
||||
<div className={ HEADER_BASE }>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px]') }>Symbol</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px] text-right') }>Price</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px] text-right') }>Quantity</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px]') }>Buyer</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[100px]') }>Seller</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[60px] text-right') }>Fee</div>
|
||||
<div className={ cn(COL_HEADER, 'ml-auto text-right') }>Time</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[160px]') }>Pair</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[140px]') }>Taker</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[140px] text-right') }>Amount Out</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[90px] text-right') }>Block</div>
|
||||
<div className={ cn(COL_HEADER, 'ml-auto text-right') }>Txn</div>
|
||||
</div>
|
||||
{ isLoading && <LoadingSkeleton/> }
|
||||
{ !isLoading && trades.map((trade) => (
|
||||
<TradeRow key={ trade.id } trade={ trade }/>
|
||||
)) }
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ /* Pools tab */ }
|
||||
{ activeTab === TAB_IDS.pools && (
|
||||
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
||||
<div className={ HEADER_BASE }>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[120px]') }>Pair</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[120px] text-right') }>Reserve A</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[120px] text-right') }>Reserve B</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[120px] text-right') }>TVL</div>
|
||||
<div className={ cn(COL_HEADER, 'min-w-[120px] text-right') }>24h Volume</div>
|
||||
<div className={ cn(COL_HEADER, 'ml-auto text-right') }>Fee</div>
|
||||
</div>
|
||||
{ isLoading && <LoadingSkeleton/> }
|
||||
{ !isLoading && pools.map((pool) => (
|
||||
<PoolRow key={ pool.id } pool={ pool }/>
|
||||
{ !isLoading && fills.length === 0 && <EmptyState text="No recent trades"/> }
|
||||
{ !isLoading && fills.map((fill) => (
|
||||
<FillRow key={ fill.id } fill={ fill } pair={ pairByMarket.get(fill.market) ?? shortenString(fill.market, 10) }/>
|
||||
)) }
|
||||
</div>
|
||||
) }
|
||||
@@ -338,21 +217,4 @@ const DexPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
function formatTime(timestamp: string): string {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
}
|
||||
|
||||
function getStatusClassName(status: DexOrder['status']): string {
|
||||
switch (status) {
|
||||
case 'open': return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
||||
case 'filled': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||
case 'partial': return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200';
|
||||
case 'cancelled': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||
}
|
||||
}
|
||||
|
||||
export default React.memo(DexPage);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import React from 'react';
|
||||
|
||||
import type { NovesDescribeTxsResponse } from 'types/api/noves';
|
||||
@@ -5,7 +6,6 @@ import type { Transaction } from 'types/api/transaction';
|
||||
import type { ClusterChainConfig } from 'types/multichain';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import AddressFromTo from 'ui/shared/address/AddressFromTo';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
@@ -39,7 +39,6 @@ const TxsListItem = ({
|
||||
showBlockInfo,
|
||||
currentAddress,
|
||||
enableTimeIncrement,
|
||||
animation,
|
||||
chainData,
|
||||
translationIsLoading,
|
||||
translationData,
|
||||
@@ -73,7 +72,7 @@ const TxsListItem = ({
|
||||
hash={ tx.hash }
|
||||
truncation="constant_long"
|
||||
className="font-bold"
|
||||
icon={ !tx.is_pending_update && tx.transaction_types.includes('blob_transaction') ? { name: 'blob' } : undefined }
|
||||
icon={ !tx.is_pending_update && tx.transaction_types?.includes('blob_transaction') ? { name: 'blob' } : undefined }
|
||||
chain={ chainData }
|
||||
isPendingUpdate={ tx.is_pending_update }
|
||||
/>
|
||||
@@ -81,7 +80,7 @@ const TxsListItem = ({
|
||||
timestamp={ tx.timestamp }
|
||||
enableIncrement={ enableTimeIncrement }
|
||||
isLoading={ isLoading }
|
||||
|
||||
|
||||
/>
|
||||
</div>
|
||||
{ tx.method && (
|
||||
|
||||
Reference in New Issue
Block a user