import { Value as core_Value } from 'ox' import * as Ttl from '../../internal/Ttl.js' /** A pluggable fiat FX rate oracle consumed by the valuation endpoint. */ export type Oracle = { /** Returns the cache-key suffix for the oracle's current publication window. */ cacheKey?: (() => string) | undefined /** Oracle identifier, surfaced as valuation rate provenance (e.g. `ecb`). */ name: string /** Fetches the oracle's current rate set. */ rates: () => Promise /** Cache lifetime for a fetched rate set, in milliseconds. */ ttl: number } /** One oracle-published set of exchange rates quoted against a base currency. */ export type RateSet = { /** Publication date of the rate set as an ISO 8601 timestamp. */ asOf: string /** Currency the rates are quoted against (e.g. `EUR`). */ base: string /** Currency code to units of that currency per one `base` unit, as decimal strings. */ rates: Record } /** Fixed-point scale (decimal places) of cross rates returned by {@link rate}. */ export const precision = 18 /** Canonical ECB daily reference-rate feed (public, no key required). */ export const ecbUrl = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml' /** * Normalizes an oracle definition into an {@link Oracle}: applies the default * `ttl` and rethrows `rates()` failures as {@link RatesError}. */ export function from(oracle: from.Value): Oracle { return { ...(oracle.cacheKey ? { cacheKey: oracle.cacheKey } : {}), name: oracle.name, rates: async () => { try { return await oracle.rates() } catch (cause) { throw new RatesError(oracle.name, { cause }) } }, ttl: oracle.ttl ?? Ttl.hours(1), } } export declare namespace from { /** Oracle definition accepted by {@link from}. */ type Value = Pick & { /** Cache lifetime for a fetched rate set, in milliseconds. @default `Ttl.hours(1)` */ ttl?: number | undefined } } /** European Central Bank reference rates, cached for one day to match their publication cadence. */ export function ecb(options: ecb.Options = {}): Oracle { const { fetch = globalThis.fetch, url = ecbUrl } = options return from({ cacheKey: () => `ecb:${ecbPublication(new Date())}`, name: 'ecb', rates: async () => { const response = await fetch(url, { headers: { accept: 'application/xml' }, signal: AbortSignal.timeout(10_000), }) if (!response.ok) throw new ResponseError(response.status) const text = await response.text() // The daily file is a flat, stable XML shape; extract the `Cube` // attributes directly rather than pulling in an XML parser. const asOf = text.match(/ = {} for (const match of text.matchAll( / [part.type, part.value]), ) // Rotate one hour after the ECB's usual 16:00 CET publication time. const date = Number(parts['hour']) >= 17 ? now : new Date(now.getTime() - Ttl.days(1)) const publication = Object.fromEntries( ecbTime.formatToParts(date).map((part) => [part.type, part.value]), ) return `${publication['year']}-${publication['month']}-${publication['day']}` } export declare namespace ecb { /** Options for the ECB oracle. */ type Options = { /** Fetch implementation used to retrieve the feed. @default `globalThis.fetch` */ fetch?: typeof globalThis.fetch | undefined /** Feed URL. @default {@link ecbUrl} */ url?: string | undefined } } /** * Derives the cross rate from one currency to another through the set's base, * scaled to {@link precision} decimal places. Returns `undefined` when the set * prices neither leg of a currency. */ export function rate(set: RateSet, options: rate.Options): bigint | undefined { const scaled = (code: string) => { if (code === set.base) return 10n ** BigInt(precision) const value = set.rates[code] return value === undefined ? undefined : core_Value.from(value, precision) } const source = scaled(options.from) const target = scaled(options.to) if (source === undefined || target === undefined || source === 0n) return undefined return (target * 10n ** BigInt(precision)) / source } export declare namespace rate { /** Options for deriving a cross rate. */ type Options = { /** Currency code to convert from. */ from: string /** Currency code to convert to. */ to: string } } /** Thrown when an oracle fails to produce its rate set. */ export class RatesError extends Error { override name = 'FxOracle.RatesError' constructor(oracle: string, options: { cause: unknown }) { super(`FX oracle "${oracle}" failed to produce rates`, options) } } class ParseError extends Error { override name = 'FxOracle.ParseError' constructor() { super('Rate feed returned an unrecognized payload') } } class ResponseError extends Error { override name = 'FxOracle.ResponseError' constructor(status: number) { super(`Rate feed request failed with status ${status}`) } }