import { Db, Schema } from 'tapimo/server' import type * as z from 'zod/mini' import * as Scope from '../../../Scope.js' import * as core_EarnVaults from '../../../db/tables/earnVaults.js' import * as RewardAccounts from '../../../db/tables/rewardAccounts.js' import * as RewardCampaigns from '../../../db/tables/rewardCampaigns.js' import * as RewardEligibilityAssociations from '../../../db/tables/rewardEligibilityAssociations.js' import * as TestApp from '../../../../test/App.js' import * as TestEarn from '../../../../test/Earn.js' import * as Runtime from '../../../../test/runtime.js' import * as Cursor from '../../../internal/Cursor.js' import * as EarnSharePrices from '../../../internal/EarnSharePrices.js' import type * as Log from '../../../internal/Log.js' import * as Store from '../../../internal/Store.js' import * as Tidx from '../../../internal/Tidx.js' import * as Ttl from '../../../internal/Ttl.js' import * as Value from '../../../internal/Value.js' import * as Viem from '../../../internal/Viem.js' import * as Earn from './earn.js' /** * One read cache for the whole file, so the chain-scoped rate anchor (boundary * blocks and the migration scan) is fetched once per run instead of once per * test, matching the colo-shared cache production runs on. The edge response * cache stays off so no test is served another test's body. */ const cache = { edge: false, store: Store.memory() } as const /** Fields every vault read enriches with live state, value, and rate data. */ type EnrichedVault = Pick< z.output, 'apy' | 'instantLiquidity' | 'sharePrice' | 'state' | 'tvl' > type OpenApiSchema = { $ref?: string } type OpenApiOperation = { description?: string operationId?: string parameters?: readonly { description?: string name?: string schema?: { const?: unknown enum?: readonly unknown[] examples?: readonly unknown[] pattern?: string } }[] responses?: Record< string, { content?: { 'application/json'?: { schema?: OpenApiSchema } }; description?: string } > } type OpenApiDocument = { components?: { schemas?: Record } paths: Record< string, { get?: OpenApiOperation post?: OpenApiOperation } > } function errorSchemas(document: OpenApiDocument, operation: OpenApiOperation | undefined) { return JSON.stringify( Object.entries(operation?.responses ?? {}) .filter(([status]) => !status.startsWith('2')) .map(([, response]) => { const schema = response.content?.['application/json']?.schema const name = schema?.$ref?.split('/').at(-1) return name ? document.components?.schemas?.[name] : schema }), ) } function successSchema(operation: OpenApiOperation | undefined) { return operation?.responses?.['200']?.content?.['application/json']?.schema } function resolveSchema( document: OpenApiDocument, value: unknown, seen: ReadonlySet = new Set(), ): unknown { if (Array.isArray(value)) return value.map((item) => resolveSchema(document, item, seen)) if (!value || typeof value !== 'object') return value const object = value as Record if (typeof object['$ref'] === 'string') { const name = object['$ref'].split('/').at(-1) if (!name || seen.has(name)) return value return resolveSchema(document, document.components?.schemas?.[name], new Set([...seen, name])) } return Object.fromEntries( Object.entries(object).map(([key, item]) => [key, resolveSchema(document, item, seen)]), ) } describe('schema.registerRewardEligibility.Body', () => { test('parses a reward eligibility registration', () => { expect( Earn.schema.registerRewardEligibility.Body.parse({ chainId: 4217, vaultAddress: `0x${'11'.repeat(20)}`, walletAddress: `0x${'22'.repeat(20)}`, }), ).toMatchInlineSnapshot(` { "chainId": 4217, "vaultAddress": "0x1111111111111111111111111111111111111111", "walletAddress": "0x2222222222222222222222222222222222222222", } `) }) test('rejects missing, invalid, and unknown fields', () => { expect({ invalidAddress: Earn.schema.registerRewardEligibility.Body.safeParse({ chainId: 4217, vaultAddress: `0x${'11'.repeat(20)}`, walletAddress: 'invalid', }).success, invalidChainId: Earn.schema.registerRewardEligibility.Body.safeParse({ chainId: 0, vaultAddress: `0x${'11'.repeat(20)}`, walletAddress: `0x${'22'.repeat(20)}`, }).success, invalidTransactionHash: Earn.schema.registerRewardEligibility.Body.safeParse({ chainId: 4217, transactionHash: '0x1234', vaultAddress: `0x${'11'.repeat(20)}`, walletAddress: `0x${'22'.repeat(20)}`, }).success, missing: Earn.schema.registerRewardEligibility.Body.safeParse({ chainId: 4217, vaultAddress: `0x${'11'.repeat(20)}`, }).success, unknown: Earn.schema.registerRewardEligibility.Body.safeParse({ chainId: 4217, source: 'other', sourceInteractionId: 'interaction_1', transactionHash: `0x${'33'.repeat(32)}`, vaultAddress: `0x${'11'.repeat(20)}`, walletAddress: `0x${'22'.repeat(20)}`, }).success, }).toEqual({ invalidAddress: false, invalidChainId: false, invalidTransactionHash: false, missing: false, unknown: false, }) }) }) describe('schema.getRewardEligibility.Query', () => { test('accepts up to 100 items per page', () => { expect([ Earn.schema.getRewardEligibility.Query.parse({ chainId: 4217, limit: '100', vaultAddress: `0x${'11'.repeat(20)}`, }).limit, Earn.schema.getRewardEligibility.Query.parse({ chainId: 4217, limit: '101', vaultAddress: `0x${'11'.repeat(20)}`, }).limit, ]).toMatchInlineSnapshot(` [ 100, 100, ] `) }) }) describe('POST /v1/earn/rewards/eligibility', () => { const body = { chainId: Viem.chainId.mainnet, transactionHash: Schema.Hash.parse(`0x${'33'.repeat(32)}`), vaultAddress: `0x${'Aa'.repeat(20)}`, walletAddress: `0x${'Bb'.repeat(20)}`, } const legacyKey = { id: 'key_rewards_writer', orgId: 'org_test', scopes: ['rewards:write'], token: 'secret_rewards_writer', } satisfies TestApp.kvStore.Key const superAdminSecret = 'secret_rewards_super_admin' const auth = { headers: { authorization: `Bearer ${superAdminSecret}` } } test('is omitted from public OpenAPI documentation', async () => { const app = TestApp.create() // SAFETY: `/openapi.json` is the app's generated OpenAPI document. const document = (await (await app.request('/openapi.json')).json()) as OpenApiDocument expect(document.paths['/v1/earn/rewards/eligibility']?.post).toBeUndefined() }) test('requires authentication', async () => { const response = await TestApp.create().request('/v1/earn/rewards/eligibility', { body: JSON.stringify(body), headers: { 'content-type': 'application/json' }, method: 'POST', }) expect(response.status).toBe(401) }) test('rejects an ordinary authenticated key', async () => { const response = await TestApp.create().request('/v1/earn/rewards/eligibility', { ...TestApp.auth, body: JSON.stringify(body), headers: { ...TestApp.auth.headers, 'content-type': 'application/json' }, method: 'POST', }) expect(response.status).toBe(403) }) test('rejects a legacy rewards writer key', async () => { const response = await TestApp.create({ auth: { keys: [legacyKey] } }).request( '/v1/earn/rewards/eligibility', { body: JSON.stringify(body), headers: { authorization: `Bearer ${legacyKey.token}`, 'content-type': 'application/json', }, method: 'POST', }, ) expect(response.status).toBe(403) }) test('accepts a provisioned wildcard key', async () => { const key = { id: 'key_rewards_operator', orgId: 'org_tempo', scopes: [Scope.wildcard], token: 'secret_rewards_operator', } satisfies TestApp.kvStore.Key const response = await TestApp.create({ auth: { keys: [key] } }).request( '/v1/earn/rewards/eligibility', { body: JSON.stringify(body), headers: { authorization: `Bearer ${key.token}`, 'content-type': 'application/json', }, method: 'POST', }, ) expect(response.status).toBe(200) }) test('persists the first write and idempotently refreshes a replay', async () => { const db = TestApp.database() const app = TestApp.create({ auth: { superAdmin: { secret: superAdminSecret } }, db }) const request = (input = body) => app.request('/v1/earn/rewards/eligibility', { ...auth, body: JSON.stringify(input), headers: { ...auth.headers, 'content-type': 'application/json' }, method: 'POST', }) const first = await TestApp.json( await request(), Earn.schema.registerRewardEligibility.Response, ) const replay = await TestApp.json( await request({ ...body, transactionHash: `0x${'44'.repeat(32)}`, }), Earn.schema.registerRewardEligibility.Response, ) expect({ first, replay }).toMatchObject({ first: { association: { vaultAddress: body.vaultAddress.toLowerCase(), walletAddress: body.walletAddress.toLowerCase(), }, created: true, }, replay: { association: { firstRegisteredAt: first.association.firstRegisteredAt, transactionHash: `0x${'44'.repeat(32)}`, }, created: false, }, }) }) test('rejects invalid and unsupported chain input', async () => { const app = TestApp.create({ auth: { superAdmin: { secret: superAdminSecret } } }) const invalid = await app.request('/v1/earn/rewards/eligibility', { ...auth, body: JSON.stringify({ ...body, walletAddress: 'invalid' }), headers: { ...auth.headers, 'content-type': 'application/json' }, method: 'POST', }) const unsupported = await app.request('/v1/earn/rewards/eligibility', { ...auth, body: JSON.stringify({ ...body, chainId: 9_999_999 }), headers: { ...auth.headers, 'content-type': 'application/json' }, method: 'POST', }) expect([invalid.status, unsupported.status]).toEqual([400, 400]) }) test('accepts supported chains without data upstreams', async () => { const chainId = 31_337 const response = await TestApp.create({ auth: { superAdmin: { secret: superAdminSecret } }, rpc: { url: {} }, supportedChainIds: [chainId], tidx: { baseUrl: {} }, }).request('/v1/earn/rewards/eligibility', { ...auth, body: JSON.stringify({ ...body, chainId }), headers: { ...auth.headers, 'content-type': 'application/json' }, method: 'POST', }) expect(response.status).toBe(200) }) test('accepts configured Zones for super admins', async () => { const zone = TestApp.zone({ chainId: 421_700_001, rpcUrl: 'https://zone.example', }) const response = await TestApp.create({ auth: { superAdmin: { secret: superAdminSecret } }, zones: [zone], }).request('/v1/earn/rewards/eligibility', { ...auth, body: JSON.stringify({ ...body, chainId: zone.id }), headers: { ...auth.headers, 'content-type': 'application/json' }, method: 'POST', }) expect(response.status).toBe(200) }) test('surfaces database failures as operational errors', async () => { const app = TestApp.create({ auth: { superAdmin: { secret: superAdminSecret } }, db: () => { throw new Error('database unavailable') }, }) const response = await app.request('/v1/earn/rewards/eligibility', { ...auth, body: JSON.stringify(body), headers: { ...auth.headers, 'content-type': 'application/json' }, method: 'POST', }) expect(response.status).toBe(500) }) }) describe('GET /v1/earn/rewards/eligibility', () => { const readKey = { id: 'key_rewards_reader', orgId: 'org_test', scopes: ['rewards:read'], token: 'secret_rewards_reader', } satisfies TestApp.kvStore.Key const vaultAddress = Schema.Address.parse(`0x${'Aa'.repeat(20)}`) const input = { chainId: Viem.chainId.mainnet, transactionHash: Schema.Hash.parse(`0x${'33'.repeat(32)}`), vaultAddress, walletAddress: Schema.Address.parse(`0x${'Bb'.repeat(20)}`), } as const test('is omitted from public OpenAPI documentation', async () => { const app = TestApp.create({ auth: { keys: [readKey] } }) // SAFETY: `/openapi.json` is the app's generated OpenAPI document. const document = (await (await app.request('/openapi.json')).json()) as OpenApiDocument expect(document.paths['/v1/earn/rewards/eligibility']?.get).toBeUndefined() }) test('requires the read scope', async () => { const response = await TestApp.create().request( `/v1/earn/rewards/eligibility?chainId=4217&vaultAddress=${vaultAddress}`, TestApp.auth, ) expect(response.status).toBe(403) }) test('reads supported chains without data upstreams', async () => { const chainId = 31_337 const db = TestApp.database() await RewardEligibilityAssociations.upsert(db, { ...input, chainId }) const response = await TestApp.create({ auth: { keys: [readKey] }, db, rpc: { url: {} }, supportedChainIds: [chainId], tidx: { baseUrl: {} }, }).request(`/v1/earn/rewards/eligibility?chainId=${chainId}&vaultAddress=${vaultAddress}`, { headers: { authorization: `Bearer ${readKey.token}` }, }) expect(response.status).toBe(200) }) test('rejects sandbox reads from mainnet-backed Zones', async () => { const zone = TestApp.zone({ chainId: 421_700_001, rpcUrl: 'https://mainnet-zone.example', sourceChainId: Viem.chainId.mainnet, }) const key = { ...readKey, environment: 'sandbox', scopes: [...readKey.scopes, `zone:${zone.id}:read`], } satisfies TestApp.kvStore.Key const response = await TestApp.create({ auth: { keys: [key] }, zones: [zone] }).request( `/v1/earn/rewards/eligibility?chainId=${zone.id}&vaultAddress=${vaultAddress}`, { headers: { authorization: `Bearer ${key.token}` } }, ) expect(response.status).toBe(403) }) test('rejects sandbox reads from Zones without a source chain', async () => { const { sourceId: _sourceId, ...zone } = TestApp.zone({ chainId: 421_700_001, rpcUrl: 'https://unknown-zone.example', }) const key = { ...readKey, environment: 'sandbox', scopes: [...readKey.scopes, `zone:${zone.id}:read`], } satisfies TestApp.kvStore.Key const response = await TestApp.create({ auth: { keys: [key] }, zones: [zone] }).request( `/v1/earn/rewards/eligibility?chainId=${zone.id}&vaultAddress=${vaultAddress}`, { headers: { authorization: `Bearer ${key.token}` } }, ) expect(response.status).toBe(403) }) test('returns checkpointed pages that exclude later registrations', async () => { const db = TestApp.database() const app = TestApp.create({ auth: { keys: [readKey] }, db }) const wallets = ['bb', 'cc', 'dd', 'ee', 'ff', '11'] for (const wallet of wallets) await RewardEligibilityAssociations.upsert(db, { ...input, walletAddress: Schema.Address.parse(`0x${wallet.repeat(20)}`), }) const request = (cursor?: string) => { const query = new URLSearchParams({ chainId: String(input.chainId), limit: '5', vaultAddress, }) if (cursor) query.set('cursor', cursor) return app.request(`/v1/earn/rewards/eligibility?${query}`, { headers: { authorization: `Bearer ${readKey.token}` }, }) } const first = await TestApp.json(await request(), Earn.schema.getRewardEligibility.Response) await RewardEligibilityAssociations.upsert(db, { ...input, walletAddress: Schema.Address.parse(`0x${'22'.repeat(20)}`), }) if (!first.nextCursor) throw new Error('Expected another eligibility page.') const next = await TestApp.json( await request(first.nextCursor), Earn.schema.getRewardEligibility.Response, ) expect({ first, next }).toMatchObject({ first: { chainId: input.chainId, data: wallets.slice(0, 5).map((wallet) => `0x${wallet.repeat(20)}`), vaultAddress, }, next: { chainId: input.chainId, data: [`0x${'11'.repeat(20)}`], nextCursor: null, vaultAddress, }, }) }) }) describe('schema.getEarnVaults.Query', () => { test('parses collection filters', () => { const query = Earn.schema.getEarnVaults.Query.parse({ capability: 'deposit,exactWithdraw', 'engine.type': 'erc4626', include: 'access,apy,capabilities,zone,zones,token.logoUri,tvl', limit: '10', }) expect(query.capability).toStrictEqual(['deposit', 'exactWithdraw']) expect(query['engine.type']).toBe('erc4626') expect(query.include).toStrictEqual([ 'access', 'apy', 'capabilities', 'zone', 'zones', 'token.logoUri', 'tvl', ]) expect(query.limit).toBe(10) }) test('defaults the page size', () => { expect(Earn.schema.getEarnVaults.Query.parse({}).limit).toBe(10) }) test('rejects obsolete and detail-only parameters', () => { expect(() => Earn.schema.getEarnVaults.Query.parse({ 'engine.kind': 'erc4626' })).toThrow() expect(() => Earn.schema.getEarnVaults.Query.parse({ status: 'live-demo' })).toThrow() expect(() => Earn.schema.getEarnVault.Query.parse({ gateway: '0x0000000000000000000000000000000000000001', }), ).toThrow() }) test('accepts every selectable rate window across every vault read', () => { expect([ // An omitted window stays absent, so no rate is measured. Earn.schema.getEarnVaults.Query.parse({})['apy.window'], Earn.schema.getEarnVaults.Query.parse({ 'apy.window': '1h' })['apy.window'], Earn.schema.getEarnVaults.Query.parse({ 'apy.window': '1d' })['apy.window'], Earn.schema.getVerifiedEarnVaults.Query.parse({ 'apy.window': '7d' })['apy.window'], Earn.schema.getEarnVault.Query.parse({ 'apy.window': '30d' })['apy.window'], ]).toStrictEqual([undefined, '1h', '1d', '7d', '30d']) }) }) describe('parseIndexedBlockAtOrBefore', () => { test('rejects an empty upstream result before classifying the boundary', () => { expect(() => Earn.parseIndexedBlockAtOrBefore({ asOf: '2026-07-21T09:00:00.000Z', rows: [], }), ).toThrowErrorMatchingInlineSnapshot( `[Error: TIDX returned no indexed head for a historical Earn position.]`, ) }) test('classifies a missing boundary only after validating the indexed head', () => { expect(() => Earn.parseIndexedBlockAtOrBefore({ asOf: '2026-07-21T09:00:00.000Z', rows: [{ kind: 'head', num: 10, timestamp: '2026-07-21T10:00:00.000Z' }], }), ).toThrowErrorMatchingInlineSnapshot( `[Earn.IndexedBlockNotFoundError: No indexed block exists at or before 2026-07-21T09:00:00.000Z.]`, ) }) }) describe('schema.getEarnVaultSharePrices.Query', () => { test('defaults to daily observations and accepts a 31-point range', () => { expect( Earn.schema.getEarnVaultSharePrices.Query.parse({ from: '2026-01-01T00:00:00Z', to: '2026-01-31T00:00:00Z', }).interval, ).toBe('day') }) test('rejects reversed and oversized ranges', () => { expect({ oversized: Earn.schema.getEarnVaultSharePrices.Query.safeParse({ from: '2026-01-01T00:00:00Z', to: '2026-02-01T00:00:00Z', }).success, reversed: Earn.schema.getEarnVaultSharePrices.Query.safeParse({ from: '2026-01-02T00:00:00Z', to: '2026-01-01T00:00:00Z', }).success, }).toEqual({ oversized: false, reversed: false }) }) }) describe('schema.getEarnVaultPosition.Query', () => { test('accepts offset-bearing ISO 8601 timestamps', () => { expect( Earn.schema.getEarnVaultPosition.Query.parse({ asOf: '2026-07-21T10:00:00+01:00', }).asOf, ).toBe('2026-07-21T10:00:00+01:00') }) }) describe('schema.getEarnAddressPositions.Query', () => { test('parses collection filters', () => { const query = Earn.schema.getEarnAddressPositions.Query.parse({ limit: '5', 'valuation.currency': 'usd', verified: 'true', }) expect(query.limit).toBe(5) expect(query['valuation.currency']).toBe('USD') expect(query.verified).toBe(true) }) test('defaults the page size', () => { expect(Earn.schema.getEarnAddressPositions.Query.parse({}).limit).toBe(10) }) test('rejects vault-collection filters', () => { expect(() => Earn.schema.getEarnAddressPositions.Query.parse({ capability: 'deposit' }), ).toThrow() expect(() => Earn.schema.getEarnAddressPositions.Query.parse({ asset: `0x${'aa'.repeat(20)}` }), ).toThrow() }) }) describe('schema.getEarnVaultPosition.Response', () => { test('accepts distinct position and value tokens', () => { const position = { account: TestEarn.positionAddress, assetAllowance: '0', assetBalance: '250000000', assetToken: TestEarn.assetTokenAddress, chainId: TestEarn.chain.id, id: TestEarn.positionAddress, shareAllowance: '0', shareBalance: '99500000', shareToken: `0x20c0${'11'.repeat(18)}`, value: '100000000', valueToken: TestEarn.assetTokenAddress, } expect( Earn.schema.getEarnVaultPosition.Response.safeParse({ ...position, chainId: TestEarn.zone.id, valueToken: `0x20c0${'22'.repeat(18)}`, }).success, ).toBe(true) }) }) describe('schema.getEarnVaultEarnings.Response', () => { test('enforces status-specific earnings fields', () => { const response = { account: TestEarn.earningsAddress, assetToken: TestEarn.assetTokenAddress, currentValue: '1000000', id: TestEarn.earningsAddress, } expect({ active: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, activeEarnings: '0', period: 'active', status: 'complete', }).success, activeWithLifetimeEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, lifetimeEarnings: '0', period: 'active', status: 'complete', }).success, completeWithoutEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, period: 'lifetime', status: 'complete', }).success, incomplete: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, period: 'active', status: 'incomplete_cost_basis', }).success, incompleteWindow: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, period: '30d', status: 'incomplete_cost_basis', }).success, incompleteWithEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, lifetimeEarnings: '0', period: 'lifetime', status: 'incomplete_cost_basis', }).success, lifetime: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, lifetimeEarnings: '0', period: 'lifetime', status: 'complete', totalDeposited: '1000000', totalWithdrawn: '500000', }).success, pending: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, period: 'lifetime', status: 'pending_redemption', }).success, pendingActive: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, period: 'active', status: 'pending_redemption', }).success, pendingWindow: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, period: '30d', status: 'pending_redemption', }).success, pendingWithEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, period: 'lifetime', status: 'pending_redemption', totalDeposited: '1000000', totalWithdrawn: '500000', }).success, window: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, period: '30d', status: 'complete', windowEarnings: '0', }).success, windowWithActiveEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, activeEarnings: '0', period: '30d', status: 'complete', }).success, windowWithoutEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({ ...response, period: '30d', status: 'complete', }).success, }).toMatchInlineSnapshot(` { "active": true, "activeWithLifetimeEarnings": false, "completeWithoutEarnings": false, "incomplete": true, "incompleteWindow": true, "incompleteWithEarnings": false, "lifetime": true, "pending": false, "pendingActive": false, "pendingWindow": true, "pendingWithEarnings": true, "window": true, "windowWithActiveEarnings": false, "windowWithoutEarnings": false, } `) }) }) describe('alignZoneSourceBlock', () => { test('caps Zone reports at the L1 portal watermark', () => { expect([ Earn.alignZoneSourceBlock({ reported: 122n, verified: 123n }), Earn.alignZoneSourceBlock({ reported: 124n, verified: 123n }), Earn.alignZoneSourceBlock({ verified: 123n }), ]).toStrictEqual([122n, 123n, 123n]) }) }) describe('serializeVault', () => { test('includes valuations and uses the reward target as the net APY floor', () => { const assetToken = { address: TestEarn.assetTokenAddress, currency: 'USD', decimals: 6, id: TestEarn.assetTokenAddress, name: 'Bridge Test PATHUSD', symbol: 'btPATHUSD', verified: true, } as const const shareTokenAddress = `0x20c0${'11'.repeat(18)}` as const const options = { apy: { asOf: '2026-08-26T14:00:00.000Z', methodology: 'share-price-growth-annualized:v1', net: '0.041200', window: '7d', }, assetToken, discovery: { assetToken, chainId: TestEarn.chain.id, engine: { address: TestEarn.vaultAddress, type: 'erc4626', venue: null }, instantLiquidity: '500000000', sharePrice: '1000000', shareToken: { address: shareTokenAddress, currency: 'USD', decimals: 6, name: 'Bridge Test PATHUSD Earn', symbol: 'btPATHUSDE', }, state: { depositsPaused: false, engineShares: '1000000000', feesActive: true, isAccountingAligned: true, openRedeemRequestCount: 0, totalAssets: '1000000000', totalEarnShares: '1000000000', }, vaultAddress: TestEarn.vaultAddress, }, include: ['apy', 'tvl'], instantLiquidityValue: { amount: '500000000', currency: 'USD', decimals: 6, formatted: '500', }, rewards: [ { asset: { address: shareTokenAddress, chainId: TestEarn.chain.id, decimals: 6, }, calculatedThrough: 1788134700, campaignId: 'rewards-test-moderato-boost-rewards', currentRewardsAprBps: 195, endsAt: 1790812800, startsAt: 1788134400, targetTotalAprBps: 700, }, ], shareToken: { address: shareTokenAddress, currency: 'USD', decimals: 6, id: shareTokenAddress, name: 'Bridge Test PATHUSD Earn', symbol: 'btPATHUSDE', verified: false, }, tvl: { amount: '1000000000', currency: 'USD', decimals: 6, formatted: '1000' }, zoneRoutes: [], zones: [], } satisfies Earn.serializeVault.Options const vault = Earn.serializeVault(options) expect(vault.instantLiquidityValue).toStrictEqual({ amount: '500000000', currency: 'USD', decimals: 6, formatted: '500', }) expect(vault.apy).toMatchInlineSnapshot(` { "asOf": "2026-08-26T14:00:00.000Z", "methodology": "share-price-growth-annualized:v1", "net": "0.0700", "rewards": [ { "apr": "0.0195", "asset": { "address": "0x20c0111111111111111111111111111111111111", "chainId": 42431, "decimals": 6, }, "calculatedThrough": 1788134700, "campaignId": "rewards-test-moderato-boost-rewards", "endsAt": 1790812800, "startsAt": 1788134400, "targetTotalApr": "0.0700", }, ], "vault": "0.041200", "window": "7d", } `) const reward = options.rewards[0] if (!reward) throw new Error('Expected reward fixture.') // Rewards cannot produce a complete APY without a measured vault component. expect( Earn.serializeVault({ ...options, apy: null, rewards: [{ ...reward, currentRewardsAprBps: 700 }], }).apy, ).toBeNull() expect( Earn.serializeVault({ ...options, apy: null, rewards: [{ ...reward, currentRewardsAprBps: null }], }).apy, ).toBeNull() expect(Earn.serializeVault({ ...options, apy: null, rewards: undefined }).apy).toBeNull() expect( Earn.serializeVault({ ...options, rewards: [ ...options.rewards, { ...reward, campaignId: 'second-reward', currentRewardsAprBps: 5, targetTotalAprBps: 800, }, ], }).apy?.net, ).toBe('0.0800') expect(Earn.serializeVault({ ...options, rewards: undefined }).apy).toMatchObject({ net: '0.041200', vault: '0.041200', }) expect( Earn.serializeVault({ ...options, rewards: [{ ...reward, currentRewardsAprBps: null }], }).apy, ).toMatchObject({ net: '0.0700', rewards: [{ apr: null, targetTotalApr: '0.0700' }], vault: '0.041200', }) expect( Earn.serializeVault({ ...options, apy: { ...options.apy, net: '0.080000' }, }).apy?.net, ).toBe('0.080000') vi.useFakeTimers({ now: new Date('2026-09-01T01:30:00.000Z'), toFake: ['Date'] }) try { expect( Earn.serializeVault({ ...options, rewards: [{ ...reward, startsAt: 1_788_229_800 }], }).apy, ).toMatchObject({ net: '0.041200', rewards: [{ targetTotalApr: '0.0700' }], vault: '0.041200', }) } finally { vi.useRealTimers() } }) }) describe('schema.getEarnVaultEarnings.Query', () => { test('defaults to lifetime and accepts active and trailing earnings', () => { expect([ Earn.schema.getEarnVaultEarnings.Query.parse({}).period, Earn.schema.getEarnVaultEarnings.Query.parse({ period: '30d' }).period, Earn.schema.getEarnVaultEarnings.Query.parse({ period: 'active' }).period, ]).toStrictEqual(['lifetime', '30d', 'active']) }) }) describe('calculateWindowEarnings', () => { test('adjusts snapshot growth for deposits and realized assets', () => { expect( Earn.calculateWindowEarnings({ deposited: 50n, endingValue: 130n, finalized: 10n, openingValue: 100n, redeemed: 15n, withdrewExact: 5n, }), ).toBe(10n) }) }) describe('calculateActiveCostBasis', () => { test('ignores zero-share transfers', () => { expect( Earn.calculateActiveCostBasis({ actions: [ { assets: 100n, kind: 'deposit', shares: 100n }, { kind: 'unknown', shares: 0n }, { kind: 'redeem', shares: 0n }, ], shareBalance: 100n, }), ).toBe(100n) }) test('allocates weighted-average basis across partial exits', () => { expect( Earn.calculateActiveCostBasis({ actions: [ { assets: 100n, kind: 'deposit', shares: 100n }, { assets: 300n, kind: 'deposit', shares: 100n }, { kind: 'redeem', shares: 100n }, ], shareBalance: 100n, }), ).toBe(200n) }) test('allocates zero-cost reward shares across partial exits without erasing unknown basis', () => { const reward = { assets: 0n, kind: 'deposit', shares: 20n } as const expect([ Earn.calculateActiveCostBasis({ actions: [ { assets: 100n, kind: 'deposit', shares: 100n }, reward, { kind: 'redeem', shares: 60n }, ], shareBalance: 60n, }), Earn.calculateActiveCostBasis({ actions: [{ kind: 'unknown', shares: 100n }, reward], shareBalance: 120n, }), ]).toMatchInlineSnapshot(` [ 50n, undefined, ] `) }) test('calculates remaining basis while an async redemption is pending', () => { expect( Earn.calculateActiveCostBasis({ actions: [ { assets: 100n, kind: 'deposit', shares: 100n }, { kind: 'request', requestId: 'request-1', shares: 40n }, ], shareBalance: 60n, }), ).toBe(60n) }) test('restores removed basis when an async redemption is cancelled', () => { expect( Earn.calculateActiveCostBasis({ actions: [ { assets: 100n, kind: 'deposit', shares: 100n }, { kind: 'request', requestId: 'request-1', shares: 40n }, { kind: 'cancel', requestId: 'request-1', shares: 40n }, ], shareBalance: 100n, }), ).toBe(100n) }) test('recovers after unknown-basis shares fully exit', () => { expect( Earn.calculateActiveCostBasis({ actions: [ { kind: 'unknown', shares: 100n }, { kind: 'redeem', shares: 100n }, { assets: 200n, kind: 'deposit', shares: 100n }, ], shareBalance: 100n, }), ).toBe(200n) }) test('rejects remaining shares with unknown cost basis', () => { expect( Earn.calculateActiveCostBasis({ actions: [ { assets: 100n, kind: 'deposit', shares: 100n }, { kind: 'unknown', shares: 100n }, { kind: 'redeem', shares: 100n }, ], shareBalance: 100n, }), ).toBeUndefined() }) test('rejects history that does not reconcile to the indexed share balance', () => { expect( Earn.calculateActiveCostBasis({ actions: [{ assets: 100n, kind: 'deposit', shares: 100n }], shareBalance: 99n, }), ).toBeUndefined() }) }) describe('discoveryBatchLimit', () => { test('fetches one extra candidate without live filters', () => { expect( Earn.discoveryBatchLimit({ filtered: false, limit: 10, matches: 0, remaining: 250, }), ).toBe(11) }) test('overfetches filtered candidates within the scan limit', () => { expect([ Earn.discoveryBatchLimit({ filtered: true, limit: 10, matches: 0, remaining: 250, }), Earn.discoveryBatchLimit({ filtered: true, limit: 200, matches: 0, remaining: 250, }), ]).toStrictEqual([22, 50]) }) }) describe('GET /v1/earn/vaults/verified', () => { test('registers the static verified path before the vault path', async () => { const app = TestApp.create({ auth: false, cache }) const response = await app.request('/v1/earn/vaults/verified') expect(response.status).toBe(200) expect(await TestApp.json(response, Earn.schema.getVerifiedEarnVaults.Response)).toStrictEqual({ data: [], nextCursor: null, }) }) test.runIf(Runtime.get().mode === 'testnet')( 'folds scoped Zone routes into vault capabilities', async () => { const db = TestApp.database() await core_EarnVaults.upsert(db, TestEarn.zoneVerifiedVault) const otherZoneId = TestEarn.zone.id + 1 const scopedKey = { ...TestApp.key, id: 'key_earn_zones', scopes: ['data:read', `zone:${otherZoneId}:read`, `zone:${TestEarn.zone.id}:read`], token: 'secret_earn_zones', } satisfies TestApp.kvStore.Key const unrelatedKey = { ...TestApp.key, id: 'key_earn_other_zone', scopes: ['data:read', `zone:${otherZoneId}:read`], token: 'secret_earn_other_zone', } satisfies TestApp.kvStore.Key const writeOnlyKey = { ...TestApp.key, id: 'key_earn_write_zone', scopes: ['data:read', `zone:${TestEarn.zone.id}:write`], token: 'secret_earn_write_zone', } satisfies TestApp.kvStore.Key const wildcardKey = { ...TestApp.key, id: 'key_earn_all_zones', scopes: ['*'], token: 'secret_earn_all_zones', } satisfies TestApp.kvStore.Key const app = TestApp.create({ auth: { keys: [scopedKey, unrelatedKey, wildcardKey, writeOnlyKey] }, cache, db, zones: [TestEarn.zone], }) const base = `/v1/earn/vaults/verified?chainId=${TestEarn.chain.id}&limit=5` const included = `${base}&include=capabilities,zone,zones` const filtered = `${included}&capability=privateRouting` // Fill the query-credential cache entry before checking callers without its Zone scope. const scopedResponse = await app.request(`${filtered}&key=${scopedKey.token}`) const [ defaultResponse, unrelatedResponse, publicResponse, wildcardResponse, writeOnlyResponse, ] = await Promise.all([ app.request(base, { headers: { 'tempo-api-key': scopedKey.token } }), app.request(included, { headers: { 'tempo-api-key': unrelatedKey.token } }), app.request(filtered), app.request(filtered, { headers: { 'tempo-api-key': wildcardKey.token } }), app.request(filtered, { headers: { 'tempo-api-key': writeOnlyKey.token } }), ]) expect([ defaultResponse.status, scopedResponse.status, unrelatedResponse.status, publicResponse.status, wildcardResponse.status, writeOnlyResponse.status, ]).toStrictEqual([200, 200, 200, 200, 200, 200]) const [defaultBody, scopedBody, unrelatedBody, publicBody, wildcardBody, writeOnlyBody] = await Promise.all([ TestApp.json(defaultResponse, Earn.schema.getVerifiedEarnVaults.Response), TestApp.json(scopedResponse, Earn.schema.getVerifiedEarnVaults.Response), TestApp.json(unrelatedResponse, Earn.schema.getVerifiedEarnVaults.Response), TestApp.json(publicResponse, Earn.schema.getVerifiedEarnVaults.Response), TestApp.json(wildcardResponse, Earn.schema.getVerifiedEarnVaults.Response), TestApp.json(writeOnlyResponse, Earn.schema.getVerifiedEarnVaults.Response), ]) expect(defaultBody.data[0]).not.toHaveProperty('zone') expect(defaultBody.data[0]).not.toHaveProperty('zones') expect(unrelatedBody.data[0]).toMatchObject({ capabilities: { privateRouting: true, routerSwaps: true }, zone: null, zones: [], }) expect(publicBody.data[0]).toMatchObject({ capabilities: { privateRouting: true, routerSwaps: true }, zone: null, zones: [], }) expect(writeOnlyBody.data[0]).toMatchObject({ zone: null, zones: [] }) const vault = scopedBody.data[0] expect(vault?.capabilities).toMatchObject({ privateRouting: true, routerSwaps: true }) expect(vault?.zones).toHaveLength(1) expect(vault?.zones?.[0]).toMatchObject(TestEarn.zoneRoute) expect(vault?.zone).toStrictEqual( vault?.zones?.[0] ? { chainId: vault.zones[0].chainId, inputTokens: vault.zones[0].inputTokens, name: vault.zones[0].name, outputTokens: vault.zones[0].outputTokens, } : null, ) expect(wildcardBody.data[0]).toStrictEqual(vault) }, 90_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns null for an inaccessible legacy Zone route', async () => { const db = TestApp.database() await core_EarnVaults.upsert(db, { ...TestEarn.zoneVerifiedVault, privateInputTokens: [TestEarn.assetTokenAddress], privateOutputTokens: [TestEarn.assetTokenAddress], zones: [], }) const zone = { ...TestEarn.zone, contracts: { ...TestEarn.zone.contracts, earnRouter: { address: TestEarn.zoneRoute.earnRouter }, }, } const app = TestApp.create({ auth: false, cache, db, zones: [zone] }) const query = `chainId=${TestEarn.chain.id}&include=zone` const [detail, list] = await Promise.all([ app.request(`/v1/earn/vaults/${TestEarn.zoneVaultAddress}?${query}`), app.request(`/v1/earn/vaults/verified?${query}`), ]) expect([detail.status, list.status]).toStrictEqual([200, 200]) expect((await TestApp.json(detail, Earn.schema.getEarnVault.Response)).zone).toBeNull() expect( (await TestApp.json(list, Earn.schema.getVerifiedEarnVaults.Response)).data[0]?.zone, ).toBeNull() }, 90_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns curated fields for a public-only vault', async () => { const db = TestApp.database() const record = await core_EarnVaults.upsert(db, TestEarn.verifiedVault) const app = TestApp.create({ auth: false, cache, db, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/vaults/verified?chainId=${TestEarn.verifiedVault.chainId}&include=access,capabilities,zone&limit=5`, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Earn.schema.getVerifiedEarnVaults.Response) expect(body.nextCursor).toBeNull() expect(body.data).toHaveLength(1) const [vault] = body.data expect(vault).toBeDefined() if (!vault) return expect(vault.slug).toBe(record.slug) expect(vault).not.toHaveProperty('gateway') // The rate and the valuation are include-gated, and neither was requested. expect(vault).not.toHaveProperty('apy') expect(vault).not.toHaveProperty('tvl') const capabilities = vault.capabilities expect(capabilities).toBeDefined() if (!capabilities) return expect({ access: vault.access, assetTokenVerified: vault.assetToken.verified, description: vault.description, engine: vault.engine, id: vault.id, label: vault.label, privateRouting: capabilities.privateRouting, routerSwaps: capabilities.routerSwaps, shareTokenVerified: vault.shareToken.verified, vaultAddress: vault.vaultAddress, verified: vault.verified, zone: vault.zone, }).toMatchInlineSnapshot(` { "access": { "status": "allowlisted", }, "assetTokenVerified": false, "description": "Bridge test PATHUSD deposited into the canonical Earn v1 stack.", "engine": { "address": "0xd85fc943333a6cd7c0e3391acd54fc4e572f0baf", "type": "erc4626", "venue": "0xa8b4f0e69cd4b0e56b676343f02acd8c3b355322", }, "id": "0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a", "label": "btPATHUSD Earn", "privateRouting": false, "routerSwaps": false, "shareTokenVerified": false, "vaultAddress": "0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a", "verified": true, "zone": null, } `) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'hides stale private fields without a configured router', async () => { const db = TestApp.database() await core_EarnVaults.upsert(db, { ...TestEarn.verifiedVault, privateInputTokens: [TestEarn.assetTokenAddress], privateOutputTokens: [TestEarn.assetTokenAddress], }) const app = TestApp.create({ auth: false, cache, db, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/vaults/verified?chainId=${TestEarn.verifiedVault.chainId}&include=access,capabilities,zone&limit=5`, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Earn.schema.getVerifiedEarnVaults.Response) expect(body.data[0]).toMatchObject({ capabilities: { privateRouting: false, routerSwaps: false }, verified: true, zone: null, }) expect(body.data[0]).not.toHaveProperty('gateway') }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'reuses live discovery across public endpoints', async () => { const db = TestApp.database() await core_EarnVaults.upsert(db, TestEarn.verifiedVault) // Its own cache: the assertion needs a cold discovery followed by a warm one. const app = TestApp.create({ auth: false, cache: { edge: false, store: Store.memory() }, db, zones: [TestEarn.zone], }) const detail = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}&include=access,capabilities,zone`, ) const verified = await app.request( `/v1/earn/vaults/verified?chainId=${TestEarn.chain.id}&include=access,capabilities,zone&limit=5`, ) expect([detail.status, verified.status]).toStrictEqual([200, 200]) expect(detail.headers.get('server-timing')).toContain('earn_vault_discovery') expect(verified.headers.get('server-timing')).not.toContain('earn_vault_discovery') }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'shares live enrichment across all three vault reads', async () => { const db = TestApp.database() await core_EarnVaults.upsert(db, TestEarn.verifiedVault) const app = TestApp.create({ auth: false, cache, db, zones: [TestEarn.zone] }) const cursor = Cursor.encode([ TestEarn.indexedDeployment.blockNumber, TestEarn.indexedDeployment.logIndex, ]) const query = `chainId=${TestEarn.chain.id}&apy.window=1h&include=tvl` const [all, verified, detail] = await Promise.all([ app.request(`/v1/earn/vaults?${query}&cursor=${encodeURIComponent(cursor)}&limit=5`), app.request(`/v1/earn/vaults/verified?${query}&limit=5`), app.request(`/v1/earn/vaults/${TestEarn.vaultAddress}?${query}`), ]) expect([all.status, verified.status, detail.status]).toStrictEqual([200, 200, 200]) for (const response of [all, verified, detail]) expect(response.headers.get('cache-control')).toBe( 'private, max-age=30, stale-while-revalidate=120', ) const [allBody, verifiedBody, vault] = await Promise.all([ TestApp.json(all, Earn.schema.getEarnVaults.Response), TestApp.json(verified, Earn.schema.getVerifiedEarnVaults.Response), TestApp.json(detail, Earn.schema.getEarnVault.Response), ]) const enriched = ({ apy, instantLiquidity, sharePrice, state, tvl }: EnrichedVault) => ({ apy, instantLiquidity, sharePrice, state, tvl, }) const rows = [allBody, verifiedBody].map((body) => body.data.find((candidate) => candidate.id === vault.id), ) expect(rows.every((row) => row !== undefined)).toBe(true) // The window promotes the rate and `include` selects the valuation, so all // three reads carry both fields. expect(['apy' in vault, 'tvl' in vault]).toStrictEqual([true, true]) for (const row of rows) if (row) expect(enriched(row)).toStrictEqual(enriched(vault)) // Every asset on these pages is USD-denominated, so no rate was consulted. expect([allBody.meta, verifiedBody.meta, vault.meta]).toStrictEqual([ undefined, undefined, undefined, ]) }, 90_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'applies the verified-vault address cursor', async () => { const db = TestApp.database() const vaultAddresses = [ TestEarn.vaultAddress, TestEarn.indexedDeployment.vaultAddress, ] as const for (const vaultAddress of vaultAddresses) await core_EarnVaults.upsert(db, { chainId: TestEarn.chain.id, description: null, label: 'Pagination vault', privateInputTokens: [], privateOutputTokens: [], vaultAddress, zones: [], }) const app = TestApp.create({ auth: false, cache, db, zones: [TestEarn.zone] }) const cursor = Cursor.encode([TestEarn.indexedDeployment.vaultAddress]) const response = await app.request( `/v1/earn/vaults/verified?chainId=${TestEarn.chain.id}&limit=5&cursor=${encodeURIComponent(cursor)}`, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Earn.schema.getVerifiedEarnVaults.Response) expect(body.data.map((vault) => vault.id)).toStrictEqual([TestEarn.vaultAddress]) expect(body.nextCursor).toBeNull() }, 90_000, ) }) describe('GET /v1/earn/addresses/:address/positions', () => { test.runIf(Runtime.get().mode === 'testnet')( 'batches lifetime cash flows for multiple positions into one query', async () => { const tidx = Tidx.getClient({ chainId: TestEarn.chain.id, tidx: { auth: process.env.TIDX_AUTH, baseUrl: Runtime.get().tidxUrl }, }) let queryCount = 0 const client = new Proxy(tidx, { get(target, property, receiver) { if (property !== 'fetch') return Reflect.get(target, property, receiver) return (...options: Parameters) => { queryCount += 1 return target.fetch(...options) } }, }) const emptyVault = Schema.Address.parse(`0x${'11'.repeat(20)}`) const cashFlows = await Earn.getVaultLifetimeCashFlows(client, { account: TestEarn.earningsAddress, blockNumber: 1_000_000_000, vaults: [TestEarn.vaultAddress, emptyVault], }) expect(queryCount).toBe(1) expect(cashFlows.get(TestEarn.vaultAddress)).toMatchObject({ status: 'complete', totalDeposited: expect.stringMatching(/^[1-9]\d*$/), totalWithdrawn: expect.stringMatching(/^\d+$/), }) expect(cashFlows.get(emptyVault)).toStrictEqual({ status: 'complete', totalDeposited: '0', totalWithdrawn: '0', }) }, 60_000, ) test('requires read access to a selected Zone', async () => { const app = TestApp.create({ cache, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/addresses/${TestEarn.positionAddress}/positions?chainId=${TestEarn.zone.id}`, { headers: { 'tempo-api-key': TestApp.key.token } }, ) expect(response.status).toBe(403) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'api_key_forbidden' }, }) }) test('rejects Zone earnings includes', async () => { const key = { ...TestApp.key, id: 'key_earn_zone_positions', scopes: ['data:read', `zone:${TestEarn.zone.id}:read`], token: 'secret_earn_zone_positions', } satisfies TestApp.kvStore.Key const app = TestApp.create({ auth: { keys: [key] }, cache, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/addresses/${TestEarn.positionAddress}/positions?chainId=${TestEarn.zone.id}&include=earnings`, { headers: { 'tempo-api-key': key.token } }, ) expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'query_invalid', message: 'Zone positions do not support the earnings include.', }, }) }) test('returns chain selection errors', async () => { const app = TestApp.create({ auth: false, cache }) const invalid = await app.request( `/v1/earn/addresses/${TestEarn.positionAddress}/positions?chainId=abc`, ) const unsupported = await app.request( `/v1/earn/addresses/${TestEarn.positionAddress}/positions?chainId=999999`, ) expect([ (await TestApp.json(invalid, Schema.ErrorResponse)).error.code, (await TestApp.json(unsupported, Schema.ErrorResponse)).error.code, ]).toStrictEqual(['chain_id_invalid', 'chain_id_unsupported']) }) test('rejects a malformed account address', async () => { const response = await TestApp.create({ auth: false, cache }).request( '/v1/earn/addresses/not-an-address/positions', ) expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'positions_invalid' }, }) }) test('returns an empty verified page without curated vaults', async () => { const app = TestApp.create({ auth: false, cache }) const response = await app.request( `/v1/earn/addresses/${TestEarn.positionAddress}/positions?include=earnings&verified=true`, ) expect(response.status).toBe(200) expect( await TestApp.json(response, Earn.schema.getEarnAddressPositions.Response), ).toStrictEqual({ data: [], nextCursor: null }) }) test.runIf(Runtime.get().mode === 'testnet')( 'lists non-zero verified positions for an account', async () => { const db = TestApp.database() // Valuation resolves through the curated verified list, so seed it. await TestApp.verifiedSeed(db, TestEarn.chain.id) await core_EarnVaults.upsert(db, TestEarn.verifiedVault) await core_EarnVaults.upsert(db, { ...TestEarn.verifiedVault, description: 'Active-earnings Earn deployment.', label: 'Active btPATHUSD Earn', vaultAddress: TestEarn.activeEarningsVaultAddress, }) const app = TestApp.create({ auth: false, cache, db, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/addresses/${TestEarn.activeEarningsAddress}/positions?chainId=${TestEarn.chain.id}&include=earnings&verified=true&valuation.currency=USD`, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Earn.schema.getEarnAddressPositions.Response) expect(body.data.every((position) => BigInt(position.shareAmount.amount) > 0n)).toBe(true) const row = body.data.find( (position) => position.vaultAddress === TestEarn.activeEarningsVaultAddress, ) expect(row).toBeDefined() if (!row) return // Amounts render in their own token's display currency and precision. expect(row.shareAmount.currency).toBe(row.shareToken.currency) expect(row.shareAmount.decimals).toBe(row.shareToken.decimals) expect(row.assetAmount.currency).toBe(row.assetToken.currency) expect(row.assetAmount.decimals).toBe(row.assetToken.decimals) expect(row.lifetimeCashFlows?.status).toMatch(/^(complete|incomplete_history)$/) // A USD-denominated asset values identically without consulting rates. expect(row.valuation).toStrictEqual({ amount: row.assetAmount.formatted, currency: 'USD' }) expect(body.meta).toBeUndefined() expect({ assetToken: row.assetToken.address, formattedShape: /^\d+(\.\d+)?$/.test(row.assetAmount.formatted), id: row.id, sharesHeld: BigInt(row.shareAmount.amount) > 0n, valued: BigInt(row.assetAmount.amount) > 0n, vaultAddress: row.vaultAddress, verified: row.verified, }).toMatchInlineSnapshot(` { "assetToken": "0x20c0000000000000000000000000000000000001", "formattedShape": true, "id": "0x3e4405801a1bee1b58394228ea0b44b667e222fc", "sharesHeld": true, "valued": true, "vaultAddress": "0x3e4405801a1bee1b58394228ea0b44b667e222fc", "verified": true, } `) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'discovers held vaults from indexed share balances', async () => { const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/addresses/${TestEarn.activeEarningsAddress}/positions?chainId=${TestEarn.chain.id}`, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Earn.schema.getEarnAddressPositions.Response) expect(body.data.map((position) => position.vaultAddress)).toContain( TestEarn.activeEarningsVaultAddress, ) expect( body.data.every( (position) => position.id === position.vaultAddress && !position.verified && /^\d+$/.test(position.assetAmount.amount) && BigInt(position.shareAmount.amount) > 0n && position.assetToken.address.startsWith('0x20c0') && position.shareToken.address.startsWith('0x20c0'), ), ).toBe(true) // An address-keyset cursor excludes every vault at or before it. const cursor = Cursor.encode([TestEarn.activeEarningsVaultAddress]) const paged = await app.request( `/v1/earn/addresses/${TestEarn.activeEarningsAddress}/positions?chainId=${TestEarn.chain.id}&cursor=${encodeURIComponent(cursor)}`, ) expect(paged.status).toBe(200) const pagedBody = await TestApp.json(paged, Earn.schema.getEarnAddressPositions.Response) expect( pagedBody.data.every( (position) => position.vaultAddress > TestEarn.activeEarningsVaultAddress, ), ).toBe(true) }, 90_000, ) }) describe('GET /v1/earn/vaults/:vaultId', () => { test('rejects a malformed vault address', async () => { const app = TestApp.create({ auth: false, cache }) const response = await app.request('/v1/earn/vaults/not-an-address') expect(response.status).toBe(400) expect((await TestApp.json(response, Schema.ErrorResponse)).error.code).toBe('vault_id_invalid') }) test.runIf(Runtime.get().mode === 'testnet')( 'resolves an unregistered vault directly from chain state', async () => { const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] }) const path = `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}` const [response, includedResponse] = await Promise.all([ app.request(path), app.request(`${path}&include=access,capabilities,zone`), ]) expect([response.status, includedResponse.status]).toStrictEqual([200, 200]) const vault = await TestApp.json(response, Earn.schema.getEarnVault.Response) expect(vault).not.toHaveProperty('gateway') expect(vault).not.toHaveProperty('access') expect(vault).not.toHaveProperty('capabilities') expect(vault).not.toHaveProperty('zone') const included = await TestApp.json(includedResponse, Earn.schema.getEarnVault.Response) expect({ access: included.access, description: included.description, engineType: included.engine.type, id: included.id, label: included.label, slug: included.slug, vaultAddress: included.vaultAddress, verified: included.verified, zone: included.zone, }).toMatchInlineSnapshot(` { "access": { "status": "allowlisted", }, "description": null, "engineType": "erc4626", "id": "0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a", "label": "Tempo Earn Live Vault (Earn)", "slug": null, "vaultAddress": "0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a", "verified": false, "zone": null, } `) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'includes token logos only when requested', async () => { const db = TestApp.database() const logoUri = 'https://assets.tempo.xyz/tokens/btpathusd.svg' await TestApp.verifiedSeed(db, TestEarn.chain.id, { tokens: [ { address: TestEarn.assetTokenAddress, currency: 'USD', decimals: 6, logoUri, name: 'Bridge Test PATHUSD', symbol: 'btPATHUSD', }, ], }) const app = TestApp.create({ auth: false, cache, db, verifiedTokens: {}, zones: [TestEarn.zone], }) const path = `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}` const [defaultResponse, includedResponse] = await Promise.all([ app.request(path), app.request(`${path}&include=token.logoUri`), ]) expect([defaultResponse.status, includedResponse.status]).toEqual([200, 200]) const [vault, included] = await Promise.all([ TestApp.json(defaultResponse, Earn.schema.getEarnVault.Response), TestApp.json(includedResponse, Earn.schema.getEarnVault.Response), ]) expect(vault.assetToken).not.toHaveProperty('logoUri') expect(included.assetToken.logoUri).toBe(logoUri) const timing = includedResponse.headers.get('server-timing') ?? '' expect(timing).not.toContain('token_metadata') expect(timing).toContain('token_logo') expect(timing).toContain('token_logo_uri') }, 90_000, ) test('rejects windows outside the selectable set', async () => { const app = TestApp.create({ auth: false, cache }) const statuses: number[] = [] const codes: string[] = [] for (const window of ['0h', '24h', '31d', '7x', '90d', '168h']) { const response = await app.request( `/v1/earn/vaults/0x${'aa'.repeat(20)}?apy.window=${window}`, ) statuses.push(response.status) codes.push((await TestApp.json(response, Schema.ErrorResponse)).error.code) } expect(statuses).toStrictEqual([400, 400, 400, 400, 400, 400]) expect(codes).toStrictEqual([ 'query_invalid', 'query_invalid', 'query_invalid', 'query_invalid', 'query_invalid', 'query_invalid', ]) }) test.runIf(Runtime.get().mode === 'testnet')( 'enriches the vault with live state, share price, and liquidity', async () => { // No verified tokens, so the asset has no priced display currency. const app = TestApp.create({ auth: false, cache, verifiedTokens: false, zones: [TestEarn.zone], }) const response = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}&include=tvl`, ) expect(response.status).toBe(200) expect(response.headers.get('cache-control')).toBe( 'private, max-age=30, stale-while-revalidate=120', ) const vault = await TestApp.json(response, Earn.schema.getEarnVault.Response) // An unverified asset has no display currency, so no rate set is usable. expect(vault.meta).toBeUndefined() expect({ depositsPaused: vault.state.depositsPaused, engineType: vault.engine.type, feesActive: vault.state.feesActive, // The pinned venue exposes no withdrawal-limit view, so liquidity is // absent rather than a restatement of the backing. instantLiquidity: vault.instantLiquidity, instantLiquidityValue: vault.instantLiquidityValue, isAccountingAligned: vault.state.isAccountingAligned, openRedeemRequestCount: vault.state.openRedeemRequestCount, pricedPerShare: vault.sharePrice !== null && BigInt(vault.sharePrice.amount) > 0n, sharePrice: { currency: vault.sharePrice?.currency, decimals: vault.sharePrice?.decimals }, tvl: vault.tvl, }).toMatchInlineSnapshot(` { "depositsPaused": false, "engineType": "erc4626", "feesActive": true, "instantLiquidity": null, "instantLiquidityValue": null, "isAccountingAligned": true, "openRedeemRequestCount": 0, "pricedPerShare": true, "sharePrice": { "currency": "USD", "decimals": 6, }, "tvl": null, } `) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'reports no instant liquidity for a Veda engine', async () => { const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/vaults/${TestEarn.vedaVaultAddress}?chainId=${TestEarn.chain.id}`, ) expect(response.status).toBe(200) const vault = await TestApp.json(response, Earn.schema.getEarnVault.Response) expect({ engineType: vault.engine.type, instantLiquidity: vault.instantLiquidity, }).toMatchInlineSnapshot(` { "engineType": "veda", "instantLiquidity": null, } `) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'measures the rate over the selected window', async () => { const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}&apy.window=1d`, ) expect(response.status).toBe(200) // Selecting a window requests the rate, so no `include` is needed. const apy = (await TestApp.json(response, Earn.schema.getEarnVault.Response)).apy expect(apy).toBeDefined() expect(apy).not.toBeNull() if (!apy?.asOf || apy.net === null) throw new Error('Expected a measured APY.') expect({ bucketed: apy.asOf === new Date(apy.asOf).toISOString() && apy.asOf.endsWith(':00.000Z'), measured: /^-?\d+\.\d{6}$/.test(apy.net), methodology: apy.methodology, // The selected window is echoed back so a caller can pin what it read. window: apy.window, }).toMatchInlineSnapshot(` { "bucketed": true, "measured": true, "methodology": "share-price-growth-annualized:v1", "window": "1d", } `) }, 90_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'leaves the rate unmeasured when the window starts before deployment', async () => { // This vault was deployed on 2026-07-23, so the pinned window begins before deployment. vi.useFakeTimers({ now: new Date('2026-07-24T10:00:00Z'), toFake: ['Date'] }) try { const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}&apy.window=30d`, ) expect(response.status).toBe(200) const vault = await TestApp.json(response, Earn.schema.getEarnVault.Response) expect(vault.apy).toBeNull() } finally { vi.useRealTimers() } }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'uses a seven-day window when it is measurable', async () => { const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] }) const path = `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}` const request = { headers: { 'cache-control': 'no-store' } } const [defaulted, explicit] = await Promise.all([ app.request(`${path}&include=apy`, request), app.request(`${path}&apy.window=7d`, request), ]) expect([defaulted.status, explicit.status]).toStrictEqual([200, 200]) const [a, b] = await Promise.all([defaulted.json(), explicit.json()]) expect(a).toStrictEqual(b) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'falls back to one hour when the preferred window is unavailable', async () => { // The vault was deployed on 2026-07-23, so seven days has no opening // quote while the current one-hour rate is measurable. vi.useFakeTimers({ now: new Date('2026-07-24T10:00:00Z'), toFake: ['Date'] }) try { const app = TestApp.create({ auth: false, cache: { edge: false, store: Store.memory() }, zones: [TestEarn.zone], }) const path = `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}` const request = { headers: { 'cache-control': 'no-store' } } const [defaulted, explicit] = await Promise.all([ app.request(`${path}&include=apy`, request), app.request(`${path}&apy.window=7d`, request), ]) expect([defaulted.status, explicit.status]).toStrictEqual([200, 200]) const [fallbackVault, explicitVault] = await Promise.all([ TestApp.json(defaulted, Earn.schema.getEarnVault.Response), TestApp.json(explicit, Earn.schema.getEarnVault.Response), ]) expect(fallbackVault.apy).toMatchObject({ window: '1h' }) expect(explicitVault.apy).toBeNull() } finally { vi.useRealTimers() } }, 90_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'measures rates and values assets only when requested', async () => { const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] }) const path = `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}` const request = { headers: { 'cache-control': 'no-store' } } const [bare, requested] = await Promise.all([ app.request(path, request), app.request(`${path}&include=apy,tvl`, request), ]) expect([bare.status, requested.status]).toStrictEqual([200, 200]) const [bareVault, requestedVault] = await Promise.all([ TestApp.json(bare, Earn.schema.getEarnVault.Response), TestApp.json(requested, Earn.schema.getEarnVault.Response), ]) // `earn_apy` covers the indexer boundary query and the archive quotes, // `valuation_rates` the FX rate set. A name appears only when its resolver // ran, so an absent name proves the upstream reads never happened. const shape = ( response: globalThis.Response, vault: z.output, ) => { const timing = response.headers.get('server-timing') ?? '' return { apyField: 'apy' in vault, apyTiming: timing.includes('earn_apy'), valuation: vault.meta !== undefined, ratesTiming: timing.includes('valuation_rates'), tvlField: 'tvl' in vault, } } expect({ bare: shape(bare, bareVault), requested: shape(requested, requestedVault), }).toStrictEqual({ bare: { apyField: false, apyTiming: false, valuation: false, ratesTiming: false, tvlField: false, }, requested: { apyField: true, apyTiming: true, // The asset is unverified here, so it has no display currency to // convert and the loaded rate set goes unused. valuation: false, ratesTiming: true, tvlField: true, }, }) }, 90_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'values total assets in USD for a verified asset', async () => { const db = TestApp.database() await TestApp.verifiedSeed(db, TestEarn.chain.id, { tokens: [ { address: TestEarn.assetTokenAddress, currency: 'USD', decimals: 6, name: 'Bridge Test PATHUSD', symbol: 'btPATHUSD', }, ], }) const app = TestApp.create({ auth: false, cache, db, // Read the curated list on every request; the neighbouring provenance // test seeds the same chain with a different currency. verifiedTokens: { refreshMs: 0 }, zones: [TestEarn.zone], }) const response = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}&include=tvl`, ) expect(response.status).toBe(200) const vault = await TestApp.json(response, Earn.schema.getEarnVault.Response) const tvl = vault.tvl expect(tvl).toBeDefined() expect(tvl).not.toBeNull() if (!tvl) return expect({ currency: tvl.currency, decimals: tvl.decimals, // A USD asset at six decimals values one-for-one against its backing. tracksTotalAssets: BigInt(tvl.amount) === BigInt(vault.state.totalAssets), }).toMatchInlineSnapshot(` { "currency": "USD", "decimals": 6, "tracksTotalAssets": true, } `) }, 90_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'reports rate provenance when the asset needs conversion', async () => { const db = TestApp.database() // An AUD-denominated asset forces an FX conversion into the USD TVL // denomination, which is the only case that consults a rate set. await TestApp.verifiedSeed(db, TestEarn.chain.id, { tokens: [ { address: TestEarn.assetTokenAddress, currency: 'AUD', decimals: 6, name: 'Bridge Test PATHUSD', symbol: 'btPATHUSD', }, ], }) const app = TestApp.create({ auth: false, cache, db, // Read the curated list on every request; the neighbouring TVL test seeds // the same chain with a different currency. verifiedTokens: { refreshMs: 0 }, zones: [TestEarn.zone], }) const response = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}?chainId=${TestEarn.chain.id}&include=tvl`, ) expect(response.status).toBe(200) const vault = await TestApp.json(response, Earn.schema.getEarnVault.Response) expect({ // The fixture prices AUD at 1.6 and USD at 1.0 against the EUR base. convertedFromAud: BigInt(vault.tvl?.amount ?? '0') === (BigInt(vault.state.totalAssets) * 625n) / 1000n, valuation: vault.meta?.valuation, tvlCurrency: vault.tvl?.currency, }).toMatchInlineSnapshot(` { "convertedFromAud": true, "tvlCurrency": "USD", "valuation": { "asOf": "2026-01-01T00:00:00.000Z", "basis": "nominal", "source": "ecb", }, } `) }, 90_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns 404 for an incompatible contract', async () => { const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/vaults/${TestEarn.assetTokenAddress}?chainId=${TestEarn.chain.id}`, ) expect(response.status).toBe(404) expect((await TestApp.json(response, Schema.ErrorResponse)).error.code).toBe( 'earn_vault_not_found', ) }, 60_000, ) }) describe('GET /v1/earn/vaults/:vaultId/share-prices', () => { test('classifies TIDX failures for dependency alerting', () => { expect([ Earn.sharePriceProviderFailure({ cause: new EarnSharePrices.MalformedResponseError('malformed row'), chainId: TestEarn.chain.id, }), Earn.sharePriceProviderFailure({ cause: new EarnSharePrices.TidxRequestError(new TypeError('network failed')), chainId: TestEarn.chain.id, }), ]).toMatchInlineSnapshot(` [ { "chainId": 42431, "failure": "payload", "id": "tidx", "operation": "query", }, { "chainId": 42431, "failure": "network", "id": "tidx", "operation": "query", }, ] `) }) test('validates the vault and daily range before reading upstreams', async () => { const app = TestApp.create({ auth: false, cache }) const [range, vault] = await Promise.all([ app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/share-prices?from=2026-07-02T00%3A00Z&to=2026-07-01T00%3A00Z`, ), app.request( '/v1/earn/vaults/not-an-address/share-prices?from=2026-07-01T00%3A00Z&to=2026-07-02T00%3A00Z', ), ]) expect([ (await TestApp.json(range, Schema.ErrorResponse)).error.code, (await TestApp.json(vault, Schema.ErrorResponse)).error.code, ]).toStrictEqual(['query_invalid', 'vault_id_invalid']) }) test('requires both range boundaries', async () => { const response = await TestApp.create({ auth: false, cache }).request( `/v1/earn/vaults/${TestEarn.vaultAddress}/share-prices?from=2026-07-01T00%3A00Z`, ) expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'query_invalid' }, }) }) test.runIf(Runtime.get().mode === 'testnet')( 'negative-caches incompatible vault discovery', async () => { const app = TestApp.create({ auth: false, cache }) const url = `/v1/earn/vaults/${TestEarn.assetTokenAddress}/share-prices?chainId=${TestEarn.chain.id}&from=2026-07-01T00%3A00Z&to=2026-07-02T00%3A00Z` const first = await app.request(url) const second = await app.request(url) expect({ first: { discovery: first.headers.get('server-timing')?.includes('earn_vault_discovery'), status: first.status, }, second: { discovery: second.headers.get('server-timing')?.includes('earn_vault_discovery'), status: second.status, }, }).toMatchInlineSnapshot(` { "first": { "discovery": true, "status": 404, }, "second": { "discovery": false, "status": 404, }, } `) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'reads historical share prices through a deployed JSON-RPC batch', async () => { const runtime = Runtime.get() const tidx = Tidx.getClient({ chainId: runtime.chainId, tidx: { auth: process.env.TIDX_AUTH, baseUrl: runtime.tidxUrl }, }) const indexed = await tidx.fetch({ engine: 'clickhouse', query: 'SELECT timestamp FROM blocks ORDER BY num DESC LIMIT 1 OFFSET 100', }) const timestamp = Value.toIsoDateTime(indexed.rows[0]?.['timestamp']) if (timestamp === undefined) throw new Error('expected an indexed block') const from = new Date(Date.parse(timestamp) - Ttl.days(1)).toISOString() const response = await TestApp.create({ auth: false, cache }).request( `/v1/earn/vaults/${TestEarn.vaultAddress}/share-prices?chainId=${TestEarn.chain.id}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(timestamp)}`, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Earn.schema.getEarnVaultSharePrices.Response) expect(body.data).toEqual( expect.arrayContaining([ expect.objectContaining({ sharePrice: expect.objectContaining({ amount: expect.stringMatching(/^\d+$/), decimals: 6, }), timestamp, }), ]), ) }, 60_000, ) }) describe('GET /v1/earn/vaults/:vaultId/positions/:address', () => { test('accepts and validates the wallet APY include', () => { expect(Earn.schema.getEarnVaultPosition.Query.safeParse({ include: ['apy'] }).success).toBe( true, ) expect( Earn.schema.getEarnVaultPosition.Response.safeParse({ account: TestEarn.positionAddress, apy: { breakdown: [ { assetAmount: { baseUnits: '25000000000', decimals: 6, formatted: '25000' }, net: '0.07', type: 'boost', }, { assetAmount: { baseUnits: '5000000000', decimals: 6, formatted: '5000' }, net: '0.02', type: 'base', }, ], net: '0.0616666666666667', }, assetAllowance: '0', assetBalance: '0', assetToken: TestEarn.assetTokenAddress, chainId: TestEarn.chain.id, id: TestEarn.positionAddress, shareAllowance: '0', shareBalance: '30000000000', shareToken: '0x20c000000000000000000000300e14ab91a10769', value: '30000000000', valueToken: TestEarn.assetTokenAddress, }).success, ).toBe(true) }) test('requires read access to a selected Zone', async () => { const app = TestApp.create({ cache, zones: [TestEarn.zone] }) const response = await app.request( `/v1/earn/vaults/${TestEarn.zoneVaultAddress}/positions/${TestEarn.positionAddress}?chainId=${TestEarn.zone.id}`, { headers: { 'tempo-api-key': TestApp.key.token } }, ) expect(response.status).toBe(403) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'api_key_forbidden' }, }) }) test('rejects invalid as-of timestamps', async () => { const app = TestApp.create({ auth: false, cache }) const [malformed, unix] = await Promise.all([ app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?asOf=not-a-time`, ), app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?asOf=1787312400`, ), ]) for (const response of [malformed, unix]) { expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'query_invalid' }, }) } }) test('returns chain selection errors', async () => { const app = TestApp.create({ auth: false, cache }) const invalid = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=abc`, ) const unsupported = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=999999`, ) expect([ (await TestApp.json(invalid, Schema.ErrorResponse)).error.code, (await TestApp.json(unsupported, Schema.ErrorResponse)).error.code, ]).toStrictEqual(['chain_id_invalid', 'chain_id_unsupported']) }) test('reaches validation on an RPC-only chain', async () => { const response = await TestApp.create({ auth: false, cache, rpc: { url: { 31_337: 'http://127.0.0.1:1' } }, tidx: {}, }).request('/v1/earn/vaults/not-an-address/positions/not-an-address?chainId=31337') expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'position_invalid' }, }) }) test('requires TIDX for historical reads on an RPC-only chain', async () => { const response = await TestApp.create({ auth: false, cache, rpc: { url: { 31_337: 'http://127.0.0.1:1' } }, tidx: {}, }).request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=31337&asOf=2026-07-21T09%3A00Z`, ) expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'chain_id_unsupported' }, }) }) test('validates malformed historical timestamps on an RPC-only chain', async () => { const response = await TestApp.create({ auth: false, cache, rpc: { url: { 31_337: 'http://127.0.0.1:1' } }, tidx: {}, }).request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=31337&asOf=not-a-time`, ) expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'query_invalid' }, }) }) test('validates the complete historical query on an RPC-only chain', async () => { const app = TestApp.create({ auth: false, cache, rpc: { url: { 31_337: 'http://127.0.0.1:1' } }, tidx: {}, }) const path = `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=31337&asOf=2026-07-21T09%3A00Z` const [duplicate, unknown] = await Promise.all([ app.request(`${path}&asOf=2026-07-21T10%3A00Z`), app.request(`${path}&unknown=1`), ]) for (const response of [duplicate, unknown]) { expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'query_invalid' }, }) } }) test('rejects reward includes on historical positions', async () => { const response = await TestApp.create({ auth: false, cache, rpc: { url: { 31_337: 'http://127.0.0.1:1' } }, tidx: { baseUrl: { 31_337: 'http://127.0.0.1:1' } }, }).request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=31337&asOf=2026-07-21T09%3A00Z&include=rewards`, ) expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'query_invalid', message: 'Historical position enrichments are not available.', }, }) }) test('rejects malformed vault position parameters', async () => { const response = await TestApp.create({ auth: false, cache }).request( '/v1/earn/vaults/not-an-address/positions/not-an-address', ) expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'position_invalid' }, }) }) test.runIf(Runtime.get().mode === 'testnet')( 'returns the current position through viem', async () => { const app = TestApp.create({ auth: false, cache }) const response = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=${TestEarn.chain.id}`, ) expect(response.status).toBe(200) expect(response.headers.get('cache-control')).toBe('no-store') const position = await TestApp.json(response, Earn.schema.getEarnVaultPosition.Response) expect({ account: position.account, amounts: [ position.assetAllowance, position.assetBalance, position.shareAllowance, position.shareBalance, position.value, ].every((amount) => /^\d+$/.test(amount)), assetToken: position.assetToken, id: position.id, shareToken: position.shareToken.startsWith('0x20c0'), }).toMatchInlineSnapshot(` { "account": "0xbe058e1c4df8a4366a387bf595b284246a93039e", "amounts": true, "assetToken": "0x20c000000000000000000000ff04042ee92fd449", "id": "0xbe058e1c4df8a4366a387bf595b284246a93039e", "shareToken": true, } `) expect(position).not.toHaveProperty('apy') }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'preserves the current position when APY resolution fails', async () => { const entries: Log.Entry[] = [] const app = TestApp.create({ auth: false, cache: { edge: false, store: Store.memory() }, logger: (entry) => void entries.push(entry), tidx: { baseUrl: 'http://127.0.0.1:1' }, }) const path = `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=${TestEarn.chain.id}` const [bareResponse, response] = await Promise.all([ app.request(path), app.request(`${path}&include=apy`), ]) expect(bareResponse.status).toBe(200) expect(response.status).toBe(200) expect(response.headers.get('cache-control')).toBe('no-store') const [bare, position] = await Promise.all([ TestApp.json(bareResponse, Earn.schema.getEarnVaultPosition.Response), TestApp.json(response, Earn.schema.getEarnVaultPosition.Response), ]) expect(position.apy).toBeNull() expect({ shareBalance: position.shareBalance, value: position.value }).toStrictEqual({ shareBalance: bare.shareBalance, value: bare.value, }) expect(entries.find((entry) => entry.providerFailures)?.providerFailures).toEqual([ { chainId: TestEarn.chain.id, failure: 'network', id: 'tidx', operation: 'query', }, ]) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'preserves the current position when reward APY resolution fails', async () => { const entries: Log.Entry[] = [] const app = TestApp.create({ auth: false, cache, db: Db.postgres({ connectionString: 'postgresql://127.0.0.1:1/unavailable' }), logger: (entry) => void entries.push(entry), }) const path = `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=${TestEarn.chain.id}` const [bareResponse, response] = await Promise.all([ app.request(path), app.request(`${path}&include=apy`), ]) expect(bareResponse.status).toBe(200) expect(response.status).toBe(200) expect(response.headers.get('cache-control')).toBe('no-store') const [bare, position] = await Promise.all([ TestApp.json(bareResponse, Earn.schema.getEarnVaultPosition.Response), TestApp.json(response, Earn.schema.getEarnVaultPosition.Response), ]) expect(position.apy).toBeNull() expect({ shareBalance: position.shareBalance, value: position.value }).toStrictEqual({ shareBalance: bare.shareBalance, value: bare.value, }) expect(entries.find((entry) => entry.providerFailures)?.providerFailures).toEqual([ { chainId: TestEarn.chain.id, failure: 'unknown', id: 'earn-rewards', operation: 'getPositionApy', }, ]) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'includes current reward state from Postgres', async () => { const db = TestApp.database() const now = Math.floor(Date.now() / 1_000) const startsAt = Math.floor(now / 300) * 300 await core_EarnVaults.upsert(db, TestEarn.verifiedVault) await RewardCampaigns.upsert(db, { assetAddress: TestEarn.assetTokenAddress, assetDecimals: 6, chainId: TestEarn.chain.id, config: { boostRewards: { endTimestamp: startsAt + 3_600, excludedAddresses: [], funding: { wallet: '0x5000000000000000000000000000000000000005' }, intervalSeconds: 300, perUserPrincipalCapAssets: '1000000', startTimestamp: startsAt, targetAnnualRateBps: 700, totalPrincipalCapAssets: '10000000', treasury: '0x6000000000000000000000000000000000000006', }, }, earnShareAddress: '0x2000000000000000000000000000000000000002', earnShareDecimals: 18, now: startsAt - 1, vaultAddress: TestEarn.vaultAddress, }) await RewardAccounts.replace(db, { accounts: [ { accrualRemainder: '0', allocatedPrincipalAssets: '100', cumulativeEntitlement: '15', cumulativePaid: '10', deferral: null, eligibilityRegisteredAt: null, lots: [], pendingRewardAssets: '0', publicEarnShares: '100', qualifiedEarnShares: '100', recipient: TestEarn.positionAddress, registrationOrder: '1', updatedAt: new Date().toISOString(), }, ], chainId: TestEarn.chain.id, vaultAddress: TestEarn.vaultAddress, }) const response = await TestApp.create({ auth: false, cache, db }).request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=${TestEarn.chain.id}&include=apy,rewards.proof`, ) expect(response.status).toBe(200) expect(response.headers.get('cache-control')).toBe('no-store') const position = await TestApp.json(response, Earn.schema.getEarnVaultPosition.Response) expect(position.apy).toMatchObject({ breakdown: expect.any(Array), net: expect.any(String), }) expect(position.rewards).toStrictEqual({ cumulativeEntitlement: '15', cumulativePaid: '10', pending: '5', }) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns not found for an address without an Earn vault', async () => { const response = await TestApp.create({ auth: false, cache }).request( `/v1/earn/vaults/0x1111111111111111111111111111111111111111/positions/${TestEarn.positionAddress}?chainId=${TestEarn.chain.id}`, ) expect(response.status).toBe(404) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'earn_vault_not_found' }, }) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns a position as of an indexed ISO 8601 timestamp', async () => { const runtime = Runtime.get() const tidx = Tidx.getClient({ chainId: runtime.chainId, tidx: { auth: process.env.TIDX_AUTH, baseUrl: runtime.tidxUrl }, }) const indexed = await tidx.fetch({ engine: 'clickhouse', query: 'SELECT num, timestamp FROM blocks ORDER BY num DESC LIMIT 1 OFFSET 100', }) const blockNumber = Value.toNumber(indexed.rows[0]?.['num']) const asOf = Value.toIsoDateTime(indexed.rows[0]?.['timestamp']) if (blockNumber === undefined || asOf === undefined) throw new Error('expected an indexed block') const response = await TestApp.create({ auth: false, cache }).request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=${TestEarn.chain.id}&asOf=${encodeURIComponent(asOf)}`, ) expect(response.status).toBe(200) const position = await TestApp.json(response, Earn.schema.getEarnVaultPosition.Response) expect(position.asOf).toBe(asOf) expect(position.block?.number).toBeGreaterThanOrEqual(blockNumber) expect(position.block?.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/) expect(Date.parse(position.block?.timestamp ?? '')).toBeLessThanOrEqual(Date.parse(asOf)) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns not found before the vault was deployed', async () => { const runtime = Runtime.get() const tidx = Tidx.getClient({ chainId: runtime.chainId, tidx: { auth: process.env.TIDX_AUTH, baseUrl: runtime.tidxUrl }, }) const indexed = await tidx.fetch({ engine: 'clickhouse', query: 'SELECT timestamp FROM blocks ORDER BY num ASC LIMIT 1', }) const asOf = Value.toIsoDateTime(indexed.rows[0]?.['timestamp']) if (asOf === undefined) throw new Error('expected the pre-deployment block to be indexed') const response = await TestApp.create({ auth: false, cache }).request( `/v1/earn/vaults/${TestEarn.indexedDeployment.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=${TestEarn.chain.id}&asOf=${encodeURIComponent(asOf)}`, ) expect(response.status).toBe(404) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'earn_vault_not_found' }, }) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns not found for a deployed incompatible contract at a historical block', async () => { const runtime = Runtime.get() const tidx = Tidx.getClient({ chainId: runtime.chainId, tidx: { auth: process.env.TIDX_AUTH, baseUrl: runtime.tidxUrl }, }) const indexed = await tidx.fetch({ engine: 'clickhouse', query: 'SELECT timestamp FROM blocks ORDER BY num DESC LIMIT 1', }) const asOf = Value.toIsoDateTime(indexed.rows[0]?.['timestamp']) if (asOf === undefined) throw new Error('expected an indexed block') const response = await TestApp.create({ auth: false, cache }).request( `/v1/earn/vaults/${TestEarn.assetTokenAddress}/positions/${TestEarn.positionAddress}?chainId=${TestEarn.chain.id}&asOf=${encodeURIComponent(asOf)}`, ) expect(response.status).toBe(404) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'earn_vault_not_found' }, }) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'rejects timestamps beyond indexed coverage', async () => { const asOf = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString() const response = await TestApp.create({ auth: false, cache }).request( `/v1/earn/vaults/${TestEarn.vaultAddress}/positions/${TestEarn.positionAddress}?chainId=${TestEarn.chain.id}&asOf=${encodeURIComponent(asOf)}`, ) expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'query_invalid', message: `Indexed history does not yet cover ${asOf}.`, }, }) }, 60_000, ) }) describe('GET /v1/earn/vaults/:vaultId/earnings/:address', () => { test.runIf(Runtime.get().mode === 'testnet')( 'counts registered reward payouts without treating external shares as deposits', async () => { const account = '0xa3b9eb50b9d1a3f584fbe1122a7bc82084eaae2f' const chainId = Viem.chainId.mainnet const previousDistributorAddress = '0x6c857bf1fe7de1bc1d5c639c721b2633f8fe34f3' const distributorAddress = '0xa65c6ed737b52543123817977d8d2c0e554bfc2d' const vaultAddress = '0xd730394f3bb85a4828e35fc5e361dcc9f894fa4f' const db = TestApp.database() const errors: Error[] = [] const app = TestApp.create({ auth: false, cache, db, logger: (_, error) => { if (error) errors.push(error) }, tidx: { auth: process.env.TIDX_AUTH, baseUrl: Viem.resolveUrl(process.env.TIDX_URL, chainId) ?? Tidx.url[chainId], }, }) const read = async (period: 'lifetime' | '30d' | 'active') => { const response = await app.request( `/v1/earn/vaults/${vaultAddress}/earnings/${account}?chainId=${chainId}&period=${period}`, ) expect(response.status, errors.map((error) => error.message).join('\n')).toBe(200) return TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response) } // This account's indexed share receipts include both vault mints and distributor payouts. expect((await read('lifetime')).status).toBe('incomplete_cost_basis') await core_EarnVaults.upsert(db, { chainId, description: null, label: 'Tempo Cash', privateInputTokens: [], privateOutputTokens: [], vaultAddress, zones: [], }) await RewardCampaigns.upsert(db, { assetAddress: '0x20c0000000000000000000000000000000000000', assetDecimals: 6, chainId, config: { boostRewards: { enabled: false, endTimestamp: 1790812800, excludedAddresses: [], funding: { wallet: account }, intervalSeconds: 300, perUserPrincipalCapAssets: '25000000000', startTimestamp: 1788206400, targetAnnualRateBps: 700, totalPrincipalCapAssets: '10000000000000', treasury: account, }, }, earnShareAddress: '0x20c000000000000000000000baac91f6ca72f768', earnShareDecimals: 6, now: 1788206399, vaultAddress, }) await RewardCampaigns.setBindings(db, { chainId, distributorAddress: previousDistributorAddress, vaultAddress, }) await RewardCampaigns.setBindings(db, { chainId, distributorAddress, vaultAddress }) await RewardCampaigns.setPaused(db, { chainId, paused: true, vaultAddress }) // Pausing new rewards must not erase the cost-basis treatment of already paid rewards. const earnings = await Promise.all((['lifetime', '30d', 'active'] as const).map(read)) expect(earnings.map(({ period, status }) => ({ period, status }))).toMatchInlineSnapshot(` [ { "period": "lifetime", "status": "complete", }, { "period": "30d", "status": "complete", }, { "period": "active", "status": "complete", }, ] `) const lifetime = earnings[0]! if (lifetime.status !== 'complete' || lifetime.period !== 'lifetime') throw new Error('Expected complete lifetime earnings.') expect(BigInt(lifetime.lifetimeEarnings)).toBe( BigInt(lifetime.currentValue) + BigInt(lifetime.totalWithdrawn) - BigInt(lifetime.totalDeposited), ) }, 120_000, ) test('returns chain selection errors', async () => { const app = TestApp.create({ auth: false, cache }) const invalid = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.earningsAddress}?chainId=abc`, ) const unsupported = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.earningsAddress}?chainId=999999`, ) expect([ (await TestApp.json(invalid, Schema.ErrorResponse)).error.code, (await TestApp.json(unsupported, Schema.ErrorResponse)).error.code, ]).toStrictEqual(['chain_id_invalid', 'chain_id_unsupported']) }) test('rejects malformed vault earnings parameters', async () => { const response = await TestApp.create({ auth: false, cache }).request( '/v1/earn/vaults/not-an-address/earnings/not-an-address', ) expect(response.status).toBe(400) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'earnings_invalid' }, }) }) test.runIf(Runtime.get().mode === 'testnet')( 'returns lifetime return through indexed vault events', async () => { const app = TestApp.create({ auth: false, cache }) const response = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.earningsAddress}?chainId=${TestEarn.chain.id}`, ) expect(response.status).toBe(200) const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response) if (earnings.status !== 'complete') throw new Error('Expected complete earnings history.') expect(earnings).toMatchObject({ account: TestEarn.earningsAddress, assetToken: TestEarn.assetTokenAddress, currentValue: expect.stringMatching(/^\d+$/), id: TestEarn.earningsAddress, lifetimeEarnings: expect.stringMatching(/^-?\d+$/), period: 'lifetime', status: 'complete', totalDeposited: expect.stringMatching(/^\d+$/), totalWithdrawn: expect.stringMatching(/^\d+$/), }) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns earnings on shares still held', async () => { const app = TestApp.create({ auth: false, cache }) const response = await app.request( `/v1/earn/vaults/${TestEarn.activeEarningsVaultAddress}/earnings/${TestEarn.activeEarningsAddress}?chainId=${TestEarn.chain.id}&period=active`, ) expect(response.status).toBe(200) const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response) if (earnings.status !== 'complete' || earnings.period !== 'active') throw new Error('Expected complete active earnings history.') expect(earnings).toMatchObject({ account: TestEarn.activeEarningsAddress, activeEarnings: expect.stringMatching(/^-?\d+$/), period: 'active', status: 'complete', }) expect(earnings).not.toHaveProperty('lifetimeEarnings') }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns cash-flow-adjusted trailing earnings', async () => { const app = TestApp.create({ auth: false, cache }) const response = await app.request( `/v1/earn/vaults/${TestEarn.activeEarningsVaultAddress}/earnings/${TestEarn.activeEarningsAddress}?chainId=${TestEarn.chain.id}&period=30d`, ) expect(response.status).toBe(200) const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response) if (earnings.status !== 'complete' || earnings.period !== '30d') throw new Error('Expected complete trailing earnings history.') expect(earnings).toMatchObject({ account: TestEarn.activeEarningsAddress, period: '30d', status: 'complete', windowEarnings: expect.stringMatching(/^-?\d+$/), }) expect(earnings).not.toHaveProperty('activeEarnings') expect(earnings).not.toHaveProperty('lifetimeEarnings') }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'omits lifetime earnings when venue shares make the cost basis incomplete', async () => { const app = TestApp.create({ auth: false, cache }) const response = await app.request( `/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.incompleteEarningsAddress}?chainId=${TestEarn.chain.id}`, ) expect(response.status).toBe(200) const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response) expect(earnings.status).toBe('incomplete_cost_basis') expect(earnings).not.toHaveProperty('lifetimeEarnings') }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'returns not found for an address without an Earn vault', async () => { const response = await TestApp.create({ auth: false, cache }).request( `/v1/earn/vaults/0x1111111111111111111111111111111111111111/earnings/${TestEarn.earningsAddress}?chainId=${TestEarn.chain.id}`, ) expect(response.status).toBe(404) expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({ error: { code: 'earn_vault_not_found' }, }) }, 60_000, ) }) describe('GET /v1/earn/vaults', () => { test.runIf(Runtime.get().mode === 'testnet')( 'extracts an indexed deployment and applies onchain filters', async () => { const db = TestApp.database() const record = await core_EarnVaults.upsert(db, TestEarn.indexedVerifiedVault) const app = TestApp.create({ auth: false, cache, db, zones: [TestEarn.zone] }) const query = new URLSearchParams({ asset: TestEarn.assetTokenAddress, capability: 'deposit,exactWithdraw', chainId: String(TestEarn.chain.id), cursor: Cursor.encode([ TestEarn.indexedDeployment.blockNumber, TestEarn.indexedDeployment.logIndex + 1, ]), 'engine.type': 'erc4626', limit: '5', }) const response = await app.request(`/v1/earn/vaults?${query}`) expect(response.status).toBe(200) const timing = response.headers.get('server-timing') ?? '' expect(timing).not.toContain('token_metadata') expect(timing).not.toContain('token_logo') // Neither the rate measurement nor the FX rate set is reached on a page // that asked for neither. expect(timing).not.toContain('earn_apy') expect(timing).not.toContain('valuation_rates') const body = await TestApp.json(response, Earn.schema.getEarnVaults.Response) const vault = body.data.find( (candidate) => candidate.vaultAddress === TestEarn.indexedDeployment.vaultAddress, ) expect(vault).toBeDefined() if (!vault) return expect(vault.slug).toBe(record.slug) expect(vault.assetToken).not.toHaveProperty('logoUri') expect(vault.shareToken).not.toHaveProperty('logoUri') expect(vault).not.toHaveProperty('gateway') expect(vault).not.toHaveProperty('access') expect(vault).not.toHaveProperty('apy') expect(vault).not.toHaveProperty('capabilities') expect(vault).not.toHaveProperty('tvl') expect(vault).not.toHaveProperty('zone') query.set('include', 'access,apy,capabilities,token.logoUri,tvl,zone') const includedResponse = await app.request(`/v1/earn/vaults?${query}`) expect(includedResponse.status).toBe(200) const includedTiming = includedResponse.headers.get('server-timing') ?? '' expect(includedTiming).toContain('token_logo') expect(includedTiming).toContain('earn_apy') expect(includedTiming).toContain('valuation_rates') const includedBody = await TestApp.json(includedResponse, Earn.schema.getEarnVaults.Response) const included = includedBody.data.find( (candidate) => candidate.vaultAddress === TestEarn.indexedDeployment.vaultAddress, ) expect(included).toBeDefined() if (!included) return expect(['apy' in included, 'tvl' in included]).toStrictEqual([true, true]) expect({ access: included.access, assetToken: { address: included.assetToken.address, decimals: included.assetToken.decimals, name: included.assetToken.name, symbol: included.assetToken.symbol, }, capabilities: included.capabilities, description: included.description, engine: included.engine, id: included.id, label: included.label, shareToken: { address: included.shareToken.address, decimals: included.shareToken.decimals, name: included.shareToken.name, symbol: included.shareToken.symbol, }, vaultAddress: included.vaultAddress, verified: included.verified, }).toMatchInlineSnapshot(` { "access": { "status": "allowlisted", }, "assetToken": { "address": "0x20c000000000000000000000ff04042ee92fd449", "decimals": 6, "name": "Bridge Test PATHUSD", "symbol": "btPATHUSD", }, "capabilities": { "asyncRedeem": false, "boundedRedeem": true, "deposit": true, "exactWithdraw": true, "inKindDeposit": true, "privateRouting": false, "redeem": true, "routerSwaps": false, }, "description": "Indexed Bridge Test PATHUSD Earn deployment.", "engine": { "address": "0x3bba4aa05eee3c6e568e0e60185a4eb630927928", "type": "erc4626", "venue": "0xa8b4f0e69cd4b0e56b676343f02acd8c3b355322", }, "id": "0xd4a673d20aeac2773d340ed595c497d460663ca2", "label": "Indexed btPATHUSD Earn", "shareToken": { "address": "0x20c0000000000000000000006ee48bc223de1c5b", "decimals": 6, "name": "Tempo Earn Live Vault (Earn)", "symbol": "teLIVEE", }, "vaultAddress": "0xd4a673d20aeac2773d340ed595c497d460663ca2", "verified": true, } `) }, 60_000, ) test.runIf(Runtime.get().mode === 'testnet')( 'traverses live indexed deployments from an opaque cursor', async () => { const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] }) const cursor = Cursor.encode([ TestEarn.indexedDeployment.blockNumber, TestEarn.indexedDeployment.logIndex, ]) const response = await app.request( `/v1/earn/vaults?chainId=${TestEarn.chain.id}&cursor=${encodeURIComponent(cursor)}&limit=5`, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Earn.schema.getEarnVaults.Response) expect({ ids: body.data.map((vault) => vault.id), nextCursor: body.nextCursor, valid: body.data.every( (vault) => vault.vaultAddress === vault.id && vault.assetToken.address.startsWith('0x20c0') && vault.engine.address.startsWith('0x') && vault.shareToken.address.startsWith('0x20c0') && vault.slug === null && !vault.verified, ), }).toMatchInlineSnapshot(` { "ids": [ "0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a", ], "nextCursor": null, "valid": true, } `) }, 90_000, ) }) describe('OpenAPI', () => { test('publishes stable Earn components', async () => { const app = TestApp.create({ auth: false, cache }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument expect( Object.fromEntries( Object.entries(spec.paths) .filter(([path]) => path.startsWith('/v1/earn/')) .map(([path, item]) => [item.get?.operationId, [path, successSchema(item.get)?.$ref]]), ), ).toMatchInlineSnapshot(` { "getEarnAddressPositions": [ "/v1/earn/addresses/{address}/positions", "#/components/schemas/EarnAddressPositionList", ], "getEarnVault": [ "/v1/earn/vaults/{vaultId}", "#/components/schemas/EarnVaultDetail", ], "getEarnVaultEarnings": [ "/v1/earn/vaults/{vaultId}/earnings/{address}", "#/components/schemas/EarnVaultEarnings", ], "getEarnVaultPosition": [ "/v1/earn/vaults/{vaultId}/positions/{address}", "#/components/schemas/EarnVaultPosition", ], "getEarnVaultSharePrices": [ "/v1/earn/vaults/{vaultId}/share-prices", "#/components/schemas/EarnVaultSharePriceList", ], "getEarnVaults": [ "/v1/earn/vaults", "#/components/schemas/EarnVaultList", ], "getVerifiedEarnVaults": [ "/v1/earn/vaults/verified", "#/components/schemas/VerifiedEarnVaultList", ], } `) expect( Object.keys(spec.components?.schemas ?? {}) .filter((name) => name.startsWith('Earn') && !name.endsWith('Error')) .sort(), ).toMatchInlineSnapshot(` [ "EarnAddressPosition", "EarnAddressPositionList", "EarnVault", "EarnVaultDetail", "EarnVaultEarnings", "EarnVaultEngine", "EarnVaultList", "EarnVaultPosition", "EarnVaultSharePriceList", "EarnVaultZone", "EarnVaultZoneRoute", ] `) expect(spec.components?.schemas).toHaveProperty('VerifiedEarnVault') expect(spec.components?.schemas).toHaveProperty('VerifiedEarnVaultList') expect(JSON.stringify(spec.components?.schemas?.['EarnAddressPosition'])).toContain( 'An account’s position in one earn vault.', ) expect({ detail: spec.paths['/v1/earn/vaults/{vaultId}']?.get?.responses?.['400']?.content?.[ 'application/json' ]?.schema?.$ref, list: spec.paths['/v1/earn/vaults']?.get?.responses?.['400']?.content?.['application/json'] ?.schema?.$ref, verified: spec.paths['/v1/earn/vaults/verified']?.get?.responses?.['400']?.content?.[ 'application/json' ]?.schema?.$ref, }).toMatchInlineSnapshot(` { "detail": "#/components/schemas/ApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidOrVaultIdInvalidError", "list": "#/components/schemas/ApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidError", "verified": "#/components/schemas/ApiKeyMalformedOrChainIdInvalidOrChainIdUnsupportedOrQueryInvalidError", } `) }) test('publishes the flattened vault shape', async () => { const app = TestApp.create({ auth: false, cache }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const operation = spec.paths['/v1/earn/vaults']?.get const parameters = operation?.parameters?.map((parameter) => parameter.name) const response = JSON.stringify(resolveSchema(spec, successSchema(operation))) expect(Object.keys(spec.paths)).toEqual( expect.arrayContaining([ '/v1/earn/addresses/{address}/positions', '/v1/earn/vaults', '/v1/earn/vaults/verified', '/v1/earn/vaults/{vaultId}', '/v1/earn/vaults/{vaultId}/earnings/{address}', '/v1/earn/vaults/{vaultId}/positions/{address}', '/v1/earn/vaults/{vaultId}/share-prices', ]), ) expect(parameters).toContain('apy.window') expect(parameters).toContain('engine.type') expect(parameters).toContain('include') expect(parameters).not.toContain('engine.kind') expect(parameters).not.toContain('status') expect(response).toContain('"assetToken"') expect(response).toContain('"engine"') expect(response).toContain('"type"') expect(response).toContain('"vaultAddress"') expect(response).toContain('"zone"') expect(response).toContain('"zones"') expect(response).toContain('"deploymentBlock"') expect(response).toContain('"earnRouter"') expect(response).toContain('"apy"') expect(response).toContain('"instantLiquidity"') expect(response).toContain('"sharePrice"') expect(response).toContain('"state"') expect(response).toContain('"tvl"') expect(response).toContain('"valuation"') expect(response).not.toContain('"adapter"') expect(response).not.toContain('"gateway"') expect(response).not.toContain('"product"') }) test('documents the rate window parameter', async () => { const app = TestApp.create({ auth: false, cache }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const window = spec.paths['/v1/earn/vaults']?.get?.parameters?.find( (parameter) => parameter.name === 'apy.window', ) expect(window?.schema?.enum).toStrictEqual(['1h', '1d', '7d', '30d']) expect(window?.description).toContain('includes `apy`') expect(window?.schema?.examples).toStrictEqual(['7d']) }) test('documents the include-gated vault fields', async () => { const app = TestApp.create({ auth: false, cache }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const parameters = spec.paths['/v1/earn/vaults']?.get?.parameters ?? [] const include = parameters.find((parameter) => parameter.name === 'include') expect(include?.description).toContain('`apy,tvl`') expect(JSON.stringify(include?.schema)).toContain('"apy"') expect(JSON.stringify(include?.schema)).toContain('"tvl"') expect(JSON.stringify(include?.schema)).toContain('"zones"') expect( JSON.stringify( resolveSchema(spec, successSchema(spec.paths['/v1/earn/vaults/{vaultId}']?.get)), ), ).toContain('Instant liquidity valued in USD') }) test('documents vault-position inputs and responses', async () => { const app = TestApp.create({ auth: false, cache }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const operation = spec.paths['/v1/earn/vaults/{vaultId}/positions/{address}']?.get const address = operation?.parameters?.find((parameter) => parameter.name === 'address') const asOf = operation?.parameters?.find((parameter) => parameter.name === 'asOf') const errors = errorSchemas(spec, operation) const response = JSON.stringify(resolveSchema(spec, successSchema(operation))) const vaultId = operation?.parameters?.find((parameter) => parameter.name === 'vaultId') expect(errors).toContain('"chain_id_invalid"') expect(errors).toContain('"chain_id_unsupported"') expect(errors).toContain('"position_invalid"') expect(errors).toContain('"query_invalid"') expect(errors).toContain('"earn_vault_not_found"') expect(response).toContain('Stable resource ID for this vault position') expect(response).toContain('Chain containing the account position') expect(response).toContain('Always zero for Zone-routed positions') expect(response).toContain('0x20c000000000000000000000b9537d11c60e8b50') expect(response).toContain('0x20c000000000000000000000300e14ab91a10769') expect(address?.schema?.examples).toStrictEqual([TestEarn.positionAddress]) expect(asOf?.description).toContain('latest indexed block at or before') expect(asOf?.schema?.examples).toStrictEqual(['2026-07-21T09:00:00.000Z']) expect(response).toContain('Indexed block used for a historical Earn position observation') expect(response).toContain('Assets held by this account at the observation block') expect(response).toContain('Earn shares held by this account at the observation block') expect(response).toContain('Asset value of this account’s earn shares at the observation block') expect(response).toContain('TIP-20 asset denominating `value`') expect(response).not.toContain('Assets this account currently holds') expect(response).not.toContain('Earn shares this account currently holds') expect(operation?.responses?.['200']?.description).toBe( 'Current or historical earn position for one account in one vault.', ) expect(vaultId?.schema?.examples).toStrictEqual([TestEarn.mainnetVaultAddress]) }) test('documents vault share-price inputs and responses', async () => { const app = TestApp.create({ auth: false, cache }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const operation = spec.paths['/v1/earn/vaults/{vaultId}/share-prices']?.get const interval = operation?.parameters?.find((parameter) => parameter.name === 'interval') const response = JSON.stringify(resolveSchema(spec, successSchema(operation))) expect(interval?.schema?.const).toBe('day') expect(interval?.schema?.examples).toStrictEqual(['day']) expect(response).toContain('Assets returned for one whole Earn share') expect(response).toContain('Daily Earn vault share prices') expect(response).toContain('"formatted":"1"') }) test('documents address-position inputs and responses', async () => { const app = TestApp.create({ auth: false, cache }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const operation = spec.paths['/v1/earn/addresses/{address}/positions']?.get const address = operation?.parameters?.find((parameter) => parameter.name === 'address') const denomination = operation?.parameters?.find( (parameter) => parameter.name === 'valuation.currency', ) const include = operation?.parameters?.find((parameter) => parameter.name === 'include') const verified = operation?.parameters?.find((parameter) => parameter.name === 'verified') const errors = errorSchemas(spec, operation) const response = JSON.stringify(resolveSchema(spec, successSchema(operation))) const tokenReference = JSON.stringify(spec.components?.schemas?.['TokenReference']) expect(operation?.description).toContain('currently holds shares') expect(address?.schema?.examples).toStrictEqual([TestEarn.positionAddress]) expect(denomination?.description).toContain('nominal value') expect(include?.description).toContain('`earnings`') expect(verified?.description).toContain('registry-curated') expect(verified?.schema?.examples).toStrictEqual([true]) expect(errors).toContain('"chain_id_invalid"') expect(errors).toContain('"chain_id_unsupported"') expect(errors).toContain('"positions_invalid"') expect(errors).toContain('"query_invalid"') expect(response).toContain('Stable resource ID for this position') expect(response).toContain('Current asset value of the held shares') expect(response).toContain('Chain containing the held shares') expect(response).toContain('"assetAmount"') expect(response).toContain('"shareAmount"') expect(response).toContain('"lifetimeCashFlows"') expect(response).toContain('"valuation"') expect(response).toContain('"valueToken"') expect(response).toContain('"vaultAddress"') expect(response).toContain('"nextCursor"') expect(JSON.stringify(spec.components?.schemas?.['EarnAddressPosition'])).toContain( '#/components/schemas/TokenReference', ) // Token references retain display metadata through the shared component. expect(tokenReference).toContain('"symbol"') }) test('documents vault-earnings variants', async () => { const app = TestApp.create({ auth: false, cache }) const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument const operation = spec.paths['/v1/earn/vaults/{vaultId}/earnings/{address}']?.get const errors = errorSchemas(spec, operation) const period = operation?.parameters?.find((parameter) => parameter.name === 'period') const response = JSON.stringify(resolveSchema(spec, successSchema(operation))) expect(operation?.description).toContain('the trailing 30 days') expect(period?.description).toContain('trailing 30 days') expect(period?.schema?.enum).toStrictEqual(['30d', 'active', 'lifetime']) expect(period?.schema?.examples).toStrictEqual(['lifetime']) expect(errors).toContain('"earn_vault_not_found"') expect(response).toContain('Stable resource ID for these vault earnings') expect(response).toContain('Current share value plus completed redemptions') expect(response).toContain('Assets deposited with this account as receiver') expect(response).toContain('Assets returned by completed redemptions') expect(response).toContain('weighted-average asset cost basis') expect(response).toContain('Ending share value plus realized assets') expect(response).toContain('cannot reconstruct the asset cost basis for the requested period') expect(response).toContain('"active"') expect(response).toContain('"30d"') expect(response).toContain('"incomplete_cost_basis"') expect(response).toContain('"pending_redemption"') }) })