/** @module-tag localnet */ import * as fs from 'node:fs' import { Value as core_Value } from 'ox' import { Schema } from 'tapimo' import { FxOracle } from 'tapimo/apps' 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 TestViem from '../../../../test/Viem.js' import * as Store from '../../../internal/Store.js' import * as Tokens from './tokens.js' import * as Valuation from './valuation.js' type OpenApiDocument = { paths: Record> } type OpenApiOperation = { parameters?: readonly OpenApiParameter[] | undefined } type OpenApiParameter = { name: string } const runtime = Runtime.get() const db = TestApp.database() await TestApp.verifiedSeed(db, runtime.chainId) const ecbXml = `` const ecb = FxOracle.ecb({ url: `data:application/xml,${encodeURIComponent(ecbXml)}`, }) function app(options: TestApp.create.Options = {}) { return TestApp.client({ db, fx: { oracle: ecb }, ...options }) } // Verified snapshots cache per isolate and `verifiedSeed` primes that cache // (read-your-write), so tests that depend on a specific list re-seed at their // own start. `usdDb` restricts the list to pathUSD so every holding shares one // currency, independent of which seed tokens exist on the dev chain. const usdDb = TestApp.database() const usdTokens = [ { address: TestApp.tokenWithHolders, currency: 'USD', decimals: 6, name: 'PathUSD', symbol: 'pathUSD', }, ] // Curated seed backing `verifiedSeed`, used to recompute expected sums from // chain state. Localnet reuses the Moderato testnet list (see `verifiedSeed`). type SeedToken = { address: `0x${string}`; currency: string; decimals: number } const seedChainId = runtime.chainId === 1337 ? 42431 : runtime.chainId const seed: readonly SeedToken[] = JSON.parse( fs.readFileSync( new URL(`../../../../seeds/${seedChainId}/verified-tokens.json`, import.meta.url), 'utf8', ), ) // 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' }, }), }) async function holderWithBalance(client: ReturnType) { if (runtime.mode === 'localnet') return runtime.fixtures!.fundedAccounts[0]!.toLowerCase() as `0x${string}` const holdersResponse = await client.v1.tokens[':token'].holders.$get( { param: { token: TestApp.tokenWithHolders }, query: { limit: '5' } }, TestApp.auth, ) const holders = await TestApp.json(holdersResponse, Tokens.schema.getTokenHolders.Response) expect(holdersResponse.status).toBe(200) const holder = holders.data[0]?.address if (!holder) throw new Error('expected at least one holder') return holder } /** Fresh account address guaranteed to hold nothing. */ function emptyAccount() { return privateKeyToAccount(generatePrivateKey()).address.toLowerCase() as `0x${string}` } /** * Sums the holder's on-chain balances across curated tokens of one currency, * in 6-dp base units. Unreadable tokens (undeployed or uninitialized on this * chain) count as empty, matching the endpoint's declared scope. */ async function chainSum(holder: `0x${string}`, currency: string) { const rpc = TestViem.getClient() const amounts = await Promise.all( seed .filter((token) => token.currency === currency) .map((token) => Actions.token .getBalance(rpc, { account: holder, decimals: token.decimals, token: token.address }) .catch(() => ({ amount: 0n })), ), ) return amounts.reduce((sum, { amount }) => sum + amount, 0n) } describe('GET /addresses/:address/valuation', () => { test('reuses rates populated by another app', async () => { const store = Store.memory() const warm = app({ cache: { edge: false, store } }) const holder = await holderWithBalance(warm) const first = await warm.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: { currency: 'AUD' } }, TestApp.auth, ) expect(first.status).toBe(200) const reuse = app({ cache: { edge: false, store }, fx: { oracle: FxOracle.ecb({ url: 'http://127.0.0.1:1' }) }, }) const second = await reuse.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: { currency: 'EUR' } }, TestApp.auth, ) const body = await TestApp.json(second, Valuation.schema.getAddressValuation.Response) expect(second.status).toBe(200) expect(body.currency).toBe('EUR') }) test('requires a full pricing timestamp', () => { const pricing = { asOf: '2026-01-01', basis: 'nominal', source: 'fixed' } expect(Valuation.schema.Pricing.safeParse(pricing).success).toBe(false) expect( Valuation.schema.Pricing.safeParse({ ...pricing, asOf: '2026-01-01T00:00:00.000Z', }).success, ).toBe(true) }) test('normalizes the valuation currency', () => { expect( Valuation.schema.getAddressValuation.Query.parse({ currency: 'aud' }).currency, ).toMatchInlineSnapshot(`"AUD"`) expect( Valuation.schema.getAddressValuation.Query.safeParse({ currency: 'DOLLARS', }).success, ).toBe(false) expect( Valuation.schema.getAddressValuation.Query.safeParse({ denomination: 'AUD' }).success, ).toBe(false) expect( Valuation.schema.getAddressValuation.Query.safeParse({ 'valuation.currency': 'AUD', }).success, ).toBe(false) }) test('values verified holdings in the default denomination', async () => { await TestApp.verifiedSeed(db, runtime.chainId) const client = app() const holder = await holderWithBalance(client) const response = await client.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')?.includes('valuation_balances;dur=')).toBe(true) expect(body.address).toBe(holder) expect(body.id).toBe(holder) expect(body.currency).toBe('USD') // The total is the exact on-chain sum of the curated USD holdings; every // non-USD currency in the seed is unpriced by ECB, so none of it leaks in. expect(body.amount).toBe(core_Value.format(await chainSum(holder, 'USD'), 6)) }) test('returns a zero valuation for an address with no holdings', async () => { const account = emptyAccount() const response = await app().v1.addresses[':address'].valuation.$get( { param: { address: account }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response) const { address, id, ...stable } = body expect(response.status).toBe(200) expect(address).toBe(account) expect(id).toBe(account) expect(stable).toMatchInlineSnapshot(` { "amount": "0", "currency": "USD", "pricing": null, "unpriced": [], } `) }) test.skipIf(runtime.mode !== 'localnet')( 'converts holdings into a requested denomination', async () => { await TestApp.verifiedSeed(usdDb, runtime.chainId, { tokens: usdTokens }) const client = app({ db: usdDb, fx: { oracle: fixed } }) const holder = await holderWithBalance(client) const usdResponse = await client.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: {} }, TestApp.auth, ) const usd = await TestApp.json(usdResponse, Valuation.schema.getAddressValuation.Response) // Denominations are case-insensitive. const audResponse = await client.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: { currency: 'aud' } }, TestApp.auth, ) const aud = await TestApp.json(audResponse, Valuation.schema.getAddressValuation.Response) expect(usdResponse.status).toBe(200) expect(audResponse.status).toBe(200) // Every verified holding is USD here, so the USD valuation needs no rates. expect(usd.pricing).toBeNull() expect(Number(usd.amount)).toBeGreaterThan(0) // USD -> AUD at the fixed 1.6 cross rate, floored to 6 dp. const sum = core_Value.from(usd.amount, 6) expect(aud.currency).toBe('AUD') expect(aud.amount).toBe(core_Value.format((sum * 16n) / 10n, 6)) expect(aud.pricing).toMatchInlineSnapshot(` { "asOf": "2026-01-01T00:00:00.000Z", "basis": "nominal", "source": "fixed", } `) }, ) test('converts via the ECB oracle', async () => { await TestApp.verifiedSeed(db, runtime.chainId) const client = app() const holder = await holderWithBalance(client) const response = await client.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: { currency: 'EUR' } }, TestApp.auth, ) const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response) expect(response.status).toBe(200) expect(response.headers.get('server-timing')?.includes('valuation_rates;dur=')).toBe(true) expect(body.currency).toBe('EUR') expect(Number(body.amount)).toBeGreaterThan(0) expect(body.pricing?.basis).toBe('nominal') expect(body.pricing?.source).toBe('ecb') expect(body.pricing?.asOf).toBe('2026-01-01T00:00:00.000Z') }) test.skipIf(runtime.mode !== 'localnet')( 'discloses holdings the oracle cannot price', async () => { // Publish a curated list where one genuinely held token carries a // non-fiat display currency the oracle has no rate for. const rpc = TestViem.getClient() const holder = runtime.fixtures!.fundedAccounts[0]!.toLowerCase() as `0x${string}` const held = await (async () => { for (const token of runtime.fixtures!.transferTokenAddresses) { if (token.toLowerCase() === TestApp.tokenWithHolders) continue const { amount } = await Actions.token.getBalance(rpc, { account: holder, token }) if (amount > 0n) return token.toLowerCase() as `0x${string}` } throw new Error('expected a held fixture token') })() const unpricedDb = TestApp.database() await TestApp.verifiedSeed(unpricedDb, runtime.chainId, { tokens: [ { address: TestApp.tokenWithHolders, currency: 'USD', decimals: 6, name: 'PathUSD', symbol: 'pathUSD', }, { address: held, currency: 'sUSDe', decimals: 6, name: 'Staked USDe', symbol: 'sUSDe', }, ], }) const client = app({ db: unpricedDb, fx: { oracle: fixed } }) const response = await client.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response) expect(response.status).toBe(200) // The unpriced holding is excluded from the total and disclosed. expect(body.unpriced).toMatchInlineSnapshot(` [ "sUSDe", ] `) expect(body.pricing?.source).toBe('fixed') const { amount } = await Actions.token.getBalance(rpc, { account: holder, token: TestApp.tokenWithHolders, }) expect(body.amount).toBe(core_Value.format(amount, 6)) }, ) test.skipIf(runtime.mode !== 'localnet')( 'ignores the FX oracle when no conversion is needed', async () => { // Every verified holding is already USD, so an unreachable oracle must // not affect the default valuation. await TestApp.verifiedSeed(usdDb, runtime.chainId, { tokens: usdTokens }) const client = app({ db: usdDb, fx: { oracle: FxOracle.ecb({ url: 'http://127.0.0.1:1' }) }, }) const holder = await holderWithBalance(client) const response = await client.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: {} }, TestApp.auth, ) const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response) expect(response.status).toBe(200) expect(body.pricing).toBeNull() }, ) test.skipIf(runtime.mode !== 'localnet')( 'surfaces a 502 when rates are unavailable', async () => { const client = app({ fx: { oracle: FxOracle.ecb({ url: 'http://127.0.0.1:1' }) } }) const holder = await holderWithBalance(client) const response = await client.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: { currency: 'AUD' } }, TestApp.auth, ) const body = await TestApp.json(response, Schema.ErrorResponse) const { requestId, error } = body const { message, ...stableError } = error // Rate failures report honestly rather than serving a partial total. expect(response.status).toMatchInlineSnapshot(`502`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(typeof message).toMatchInlineSnapshot(`"string"`) expect(stableError).toMatchInlineSnapshot(` { "code": "upstream_error", } `) }, ) test.skipIf(runtime.mode !== 'localnet')( 'rejects denominations the oracle does not price', async () => { const client = app({ fx: { oracle: fixed } }) const holder = await holderWithBalance(client) const response = await client.v1.addresses[':address'].valuation.$get( { param: { address: holder }, query: { 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).toMatchInlineSnapshot(` { "error": { "code": "query_invalid", "message": "Denomination "JPY" is not priced by the "fixed" FX oracle", }, } `) }, ) test('rejects unsupported denominations for an address with no holdings', async () => { const response = await app({ fx: { oracle: fixed } }).v1.addresses[':address'].valuation.$get( { param: { address: emptyAccount() }, query: { 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).toMatchInlineSnapshot(` { "error": { "code": "query_invalid", "message": "Denomination "JPY" is not priced by the "fixed" FX oracle", }, } `) }) test('surfaces a 502 when balance reads are unavailable', async () => { const client = app({ rpc: { url: () => 'http://127.0.0.1:1' } }) const response = await client.v1.addresses[':address'].valuation.$get( { param: { address: emptyAccount() }, 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/valuation', { 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 malformed denominations', async () => { const response = await app().v1.addresses[':address'].valuation.$get( { param: { address: emptyAccount() }, query: { currency: 'DOLLARS' } }, 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"`) }) }) describe('OpenAPI', () => { test('publishes scoped valuation currency parameters', async () => { const application = TestApp.create({ auth: false }) const spec = (await (await application.request('/openapi.json')).json()) as OpenApiDocument const operations = [ spec.paths['/v1/addresses/{address}/activities']?.['get'], spec.paths['/v1/addresses/{address}/balances']?.['get'], spec.paths['/v1/exchange/quotes']?.['post'], spec.paths['/v1/exchange/swaps']?.['get'], spec.paths['/v1/transactions/receipts']?.['get'], spec.paths['/v1/transactions/{transactionHash}/activities']?.['get'], spec.paths['/v1/transactions/{transactionHash}/receipt']?.['get'], spec.paths['/v1/transfers']?.['get'], ] for (const operation of operations) { const names = operation?.parameters?.map((parameter) => parameter.name) expect(names).toContain('valuation.currency') expect(names).not.toContain('denomination') } const valuation = spec.paths['/v1/addresses/{address}/valuation']?.['get'] const names = valuation?.parameters?.map((parameter) => parameter.name) expect(names).toContain('currency') expect(names).not.toContain('valuation.currency') expect(names).not.toContain('denomination') }) })