/** @module-tag localnet */ import { Schema } from 'tapimo' import { FxOracle } from 'tapimo/apps' import { Value as core_Value } from 'ox' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { Actions } from 'viem/tempo' import * as TestApp from '../../../../test/App.js' import * as Runtime from '../../../../test/runtime.js' import * as Tempo from '../../../../test/Tempo.js' import * as TestViem from '../../../../test/Viem.js' import * as Tidx from '../../../internal/Tidx.js' import * as Balances from './balances.js' // Deterministic EUR-based rate set so conversion math is exact in assertions. const fixed = FxOracle.from({ name: 'fixed', rates: async () => ({ asOf: '2026-01-01T00:00:00.000Z', base: 'EUR', rates: { AUD: '1.6', USD: '1.0' }, }), }) // 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 holderWithBalance = (async () => { if (runtime.mode === 'localnet') return runtime.fixtures!.fundedAccounts[0]!.toLowerCase() as `0x${string}` const address = Tempo.accounts[1]!.address.toLowerCase() as `0x${string}` const tidx = Tidx.getClient({ chainId: runtime.chainId, tidx: { auth: process.env.TIDX_AUTH, baseUrl: runtime.tidxUrl }, }) const rpc = TestViem.getClient() // Global setup funds this ephemeral account once. Wait for TIDX to index its // faucet delta so discovery exercises a stable, low-activity holder. for (let attempt = 0; attempt < 30; attempt++) { const [balance, indexed] = await Promise.all([ Actions.token.getBalance(rpc, { account: address, token: TestApp.tokenWithHolders }), tidx.fetch({ chainId: runtime.chainId, engine: 'clickhouse', // Raw-table query, so the inline SQL is cast to `string`. query: ` SELECT token FROM address_holder_deltas WHERE holder = '${address}' AND token = '${TestApp.tokenWithHolders}' LIMIT 1 ` as string, }), ]) if (balance.amount > 0n && indexed.rows.length > 0) return address if (attempt < 29) await new Promise((resolve) => setTimeout(resolve, 500)) } throw new Error('expected funded test account to reach TIDX') })() describe('GET /addresses/:address/balances', () => { test('parses the total count include', () => { expect(Balances.schema.getAddressBalances.Query.parse({ include: 'totalCount' }).include) .toMatchInlineSnapshot(` [ "totalCount", ] `) expect(Balances.schema.getAddressBalances.Query.safeParse({ include: 'token' }).success).toBe( false, ) expect( Balances.schema.getAddressBalances.Query.safeParse({ include: 'token.logoUri' }).success, ).toBe(false) }) test('returns an amount-ranked balance page with per-row token references', async () => { const client = app() const holder = await holderWithBalance expect(holder).toMatch(/^0x[0-9a-f]{40}$/) const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')?.includes('balances_discovery;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) // Each row is a balance (`amount`/`formatted`) with the trimmed token // reference inlined at `token`, per the embedded-resource convention. expect( body.data.every( (entry) => /^\d+$/.test(entry.amount) && // `formatted` is `amount` scaled down by `decimals` (round-trips back // to the base-unit amount). /^\d+(\.\d+)?$/.test(entry.formatted) && core_Value.from(entry.formatted, entry.decimals) === BigInt(entry.amount) && entry.currency === entry.token.currency && entry.decimals === entry.token.decimals && /^0x[0-9a-f]{40}$/.test(entry.token.address) && typeof entry.currency === 'string' && Number.isInteger(entry.decimals) && typeof entry.token.name === 'string' && typeof entry.token.symbol === 'string' && (entry.token.logoUri === undefined || typeof entry.token.logoUri === 'string') && typeof entry.token.verified === 'boolean', ), ).toBe(true) // Results are restricted to TIP-20 tokens (deterministic `0x20c0…` factory // addresses), so non-TIP-20 contracts in the delta history are excluded. expect(body.data.every((entry) => entry.token.address.startsWith('0x20c0'))).toBe(true) // Amounts are returned largest-first. const amounts = body.data.map((entry) => BigInt(entry.amount)) expect(amounts.every((amount, index) => index === 0 || amounts[index - 1]! >= amount)).toBe( true, ) const rpc = TestViem.getClient() const current = await Promise.all( body.data.map((entry) => Actions.token.getBalance(rpc, { account: holder, token: entry.token.address }), ), ) expect(body.data.map((entry) => entry.amount)).toEqual( current.map((balance) => balance.amount.toString()), ) }) test.skipIf(runtime.mode !== 'localnet')( 'discovers freshly transferred assets from deltas', async () => { const fixture = runtime.fixtures?.crossTokenTransfer if (!fixture) throw new Error('expected cross-token transfer fixture') const client = app() const response = await client.v1.addresses[':address'].balances.$get( { param: { address: fixture.recipient }, query: { limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) expect(body.data).toContainEqual( expect.objectContaining({ amount: '1000000', id: fixture.destinationToken, }), ) }, ) test('embeds an exact balance total on demand via include=totalCount', async () => { const client = app() const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { include: 'totalCount', limit: '5' } }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')?.includes('balances_discovery;dur=')).toBe(true) expect(Number.isInteger(body.meta?.totalCount)).toBe(true) expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length) // The count comes from the corrected candidate set and is never capped. expect(body.meta!.totalCountCapped).toBe(false) // Without the include, counts are omitted (`meta` carries at most rate // provenance). The corrected candidate set already supplies the count, so // no second indexer query is needed. const bareResponse = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { limit: '5' } }, TestApp.auth, ) const bare = await TestApp.json(bareResponse, Balances.schema.getAddressBalances.Response) expect(bareResponse.status).toBe(200) expect(bare.meta?.totalCount).toBeUndefined() expect(bare.meta?.totalCountCapped).toBeUndefined() expect(bareResponse.headers.get('server-timing')?.includes('balances_discovery;dur=')).toBe( true, ) }) test('surfaces a 502 when balance 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.addresses[':address'].balances.$get( { param: { address: 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 // Like the dedicated holders endpoint, balance lookups report upstream // failure honestly rather than masquerading as an address holding nothing. expect(response.status).toMatchInlineSnapshot(`502`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(typeof message).toMatchInlineSnapshot(`"string"`) expect(stableError).toMatchInlineSnapshot(` { "code": "upstream_error", } `) }) test('surfaces a 502 when RPC correction is unavailable', async () => { const client = app({ rpc: { url: () => 'http://127.0.0.1:1' } }) const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Schema.ErrorResponse) const { requestId, error } = body const { message, ...stableError } = error expect(response.status).toMatchInlineSnapshot(`502`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(typeof message).toMatchInlineSnapshot(`"string"`) expect(stableError).toMatchInlineSnapshot(` { "code": "upstream_error", } `) }) test('rejects invalid account addresses', async () => { // The typed client requires `address: `Hex.Hex`` so an invalid literal // cannot pass through it; exercise the validator via `app.request`. const response = await TestApp.create().request('/v1/addresses/not-an-address/balances', { 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": "address_invalid", "details": [ { "message": "Invalid input", "path": [ "address", ], }, ], "message": "Invalid account address", }, } `) }) test('rejects invalid pagination', async () => { const client = app() const response = await client.v1.addresses[':address'].balances.$get( { param: { address: 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.error.code).toMatchInlineSnapshot(`"query_invalid"`) }) test('includes token logo and verification fields by default', async () => { const logoDb = TestApp.database() await TestApp.verifiedSeed(logoDb, runtime.chainId, { tokens: [ { address: TestApp.tokenWithHolders, currency: 'USD', decimals: 6, logoUri: 'https://example.com/pathusd.svg', name: 'Curated PathUSD', symbol: 'curatedUSD', }, ], }) const client = app({ db: logoDb }) const holder = await holderWithBalance expect(holder).toMatch(/^0x[0-9a-f]{40}$/) // The seeded holder is a holder of a verified token, so the verified // balances page must include at least that token. const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { limit: '20', verified: 'true', }, }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) expect(body.data.length).toBeGreaterThan(0) expect(response.headers.get('server-timing')).not.toContain('token_logo;dur=') expect(response.headers.get('server-timing')).not.toContain('token_metadata;dur=') for (const balance of body.data) expect(balance.token.verified).toBe(true) const pathUsd = body.data.find((balance) => balance.token.address === TestApp.tokenWithHolders) expect(pathUsd?.token).toMatchObject({ logoUri: 'https://example.com/pathusd.svg', name: 'Curated PathUSD', symbol: 'curatedUSD', }) }) test('includes uploaded logos for verified tokens without a logo URI', async () => { const logoDb = TestApp.database() await TestApp.verifiedSeed(logoDb, runtime.chainId, { tokens: [ { address: TestApp.tokenWithHolders, currency: 'USD', decimals: 6, name: 'Curated PathUSD', symbol: 'curatedUSD', }, ], }) const client = app({ db: logoDb }) const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { limit: '20', verified: 'true' }, }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) const pathUsd = body.data.find((balance) => balance.token.address === TestApp.tokenWithHolders) expect(response.status).toBe(200) expect(response.headers.get('server-timing')).toContain('token_logo;dur=') expect(response.headers.get('server-timing')).not.toContain('token_metadata;dur=') expect(pathUsd?.token.logoUri).toBe( `http://localhost/assets/${runtime.chainId}/icons/${TestApp.tokenWithHolders}`, ) }) test('narrows balances by currency and retains verification', async () => { const client = app() const holder = await holderWithBalance expect(holder).toMatch(/^0x[0-9a-f]{40}$/) const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { limit: '20', currency: 'USD' } }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) for (const balance of body.data) { expect(balance.currency).toBe('USD') expect(balance.token.currency).toBe('USD') expect(balance.token.verified).toBe(true) } }) test('includes feeEligible on balance rows', async () => { const client = app() const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { limit: '20' } }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')?.includes('fee_token_set;dur=')).toBe(true) expect(body.data.length).toBeGreaterThan(0) for (const balance of body.data) expect(typeof balance.feeEligible).toBe('boolean') }) test('skips the fee-token lookup when an address has no holdings', async () => { const client = app() const response = await client.v1.addresses[':address'].balances.$get( { param: { address: privateKeyToAccount(generatePrivateKey()).address }, query: {}, }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) expect(body.data).toEqual([]) expect(response.headers.get('server-timing')).not.toContain('fee_token_set;dur=') }) test('omits valuations unless a denomination is requested', async () => { await TestApp.verifiedSeed(db, runtime.chainId) const client = app() const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { limit: '50' } }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')).not.toContain('valuation_rates;dur=') for (const entry of body.data) expect('valuation' in entry).toBe(false) }) test('values holdings in a requested denomination', async () => { await TestApp.verifiedSeed(db, runtime.chainId) const client = app({ fx: { oracle: fixed } }) const holder = await holderWithBalance // Denominations are case-insensitive. const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { limit: '50', 'valuation.currency': 'aud' } }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) // USD -> AUD at the fixed 1.6 cross rate, floored to 6 dp. const pathUsd = body.data.find((entry) => entry.token.address === TestApp.tokenWithHolders) const expected = core_Value.format((core_Value.from(pathUsd!.formatted, 6) * 16n) / 10n, 6) expect(pathUsd?.valuation).toEqual({ amount: expected, currency: 'AUD' }) // Consulted rates surface their provenance response-wide. expect(body.meta?.valuation).toMatchInlineSnapshot(` { "asOf": "2026-01-01T00:00:00.000Z", "basis": "nominal", "source": "fixed", } `) }) test('nulls valuations when rates are unavailable', async () => { await TestApp.verifiedSeed(db, runtime.chainId) const client = app({ fx: { oracle: FxOracle.ecb({ url: 'http://127.0.0.1:1' }) } }) const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { limit: '50', 'valuation.currency': 'AUD' } }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) // The live balance page survives a rate outage; only values degrade, and // no provenance is claimed. expect(response.status).toBe(200) expect(body.data.length).toBeGreaterThan(0) for (const entry of body.data) expect(entry.valuation).toBeNull() expect(body.meta?.valuation).toBeUndefined() }) test('rejects denominations the oracle does not price', async () => { await TestApp.verifiedSeed(db, runtime.chainId) const client = app({ fx: { oracle: fixed } }) const response = await client.v1.addresses[':address'].balances.$get( { param: { address: `0x${'11'.repeat(20)}` }, query: { currency: 'JPY', 'valuation.currency': 'JPY' }, }, 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.error.code).toMatchInlineSnapshot(`"query_invalid"`) }) test('rejects unsupported denominations before balance RPC failures', async () => { const client = app({ fx: { oracle: fixed }, rpc: { url: () => 'http://127.0.0.1:1' }, }) const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { 'valuation.currency': 'JPY' }, }, TestApp.auth, ) const body = await TestApp.json(response, Schema.ErrorResponse) expect(response.status).toMatchInlineSnapshot(`400`) expect(body.error.code).toMatchInlineSnapshot(`"query_invalid"`) }) test('restricts the page to fee tokens when feeEligible=true', async () => { const client = app() const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances.$get( { param: { address: holder }, query: { feeEligible: 'true', include: 'totalCount', limit: '20' }, }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response) expect(response.status).toBe(200) // Every returned holding is a fee token, and the holder's guaranteed // pathUSD holding survives the filter. expect(body.data.length).toBeGreaterThan(0) for (const balance of body.data) expect(balance.feeEligible).toBe(true) expect(body.data.some((balance) => balance.token.address === TestApp.tokenWithHolders)).toBe( true, ) // The opt-in count shares the filter, so it is at least the page size. expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length) }) }) describe('GET /v1/addresses/:address/balances/:token', () => { test('returns one live balance without indexed holder discovery', async () => { const client = app() const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances[':token'].$get( { param: { address: holder, token: TestApp.tokenWithHolders }, query: {}, }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalance.Response) expect(response.status).toBe(200) expect(body.id).toBe(TestApp.tokenWithHolders) expect(body.token.address).toBe(TestApp.tokenWithHolders) expect(body.amount).toMatch(/^\d+$/) expect(core_Value.from(body.formatted, body.decimals)).toBe(BigInt(body.amount)) expect('valuation' in body).toBe(false) expect(response.headers.get('server-timing')).not.toContain('valuation_rates;dur=') }) test('values one balance when a denomination is requested', async () => { const client = app() const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances[':token'].$get( { param: { address: holder, token: TestApp.tokenWithHolders }, query: { 'valuation.currency': 'USD' }, }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalance.Response) expect(response.status).toBe(200) expect(body.valuation).toEqual({ amount: body.formatted, currency: 'USD' }) expect(response.headers.get('server-timing')).toContain('valuation_rates;dur=') }) test('rejects an unsupported denomination before a balance RPC failure', async () => { const client = app({ fx: { oracle: fixed }, rpc: { url: () => 'http://127.0.0.1:1' }, }) const holder = await holderWithBalance const response = await client.v1.addresses[':address'].balances[':token'].$get( { param: { address: holder, token: TestApp.tokenWithHolders }, query: { 'valuation.currency': 'JPY' }, }, TestApp.auth, ) const body = await TestApp.json(response, Schema.ErrorResponse) expect(response.status).toMatchInlineSnapshot(`400`) expect(body.error.code).toMatchInlineSnapshot(`"query_invalid"`) }) test('returns zero for a token the account does not hold', async () => { const client = app() const response = await client.v1.addresses[':address'].balances[':token'].$get( { param: { address: privateKeyToAccount(generatePrivateKey()).address, token: TestApp.tokenWithHolders, }, query: {}, }, TestApp.auth, ) const body = await TestApp.json(response, Balances.schema.getAddressBalance.Response) expect(response.status).toBe(200) expect(body.amount).toBe('0') expect(body.formatted).toBe('0') }) })