/** @module-tag localnet */ import { Abis, Addresses } from 'viem/tempo' import * as TestApp from '../../../../test/App.js' import * as Runtime from '../../../../test/runtime.js' import * as TestViem from '../../../../test/Viem.js' import * as FeeAmm from './fee-amm.js' /** Validator-side token with live fee-AMM activity on Tempo Moderato. */ const validatorToken = Runtime.get().mode === 'localnet' ? Addresses.pathUsd.toLowerCase() : '0x20c0000000000000000000000000000000000001' // Skipped: the pools listing runs a ClickHouse `GROUP BY` over every FeeManager // `Mint` log, which exceeds the live indexer's 5s execution cap on high-volume // chains (testnet) and returns `502`. Re-enable once the aggregate fits the // timeout (a cheaper query, or a per-query timeout in the `tidx.ts` client). describe.skip('GET /fee-amm/pools', () => { test('lists pools with on-chain poolId parity and consistent timestamps', async () => { const client = TestApp.client() const response = await client.v1['fee-amm'].pools.$get({ query: {} }, TestApp.auth) const body = await TestApp.json(response, FeeAmm.schema.getFeeAmmPools.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')?.includes('fee_amm_pools;dur=')).toBe(true) expect(body.data.length).toBeGreaterThan(0) // Paginated: the page carries `nextCursor` (null when no further page). expect('nextCursor' in body).toBe(true) expect('meta' in body).toBe(false) // Stable required shape; reserve amounts remain best-effort. const { userAmount: _userAmount, validatorAmount: _validatorAmount, ...poolShape } = body.data[0]! expect(Object.keys(poolShape).sort()).toMatchInlineSnapshot(` [ "createdAt", "id", "lastMintAt", "mintCount", "poolId", "userToken", "validatorToken", ] `) const validAmount = ( amount: | { baseUnits: string; currency: string; decimals: number; formatted: string } | undefined, ) => amount === undefined || (/^\d+$/.test(amount.baseUnits) && amount.currency.length > 0 && Number.isInteger(amount.decimals) && /^\d+(\.\d+)?$/.test(amount.formatted)) expect( body.data.every( (pool) => !Number.isNaN(Date.parse(pool.createdAt)) && !Number.isNaN(Date.parse(pool.lastMintAt)) && Date.parse(pool.createdAt) <= Date.parse(pool.lastMintAt) && pool.mintCount > 0 && pool.userToken.currency.length > 0 && pool.userToken.symbol.length > 0 && pool.validatorToken.currency.length > 0 && pool.validatorToken.symbol.length > 0 && validAmount(pool.userAmount) && validAmount(pool.validatorAmount), ), ).toBe(true) // Presentation order: most active first (by mint count). const mintCounts = body.data.map((pool) => pool.mintCount) expect(mintCounts.every((count, index) => index === 0 || mintCounts[index - 1]! >= count)).toBe( true, ) // Parity check: the locally computed poolId of the first pool matches the // on-chain `FeeManager.getPoolId`. const pool = body.data[0]! const chainPoolId = await TestViem.getClient().readContract({ abi: Abis.feeAmm, address: Addresses.feeManager, args: [pool.userToken.address, pool.validatorToken.address], functionName: 'getPoolId', }) expect(pool.poolId).toBe(chainPoolId.toLowerCase()) }) test('walks the cursor to a disjoint page, continuing mint-count order', async () => { const client = TestApp.client() const firstResponse = await client.v1['fee-amm'].pools.$get( { query: { limit: '5' } }, TestApp.auth, ) const first = await TestApp.json(firstResponse, FeeAmm.schema.getFeeAmmPools.Response) expect(firstResponse.status).toBe(200) expect(first.data.length).toBeGreaterThan(0) expect(first.data.length).toBeLessThanOrEqual(5) expect(first.nextCursor).toBeTypeOf('string') const secondResponse = await client.v1['fee-amm'].pools.$get( { query: { cursor: first.nextCursor!, limit: '5' } }, TestApp.auth, ) const second = await TestApp.json(secondResponse, FeeAmm.schema.getFeeAmmPools.Response) expect(secondResponse.status).toBe(200) expect(second.data.length).toBeGreaterThan(0) // No pool repeats across the page boundary (keyed by poolId). const firstIds = new Set(first.data.map((pool) => pool.poolId)) expect(second.data.every((pool) => !firstIds.has(pool.poolId))).toBe(true) // Mint-count order continues descending across the boundary. expect(second.data[0]!.mintCount).toBeLessThanOrEqual(first.data.at(-1)!.mintCount) expect( second.data.every( (pool, index) => index === 0 || second.data[index - 1]!.mintCount >= pool.mintCount, ), ).toBe(true) }) test('paginates positionally via page, disjoint from page 1', async () => { const client = TestApp.client() const pageOneResponse = await client.v1['fee-amm'].pools.$get( { query: { limit: '5' } }, TestApp.auth, ) const pageOne = await TestApp.json(pageOneResponse, FeeAmm.schema.getFeeAmmPools.Response) const pageTwoResponse = await client.v1['fee-amm'].pools.$get( { query: { limit: '5', page: '2' } }, TestApp.auth, ) const pageTwo = await TestApp.json(pageTwoResponse, FeeAmm.schema.getFeeAmmPools.Response) expect(pageTwoResponse.status).toBe(200) expect(pageTwo.data.length).toBeGreaterThan(0) // page=2 doesn't repeat page=1 and continues the mint-count ordering. const pageOneIds = new Set(pageOne.data.map((pool) => pool.poolId)) expect(pageTwo.data.every((pool) => !pageOneIds.has(pool.poolId))).toBe(true) expect(pageTwo.data[0]!.mintCount).toBeLessThanOrEqual(pageOne.data.at(-1)!.mintCount) }) test('returns core token metadata and opts into curated fields', async () => { const client = TestApp.client() const baseResponse = await client.v1['fee-amm'].pools.$get( { query: { limit: '5' } }, TestApp.auth, ) const base = await TestApp.json(baseResponse, FeeAmm.schema.getFeeAmmPools.Response) expect(baseResponse.status).toBe(200) expect(base.data.length).toBeGreaterThan(0) expect( base.data.every( (pool) => pool.userToken.symbol.length > 0 && Number.isInteger(pool.userToken.decimals) && pool.userToken.logoUri === undefined && pool.userToken.verified === undefined && pool.validatorToken.symbol.length > 0 && Number.isInteger(pool.validatorToken.decimals) && pool.validatorToken.logoUri === undefined && pool.validatorToken.verified === undefined, ), ).toBe(true) const includeResponse = await client.v1['fee-amm'].pools.$get( { query: { include: 'token.logoUri,token.verified', limit: '5' } }, TestApp.auth, ) const included = await TestApp.json(includeResponse, FeeAmm.schema.getFeeAmmPools.Response) expect(includeResponse.status).toBe(200) expect(included.data.map((pool) => pool.poolId)).toEqual(base.data.map((pool) => pool.poolId)) expect( included.data.every( (pool) => typeof pool.userToken.verified === 'boolean' && typeof pool.validatorToken.verified === 'boolean', ), ).toBe(true) }) test('falls back to the head page on a malformed cursor', async () => { const client = TestApp.client() const headResponse = await client.v1['fee-amm'].pools.$get( { query: { limit: '5' } }, TestApp.auth, ) const head = await TestApp.json(headResponse, FeeAmm.schema.getFeeAmmPools.Response) const malformedResponse = await client.v1['fee-amm'].pools.$get( { query: { cursor: 'not-a-real-cursor', limit: '5' } }, TestApp.auth, ) const malformed = await TestApp.json(malformedResponse, FeeAmm.schema.getFeeAmmPools.Response) expect(malformedResponse.status).toBe(200) expect(malformed.data.map((pool) => pool.poolId)).toEqual(head.data.map((pool) => pool.poolId)) }) }) describe('GET /fee-amm/pools query', () => { test('rejects the legacy token include', async () => { const response = await TestApp.create().request('/v1/fee-amm/pools?include=token', { headers: TestApp.auth.headers, }) expect(response.status).toBe(400) }) }) describe('GET /fee-amm/mints', () => { test('returns a decoded head page and walks the cursor to a disjoint page', async () => { const client = TestApp.client() const firstResponse = await client.v1['fee-amm'].mints.$get( { query: { limit: '5' } }, TestApp.auth, ) const first = await TestApp.json(firstResponse, FeeAmm.schema.getFeeAmmMints.Response) expect(firstResponse.status).toBe(200) expect(firstResponse.headers.get('server-timing')?.includes('fee_amm_mints;dur=')).toBe(true) expect(first.data.length).toBeGreaterThan(0) expect(first.data.length).toBeLessThanOrEqual(5) expect( first.data.every( (mint) => /^0x[0-9a-f]{40}$/.test(mint.minter) && /^0x[0-9a-f]{40}$/.test(mint.userToken.address) && /^0x[0-9a-f]{40}$/.test(mint.validatorToken.address) && /^\d+$/.test(mint.amountValidatorToken) && /^\d+$/.test(mint.liquidity) && Number.isInteger(mint.blockNumber) && /^0x[0-9a-f]{64}$/.test(mint.transactionHash) && !Number.isNaN(Date.parse(mint.timestamp)), ), ).toBe(true) // Newest-first by default. const blocks = first.data.map((mint) => mint.blockNumber) expect(blocks.every((block, index) => index === 0 || blocks[index - 1]! >= block)).toBe(true) // Timestamp parity: the head row's timestamp must match the block's // RPC timestamp exactly (guards the ClickHouse UTC normalization). const head = first.data[0]! const block = await TestViem.getClient().getBlock({ blockNumber: BigInt(head.blockNumber), }) expect(Date.parse(head.timestamp)).toBe(Number(block.timestamp) * 1000) expect(first.nextCursor).toBeTypeOf('string') const secondResponse = await client.v1['fee-amm'].mints.$get( { query: { cursor: first.nextCursor!, limit: '5' } }, TestApp.auth, ) const second = await TestApp.json(secondResponse, FeeAmm.schema.getFeeAmmMints.Response) expect(secondResponse.status).toBe(200) expect(second.data.length).toBeGreaterThan(0) // No overlap across the page boundary, and ordering continues descending. const firstIds = new Set(first.data.map(({ id }) => id)) expect(second.data.every(({ id }) => !firstIds.has(id))).toBe(true) expect(second.data[0]!.blockNumber).toBeLessThanOrEqual(first.data.at(-1)!.blockNumber) }) test('narrows mints to a single validator token', async () => { const client = TestApp.client() const response = await client.v1['fee-amm'].mints.$get( { query: { limit: '5', validatorToken } }, TestApp.auth, ) const body = await TestApp.json(response, FeeAmm.schema.getFeeAmmMints.Response) expect(response.status).toBe(200) expect(body.data.length).toBeGreaterThan(0) expect(body.data.every((mint) => mint.validatorToken.address === validatorToken)).toBe(true) }) test('orders ascending and decodes whichever signature appears', async () => { const client = TestApp.client() const response = await client.v1['fee-amm'].mints.$get( { query: { limit: '5', order: 'asc' } }, TestApp.auth, ) const body = await TestApp.json(response, FeeAmm.schema.getFeeAmmMints.Response) expect(response.status).toBe(200) expect(body.data.length).toBeGreaterThan(0) expect(body.data[0]!.blockNumber).toBeLessThanOrEqual(body.data.at(-1)!.blockNumber) // Both signatures decode to a complete row; the shapes differ only in // their signature-specific optional fields (legacy: `amountUserToken`; // current: `recipient`). expect( body.data.every( (mint) => /^0x[0-9a-f]{40}$/.test(mint.minter) && /^\d+$/.test(mint.amountValidatorToken) && /^\d+$/.test(mint.liquidity) && (mint.amountUserToken === undefined || /^\d+$/.test(mint.amountUserToken)) && (mint.recipient === undefined || /^0x[0-9a-f]{40}$/.test(mint.recipient)) && (mint.amountUserToken !== undefined || mint.recipient !== undefined), ), ).toBe(true) }) test('falls back to the head page on a malformed cursor', async () => { const client = TestApp.client() const response = await client.v1['fee-amm'].mints.$get( { query: { cursor: 'not-a-cursor', limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, FeeAmm.schema.getFeeAmmMints.Response) expect(response.status).toBe(200) expect(body.data.length).toBeGreaterThan(0) }) test('embeds an exact total count on demand via include=totalCount', async () => { const client = TestApp.client() const response = await client.v1['fee-amm'].mints.$get( { query: { include: 'totalCount', limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, FeeAmm.schema.getFeeAmmMints.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')?.includes('fee_amm_mints_count;dur=')).toBe(true) expect(body.meta).toBeDefined() expect(Number.isInteger(body.meta?.totalCount)).toBe(true) expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length) // The count is sort-key-pruned on ClickHouse, so it is exact (never capped). expect(body.meta!.totalCountCapped).toBe(false) // Without `include=totalCount`, the same page omits `meta` and the count. const bare = await client.v1['fee-amm'].mints.$get({ query: { limit: '5' } }, TestApp.auth) const bareBody = await TestApp.json(bare, FeeAmm.schema.getFeeAmmMints.Response) expect(bare.status).toBe(200) expect(bareBody.meta).toBeUndefined() }) })