import * as TestApp from '../../../../test/App.js' import * as Runtime from '../../../../test/runtime.js' import * as Db from '../../../db/Db.js' import * as Schema from '../../../internal/Schema.js' import * as VerifiedTokens from '../../../internal/VerifiedTokens.js' import * as Verified from './verified-tokens.js' type JsonSchema = { $ref?: string description?: string enum?: readonly string[] examples?: readonly unknown[] items?: JsonSchema properties?: Record required?: readonly string[] } type OpenApiResponse = { $ref?: string content?: Record } type OpenApiOperation = { operationId?: string responses?: Record } type OpenApiDocument = { components: { schemas: Record } paths: Record } /** Reader key for the data routes. */ const reader = { id: 'key_reader', orgId: 'org_test', scopes: ['data:read'], token: 'secret_reader', } satisfies TestApp.kvStore.Key /** An address not present in the seed list. */ const unknownAddress = '0x20c00000000000000000000000000000000000ff' /** A known list published into the store before exercising the read endpoints. */ const seedList = [ { address: '0x20c0000000000000000000000000000000000001', currency: 'USD', decimals: 6, logoUri: 'https://example.com/susd.svg', name: 'Seed USD', symbol: 'sUSD' }, // prettier-ignore { address: '0x20c0000000000000000000000000000000000002', currency: 'EUR', decimals: 2, name: 'Seed EUR', symbol: 'sEUR' }, // prettier-ignore { address: '0x20c0000000000000000000000000000000000003', currency: 'USD', decimals: 6, name: 'Seed USD Two', symbol: 'sUSD2' }, // prettier-ignore ] as const const chainId = Runtime.get().chainId /** RequestInit carrying a bearer token for the given key. */ function as(key: { token: string }) { return { headers: { authorization: `Bearer ${key.token}` } } as const } /** Client whose verified-token store holds a published {@link seedList} snapshot. */ async function seeded() { const db = TestApp.database() await VerifiedTokens.replace(db, chainId, seedList) return TestApp.client({ auth: { keys: [reader] }, db, }) } function component(spec: OpenApiDocument, name: string): JsonSchema { const schema = spec.components.schemas[name] if (!schema) throw new Error(`Missing OpenAPI component ${name}`) return schema } function resolveSchema(spec: OpenApiDocument, schema: JsonSchema | undefined): JsonSchema { if (!schema) throw new Error('Missing OpenAPI schema') if (!schema.$ref) return schema const name = schema.$ref.split('/').at(-1) if (!name) throw new Error(`Invalid OpenAPI component ref ${schema.$ref}`) return component(spec, name) } describe('OpenAPI', () => { test('publishes generator-ready verified-token contracts', async () => { const app = TestApp.create({ auth: false }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const operations = { currencies: spec.paths['/v1/verified-tokens/currencies']?.get, detail: spec.paths['/v1/verified-tokens/{address}']?.get, list: spec.paths['/v1/verified-tokens']?.get, } if (Object.values(operations).some((operation) => !operation)) throw new Error('Missing verified-token OpenAPI operation') const summarizeError = (response: OpenApiResponse | undefined) => { if (!response) return undefined if (response.$ref) return { $ref: response.$ref } const schema = response.content?.['application/json']?.schema return resolveSchema(spec, schema).properties?.['error']?.properties?.['code']?.enum } const summarize = (operation: OpenApiOperation | undefined) => { if (!operation) throw new Error('Missing verified-token OpenAPI operation') return { errors: Object.fromEntries( [400, 401, 402, 403, 404, 429, 500, 502, 504] .filter((status) => operation.responses?.[status]) .map((status) => [status, summarizeError(operation.responses?.[status])]), ), operationId: operation.operationId, response: operation.responses?.['200']?.content?.['application/json']?.schema, } } const token = component(spec, 'VerifiedToken') const list = component(spec, 'VerifiedTokenList') const currencies = component(spec, 'VerifiedTokenCurrencyList') expect({ components: { currencies: { dataExamples: currencies.properties?.['data']?.examples, description: currencies.description, required: currencies.required, }, list: { dataExamples: list.properties?.['data']?.examples, description: list.description, item: list.properties?.['data']?.items, required: list.required, }, token: { description: token.description, required: token.required, }, }, operations: Object.fromEntries( Object.entries(operations).map(([name, operation]) => [name, summarize(operation)]), ), }).toMatchInlineSnapshot(` { "components": { "currencies": { "dataExamples": [ [ "USD", ], ], "description": "The distinct currencies present in a chain’s verified token list.", "required": [ "data", ], }, "list": { "dataExamples": [ [ { "address": "0x20c0000000000000000000008f5425160ebe5525", "currency": "USD", "decimals": 6, "id": "0x20c0000000000000000000008f5425160ebe5525", "name": "USD Coin", "symbol": "USDC", }, ], ], "description": "A non-paginated list of curated, verified TIP-20 tokens.", "item": { "$ref": "#/components/schemas/VerifiedToken", "examples": [ { "address": "0x20c0000000000000000000008f5425160ebe5525", "currency": "USD", "decimals": 6, "id": "0x20c0000000000000000000008f5425160ebe5525", "name": "USD Coin", "symbol": "USDC", }, ], }, "required": [ "data", ], }, "token": { "description": "One curated, verified TIP-20 token that wallets and apps can present as trusted.", "required": [ "address", "currency", "decimals", "name", "symbol", "id", ], }, }, "operations": { "currencies": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getVerifiedTokenCurrencies", "response": { "$ref": "#/components/schemas/VerifiedTokenCurrencyList", }, }, "detail": { "errors": { "400": [ "address_invalid", "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "404": [ "verified_token_not_found", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "getVerifiedToken", "response": { "$ref": "#/components/schemas/VerifiedToken", }, }, "list": { "errors": { "400": [ "api_key_malformed", "chain_id_invalid", "chain_id_unsupported", "query_invalid", ], "401": [ "api_key_invalid", "api_key_missing", "unauthorized", ], "402": { "$ref": "#/components/responses/PaymentRequired", }, "403": [ "api_key_forbidden", "api_key_ip_forbidden", "forbidden", ], "429": { "$ref": "#/components/responses/RateLimited", }, "500": { "$ref": "#/components/responses/InternalError", }, "502": [ "upstream_error", ], "504": { "$ref": "#/components/responses/RequestTimeout", }, }, "operationId": "listVerifiedTokens", "response": { "$ref": "#/components/schemas/VerifiedTokenList", }, }, }, } `) }) test('returns the existing route-specific validation errors', async () => { const app = TestApp.create({ auth: false }) const invalidListChain = await app.request('/v1/verified-tokens?chainId=not-a-chain') const unsupportedListChain = await app.request('/v1/verified-tokens?chainId=999999') const invalidListQuery = await app.request('/v1/verified-tokens?unknown=true') const invalidCurrenciesChain = await app.request( '/v1/verified-tokens/currencies?chainId=not-a-chain', ) const unsupportedCurrenciesChain = await app.request( '/v1/verified-tokens/currencies?chainId=999999', ) const invalidCurrenciesQuery = await app.request('/v1/verified-tokens/currencies?unknown=true') const invalidAddress = await app.request('/v1/verified-tokens/not-an-address') const invalidDetailChain = await app.request( `/v1/verified-tokens/${unknownAddress}?chainId=not-a-chain`, ) const unsupportedDetailChain = await app.request( `/v1/verified-tokens/${unknownAddress}?chainId=999999`, ) const invalidDetailQuery = await app.request( `/v1/verified-tokens/${unknownAddress}?unknown=true`, ) expect( await Promise.all( [ invalidListChain, unsupportedListChain, invalidListQuery, invalidCurrenciesChain, unsupportedCurrenciesChain, invalidCurrenciesQuery, invalidAddress, invalidDetailChain, unsupportedDetailChain, invalidDetailQuery, ].map(async (response) => (await TestApp.json(response, Schema.ErrorResponse)).error.code), ), ).toStrictEqual([ 'chain_id_invalid', 'chain_id_unsupported', 'query_invalid', 'chain_id_invalid', 'chain_id_unsupported', 'query_invalid', 'address_invalid', 'chain_id_invalid', 'chain_id_unsupported', 'query_invalid', ]) }) }) describe('GET /verified-tokens', () => { test('maps cold cache-key store failures to the upstream error contract', async () => { const app = TestApp.create({ auth: false, db: Db.postgres({ connectionString: 'postgresql://postgres:postgres@127.0.0.1:1/none' }), verifiedTokens: { refreshMs: 0 }, }) const response = await app.request('/v1/verified-tokens') expect(response.status).toBe(502) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'upstream_error' }, }) expect(response.headers.get('cache-control')).toBe('no-store') }) test('returns the verified list', async () => { const response = await (await seeded()).v1['verified-tokens'].$get({ query: {} }, as(reader)) expect(response.status).toBe(200) const body = await TestApp.json(response, Verified.schema.listVerifiedTokens.Response) expect(body.data.map((token) => token.address)).toEqual(seedList.map((token) => token.address)) // The curated `logoUri` rides along when set, and is absent otherwise. expect(body.data[0]?.logoUri).toBe('https://example.com/susd.svg') expect(body.data[1]?.logoUri).toBeUndefined() }) test('reads through `db.cached` when configured', async () => { // Seed only the cached source: the list serving from it proves the // snapshot path resolves `cached`, not `source`. `refreshMs: 0` forces a // head check against this app's database (the per-isolate snapshot cell // is keyed by chain id, so a prior test's cell could otherwise satisfy // the read). const cached = TestApp.database() await VerifiedTokens.replace(cached, chainId, seedList) const client = TestApp.client({ auth: { keys: [reader] }, db: { cached, source: TestApp.database() }, verifiedTokens: { refreshMs: 0 }, }) const response = await client.v1['verified-tokens'].$get({ query: {} }, as(reader)) expect(response.status).toBe(200) const body = await TestApp.json(response, Verified.schema.listVerifiedTokens.Response) expect(body.data.map((token) => token.address)).toEqual(seedList.map((token) => token.address)) }) test('filters by currency (case-insensitive)', async () => { const response = await ( await seeded() ).v1['verified-tokens'].$get({ query: { currency: 'usd' } }, as(reader)) const body = await TestApp.json(response, Verified.schema.listVerifiedTokens.Response) expect(body.data.length).toBe(2) expect(body.data.every((token) => token.currency.toLowerCase() === 'usd')).toBe(true) }) test('returns 304 for a matching If-None-Match', async () => { const client = await seeded() const first = await client.v1['verified-tokens'].$get({ query: {} }, as(reader)) const etag = first.headers.get('ETag') expect(etag).toBeTruthy() const second = await client.v1['verified-tokens'].$get( { query: {} }, { headers: { 'if-none-match': etag! } }, ) expect(second.status).toBe(304) }) test('refreshes cached reads after a snapshot publish', async () => { const db = TestApp.database() await VerifiedTokens.replace(db, chainId, seedList) const client = TestApp.client({ auth: { keys: [reader] }, db }) await client.v1['verified-tokens'].$get({ query: {} }, as(reader)) await VerifiedTokens.replace(db, chainId, seedList.slice(1)) const response = await client.v1['verified-tokens'].$get({ query: {} }, as(reader)) const body = await TestApp.json(response, Verified.schema.listVerifiedTokens.Response) expect(body.data.map((token) => token.address)).toEqual( seedList.slice(1).map((token) => token.address), ) }) }) describe('GET /verified-tokens/currencies', () => { test('returns the sorted distinct currencies', async () => { const response = await ( await seeded() ).v1['verified-tokens'].currencies.$get({ query: {} }, as(reader)) const body = await TestApp.json(response, Verified.schema.getVerifiedTokenCurrencies.Response) expect([...body.data]).toEqual(['EUR', 'USD']) }) test('refreshes cached reads after a snapshot publish', async () => { const db = TestApp.database() await VerifiedTokens.replace(db, chainId, seedList) const client = TestApp.client({ auth: { keys: [reader] }, db }) await client.v1['verified-tokens'].currencies.$get({ query: {} }, as(reader)) await VerifiedTokens.replace( db, chainId, seedList.filter((token) => token.currency === 'USD'), ) const response = await client.v1['verified-tokens'].currencies.$get({ query: {} }, as(reader)) const body = await TestApp.json(response, Verified.schema.getVerifiedTokenCurrencies.Response) expect([...body.data]).toEqual(['USD']) }) }) describe('GET /verified-tokens/:address', () => { test('returns a seeded entry', async () => { const client = await seeded() const address = seedList[0].address const response = await client.v1['verified-tokens'][':address'].$get( { param: { address }, query: {} }, as(reader), ) expect(response.status).toBe(200) const body = await TestApp.json(response, Verified.schema.Token) expect(body.address).toBe(address) }) test('returns 404 for an unknown address', async () => { const response = await ( await seeded() ).v1['verified-tokens'][':address'].$get( { param: { address: unknownAddress }, query: {} }, as(reader), ) expect(response.status).toBe(404) const body = (await response.json()) as { error?: { code?: string } } expect(body.error?.code).toBe('verified_token_not_found') }) test('refreshes cached reads after a snapshot publish', async () => { const db = TestApp.database() await VerifiedTokens.replace(db, chainId, seedList) const client = TestApp.client({ auth: { keys: [reader] }, db }) const address = seedList[0].address await client.v1['verified-tokens'][':address'].$get( { param: { address }, query: {} }, as(reader), ) await VerifiedTokens.replace(db, chainId, seedList.slice(1)) const response = await client.v1['verified-tokens'][':address'].$get( { param: { address }, query: {} }, as(reader), ) expect(response.status).toBe(404) }) })