import type { Context } from 'hono' import * as z from 'zod/mini' import type * as App from '../../App.js' import * as Db from '../../db/Db.js' import * as db_Schema from '../../db/Schema.js' import * as core_Catalog from '../../db/tables/fundingCatalog.js' import * as Timing from '../Timing.js' import * as Chain from './Chain.js' import * as Route from './Route.js' import * as Token from './Token.js' import * as Transfer from './Transfer.js' const defaultRefreshMs = 10_000 const emptyUpdatedAt = '1970-01-01T00:00:00.000Z' const emptyVersion = 'empty' /** Funding catalog schemas. */ export namespace schema { const ChainId = z .string() .check( z.regex(/^(?:eip155:\d+|solana:[^:]+|tron:[^:]+)$/), z.describe('CAIP-2 funding chain identifier.'), ) const Chain = z.extend(db_Schema.FundingChain, { id: ChainId, parentChainId: z.nullable(ChainId), }) const TransferCapability = z .object({ modes: z .readonly(z.array(z.enum(Transfer.modes))) .check(z.minLength(1), z.describe('Amount modes enabled for funding transfers.')), }) .check(z.describe('Funding transfer capability enabled on one provider route.')) /** Funding capabilities enabled on one provider route. */ export const Capabilities = z .object({ depositAddress: z .optional(z.literal(true)) .check(z.describe('Deposit-address funding support enabled on the route.')), transfer: z .optional(TransferCapability) .check(z.describe('Funding transfer support enabled on the route.')), }) .check( z.refine( (capabilities) => capabilities.depositAddress !== undefined || capabilities.transfer !== undefined, ), z.describe('Funding capabilities enabled on one provider route.'), ) /** Provider-specific configuration for one funding route. */ export const Configuration = z .record(z.string(), z.unknown()) .check(z.describe('Provider-specific configuration for one funding route.')) // Absent or null `capabilities` keeps a route quote-only. Absent or null // `configuration` lets providers decide whether a route is configured. const Route = z.extend(db_Schema.FundingRoute, { capabilities: z.optional(z.nullable(Capabilities)), configuration: z.optional(z.nullable(Configuration)), }) /** Complete funding catalog content accepted by the publish boundary. */ export const Data = z .object({ chainTokens: z .readonly(z.array(db_Schema.FundingChainToken)) .check(z.describe('Chain-scoped funding tokens.')), chains: z.readonly(z.array(Chain)).check(z.describe('Funding chains.')), routes: z.readonly(z.array(Route)).check(z.describe('Funding provider routes.')), tokens: z.readonly(z.array(db_Schema.FundingToken)).check(z.describe('Funding tokens.')), }) .check(z.describe('Complete funding catalog content.')) } /** Funding capabilities enabled on one provider route. */ export type Capabilities = z.output /** Provider-specific configuration for one funding route. */ export type Configuration = z.output /** One hydrated provider route in a funding catalog snapshot. */ export type ProviderRoute = { /** Funding capabilities; absent means the route is quote-only. */ capabilities?: Capabilities | undefined /** Provider-specific route configuration. */ configuration?: Configuration | undefined /** Funding provider that supports the route. */ providerId: string /** Hydrated source-to-destination funding route. */ route: Route.Route } /** Compiled and indexed funding catalog. */ export type Snapshot = { /** Funding chains indexed by CAIP-2 id, slug, and alias. */ chainsByKey: ReadonlyMap /** Hydrated provider routes in stable catalog order. */ routes: readonly ProviderRoute[] /** Hydrated provider routes grouped by provider id. */ routesByProvider: ReadonlyMap /** Canonical funding tokens indexed by stable id. */ tokensById: ReadonlyMap /** ISO timestamp of the catalog's last publish. */ updatedAt: string /** Opaque catalog version. */ version: string } /** Compiles stored funding catalog rows into a validated snapshot. */ export function compile(rows: core_Catalog.Rows): Snapshot { const data = normalize({ chainTokens: rows.chainTokens, chains: rows.chains, routes: rows.routes, tokens: rows.tokens, }) if (!rows.catalog && hasContent(data)) throw new InvalidCatalogError('Catalog content exists without a version head.') const chainIds = new Set(data.chains.map((chain) => chain.id)) const chainsById = new Map() const chainsByKey = new Map() for (const row of data.chains) { if (row.parentChainId && !chainIds.has(row.parentChainId)) throw new InvalidCatalogError(`Unknown parent chain "${row.parentChainId}".`) const chain = Chain.from({ aliases: row.aliases, id: row.id as Chain.Id, name: row.name, ...(row.parentChainId ? { parentChainId: row.parentChainId as Chain.Id } : {}), rpcUrls: row.rpcUrls, slug: row.slug, }) if (chainsById.has(chain.id)) throw new InvalidCatalogError(`Duplicate chain "${chain.id}".`) chainsById.set(chain.id, chain) for (const key of [chain.id, chain.slug, ...chain.aliases]) { const normalized = normalizeKey(key) if (chainsByKey.has(normalized)) throw new InvalidCatalogError(`Duplicate chain key "${key}".`) chainsByKey.set(normalized, chain) } } const configs = new Map>>() const placements = new Set() const tokenIds = new Set(data.tokens.map((token) => token.id)) for (const row of data.chainTokens) { const chain = chainsById.get(row.chainId) if (!chain) throw new InvalidCatalogError(`Unknown chain "${row.chainId}" for token placement.`) if (!tokenIds.has(row.tokenId)) throw new InvalidCatalogError(`Unknown token "${row.tokenId}" for token placement.`) const placement = placementKey(row.chainId, row.tokenId) if (placements.has(placement)) throw new InvalidCatalogError(`Duplicate token placement "${placement}".`) placements.add(placement) const chains = configs.get(row.tokenId) ?? {} chains[chain.id] = { address: row.address, decimals: row.decimals, ...(row.name ? { name: row.name } : {}), standard: row.standard, } configs.set(row.tokenId, chains) } const tokensById = new Map() for (const row of data.tokens) { if (tokensById.has(row.id)) throw new InvalidCatalogError(`Duplicate token "${row.id}".`) tokensById.set( row.id, Token.from({ chains: configs.get(row.id) ?? {}, currency: row.currency, name: row.name, slug: row.id, symbol: row.symbol, }), ) } const routes: ProviderRoute[] = [] const routesByProvider = new Map() for (const row of data.routes) { const route = { destination: resolvePlacement( chainsById, tokensById, row.destinationChainId, row.destinationTokenId, ), source: resolvePlacement(chainsById, tokensById, row.sourceChainId, row.sourceTokenId), } const entry = { ...(row.capabilities ? { capabilities: row.capabilities } : {}), ...(row.configuration ? { configuration: row.configuration } : {}), providerId: row.providerId, route, } routes.push(entry) const providerRoutes = routesByProvider.get(row.providerId) if (providerRoutes) providerRoutes.push(entry) else routesByProvider.set(row.providerId, [entry]) } return { chainsByKey, routes, routesByProvider, tokensById, updatedAt: rows.catalog?.updatedAt ?? emptyUpdatedAt, version: rows.catalog?.version ?? emptyVersion, } } /** Reads and compiles the current funding catalog from Postgres. */ export async function read(db: Db.Db): Promise { return compile(await core_Catalog.read(db)) } /** Publishes a complete funding catalog and primes the current isolate. */ export async function publish(db: Db.Db, input: publish.Input): Promise { const data = normalize(input) compile({ catalog: { id: 'default', updatedAt: emptyUpdatedAt, version: 'validation' }, ...data, }) // Optional route data persists as null so every stored row has one shape. await core_Catalog.publish(db, { ...data, routes: data.routes.map((route) => ({ ...route, capabilities: route.capabilities ?? null, configuration: route.configuration ?? null, })), }) return prime(db, await read(db)) } export declare namespace publish { /** Complete funding catalog content. */ type Input = z.input } /** Replaces the current isolate's compiled catalog snapshot. */ export function prime(source: Db.Source, snapshot: Snapshot): Snapshot { cells.set(source, { checkedAt: Date.now(), snapshot }) return snapshot } /** Returns the current funding catalog snapshot with soft background refresh. */ export function snapshot(c: Context): Promise { return Timing.time(c, 'funding_catalog', async () => { const source = c.get('dbCached') const cell = cells.get(source) if (cell && Date.now() - cell.checkedAt < defaultRefreshMs) return cell.snapshot if (cell) { const pending = refresh(c, source, cell) try { c.executionCtx.waitUntil(pending) } catch { void pending } return cell.snapshot } return refresh(c, source, undefined) }) } type Cell = { checkedAt: number snapshot: Snapshot } const cells = new WeakMap() const refreshing = new WeakMap>() function refresh(c: Context, source: Db.Source, current: Cell | undefined) { const existing = refreshing.get(source) if (existing) return existing const pending = (async () => { try { const db = Db.get(source) if (current) { const head = await Timing.time(c, 'funding_catalog_head', () => core_Catalog.head(db)) if (!head || head.version === current.snapshot.version) { cells.set(source, { checkedAt: Date.now(), snapshot: current.snapshot }) return current.snapshot } } const next = await read(db) cells.set(source, { checkedAt: Date.now(), snapshot: next }) return next } catch (error) { if (!current) throw error console.error('[funding-catalog] refresh failed; serving last good snapshot', error) cells.set(source, { checkedAt: Date.now(), snapshot: current.snapshot }) return current.snapshot } finally { refreshing.delete(source) } })() refreshing.set(source, pending) return pending } function hasContent(data: z.output) { return ( data.chains.length > 0 || data.tokens.length > 0 || data.chainTokens.length > 0 || data.routes.length > 0 ) } function normalize(input: z.input): z.output { const data = schema.Data.parse(input) return { ...data, chainTokens: data.chainTokens.map((token) => ({ ...token, address: token.chainId.startsWith('eip155:') ? token.address.toLowerCase() : token.address, })), } } function normalizeKey(key: string) { return key.trim().toLowerCase() } function placementKey(chainId: string, tokenId: string) { return `${chainId}:${tokenId}` } function resolvePlacement( chains: ReadonlyMap, tokens: ReadonlyMap, chainId: string, tokenId: string, ) { const chain = chains.get(chainId) if (!chain) throw new InvalidCatalogError(`Unknown route chain "${chainId}".`) const token = tokens.get(tokenId) if (!token) throw new InvalidCatalogError(`Unknown route token "${tokenId}".`) try { return Token.resolve(token, chain) } catch { throw new InvalidCatalogError( `Unknown route token placement "${placementKey(chainId, tokenId)}".`, ) } } /** Error thrown when funding catalog references cannot be compiled. */ export class InvalidCatalogError extends Error { override name = 'Funding.Catalog.InvalidCatalogError' }