Files
zeekayandHanzo Dev f434a701dc dex: show the AMM trades that are actually landing, next to an honest CLOB
The AMM subgraph has been indexed and at chain head the whole time; nothing
read it. /dex queried only the native CLOB at 0x9999, which has genuinely
never emitted an event, so the page reported MARKETS 0 / VOLUME 0 while ~1
swap per block was landing in the WLUX/LZOO pool.

Wire the amm subgraph in as a SECOND venue rather than folding it into the
first. The two are different exchanges and their figures are never summed —
each section reports only its own state, so "live" can never be borrowed by
the venue that is not trading.

Swaps were rendering as anonymous EVM traffic; they now read as trades:
pool-signed amounts resolved into sold/bought, scaled by each token's own
decimals, with symbols, pair, fee tier and links to sender, block and txn.

Status uses the words already in use elsewhere (useNetworkValidators,
useFeeSplit) — live/idle/empty/unavailable/unknown — defined once in
lib/api/venueStatus. There is deliberately no 'stale': separating a stalled
indexer from a quiet venue needs an indexer head this API does not serve.

Also decomplected while here: one fetcher for both subgraphs, and an
unreachable endpoint is no longer flattened into "empty" — an outage and a
venue that has never traded are different facts and now render differently.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 08:03:30 -07:00

113 lines
4.2 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { AmmFactory, AmmOverview, AmmSwap, AmmSwapView } from './types';
import { getApiBase, fetchSubgraph } from 'lib/api/subgraph';
import { useHeadBlock } from 'lib/api/useHeadBlock';
import type { VenueStatus } from 'lib/api/venueStatus';
import { deriveVenueStatus } from 'lib/api/venueStatus';
// One root field per request — the subgraph engine processes a single root
// field per query, so swaps and factory totals are fetched separately.
const SWAPS_QUERY = '{ swaps(first:50, orderBy: timestamp, orderDirection: desc) ' +
'{ id timestamp amount0 amount1 sender pool { id feeTier token0 { id symbol decimals } token1 { id symbol decimals } } } }';
const FACTORY_QUERY = '{ factories { id poolCount totalValueLockedUSD totalVolumeUSD } }';
const AMM_GRAPHQL_PATH = '/v1/graph/cchain/amm/graphql';
const POLL_INTERVAL_MS = 30_000;
const AMOUNT_ACCURACY = 4;
// Absolute value of a signed subgraph amount, scaled by its own token's
// decimals. The sign only encodes direction, which the caller has already read.
function formatAmount(raw: string, decimals: number): string {
const value = BigNumber(raw).abs().div(BigNumber(10).pow(decimals));
return value.isNaN() ? '0' : value.dp(AMOUNT_ACCURACY).toFormat();
}
// Pure: turn pool-signed amounts into the trade a person made.
export function buildSwapViews(swaps: ReadonlyArray<AmmSwap>): ReadonlyArray<AmmSwapView> {
return swaps.map((swap) => {
const { token0, token1, feeTier } = swap.pool;
// Positive amount0 means the pool received token0, so the trader sold it.
const soldIsToken0 = !swap.amount0.startsWith('-');
const sold = soldIsToken0 ? token0 : token1;
const bought = soldIsToken0 ? token1 : token0;
return {
id: swap.id,
// The subgraph keys a swap as `<txHash>#<logIndex>`.
txHash: swap.id.split('#')[0],
block: swap.timestamp,
sender: swap.sender,
pair: `${ token0.symbol }/${ token1.symbol }`,
feeTier,
soldSymbol: sold.symbol,
soldAmount: formatAmount(soldIsToken0 ? swap.amount0 : swap.amount1, sold.decimals),
boughtSymbol: bought.symbol,
boughtAmount: formatAmount(soldIsToken0 ? swap.amount1 : swap.amount0, bought.decimals),
};
});
}
// Pure: headline figures, all read off the indexer rather than inferred.
export function computeAmmOverview(
factory: AmmFactory | null,
swaps: ReadonlyArray<AmmSwap>,
): AmmOverview {
return {
poolCount: factory?.poolCount ?? 0,
tvlUsd: factory?.totalValueLockedUSD ?? '0',
volumeUsd: factory?.totalVolumeUSD ?? '0',
latestBlock: swaps.length > 0 ? Math.max(...swaps.map((swap) => swap.timestamp)) : null,
};
}
async function fetchSwaps(): Promise<ReadonlyArray<AmmSwap> | null> {
const data = await fetchSubgraph<{ swaps: ReadonlyArray<AmmSwap> | null }>(AMM_GRAPHQL_PATH, SWAPS_QUERY);
return data ? data.swaps ?? [] : null;
}
async function fetchFactory(): Promise<AmmFactory | null> {
const data = await fetchSubgraph<{ factories: ReadonlyArray<AmmFactory> | null }>(AMM_GRAPHQL_PATH, FACTORY_QUERY);
return data?.factories?.[0] ?? null;
}
export interface UseAmmDataResult {
readonly swaps: ReadonlyArray<AmmSwapView>;
readonly overview: AmmOverview;
readonly status: VenueStatus;
readonly isLoading: boolean;
}
export function useAmmData(): UseAmmDataResult {
const swapsQuery = useQuery({
queryKey: [ 'amm:swaps', getApiBase() ],
queryFn: fetchSwaps,
refetchInterval: POLL_INTERVAL_MS,
});
const factoryQuery = useQuery({
queryKey: [ 'amm:factory', getApiBase() ],
queryFn: fetchFactory,
refetchInterval: POLL_INTERVAL_MS,
});
const headBlock = useHeadBlock();
const rawSwaps = swapsQuery.data ?? null;
const overview = React.useMemo(
() => computeAmmOverview(factoryQuery.data ?? null, rawSwaps ?? []),
[ factoryQuery.data, rawSwaps ],
);
const swaps = React.useMemo(() => buildSwapViews(rawSwaps ?? []), [ rawSwaps ]);
return {
swaps,
overview,
status: deriveVenueStatus(overview.latestBlock, headBlock, rawSwaps !== null),
isLoading: swapsQuery.isLoading || factoryQuery.isLoading,
};
}