import { Schema } from 'tapimo' import * as TestApp from '../../../../test/App.js' import * as Runtime from '../../../../test/runtime.js' import * as Tokens from './tokens.js' // Verified enrichment is store-backed now that the build-time bundle is gone, so // seed a shared in-memory verified store from the curated seeds file. Tests use // this wrapper instead of `TestApp.client` so the default app exposes the same // verified tokens the bundle used to provide; a test can still override // `verifiedTokens`. const runtime = Runtime.get() const db = TestApp.database() await TestApp.verifiedSeed(db, runtime.chainId) function app(options: TestApp.create.Options = {}) { return TestApp.client({ db, ...options }) } const verifiedTokenCount = 6 describe('OpenAPI', () => { test('publishes generator-ready token and balance contracts', async () => { const application = TestApp.create({ auth: false }) const spec = await (await application.request('/openapi.json')).json() const operations = { balance: spec.paths['/v1/addresses/{address}/balances/{token}'].get, balances: spec.paths['/v1/addresses/{address}/balances'].get, holders: spec.paths['/v1/tokens/{token}/holders'].get, token: spec.paths['/v1/tokens/{token}'].get, tokenBySymbol: spec.paths['/v1/tokens/{symbol}'].get, tokenLogo: spec.paths['/v1/tokens/{token}/logo'].get, tokenTransactions: spec.paths['/v1/tokens/{token}/transactions'].get, tokens: spec.paths['/v1/tokens'].get, } const summarizeError = ( response: (typeof operations)[keyof typeof operations]['responses'][number], ) => { const schema = response.$ref ?? response.content?.['application/json']?.schema if (!schema || typeof schema === 'string' || !('$ref' in schema)) return schema const name = schema.$ref.split('/').at(-1)! return { codes: spec.components.schemas[name].properties.error.properties.code.enum, schema, } } const summarize = (operation: (typeof operations)[keyof typeof operations]) => ({ errors: Object.fromEntries( [400, 401, 403, 404, 429, 500, 502] .filter((status) => operation.responses[status]) .map((status) => [status, summarizeError(operation.responses[status])]), ), operationId: operation.operationId, response: operation.responses[200].content['application/json']?.schema ?? Object.keys(operation.responses[200].content), }) expect({ components: [ 'Balance', 'BalanceList', 'BalanceListMeta', 'RpcTokenReference', 'Token', 'TokenHolder', 'TokenHolderList', 'TokenHolderListMeta', 'TokenList', 'TokenReference', 'TokenTransactionList', 'TokenTransactionListMeta', ].filter((name) => spec.components.schemas[name]), formats: { tokenCreatedAt: spec.components.schemas.Token.properties.createdAt.format, transferFirstAt: spec.components.schemas.Token.properties.transferStats.properties.firstAt.anyOf[0].format, transferLastAt: spec.components.schemas.Token.properties.transferStats.properties.lastAt.anyOf[0].format, }, models: { balanceList: spec.components.schemas.BalanceList.properties.data.items, balanceListMeta: spec.components.schemas.BalanceList.properties.meta, balanceToken: spec.components.schemas.Balance.properties.token, holderList: spec.components.schemas.TokenHolderList.properties.data.items, holderListMeta: spec.components.schemas.TokenHolderList.properties.meta, tokenList: spec.components.schemas.TokenList.properties.data.items, tokenTransactionList: (() => { const item = spec.components.schemas.TokenTransactionList.properties.data.items return item.$ref ?? { description: item.description, type: item.type } })(), tokenTransactionListMeta: spec.components.schemas.TokenTransactionList.properties.meta, }, operations: Object.fromEntries( Object.entries(operations).map(([name, operation]) => [name, summarize(operation)]), ), sharedErrorCodes: { authentication: spec.components.schemas.AuthenticationError.properties.error.properties.code.enum, forbidden: spec.components.schemas.ForbiddenError.properties.error.properties.code.enum, tokenNotFound: spec.components.schemas.TokenNotFoundError.properties.error.properties.code.enum, }, }).toMatchInlineSnapshot(` { "components": [ "Balance", "BalanceList", "BalanceListMeta", "RpcTokenReference", "Token", "TokenHolder", "TokenHolderList", "TokenHolderListMeta", "TokenList", "TokenReference", "TokenTransactionList", "TokenTransactionListMeta", ], "formats": { "tokenCreatedAt": "date-time", "transferFirstAt": "date-time", "transferLastAt": "date-time", }, "models": { "balanceList": { "$ref": "#/components/schemas/Balance", }, "balanceListMeta": { "$ref": "#/components/schemas/BalanceListMeta", }, "balanceToken": { "$ref": "#/components/schemas/TokenReference", }, "holderList": { "$ref": "#/components/schemas/TokenHolder", }, "holderListMeta": { "$ref": "#/components/schemas/TokenHolderListMeta", }, "tokenList": { "$ref": "#/components/schemas/Token", }, "tokenTransactionList": "#/components/schemas/Transaction", "tokenTransactionListMeta": { "$ref": "#/components/schemas/TokenTransactionListMeta", }, }, "operations": { "balance": { "errors": { "400": { "codes": [ "address_invalid", "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", ], "schema": { "$ref": "#/components/schemas/AddressInvalidOrApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidError", }, }, "401": { "codes": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "schema": { "$ref": "#/components/schemas/AuthenticationError", }, }, "403": { "codes": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "schema": { "$ref": "#/components/schemas/ForbiddenError", }, }, "404": { "codes": [ "token_not_found", ], "schema": { "$ref": "#/components/schemas/TokenNotFoundError", }, }, "429": "#/components/responses/RateLimited", "500": "#/components/responses/InternalError", "502": { "codes": [ "upstream_error", ], "schema": { "$ref": "#/components/schemas/UpstreamError", }, }, }, "operationId": "getAddressBalance", "response": { "$ref": "#/components/schemas/Balance", }, }, "balances": { "errors": { "400": { "codes": [ "address_invalid", "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", ], "schema": { "$ref": "#/components/schemas/AddressInvalidOrApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidError", }, }, "401": { "codes": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "schema": { "$ref": "#/components/schemas/AuthenticationError", }, }, "403": { "codes": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "schema": { "$ref": "#/components/schemas/ForbiddenError", }, }, "429": "#/components/responses/RateLimited", "500": "#/components/responses/InternalError", "502": { "codes": [ "upstream_error", ], "schema": { "$ref": "#/components/schemas/UpstreamError", }, }, }, "operationId": "getAddressBalances", "response": { "$ref": "#/components/schemas/BalanceList", }, }, "holders": { "errors": { "400": { "codes": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", "token_invalid", ], "schema": { "$ref": "#/components/schemas/ApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidOrTokenInvalidError", }, }, "401": { "codes": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "schema": { "$ref": "#/components/schemas/AuthenticationError", }, }, "403": { "codes": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "schema": { "$ref": "#/components/schemas/ForbiddenError", }, }, "429": "#/components/responses/RateLimited", "500": "#/components/responses/InternalError", "502": { "codes": [ "upstream_error", ], "schema": { "$ref": "#/components/schemas/UpstreamError", }, }, }, "operationId": "getTokenHolders", "response": { "$ref": "#/components/schemas/TokenHolderList", }, }, "token": { "errors": { "400": { "codes": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", "token_invalid", ], "schema": { "$ref": "#/components/schemas/ApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidOrTokenInvalidError", }, }, "401": { "codes": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "schema": { "$ref": "#/components/schemas/AuthenticationError", }, }, "403": { "codes": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "schema": { "$ref": "#/components/schemas/ForbiddenError", }, }, "404": { "codes": [ "token_not_found", ], "schema": { "$ref": "#/components/schemas/TokenNotFoundError", }, }, "429": "#/components/responses/RateLimited", "500": "#/components/responses/InternalError", "502": { "codes": [ "upstream_error", ], "schema": { "$ref": "#/components/schemas/UpstreamError", }, }, }, "operationId": "getToken", "response": { "$ref": "#/components/schemas/Token", }, }, "tokenBySymbol": { "errors": { "400": { "codes": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", "symbol_invalid", ], "schema": { "$ref": "#/components/schemas/ApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidOrSymbolInvalidError", }, }, "401": { "codes": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "schema": { "$ref": "#/components/schemas/AuthenticationError", }, }, "403": { "codes": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "schema": { "$ref": "#/components/schemas/ForbiddenError", }, }, "404": { "codes": [ "token_not_found", ], "schema": { "$ref": "#/components/schemas/TokenNotFoundError", }, }, "429": "#/components/responses/RateLimited", "500": "#/components/responses/InternalError", "502": { "codes": [ "upstream_error", ], "schema": { "$ref": "#/components/schemas/UpstreamError", }, }, }, "operationId": "getTokenBySymbol", "response": { "$ref": "#/components/schemas/Token", }, }, "tokenLogo": { "errors": { "400": { "codes": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", "token_invalid", ], "schema": { "$ref": "#/components/schemas/ApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidOrTokenInvalidError", }, }, "401": { "codes": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "schema": { "$ref": "#/components/schemas/AuthenticationError", }, }, "403": { "codes": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "schema": { "$ref": "#/components/schemas/ForbiddenError", }, }, "404": { "codes": [ "token_logo_not_found", ], "schema": { "$ref": "#/components/schemas/TokenLogoNotFoundError", }, }, "429": "#/components/responses/RateLimited", "500": "#/components/responses/InternalError", "502": { "codes": [ "upstream_error", ], "schema": { "$ref": "#/components/schemas/UpstreamError", }, }, }, "operationId": "getTokenLogo", "response": [ "image/*", ], }, "tokenTransactions": { "errors": { "400": { "codes": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", "token_invalid", ], "schema": { "$ref": "#/components/schemas/ApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidOrTokenInvalidError", }, }, "401": { "codes": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "schema": { "$ref": "#/components/schemas/AuthenticationError", }, }, "403": { "codes": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "schema": { "$ref": "#/components/schemas/ForbiddenError", }, }, "429": "#/components/responses/RateLimited", "500": "#/components/responses/InternalError", "502": { "codes": [ "upstream_error", ], "schema": { "$ref": "#/components/schemas/UpstreamError", }, }, }, "operationId": "getTokenTransactions", "response": { "$ref": "#/components/schemas/TokenTransactionList", }, }, "tokens": { "errors": { "400": { "codes": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", ], "schema": { "$ref": "#/components/schemas/ApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidError", }, }, "401": { "codes": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "schema": { "$ref": "#/components/schemas/AuthenticationError", }, }, "403": { "codes": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "schema": { "$ref": "#/components/schemas/ForbiddenError", }, }, "429": "#/components/responses/RateLimited", "500": "#/components/responses/InternalError", "502": { "codes": [ "upstream_error", ], "schema": { "$ref": "#/components/schemas/UpstreamError", }, }, }, "operationId": "getTokens", "response": { "$ref": "#/components/schemas/TokenList", }, }, }, "sharedErrorCodes": { "authentication": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "forbidden": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "tokenNotFound": [ "token_not_found", ], }, } `) }) test('uses coherent token responses and canonical asset URLs', async () => { const application = TestApp.create({ auth: false }) const spec = (await (await application.request('/openapi.json')).json()) as { paths: Record< string, { get?: { operationId?: string responses?: Record } } > } for (const operationId of ['getToken', 'getTokenBySymbol']) { const operation = Object.values(spec.paths).find( (path) => path.get?.operationId === operationId, ) expect(operation?.get?.responses?.['200']?.content?.['application/json']?.example).toEqual( Tokens.tokenExample, ) } const assetUrls = JSON.stringify(spec).match(/https:\/\/api\.tempo\.xyz\/assets\/[^"\\]+/g) expect(new Set(assetUrls)).toEqual(new Set([Tokens.tokenExample.logoUri])) }) }) describe('GET /tokens', () => { test.runIf(runtime.mode === 'testnet')( 'serves a bounded positional page via `page` (indexed mode)', async () => { const client = app() // Ascending creation order anchors the indexed feed at the first // `TokenCreated`, so positional pages are deterministic against the head. const headResponse = await client.v1.tokens.$get( { query: { limit: '10', order: 'asc' } }, TestApp.auth, ) const head = await TestApp.json(headResponse, Tokens.schema.getTokens.Response) expect(headResponse.status).toBe(200) expect(head.data.length).toBe(10) const offsetResponse = await client.v1.tokens.$get( { query: { limit: '5', order: 'asc', page: '2' } }, TestApp.auth, ) const page = await TestApp.json(offsetResponse, Tokens.schema.getTokens.Response) expect(offsetResponse.status).toBe(200) expect(page.data.map((token) => token.address)).toEqual( head.data.slice(5, 10).map((token) => token.address), ) // Offset pages still return a keyset cursor for deep traversal. expect(typeof page.nextCursor).toBe('string') }, ) test('returns an indexer-backed token page', async () => { const client = app() const response = await client.v1.tokens.$get({ query: { limit: '5' } }, TestApp.auth) const body = await TestApp.json(response, Tokens.schema.getTokens.Response) const timing = response.headers.get('server-timing') expect(response.status).toMatchInlineSnapshot(`200`) expect(timing?.includes('verified_tokens;dur=')).toMatchInlineSnapshot(`true`) expect(timing?.includes('tokens_indexed;dur=')).toMatchInlineSnapshot(`true`) expect(timing?.includes('request;dur=')).toMatchInlineSnapshot(`true`) // Holder counts are opt-in: the base page must not run the indexer query. expect(timing?.includes('tokens_holders;dur=')).toMatchInlineSnapshot(`false`) expect(body.data.every((token) => token.holderCount === undefined)).toMatchInlineSnapshot( `true`, ) expect(body.data.length).toBeGreaterThan(0) expect(body.data.length).toBeLessThanOrEqual(5) // Cursor (keyset) pagination: the head page has no `page`, but carries an // opaque `nextCursor` (a string when more rows follow, else null). expect('page' in body).toBe(false) expect(body.nextCursor === null || typeof body.nextCursor === 'string').toBe(true) expect( body.data.every((token) => /^0x[0-9a-f]{40}$/.test(token.address)), ).toMatchInlineSnapshot(`true`) expect(body.data.every((token) => token.decimals === 6)).toMatchInlineSnapshot(`true`) expect(body.data.every((token) => typeof token.currency === 'string')).toMatchInlineSnapshot( `true`, ) // `createdAt` is opt-in: absent on the base page. expect(body.data.every((token) => token.createdAt === undefined)).toMatchInlineSnapshot(`true`) // With `include=createdAt`, every row carries a valid ISO timestamp. const withCreatedAt = await client.v1.tokens.$get( { query: { include: 'createdAt', limit: '5' } }, TestApp.auth, ) const withCreatedAtBody = await TestApp.json(withCreatedAt, Tokens.schema.getTokens.Response) expect( withCreatedAtBody.data.every( (token) => typeof token.createdAt === 'string' && !Number.isNaN(Date.parse(token.createdAt)), ), ).toMatchInlineSnapshot(`true`) }) test('includes batched holder counts for a token page when requested', async () => { const client = app() const response = await client.v1.tokens.$get( { query: { include: 'holderCount', limit: '10' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokens.Response) const timing = response.headers.get('server-timing') expect(response.status).toBe(200) expect(timing?.includes('tokens_holders;dur=')).toBe(true) // Tokens with no holders are omitted from the batched result, so each item // either carries a non-negative integer count or no count at all. expect( body.data.every( (token) => token.holderCount === undefined || Number.isInteger(token.holderCount), ), ).toBe(true) }) test.runIf(runtime.mode === 'testnet')( 'walks the cursor to a disjoint next token page', async () => { const client = app() const first = await client.v1.tokens.$get({ query: { limit: '5' } }, TestApp.auth) const firstBody = await TestApp.json(first, Tokens.schema.getTokens.Response) expect(first.status).toMatchInlineSnapshot(`200`) expect(typeof firstBody.nextCursor).toMatchInlineSnapshot(`"string"`) const second = await client.v1.tokens.$get( { query: { limit: '5', cursor: firstBody.nextCursor! } }, TestApp.auth, ) const secondBody = await TestApp.json(second, Tokens.schema.getTokens.Response) expect(second.status).toMatchInlineSnapshot(`200`) expect(secondBody.data.length).toMatchInlineSnapshot(`5`) expect( secondBody.data.every((token) => /^0x[0-9a-f]{40}$/.test(token.address)), ).toMatchInlineSnapshot(`true`) // Walking the cursor must yield a disjoint next page (no offset-style overlap). const firstAddresses = new Set(firstBody.data.map((token) => token.address)) expect( secondBody.data.every((token) => !firstAddresses.has(token.address)), ).toMatchInlineSnapshot(`true`) }, ) test('narrows tokens by currency (case-insensitive)', async () => { const client = app() // Lowercase input must match the on-chain `USD` casing. const response = await client.v1.tokens.$get( { query: { currency: 'usd', limit: '20' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokens.Response) expect(response.status).toBe(200) expect(body.data.length).toBeGreaterThan(0) for (const token of body.data) expect(token.currency.toLowerCase()).toBe('usd') }) test('falls back to the head page for a malformed cursor', async () => { const client = app() // An opaque cursor is decode-validated; a forged/garbage value degrades to // the head page rather than erroring (or reaching SQL). const response = await client.v1.tokens.$get( { query: { limit: '5', cursor: 'not-a-real-cursor' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokens.Response) expect(response.status).toMatchInlineSnapshot(`200`) expect(body.data.length).toBeGreaterThan(0) expect(body.data.length).toBeLessThanOrEqual(5) }) }) describe('GET /tokens?verified=true', () => { test('returns the curated verified token list in the same shape as a normal page', async () => { const client = app() const response = await client.v1.tokens.$get({ query: { verified: 'true' } }, TestApp.auth) const body = await TestApp.json(response, Tokens.schema.getTokens.Response) const timing = response.headers.get('server-timing') expect(response.status).toMatchInlineSnapshot(`200`) // Verified mode is served from the static list, so the indexer token // listing query does not fire (`createdAt` is opt-in via `include` and so // is not fetched on this base page). expect(timing?.includes('verified_tokens;dur=')).toMatchInlineSnapshot(`true`) expect(timing?.includes('tokens_indexed;dur=')).toMatchInlineSnapshot(`false`) expect(body.data.length).toBeGreaterThan(0) expect(body.data.every((token) => token.verified === true)).toBe(true) // Logos ride along on the verified page (resolved from the icon asset // store), so at least one curated token exposes a `logoUri`. if (runtime.mode === 'testnet') expect(body.data.some((token) => typeof token.logoUri === 'string')).toBe(true) // Same pagination envelope as the unfiltered page so clients don't need a // separate response shape. expect(body.nextCursor).toBeNull() }) test('narrows verified tokens by currency (case-insensitive)', async () => { const client = app() const response = await client.v1.tokens.$get( { query: { verified: 'true', currency: 'usd' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokens.Response) expect(response.status).toBe(200) expect(body.data.length).toBeGreaterThan(0) for (const token of body.data) { expect(token.currency.toLowerCase()).toBe('usd') expect(token.verified).toBe(true) } }) test('returns verified tokens even when no asset loader is configured', async () => { const client = app({ assetsPath: false }) const response = await client.v1.tokens.$get({ query: { verified: 'true' } }, TestApp.auth) const body = await TestApp.json(response, Tokens.schema.getTokens.Response) expect(response.status).toMatchInlineSnapshot(`200`) expect(body.data.length).toBe(verifiedTokenCount) expect(body.data.every((token) => token.verified === true)).toMatchInlineSnapshot(`true`) }) test('paginates the verified list via limit + cursor', async () => { const client = app({ assetsPath: false }) // Full list for comparison (6 verified tokens on the test chain). const full = await TestApp.json( await client.v1.tokens.$get({ query: { verified: 'true' } }, TestApp.auth), Tokens.schema.getTokens.Response, ) // First page: a bounded slice with a cursor to the next page (`minLimit` is // 5, and the test chain has 6 verified tokens, so a page of 5 leaves 1). const first = await TestApp.json( await client.v1.tokens.$get({ query: { verified: 'true', limit: '5' } }, TestApp.auth), Tokens.schema.getTokens.Response, ) expect(first.data.length).toBe(5) expect(first.nextCursor).toBeTypeOf('string') // Following the cursor returns the remainder with no further pages. const second = await TestApp.json( await client.v1.tokens.$get( { query: { verified: 'true', limit: '5', cursor: first.nextCursor! } }, TestApp.auth, ), Tokens.schema.getTokens.Response, ) expect(second.data.length).toBe(1) expect(second.nextCursor).toBeNull() // The two pages reconstruct the full list in order, with no overlap. expect([...first.data, ...second.data].map((token) => token.address)).toEqual( full.data.map((token) => token.address), ) }) test('reverses the verified list with order=asc', async () => { const client = app({ assetsPath: false }) const desc = await TestApp.json( await client.v1.tokens.$get({ query: { verified: 'true' } }, TestApp.auth), Tokens.schema.getTokens.Response, ) const asc = await TestApp.json( await client.v1.tokens.$get({ query: { verified: 'true', order: 'asc' } }, TestApp.auth), Tokens.schema.getTokens.Response, ) expect(asc.data.map((token) => token.address)).toEqual( [...desc.data].reverse().map((token) => token.address), ) }) }) describe('GET /tokens?addresses=', () => { test('separates repeated addresses in the response cache', async () => { const routed = TestApp.create({ db }) const repeated = new URLSearchParams() repeated.append('addresses', TestApp.token) repeated.append('addresses', TestApp.tokenWithHolders) const single = new URLSearchParams({ addresses: TestApp.tokenWithHolders, }) const primed = await routed.request(`/v1/tokens?${repeated}`) const miss = await routed.request(`/v1/tokens?${single}`) const hit = await routed.request(`/v1/tokens?${single}`) const primedBody = await TestApp.json(primed, Tokens.schema.getTokens.Response) const body = await TestApp.json(hit, Tokens.schema.getTokens.Response) expect(primed.status).toBe(200) expect(miss.status).toBe(200) expect(hit.status).toBe(200) expect(primedBody.data.map((token) => token.address)).toEqual([ TestApp.token, TestApp.tokenWithHolders, ]) expect(miss.headers.has('RateLimit-Limit')).toBe(true) expect(hit.headers.has('RateLimit-Limit')).toBe(false) expect(body.data.map((token) => token.address)).toEqual([TestApp.tokenWithHolders]) }) test('separates repeated includes in the response cache', async () => { const routed = TestApp.create({ db }) const repeated = new URLSearchParams({ limit: '5' }) repeated.append('include', 'createdAt') repeated.append('include', 'holderCount') const single = new URLSearchParams({ include: 'holderCount', limit: '5', }) const primed = await routed.request(`/v1/tokens?${repeated}`) const miss = await routed.request(`/v1/tokens?${single}`) const hit = await routed.request(`/v1/tokens?${single}`) const primedBody = await TestApp.json(primed, Tokens.schema.getTokens.Response) const body = await TestApp.json(hit, Tokens.schema.getTokens.Response) expect(primed.status).toBe(200) expect(miss.status).toBe(200) expect(hit.status).toBe(200) expect(primedBody.data.every((token) => typeof token.createdAt === 'string')).toBe(true) expect(miss.headers.has('RateLimit-Limit')).toBe(true) expect(hit.headers.has('RateLimit-Limit')).toBe(false) expect(body.data.every((token) => token.createdAt === undefined)).toBe(true) }) test('returns the requested tokens as a single page in input order', async () => { const client = app() const response = await client.v1.tokens.$get( { query: { addresses: `${TestApp.token},${TestApp.tokenWithHolders}` } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokens.Response) expect(response.status).toMatchInlineSnapshot(`200`) expect(body.data.map((token) => token.address)).toEqual([ TestApp.token, TestApp.tokenWithHolders, ]) // The addressed page is degenerate: a single page with no cursor. expect(body.nextCursor).toBeNull() // Rows resolve through `resolveToken` (RPC metadata), so each carries the // detail shape, including a live `totalSupply`. expect(body.data.every((token) => /^\d+$/.test(token.totalSupply!))).toBe(true) }) test('carries batched holder counts and drops unresolvable addresses', async () => { const client = app() // Well-formed but nonexistent TIP-20 address: resolution fails upstream // and the row is dropped rather than failing the page. const missing = '0x20c0000000000000000000000000000000099999' const response = await client.v1.tokens.$get( { query: { addresses: `${TestApp.token},${missing},${TestApp.tokenWithHolders}`, include: 'holderCount', }, }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokens.Response) const timing = response.headers.get('server-timing') expect(response.status).toBe(200) // One batched holder-count query serves the whole page. expect(timing?.includes('tokens_holders;dur=')).toBe(true) expect(body.data.map((token) => token.address)).toEqual([ TestApp.token, TestApp.tokenWithHolders, ]) // pathUSD is the busiest token on testnet, so its count is always // positive. (The fixture token currently has no positive-balance holders, // so its count is legitimately absent.) const pathUsd = body.data.find((token) => token.address === TestApp.tokenWithHolders) if (runtime.mode === 'testnet') expect(pathUsd?.holderCount).toBeGreaterThan(0) }) test('rejects more than 50 addresses', async () => { const client = app() const addresses = Array.from( { length: 51 }, (_, index) => `0x20c${'0'.repeat(32)}${10000 + index}`, ).join(',') const response = await client.v1.tokens.$get({ query: { addresses } }, TestApp.auth) const body = await TestApp.json(response, Schema.ErrorResponse) const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`400`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "query_invalid", "details": [ { "message": "Too big: expected array to have <=50 items", "path": [ "addresses", ], }, ], "message": "Invalid query parameters", }, } `) }) }) describe('GET /tokens/:token', () => { test('returns token metadata for a small token', async () => { const client = app() const response = await client.v1.tokens[':token'].$get( { param: { token: TestApp.token }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) const timing = response.headers.get('server-timing') const { totalSupply, ...stable } = body expect(response.status).toMatchInlineSnapshot(`200`) expect(timing?.includes('token_metadata;dur=')).toMatchInlineSnapshot(`true`) expect(timing?.includes('verified_tokens;dur=')).toMatchInlineSnapshot(`true`) expect(timing?.includes('token_logo;dur=')).toMatchInlineSnapshot(`true`) expect(timing?.includes('token;dur=')).toMatchInlineSnapshot(`true`) expect(timing?.includes('request;dur=')).toMatchInlineSnapshot(`true`) // Holder counts are opt-in: the base request must not run the indexer query. expect(timing?.includes('token_holders;dur=')).toMatchInlineSnapshot(`false`) expect(body.holderCount).toMatchInlineSnapshot(`undefined`) expect(/^\d+$/.test(totalSupply!)).toMatchInlineSnapshot(`true`) if (runtime.mode === 'testnet') expect(stable).toMatchInlineSnapshot(` { "address": "0x20c0000000000000000000008f5425160ebe5525", "currency": "USD", "decimals": 6, "id": "0x20c0000000000000000000008f5425160ebe5525", "name": "Alpha Land", "symbol": "ALD", "verified": false, } `) else { expect(stable.address).toBe(TestApp.token) expect(stable.currency).toBe('USD') expect(stable.decimals).toBe(6) expect(stable.verified).toBe(true) } }) test('rejects an unsupported chain id', async () => { // `chainId` now accepts any positive integer syntactically, so an unknown // chain (`1`) is rejected by the App-level supported-chain guard rather than // schema validation; exercise it via `app.request`. const response = await TestApp.create().request(`/v1/tokens/${TestApp.token}?chainId=1`, { headers: { authorization: `Bearer ${TestApp.key.token}` }, }) const body = await TestApp.json(response, Schema.ErrorResponse) const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`400`) expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable.error.code).toBe('chain_id_unsupported') expect(stable.error.message).toContain('Unsupported chain id: 1.') }) test.runIf(runtime.mode === 'testnet')( 'returns static logo URI when token icon exists', async () => { const client = app() const response = await client.v1.tokens[':token'].$get( { param: { token: TestApp.tokenWithLogo }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) const { totalSupply, ...stable } = body expect(response.status).toMatchInlineSnapshot(`200`) expect(/^\d+$/.test(totalSupply!)).toMatchInlineSnapshot(`true`) expect(stable).toMatchInlineSnapshot(` { "address": "0x20c0000000000000000000009e8d7eb59b783726", "currency": "USDC", "decimals": 6, "id": "0x20c0000000000000000000009e8d7eb59b783726", "logoUri": "http://localhost/assets/42431/icons/0x20c0000000000000000000009e8d7eb59b783726", "name": "Bridged USDC (Stargate)", "symbol": "USDC.e", "verified": true, } `) }, ) test('includes the holder count when requested via include', async () => { const client = app() const response = await client.v1.tokens[':token'].$get( { param: { token: TestApp.tokenWithHolders }, query: { include: 'holderCount' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) const timing = response.headers.get('server-timing') expect(response.status).toBe(200) expect(timing?.includes('token_holders;dur=')).toBe(true) if (runtime.mode === 'testnet') { expect(Number.isInteger(body.holderCount)).toBe(true) expect(body.holderCount).toBeGreaterThan(0) } }) test.runIf(runtime.mode === 'testnet')( 'includes TokenCreated admin and quote token when requested via include', async () => { const client = app() const response = await client.v1.tokens[':token'].$get( { param: { token: TestApp.token }, query: { include: 'admin,quoteToken' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) expect(response.status).toMatchInlineSnapshot(`200`) expect(body.admin).toMatchInlineSnapshot(`"0xb2f15e10adea883417de870aa99912cc98b358c7"`) expect(body.quoteToken).toMatchInlineSnapshot(`"0x20c0000000000000000000000000000000000000"`) }, ) test.runIf(runtime.mode === 'testnet')( 'accepts repeated include parameters alongside the comma form', async () => { const client = app() // `?include=admin&include=quoteToken` — each repeat arrives at the // validator as one array entry; equivalent to `include=admin,quoteToken`. const response = await client.v1.tokens[':token'].$get( { param: { token: TestApp.token }, query: { include: ['admin', 'quoteToken'] } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) expect(response.status).toMatchInlineSnapshot(`200`) expect(body.admin).toMatchInlineSnapshot(`"0xb2f15e10adea883417de870aa99912cc98b358c7"`) expect(body.quoteToken).toMatchInlineSnapshot(`"0x20c0000000000000000000000000000000000000"`) }, ) test.runIf(runtime.mode === 'testnet')( 'omits admin when only quoteToken is requested', async () => { const client = app() const response = await client.v1.tokens[':token'].$get( { param: { token: TestApp.token }, query: { include: 'quoteToken' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) expect(response.status).toBe(200) // `admin` and `quoteToken` ride one `TokenCreated` fetch but surface // independently: asking for one must not leak the other. expect(body.quoteToken).toMatchInlineSnapshot(`"0x20c0000000000000000000000000000000000000"`) expect('admin' in body).toBe(false) }, ) test('includes lifetime transfer statistics when requested via include', async () => { const client = app() const response = await client.v1.tokens[':token'].$get( { param: { token: TestApp.token }, query: { include: 'transferStats' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) expect(response.status).toBe(200) // The transfer count grows with chain activity; assert shape, not value. expect(body.transferStats?.count).toBeGreaterThan(0) // Timezone-correctness guard: ClickHouse returns naive-UTC timestamps // (`2026-05-25 22:35:11.000`); a local-time misparse would shift this // immutable first-transfer instant by the host's UTC offset. if (runtime.mode === 'testnet') expect(body.transferStats?.firstAt).toMatchInlineSnapshot(`"2026-05-25T22:35:11.000Z"`) else expect(typeof body.transferStats?.firstAt).toBe('string') expect(Date.parse(body.transferStats!.firstAt!)).toBeLessThanOrEqual( Date.parse(body.transferStats!.lastAt!), ) }) test('omits TokenCreated extras and transfer stats without include', async () => { const client = app() const response = await client.v1.tokens[':token'].$get( { param: { token: TestApp.token }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) expect(response.status).toBe(200) expect('admin' in body).toBe(false) expect('quoteToken' in body).toBe(false) expect('transferStats' in body).toBe(false) }) test('returns 200 without TokenCreated fields for a genesis token', async () => { const client = app() // pathUSD is a genesis token: it has no `TokenCreated` log, so the // included fields resolve as absent rather than erroring. const response = await client.v1.tokens[':token'].$get( { param: { token: TestApp.tokenWithHolders }, query: { include: 'admin,quoteToken,createdAt' }, }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) expect(response.status).toMatchInlineSnapshot(`200`) expect('admin' in body).toBe(false) expect('quoteToken' in body).toBe(false) expect('createdAt' in body).toBe(false) }) }) describe('GET /tokens/:token/logo', () => { test.runIf(runtime.mode === 'testnet')( 'serves the curated R2 icon bytes when present', async () => { const client = app() const response = await client.v1.tokens[':token'].logo.$get( { param: { token: TestApp.tokenWithLogo }, query: {} }, TestApp.auth, ) expect(response.status).toBe(200) expect(response.headers.get('content-type')).toBe('image/svg+xml') expect(response.headers.get('cache-control')).toBe( 'public, max-age=86400, stale-while-revalidate=604800', ) expect(response.headers.get('vary')).toBe('Accept-Encoding') const body = await response.text() expect(body.includes(' { const client = app({ assetsPath: false }) const response = await client.v1.tokens[':token'].logo.$get( { param: { token: TestApp.token }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Schema.ErrorResponse) expect(response.status).toBe(404) expect(body.error.code).toBe('token_logo_not_found') }) test('returns 404 when the on-chain metadata read reverts for an unregistered token', async () => { const client = app() const response = await client.v1.tokens[':token'].logo.$get( // A syntactically valid address that is not a registered TIP-20: the // `getMetadata` read reverts, which must surface as a `404`, not a `502`. { param: { token: '0xc97613eb1c39b0b57267739b5400ab08f8c4c285' }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Schema.ErrorResponse) expect(response.status).toBe(404) expect(body.error.code).toBe('token_logo_not_found') }) test('rejects an invalid token address', async () => { const response = await TestApp.create().request('/v1/tokens/not-an-address/logo', { headers: { authorization: `Bearer ${TestApp.key.token}` }, }) const body = await TestApp.json(response, Schema.ErrorResponse) expect(response.status).toBe(400) expect(body.error.code).toBe('token_invalid') }) }) describe('GET /tokens/:token/holders', () => { test('returns a balance-ranked holder page', async () => { const client = app() const response = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.tokenWithHolders }, query: { limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokenHolders.Response) const timing = response.headers.get('server-timing') expect(response.status).toBe(200) expect(timing?.includes('token_holders;dur=')).toBe(true) expect(Array.isArray(body.data)).toBe(true) expect(body.data.length).toBeLessThanOrEqual(5) // Pagination fields stay at the response root; cursor pagination has no // `page`, just an opaque `nextCursor`. expect('page' in body).toBe(false) expect(body.nextCursor === null || typeof body.nextCursor === 'string').toBe(true) expect( body.data.every( (holder) => /^0x[0-9a-f]{40}$/.test(holder.address) && /^\d+$/.test(holder.balance), ), ).toBe(true) // Holders are returned largest-first. const balances = body.data.map((holder) => BigInt(holder.balance)) expect(balances.every((balance, index) => index === 0 || balances[index - 1]! >= balance)).toBe( true, ) // Token metadata is opt-in: the base page must not embed `meta.token`. expect(body.meta).toBeUndefined() }) test('embeds the exact holder total on demand via include=totalCount', async () => { const client = app() const response = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.tokenWithHolders }, query: { include: 'totalCount', limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokenHolders.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')?.includes('token_holders_count;dur=')).toBe(true) if (runtime.mode === 'testnet') { expect(body.meta?.totalCount).toBeDefined() expect(Number.isInteger(body.meta?.totalCount)).toBe(true) expect(body.meta!.totalCount!).toBeGreaterThanOrEqual(body.data.length) // The holder total comes from the pre-aggregated view, so it is exact. expect(body.meta?.totalCountCapped).toBe(false) } // Without the include, `meta` is omitted and no count query runs. const bareResponse = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.tokenWithHolders }, query: { limit: '5' } }, TestApp.auth, ) const bare = await TestApp.json(bareResponse, Tokens.schema.getTokenHolders.Response) expect(bareResponse.status).toBe(200) expect(bare.meta).toBeUndefined() expect(bareResponse.headers.get('server-timing')?.includes('token_holders_count;dur=')).toBe( false, ) }) test('serves a bounded positional holder page via `page`', async () => { const client = app() const headResponse = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.tokenWithHolders }, query: { limit: '10' } }, TestApp.auth, ) const head = await TestApp.json(headResponse, Tokens.schema.getTokenHolders.Response) expect(headResponse.status).toBe(200) if (head.data.length < 10) return const offsetResponse = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.tokenWithHolders }, query: { limit: '5', page: '2' } }, TestApp.auth, ) const page = await TestApp.json(offsetResponse, Tokens.schema.getTokenHolders.Response) expect(offsetResponse.status).toBe(200) // The balance ranking is stable within the test window, so the offset page // matches rows 5..9 of the head page (best-effort, as with cursor pages on // a mutable ranking). expect(page.data.map((holder) => holder.address)).toEqual( head.data.slice(5, 10).map((holder) => holder.address), ) }) test('walks the cursor to a disjoint next holder page', async () => { const client = app() const first = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.tokenWithHolders }, query: { limit: '5' } }, TestApp.auth, ) const firstBody = await TestApp.json(first, Tokens.schema.getTokenHolders.Response) expect(first.status).toBe(200) if (!firstBody.nextCursor) return const second = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.tokenWithHolders }, query: { limit: '5', cursor: firstBody.nextCursor }, }, TestApp.auth, ) const secondBody = await TestApp.json(second, Tokens.schema.getTokenHolders.Response) expect(second.status).toBe(200) expect(secondBody.data.length).toBeGreaterThan(0) // Walking the cursor must yield a disjoint next page (the mixed-direction // ranking keyset, exercised end-to-end against ClickHouse). const firstAddresses = new Set(firstBody.data.map((holder) => holder.address)) expect(secondBody.data.every((holder) => !firstAddresses.has(holder.address))).toBe(true) }) test('embeds token metadata when requested via include=token', async () => { const client = app() const response = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.tokenWithHolders }, query: { limit: '5', include: 'token' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokenHolders.Response) const timing = response.headers.get('server-timing') expect(response.status).toBe(200) expect(Array.isArray(body.data)).toBe(true) // The embedded token resolves alongside the holder page. expect(timing?.includes('token;dur=')).toBe(true) expect(body.meta?.token?.address).toBe(TestApp.tokenWithHolders) expect(typeof body.meta?.token?.symbol).toBe('string') expect(Number.isInteger(body.meta?.token?.decimals)).toBe(true) expect(typeof body.meta?.token?.verified).toBe('boolean') // The embed is a trimmed reference: supply/holder/created fields are omitted. expect(body.meta?.token).not.toHaveProperty('totalSupply') expect(body.meta?.token).not.toHaveProperty('holderCount') expect(body.meta?.token).not.toHaveProperty('createdAt') }) test('surfaces a 502 when holder data is unavailable', async () => { // Point at an unreachable indexer so the upstream query fails deterministically. const client = app({ tidx: { baseUrl: 'http://127.0.0.1:1' } }) const response = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.token }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Schema.ErrorResponse) // The upstream message varies by failure; only the envelope is stable. const { requestId, error } = body const { message, ...stableError } = error // Unlike the opt-in holder count (optional enrichment that degrades to // null), this dedicated endpoint reports upstream failure honestly rather // than masquerading as a token with zero holders. expect(response.status).toMatchInlineSnapshot(`502`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(typeof message).toMatchInlineSnapshot(`"string"`) expect(stableError).toMatchInlineSnapshot(` { "code": "upstream_error", } `) }) test('rejects invalid token addresses', async () => { // The typed client requires `token: `Hex.Hex`` so an invalid literal // cannot pass through it; exercise the validator via `app.request`. const response = await TestApp.create().request('/v1/tokens/not-an-address/holders', { headers: { authorization: `Bearer ${TestApp.key.token}` }, }) const body = await TestApp.json(response, Schema.ErrorResponse) const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`400`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "token_invalid", "details": [ { "message": "Invalid input", "path": [ "token", ], }, ], "message": "Invalid token address", }, } `) }) test('rejects invalid pagination', async () => { const client = app() const response = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.token }, query: { limit: '0' } }, TestApp.auth, ) const body = await TestApp.json(response, Schema.ErrorResponse) const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`400`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "query_invalid", "details": [ { "message": "Too small: expected number to be >=5", "path": [ "limit", ], }, ], "message": "Invalid query parameters", }, } `) }) }) describe('GET /tokens/:symbol', () => { // Hono encodes the regex-constrained `/:symbol{}` route into the // typed client under a literal path key that includes the full pattern. test.runIf(runtime.mode === 'testnet')( 'returns verified token metadata with live supply by symbol', async () => { const client = app() const response = await client.v1.tokens[':symbol{[A-Za-z][A-Za-z0-9._]{0,63}}'].$get( { param: { symbol: 'USDC.e' }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) const { totalSupply, ...stable } = body expect(response.status).toMatchInlineSnapshot(`200`) expect(/^\d+$/.test(totalSupply!)).toMatchInlineSnapshot(`true`) expect(stable).toMatchInlineSnapshot(` { "address": "0x20c0000000000000000000009e8d7eb59b783726", "currency": "USDC", "decimals": 6, "id": "0x20c0000000000000000000009e8d7eb59b783726", "logoUri": "http://localhost/assets/42431/icons/0x20c0000000000000000000009e8d7eb59b783726", "name": "Bridged USDC (Stargate)", "symbol": "USDC.e", "verified": true, } `) }, ) test('does not resolve unverified token symbols', async () => { const client = app() const response = await client.v1.tokens[':symbol{[A-Za-z][A-Za-z0-9._]{0,63}}'].$get( { param: { symbol: 'ALD' }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Schema.ErrorResponse) const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`404`) expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "token_not_found", "message": "Token not found", }, } `) }) test.runIf(runtime.mode === 'testnet')( 'resolves verified symbols without a logo when no asset loader is configured', async () => { const client = app({ assetsPath: false }) const response = await client.v1.tokens[':symbol{[A-Za-z][A-Za-z0-9._]{0,63}}'].$get( { param: { symbol: 'USDC.e' }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getToken.Response) const { totalSupply, ...stable } = body expect(response.status).toMatchInlineSnapshot(`200`) expect(/^\d+$/.test(totalSupply!)).toMatchInlineSnapshot(`true`) expect(stable).toMatchInlineSnapshot(` { "address": "0x20c0000000000000000000009e8d7eb59b783726", "currency": "USDC", "decimals": 6, "id": "0x20c0000000000000000000009e8d7eb59b783726", "name": "Bridged USDC (Stargate)", "symbol": "USDC.e", "verified": true, } `) }, ) }) describe('GET /tokens/:token/transactions', () => { test('parses granular fee-token includes', () => { expect( Tokens.schema.getTokenTransactions.Query.parse({ include: 'feeToken.logoUri,feeToken.verified,token,totalCount', }).include, ).toMatchInlineSnapshot(` [ "feeToken.logoUri", "feeToken.verified", "token", "totalCount", ] `) expect( Tokens.schema.getTokenTransactions.Query.safeParse({ include: 'feeToken' }).success, ).toBe(false) expect( Tokens.schema.getTokenTransactions.Query.safeParse({ include: 'token.logoUri' }).success, ).toBe(false) }) test('returns a transaction page touching the token contract', async () => { const client = app() const response = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response) expect(response.status).toBe(200) expect(Array.isArray(body.data)).toBe(true) expect(body.data.length).toBeLessThanOrEqual(5) expect(body.nextCursor === null || typeof body.nextCursor === 'string').toBe(true) // Every transaction either originated from or was sent to the token contract. for (const tx of body.data) { const involves = tx.sender === TestApp.token || tx.recipient === TestApp.token expect(involves).toBe(true) if (tx.feeToken) { expect(tx.feeToken.address).toBe(tx.meta.rpc.feeToken) expect(typeof tx.feeToken.symbol).toBe('string') expect(tx.feeToken.logoUri).toBeUndefined() expect(tx.feeToken.verified).toBeUndefined() } } // Default ordering is newest-first (descending block then in-block index). const blocks = body.data .map((entry) => entry.blockNumber) .filter((b): b is number => b !== null) expect(blocks.every((block, index) => index === 0 || blocks[index - 1]! >= block)).toBe(true) // Token metadata is opt-in: the base page must not embed it. expect(body.meta).toBeUndefined() }) test('includes requested fee-token fields', async () => { const client = app() const response = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { include: 'feeToken.logoUri,feeToken.verified', limit: '5' }, }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response) expect(response.status).toBe(200) const withFeeToken = body.data.find((transaction) => transaction.feeToken) if (withFeeToken) expect(typeof withFeeToken.feeToken!.verified).toBe('boolean') }) test('embeds a capped total count on demand via include=totalCount', async () => { const client = app() const response = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { limit: '5', include: 'totalCount' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response) expect(response.status).toBe(200) expect(body.meta?.totalCount).toBeDefined() expect(Number.isInteger(body.meta?.totalCount)).toBe(true) expect(body.meta!.totalCount!).toBeGreaterThanOrEqual(body.data.length) expect(body.meta!.totalCount!).toBeLessThanOrEqual(Schema.countCap) expect(body.meta!.totalCountCapped).toBe(body.meta!.totalCount! >= Schema.countCap) // `token` was not requested, so only the count rides in `meta`. expect(body.meta?.token).toBeUndefined() }) test('embeds both the scoped token and the total count together', async () => { const client = app() const response = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { limit: '5', include: 'token,totalCount' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response) expect(response.status).toBe(200) expect(body.meta?.token?.address).toBe(TestApp.token) expect(Number.isInteger(body.meta?.totalCount)).toBe(true) }) test('embeds the scoped token when requested via include=token', async () => { const client = app() const response = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { limit: '5', include: 'token' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response) expect(response.status).toBe(200) expect(body.meta?.token?.address).toBe(TestApp.token) expect(typeof body.meta?.token?.symbol).toBe('string') // The embed is a trimmed reference: supply/holder/created fields are omitted. expect(body.meta?.token).not.toHaveProperty('totalSupply') expect(body.meta?.token).not.toHaveProperty('holderCount') expect(body.meta?.token).not.toHaveProperty('createdAt') }) test('walks the cursor to a disjoint next transaction page', async () => { const client = app() const first = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { limit: '5' } }, TestApp.auth, ) const firstBody = await TestApp.json(first, Tokens.schema.getTokenTransactions.Response) expect(first.status).toBe(200) if (!firstBody.nextCursor) return const second = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { limit: '5', cursor: firstBody.nextCursor }, }, TestApp.auth, ) const secondBody = await TestApp.json(second, Tokens.schema.getTokenTransactions.Response) expect(second.status).toBe(200) expect(secondBody.data.length).toBeGreaterThan(0) // Walking the cursor must yield a disjoint next page. const firstHashes = new Set(firstBody.data.map((entry) => entry.hash)) expect(secondBody.data.every((entry) => !firstHashes.has(entry.hash))).toBe(true) }) test('filters and counts by fee payer through receipts with pagination', async () => { const client = app() const seedResponse = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { limit: '50' } }, TestApp.auth, ) const seed = await TestApp.json(seedResponse, Tokens.schema.getTokenTransactions.Response) const feePayers = seed.data.flatMap((transaction) => { const feePayer = transaction.meta.rpc.feePayer return typeof feePayer === 'string' ? [feePayer] : [] }) const feePayer = feePayers.find( (candidate) => feePayers.filter((value) => value === candidate).length > 5, ) if (!feePayer) return const first = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { feePayer, include: 'totalCount', limit: '5' }, }, TestApp.auth, ) const firstBody = await TestApp.json(first, Tokens.schema.getTokenTransactions.Response) expect(first.status).toBe(200) expect(first.headers.get('server-timing')).toContain('receipts;dur=') expect(first.headers.get('server-timing')).toContain('receipts_count;dur=') expect(firstBody.data).toHaveLength(5) expect(firstBody.meta?.totalCount).toBeGreaterThanOrEqual(firstBody.data.length) expect(firstBody.data.every((transaction) => transaction.meta.rpc.feePayer === feePayer)).toBe( true, ) if (!firstBody.nextCursor) return const second = await client.v1.tokens[':token'].transactions.$get( { param: { token: TestApp.token }, query: { cursor: firstBody.nextCursor, feePayer, limit: '5' }, }, TestApp.auth, ) const secondBody = await TestApp.json(second, Tokens.schema.getTokenTransactions.Response) expect(second.status).toBe(200) expect(secondBody.data.length).toBeGreaterThan(0) expect(secondBody.data.every((transaction) => transaction.meta.rpc.feePayer === feePayer)).toBe( true, ) const firstHashes = new Set(firstBody.data.map((transaction) => transaction.hash)) expect(secondBody.data.every((transaction) => !firstHashes.has(transaction.hash))).toBe(true) }) test('returns 400 for an invalid token address', async () => { // The typed client requires `token: `Hex.Hex`` so an invalid literal // cannot pass through it; exercise the validator via `app.request`. const response = await TestApp.create().request('/v1/tokens/not-an-address/transactions', { headers: { authorization: `Bearer ${TestApp.key.token}` }, }) const body = await TestApp.json(response, Schema.ErrorResponse) expect(response.status).toBe(400) expect(body.error.code).toBe('token_invalid') }) // Regression: high-traffic token contracts (verified `pathUSD` is the // busiest token on testnet) used to crash TIDX with `422 db error` because // the planner can't handle a UNION-of-sides whose `ORDER BY` lives in the // outer query. Pushing the per-side `ORDER BY block_num DESC, idx DESC // LIMIT N+1` *inside* each branch lets the planner use each side's index // and keeps the response fast. test('returns a transaction page for a high-traffic token contract', async () => { const client = app() const pathUsd = '0x20c0000000000000000000000000000000000000' const response = await client.v1.tokens[':token'].transactions.$get( { param: { token: pathUsd }, query: { limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, Tokens.schema.getTokenTransactions.Response) expect(response.status).toBe(200) expect(Array.isArray(body.data)).toBe(true) expect(body.data.length).toBeGreaterThan(0) for (const tx of body.data) { const involves = tx.sender === pathUsd || tx.recipient === pathUsd expect(involves).toBe(true) } }) })