import { Hono, type Context } from 'hono' import { Value as core_Value } from 'ox' import { BaseError, ContractFunctionRevertedError, ContractFunctionZeroDataError } from 'viem' import { Actions } from 'viem/tempo' import * as z from 'zod/mini' import type * as App from '../../../App.js' import * as Auth from '../../../internal/Auth.js' import * as Cache from '../../../internal/Cache.js' import * as OpenApi from '../../../internal/OpenApi.js' import * as Response from '../../../internal/Response.js' import * as Schema from '../../../internal/Schema.js' import * as Store from '../../../internal/Store.js' import * as Timing from '../../../internal/Timing.js' import * as VerifiedTokens from '../../../internal/VerifiedTokens.js' import * as FxOracle from '../FxOracle.js' // How many balance reads to fan out per batch. Concurrent reads in one // macrotask collapse into a single deployless multicall; small chunks keep // each call under the Tempo RPC's per-call size limit. const tokenReadChunkSize = 20 /** Zod schemas owned by the valuation handlers. */ export namespace schema { /** A holding's nominal value in the requested denomination. */ export const Value = OpenApi.component( z .object({ amount: z .string() .check( z.regex(/^\d+(\.\d+)?$/), z.describe('The holding’s nominal value, as a decimal string in `currency`.'), z.meta({ examples: ['1.50'] }), ), currency: z .string() .check( z.describe('The denomination currency of this value.'), z.meta({ examples: ['USD'] }), ), }) .check(z.describe('The holding’s nominal value in the requested denomination.')), 'ValuationValue', ) /** Rate provenance for a converted valuation. */ export const Pricing = OpenApi.component( z .object({ asOf: z.iso .datetime() .check( z.describe('Publication date of the rate set as an ISO 8601 timestamp.'), z.meta({ examples: ['2026-07-20T00:00:00.000Z'] }), ), basis: z .literal('nominal') .check( z.describe( 'Valuation basis: one token unit counts as one unit of its display currency.', ), z.meta({ examples: ['nominal'] }), ), source: z .string() .check( z.describe('FX rate oracle that supplied the conversion rates.'), z.meta({ examples: ['ecb'] }), ), }) .check(z.describe('Rate provenance for a converted valuation.')), 'ValuationPricing', ) /** Schemas for the getAddressValuation operation. */ export namespace getAddressValuation { /** Path parameters for address valuation requests. */ export const Params = z .object({ address: Schema.Address.check( z.describe('The account address whose holdings you want to value.'), ), }) .check(z.describe('Path parameters for valuing an account’s verified holdings.')) /** Query parameters for address valuation requests. */ export const Query = z .strictObject({ chainId: Schema.ChainIdQuery, currency: Schema.Denomination.check( z.describe( 'Currency to denominate the account value in. Defaults to `USD` and must be priced by the configured FX oracle.', ), ), }) .check(z.describe('Query parameters for valuing an account’s verified holdings.')) /** The nominal valuation of an account's verified holdings. */ export const Response = OpenApi.component( z .object({ address: Schema.Address.check(z.describe('The account address this valuation covers.')), amount: z .string() .check( z.regex(/^\d+(\.\d+)?$/), z.describe( 'The total value of the account’s verified holdings, as a decimal string in `currency`.', ), z.meta({ examples: ['160.50'] }), ), currency: z .string() .check( z.describe('The denomination currency of `amount`.'), z.meta({ examples: ['USD'] }), ), id: z .string() .check( z.describe('A stable resource ID for this valuation, equal to the account address.'), z.meta({ examples: ['0xbe058e1c4df8a4366a387bf595b284246a93039e'] }), ), pricing: z .nullable(Pricing) .check( z.describe( 'Rate provenance, or `null` when every valued holding was already denominated in `currency`.', ), ), unpriced: z .array(z.string()) .check( z.describe( 'Display currencies excluded from `amount` because the FX oracle has no rate for them.', ), z.meta({ examples: [['BTC']] }), ), }) .check(z.describe('The nominal valuation of an account’s verified token holdings.')), 'AddressValuation', ) } } /** * Creates address-scoped valuation handlers, mounted under the `/addresses` * composer. Exposes `GET /:address/valuation`, the holdings valuation. */ export function addresses(options: addresses.Options = {}) { const oracle = options.fx?.oracle ?? FxOracle.ecb() return new Hono().get( '/v1/addresses/:address/valuation', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('param', schema.getAddressValuation.Params, { code: 'address_invalid', message: 'Invalid account address', }), OpenApi.validate('query', schema.getAddressValuation.Query, { code: 'query_invalid', message: 'Invalid query parameters', }), OpenApi.describeRoute({ description: 'Values every verified TIP-20 token an account holds as one unit of its display currency, converts each currency into the requested denomination using the configured FX rates, and returns the total.', operationId: 'getAddressValuation', responses: OpenApi.responses({ errors: { 400: { codes: ['address_invalid', 'chain_id_invalid', 'chain_id_unsupported', 'query_invalid'], }, 502: 'Could not read balances or exchange rates from an upstream service.', }, success: { description: 'The valuation of this account’s verified holdings.', schema: schema.getAddressValuation.Response, }, }), summary: 'Get address valuation', tags: ['Balances'], }), Cache.response({ cacheControl: Cache.policies.noStore, name: 'tempo-api:addresses:v1', key: (c) => Cache.urlKey(c, schema.getAddressValuation.Query), }), async (c) => { if (Auth.narrowAccess) return Auth.paidAccessError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'address_invalid', message: 'Invalid request parameters', }) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'query_invalid', message: 'Invalid request parameters', }) const { address } = c.req.valid('param') const query = c.req.valid('query') try { const valuation = await getAddressValuation(c, { address, chainId: query.chainId ?? c.get('chainId'), denomination: query.currency ?? 'USD', oracle, }) return c.json(Response.validated(schema.getAddressValuation.Response, valuation), 200) } catch (cause) { if (cause instanceof UnsupportedDenominationError) return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 }) return Response.upstream(c, cause) } }, ) } export declare namespace addresses { /** Options for the address-scoped valuation handlers. */ type Options = { /** FX configuration backing currency conversion. */ fx?: Fx | undefined } /** FX configuration for valuation handlers. */ type Fx = { /** FX rate oracle. Defaults to `FxOracle.ecb()`. */ oracle?: FxOracle.Oracle | undefined } } async function getAddressValuation( c: Context, options: getAddressValuation.Options, ) { const { address, chainId, denomination, oracle } = options const [set, ratesCause] = await rateSet(c, oracle).then( (set) => [set, undefined] as const, (cause: unknown) => [undefined, cause] as const, ) if (set && denomination !== set.base && set.rates[denomination] === undefined) throw new UnsupportedDenominationError(denomination, oracle.name) const snapshot = await VerifiedTokens.snapshot(c, chainId) type Holding = { amount: bigint; token: VerifiedTokens.Snapshot['list'][number] } const holdings = await Timing.time(c, 'valuation_balances', async () => { const client = c.get('getClient')(chainId) if (snapshot.list.length === 0) return [] // Pin every chunk to one chain-state snapshot. const blockNumber = await client.getBlockNumber({ cacheTime: 0 }) const rows: Holding[] = [] for (let index = 0; index < snapshot.list.length; index += tokenReadChunkSize) { const group = snapshot.list.slice(index, index + tokenReadChunkSize) // Concurrent reads enter viem's shared batch window and become one // deployless multicall. A failed read fails the request rather than // serving a partial total. const results = await Promise.all( group.map(async (token) => { try { const { amount } = await Actions.token.getBalance(client, { account: address, blockNumber, decimals: token.decimals, token: token.address, }) return amount === 0n ? undefined : { amount, token } } catch (error) { // A verified token that is undeployed (zero data) or uninitialized // on this chain cannot be held; count it as an empty holding // rather than failing the valuation. const empty = error instanceof BaseError && error.walk( (cause) => cause instanceof ContractFunctionZeroDataError || (cause instanceof ContractFunctionRevertedError && cause.data?.errorName === 'Uninitialized'), ) if (empty) return undefined throw error } }), ) rows.push(...results.filter((row) => row !== undefined)) } return rows }) // Aggregate holdings by display currency at a shared 18-dp scale so tokens // with different decimals sum exactly. type Group = { currency: string; sum: bigint } const groups = new Map() for (const { amount, token } of holdings) { const group = groups.get(token.currency.toLowerCase()) ?? { currency: token.currency, sum: 0n } group.sum += token.decimals <= FxOracle.precision ? amount * 10n ** BigInt(FxOracle.precision - token.decimals) : amount / 10n ** BigInt(token.decimals - FxOracle.precision) groups.set(token.currency.toLowerCase(), group) } let total = 0n const unpriced: string[] = [] let pricing: z.output | null = null const pending = [...groups.values()].filter( (group) => group.currency.toUpperCase() !== denomination, ) for (const group of groups.values()) if (group.currency.toUpperCase() === denomination) total += group.sum if (pending.length > 0) { if (!set) throw ratesCause for (const group of pending) { const rate = FxOracle.rate(set, { from: group.currency.toUpperCase(), to: denomination }) if (rate === undefined) unpriced.push(group.currency) else total += (group.sum * rate) / 10n ** BigInt(FxOracle.precision) } pricing = { asOf: set.asOf, basis: 'nominal', source: oracle.name } } unpriced.sort() return { address, // Floor to 6 dp, the TIP-20 native precision; same-currency sums stay exact. amount: core_Value.format(total / 10n ** BigInt(FxOracle.precision - 6), 6), currency: denomination, id: address, pricing, unpriced, } } declare namespace getAddressValuation { type Options = { address: z.output chainId: z.output /** Currency the valuation is denominated in (uppercase). */ denomination: string /** FX rate oracle backing currency conversion. */ oracle: FxOracle.Oracle } } /** Loads the oracle's rate set through the request's store cache. */ export function rateSet(c: Context, oracle: FxOracle.Oracle) { return Timing.time(c, 'valuation_rates', () => Store.memoize(() => oracle.rates(), { key: `fx:v2:${oracle.cacheKey?.() ?? oracle.name}`, store: c.get('store'), ttl: oracle.ttl, }), ) } /** * Loads rates best-effort to validate the denomination and value a page. * Returns `undefined` when unavailable or unnecessary; a known unsupported denomination still throws. */ export async function ratesFor( c: Context, options: ratesFor.Options, ): Promise { const { denomination, oracle } = options const rates = await rateSet(c, oracle).catch(() => undefined) if (!rates) return undefined if (denomination !== rates.base && rates.rates[denomination] === undefined) throw new UnsupportedDenominationError(denomination, oracle.name) const conversion = [...options.currencies].some( (currency) => currency.toUpperCase() !== denomination, ) if (!conversion) return undefined return rates } export declare namespace ratesFor { /** Options for loading a page's valuation rates. */ type Options = { /** Display currencies of the curated holdings appearing on the page. */ currencies: Iterable /** Currency values are denominated in (uppercase). */ denomination: string /** FX rate oracle backing currency conversion. */ oracle: FxOracle.Oracle } } /** Builds the `pricing` provenance block for a consulted rate set. */ export function pricing(rates: FxOracle.RateSet, oracle: FxOracle.Oracle) { return { asOf: rates.asOf, basis: 'nominal' as const, source: oracle.name } } /** * Values one holding in the requested denomination via its curated display * currency. Returns `null` when the token is unverified, its currency has no * rate, or the rate set is unavailable. */ export function valuationFor(options: valuationFor.Options) { const { amount, denomination, rates, token } = options if (!token) return null const rate = (() => { if (token.currency.toUpperCase() === denomination) return 10n ** BigInt(FxOracle.precision) if (!rates) return undefined return FxOracle.rate(rates, { from: token.currency.toUpperCase(), to: denomination }) })() if (rate === undefined) return null // Normalize to the shared 18-dp scale, convert, then floor to 6 dp. const scaled = token.decimals <= FxOracle.precision ? amount * 10n ** BigInt(FxOracle.precision - token.decimals) : amount / 10n ** BigInt(token.decimals - FxOracle.precision) const value = (scaled * rate) / 10n ** BigInt(FxOracle.precision) return { amount: core_Value.format(value / 10n ** BigInt(FxOracle.precision - 6), 6), currency: denomination, } } export declare namespace valuationFor { /** Options for valuing one holding. */ type Options = { /** Holding amount in the token's base units. */ amount: bigint /** Currency the value is denominated in (uppercase). */ denomination: string /** Oracle rate set, when loaded. */ rates: FxOracle.RateSet | undefined /** Display currency and decimal scale backing the holding, when known. */ token: Pick | undefined } } /** Thrown when the requested denomination is not priced by the FX oracle. */ export class UnsupportedDenominationError extends Error { override name = 'Valuation.UnsupportedDenominationError' constructor(denomination: string, oracle: string) { super(`Denomination "${denomination}" is not priced by the "${oracle}" FX oracle`) } }