diff --git a/services/token-aggregation/src/services/omnl-ho-liquidity-enrichment.ts b/services/token-aggregation/src/services/omnl-ho-liquidity-enrichment.ts new file mode 100644 index 0000000..1177ba4 --- /dev/null +++ b/services/token-aggregation/src/services/omnl-ho-liquidity-enrichment.ts @@ -0,0 +1,106 @@ +import { existsSync, readFileSync } from 'fs'; +import { resolve } from 'path'; +import { fetchBlockscoutTokenMeta } from './blockscout-token-meta'; +import { evaluateSupplyNostroMatch, type SupplyNostroMatchInfo } from './omnl-cash-in-bank-audit'; +import { runTripleStateReconcile, type TripleReconcileReport } from './omnl-triple-reconcile'; +import { resolveCanonicalPriceUsd } from './canonical-price-oracle'; + +const CHAIN_138 = 138; +const DEFAULT_CUSDC_ADDRESS = '0xf22258f57794CC8E06237084b353Ab30fFfa640b'; + +export interface HoLiquidityTripleStateSummary { + aligned?: boolean; + breaksCount?: number; + generatedAt?: string; +} + +export interface HoLiquidityPerTokenSample { + address: string; + totalSupplyUsd?: number; + cashInBankUsd?: number; + supplyNostroMatch?: SupplyNostroMatchInfo; +} + +function decimalStringToNumber(value?: string, decimals?: number): number | null { + if (value == null || value === '') return null; + const dec = Number.isInteger(decimals) ? Number(decimals) : 18; + try { + const raw = BigInt(value); + const base = 10n ** BigInt(dec); + const whole = raw / base; + const frac = raw % base; + return Number(whole) + Number(frac) / Number(base); + } catch { + return null; + } +} + +function readOptionalTripleStateFile(): TripleReconcileReport | null { + const candidates = [ + process.env.OMNL_TRIPLE_RECONCILE_FILE?.trim(), + resolve(process.cwd(), 'reports/status/omnl-triple-reconcile-latest.json'), + process.env.PROXMOX_ROOT + ? resolve(process.env.PROXMOX_ROOT, 'reports/status/omnl-triple-reconcile-latest.json') + : '', + ].filter(Boolean) as string[]; + + for (const file of candidates) { + if (!existsSync(file)) continue; + try { + return JSON.parse(readFileSync(file, 'utf8')) as TripleReconcileReport; + } catch { + continue; + } + } + return null; +} + +export async function loadHoLiquidityTripleStateSummary(): Promise { + let triple: TripleReconcileReport | null = null; + try { + triple = await runTripleStateReconcile(process.env.OMNL_HEALTH_LINE_ID?.trim() || undefined); + } catch { + triple = null; + } + + const fileTriple = readOptionalTripleStateFile(); + if ((!triple || (triple.breaks?.length ?? 0) > 0) && fileTriple) { + triple = fileTriple; + } else if (!triple && fileTriple) { + triple = fileTriple; + } + + if (!triple) return undefined; + + return { + aligned: triple.aligned, + breaksCount: (triple.breaks || []).length, + generatedAt: triple.generatedAt, + }; +} + +export async function loadCusdcPerTokenSample(input: { + cashInBankUsd: number; + officeId: number; +}): Promise { + const address = (process.env.CUSDC_ADDRESS_138 || DEFAULT_CUSDC_ADDRESS).toLowerCase(); + const meta = await fetchBlockscoutTokenMeta(CHAIN_138, address); + const decimals = meta?.decimals ?? 6; + const supplyUnits = decimalStringToNumber(meta?.totalSupply, decimals); + const { priceUsd } = resolveCanonicalPriceUsd(CHAIN_138, address); + const pegPriceUsd = priceUsd != null && priceUsd > 0 ? priceUsd : 1; + const totalSupplyUsd = supplyUnits != null && supplyUnits > 0 ? supplyUnits * pegPriceUsd : undefined; + + const supplyNostroMatch = evaluateSupplyNostroMatch({ + totalSupplyUsd, + externalNostroCashUsd: input.cashInBankUsd, + headOfficeId: input.officeId, + }); + + return { + address, + totalSupplyUsd, + cashInBankUsd: input.cashInBankUsd > 0 ? input.cashInBankUsd : undefined, + supplyNostroMatch, + }; +} diff --git a/services/token-aggregation/src/services/omnl-ho-liquidity-snapshot.ts b/services/token-aggregation/src/services/omnl-ho-liquidity-snapshot.ts index 504eadb..02a1a9f 100644 --- a/services/token-aggregation/src/services/omnl-ho-liquidity-snapshot.ts +++ b/services/token-aggregation/src/services/omnl-ho-liquidity-snapshot.ts @@ -7,6 +7,12 @@ import { type SupplyNostroMatchInfo, } from './omnl-cash-in-bank-audit'; import { fineractConfigured } from './omnl-fineract-office-gl'; +import { + loadCusdcPerTokenSample, + loadHoLiquidityTripleStateSummary, + type HoLiquidityPerTokenSample, + type HoLiquidityTripleStateSummary, +} from './omnl-ho-liquidity-enrichment'; export type HoLiquidityAlertSeverity = 'critical' | 'warning' | 'info'; @@ -25,6 +31,10 @@ export interface HoLiquiditySnapshot { cashInBank: CashInBankAuditSnapshot; reserveCoverage: HoReserveCoverageInfo; aggregateSupplyNostroMatch?: SupplyNostroMatchInfo; + perTokenSamples?: { + cUSDC?: HoLiquidityPerTokenSample; + }; + tripleState?: HoLiquidityTripleStateSummary; alerts: HoLiquidityAlert[]; auditMaxAgeMinutes: number; } @@ -149,6 +159,14 @@ export async function buildHoLiquiditySnapshot(options?: { ), ).toISOString(); + const [tripleState, cusdcSample] = await Promise.all([ + loadHoLiquidityTripleStateSummary().catch(() => undefined), + loadCusdcPerTokenSample({ + cashInBankUsd: cashInBankEnriched.cashInBankUsd, + officeId, + }).catch(() => undefined), + ]); + return { generatedAt, officeId, @@ -158,6 +176,8 @@ export async function buildHoLiquiditySnapshot(options?: { cashInBank: cashInBankEnriched, reserveCoverage, aggregateSupplyNostroMatch, + perTokenSamples: cusdcSample ? { cUSDC: cusdcSample } : undefined, + tripleState, alerts: evaluateHoLiquidityAlerts({ cashInBank: cashInBankEnriched, reserveCoverage, generatedAt }), auditMaxAgeMinutes: auditMaxAgeMinutes(), };