Files
explore/lib/api/amm/useAmmData.spec.ts
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

121 lines
4.1 KiB
TypeScript

import type { AmmFactory, AmmSwap } from './types';
import { deriveVenueStatus } from 'lib/api/venueStatus';
import { expect, test, describe } from 'vitest';
import { buildSwapViews, computeAmmOverview } from './useAmmData';
// Real shapes returned by the mainnet cchain/amm subgraph — the WLUX/LZOO
// 0.30% pool at block 1,098,074. `timestamp` really is the block number.
const POOL = {
id: '0x1c000d5dbe1246fb84ad431e933e5563f212a62b',
feeTier: 3000,
token0: { id: '0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e', symbol: 'WLUX', decimals: 18 },
token1: { id: '0x5e5290f350352768bd2bfc59c2da15dd04a7cb88', symbol: 'LZOO', decimals: 18 },
} as const;
// Trader sold WLUX: the pool received amount0 and paid out amount1.
const SELL_TOKEN0: AmmSwap = {
id: '0xf1c526609ce9aaadc70ba77e4f27db9ea79a64ac35edbc96bf719a2956e7abfc#0x2',
timestamp: 1098074,
amount0: '100000000000000000000',
amount1: '-2168447796713672942095',
sender: '0x939bc0bca6f9b9c52e6e3ad8a3c590b5d9b9d10e',
pool: POOL,
};
// The mirror trade: the pool received LZOO and paid out WLUX.
const SELL_TOKEN1: AmmSwap = {
id: '0x7dece36d553a609f37a209d65eeb851513539de111ac935840149f4b577200fb#0x2',
timestamp: 1098073,
amount0: '-99400900225431259546',
amount1: '2168447806549312938443',
sender: '0x939bc0bca6f9b9c52e6e3ad8a3c590b5d9b9d10e',
pool: POOL,
};
const FACTORY: AmmFactory = {
poolCount: 32,
totalValueLockedUSD: '125992.93',
totalVolumeUSD: '2572.68',
};
describe('buildSwapViews', () => {
test('reads a positive amount0 as the trader selling token0', () => {
const [ view ] = buildSwapViews([ SELL_TOKEN0 ]);
expect(view.soldSymbol).toBe('WLUX');
expect(view.boughtSymbol).toBe('LZOO');
});
test('reads a negative amount0 as the trader buying token0', () => {
const [ view ] = buildSwapViews([ SELL_TOKEN1 ]);
expect(view.soldSymbol).toBe('LZOO');
expect(view.boughtSymbol).toBe('WLUX');
});
test('scales each amount by its own token decimals and drops the sign', () => {
const [ view ] = buildSwapViews([ SELL_TOKEN0 ]);
expect(view.soldAmount).toBe('100');
expect(view.boughtAmount).toBe('2,168.4478');
});
test('splits the subgraph key into a linkable tx hash', () => {
const [ view ] = buildSwapViews([ SELL_TOKEN0 ]);
expect(view.txHash).toBe('0xf1c526609ce9aaadc70ba77e4f27db9ea79a64ac35edbc96bf719a2956e7abfc');
expect(view.block).toBe(1098074);
});
test('labels the pair and keeps the fee tier', () => {
const [ view ] = buildSwapViews([ SELL_TOKEN0 ]);
expect(view.pair).toBe('WLUX/LZOO');
expect(view.feeTier).toBe(3000);
});
test('returns an empty list for no swaps (empty state)', () => {
expect(buildSwapViews([])).toHaveLength(0);
});
});
describe('computeAmmOverview', () => {
test('passes factory totals through untouched and takes the newest block', () => {
expect(computeAmmOverview(FACTORY, [ SELL_TOKEN0, SELL_TOKEN1 ])).toEqual({
poolCount: 32,
tvlUsd: '125992.93',
volumeUsd: '2572.68',
latestBlock: 1098074,
});
});
test('reports no latest block when nothing has traded', () => {
expect(computeAmmOverview(FACTORY, []).latestBlock).toBeNull();
});
test('is zeroed when the factory is unreadable', () => {
expect(computeAmmOverview(null, [])).toEqual({
poolCount: 0, tvlUsd: '0', volumeUsd: '0', latestBlock: null,
});
});
});
describe('deriveVenueStatus', () => {
test('an unreachable indexer is unavailable, never empty', () => {
expect(deriveVenueStatus(null, 1098100, false)).toBe('unavailable');
});
test('a reachable venue with no trades at all is empty', () => {
expect(deriveVenueStatus(null, 1098100, true)).toBe('empty');
});
test('a trade inside the recent window is live', () => {
expect(deriveVenueStatus(1098074, 1098100, true)).toBe('live');
});
test('a trade far behind the head is idle', () => {
expect(deriveVenueStatus(1000000, 1098100, true)).toBe('idle');
});
test('without a chain head, recency is unknown rather than guessed', () => {
expect(deriveVenueStatus(1098074, null, true)).toBe('unknown');
});
});