import type { AddressTokenBalance } from '@/services/api/addresses' import type { TokenAggregationCashInBankAudit, TokenAggregationHoReserveCoverage, TokenAggregationMarketSnapshot, TokenAggregationSupplyNostroMatch, TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation' export interface ResolvedTokenMarket { priceUsd?: number liquidityUsd?: number cashLiquidityUsd?: number cashInBankUsd?: number cashInBankAudit?: TokenAggregationCashInBankAudit totalSupplyUsd?: number supplyNostroMatch?: TokenAggregationSupplyNostroMatch hoReserveCoverage?: TokenAggregationHoReserveCoverage lastUpdated?: string | null source?: 'token-aggregation' | 'blockscout' | 'derived' pricingKind?: 'spot' | 'lp-share' isCanonicalClone?: boolean } /** Wallet liquidity stack + HO audit freshness (API market or resolved balance). */ export type TokenLiquidityMarketView = Pick< TokenAggregationMarketSnapshot, | 'cashLiquidityUsd' | 'cashInBankUsd' | 'cashInBankAudit' | 'supplyNostroMatch' | 'hoReserveCoverage' | 'lastUpdated' > function normalizeLastUpdated(value: string | null | undefined): string | undefined { return value ?? undefined } export function isLikelyLpReceiptSymbol(symbol: string | undefined): boolean { if (!symbol) return false const upper = symbol.toUpperCase() return upper.includes('DLP') || upper.endsWith('-LP') || upper.startsWith('LP-') || upper === 'LP' } export function formatBalanceUsdLabel( priceUsd: number | undefined, balanceUsd: number | undefined, options?: { pricingKind?: 'spot' | 'lp-share'; atTransfer?: boolean }, ): string { if (options?.pricingKind === 'lp-share') { return 'LP share — USD N/A' } if (balanceUsd != null) { return options?.atTransfer ? `≈ ${formatUsd(balanceUsd)} (at transfer)` : `≈ ${formatUsd(balanceUsd)}` } if (priceUsd == null) { return 'USD unavailable' } return 'USD unavailable' } function formatUsd(value: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: value >= 100 ? 0 : 2, }).format(value) } function parseUsd(value: string | number | null | undefined): number | undefined { if (value == null) return undefined const numeric = typeof value === 'number' ? value : Number(value) return Number.isFinite(numeric) && numeric > 0 ? numeric : undefined } export function resolveTokenMarketForBalance( balance: Pick, tokenMarkets: Record, ): ResolvedTokenMarket { const key = balance.token_address.toLowerCase() const aggregationSnapshot = tokenMarkets[key] const aggregationMarket: TokenAggregationMarketSnapshot | null | undefined = aggregationSnapshot?.market const aggregationPrice = aggregationMarket?.priceUsd const blockscoutPrice = parseUsd(balance.exchange_rate) const pricingKind = aggregationMarket?.pricingKind ?? (isLikelyLpReceiptSymbol(balance.token_symbol ?? aggregationSnapshot?.symbol) ? 'lp-share' : 'spot') const isCanonicalClone = aggregationMarket?.isCanonicalClone if (aggregationPrice != null && aggregationPrice > 0) { return { priceUsd: aggregationPrice, liquidityUsd: aggregationMarket?.liquidityUsd, cashLiquidityUsd: aggregationMarket?.cashLiquidityUsd, cashInBankUsd: aggregationMarket?.cashInBankUsd, cashInBankAudit: aggregationMarket?.cashInBankAudit, totalSupplyUsd: aggregationMarket?.totalSupplyUsd, supplyNostroMatch: aggregationMarket?.supplyNostroMatch, hoReserveCoverage: aggregationMarket?.hoReserveCoverage, lastUpdated: normalizeLastUpdated(aggregationMarket?.lastUpdated ?? aggregationMarket?.hoReserveCoverage?.generatedAt), source: 'token-aggregation', pricingKind, isCanonicalClone, } } if (blockscoutPrice != null) { return { priceUsd: blockscoutPrice, liquidityUsd: aggregationMarket?.liquidityUsd, cashLiquidityUsd: aggregationMarket?.cashLiquidityUsd, cashInBankUsd: aggregationMarket?.cashInBankUsd, cashInBankAudit: aggregationMarket?.cashInBankAudit, totalSupplyUsd: aggregationMarket?.totalSupplyUsd, supplyNostroMatch: aggregationMarket?.supplyNostroMatch, hoReserveCoverage: aggregationMarket?.hoReserveCoverage, lastUpdated: normalizeLastUpdated(aggregationMarket?.lastUpdated ?? aggregationMarket?.hoReserveCoverage?.generatedAt), source: 'blockscout', pricingKind, isCanonicalClone, } } return { priceUsd: undefined, liquidityUsd: aggregationMarket?.liquidityUsd, cashLiquidityUsd: aggregationMarket?.cashLiquidityUsd, cashInBankUsd: aggregationMarket?.cashInBankUsd, cashInBankAudit: aggregationMarket?.cashInBankAudit, totalSupplyUsd: aggregationMarket?.totalSupplyUsd, supplyNostroMatch: aggregationMarket?.supplyNostroMatch, hoReserveCoverage: aggregationMarket?.hoReserveCoverage, lastUpdated: normalizeLastUpdated(aggregationMarket?.lastUpdated ?? aggregationMarket?.hoReserveCoverage?.generatedAt), source: 'derived', pricingKind, isCanonicalClone, } } /** Public-facing cash pool depth — never fall back to full DEX pool TVL. */ export function resolveDisplayedCashLiquidityUsd( market: Pick, ): number | undefined { const value = market.cashLiquidityUsd return value != null && value > 0 ? value : undefined } export function formatCashLiquidityLabel(value: number | undefined): string { if (value == null) return 'Cash liq. unavailable' return formatUsd(value) } export function resolveDisplayedCashInBankUsd( market: Pick, ): number | undefined { const value = market.cashInBankUsd return value != null && value > 0 ? value : undefined } export function formatCashInBankAuditLabel( audit: TokenAggregationCashInBankAudit | undefined, ): string { if (!audit) return 'audit pending' switch (audit.status) { case 'aligned': return 'audit aligned' case 'breaks': return audit.breaksCount > 0 ? `audit breaks (${audit.breaksCount})` : 'audit breaks' case 'pending': return 'audit pending' case 'unavailable': return 'audit unavailable' default: return 'audit pending' } } export function formatSupplyNostroMatchLabel( match: TokenAggregationSupplyNostroMatch | undefined, ): string { if (!match) return 'supply match pending' const ho = match.headOfficeId ?? 1 switch (match.status) { case 'matched': return `HO-${ho} supply matched (${match.multiplier}× nostro)` case 'break': return `HO-${ho} supply break (${match.multiplier}× rule)` case 'pending': return 'supply match pending' case 'unavailable': return 'supply match unavailable' default: return 'supply match pending' } } /** Explicit Total supply vs 1.2× HO cash-in-bank backing line. */ export function formatTotalSupplyHoBackingLabel( totalSupplyUsd: number | undefined, match: TokenAggregationSupplyNostroMatch | undefined, ): string { if (totalSupplyUsd == null) return 'Total supply USD unavailable' if (!match || match.expectedSupplyUsd == null) { return `Total supply ${formatUsd(totalSupplyUsd)} · HO backing check pending` } const ho = match.headOfficeId ?? 1 const mult = match.multiplier ?? 1.2 const status = match.status === 'matched' ? 'matched' : match.status === 'break' ? 'break' : match.status return `Total supply ${formatUsd(totalSupplyUsd)} · expected ${formatUsd(match.expectedSupplyUsd)} (${mult}× HO-${ho} nostro ${formatUsd(match.externalNostroCashUsd ?? 0)}) · ${status}` } export function formatHoReserveCoverageLabel( coverage: TokenAggregationHoReserveCoverage | undefined, ): string { if (!coverage || !(coverage.coverageRatio > 0)) { return coverage?.status === 'pending' ? 'pending' : 'unavailable' } const mult = coverage.multiplier ?? 1.2 const ratio = coverage.coverageRatio.toFixed(2) const gl = coverage.reserveAssetGlCodes?.join('+') ?? '1000+1050' const statusSuffix = coverage.status === 'sufficient' ? '' : coverage.status === 'insufficient' ? ' · insufficient' : ` · ${coverage.status}` return `${ratio}× (${mult}× rule · HO GL ${gl})${statusSuffix}` } /** Fineract audit freshness for HO nostro / reserve lines. */ export function resolveHoLiquidityGeneratedAt( market: Pick, ): string | undefined { const candidates = [ market.hoReserveCoverage?.generatedAt, normalizeLastUpdated(market.lastUpdated), ].filter(Boolean) as string[] if (candidates.length === 0) return undefined return candidates.sort().reverse()[0] } export function formatHoLiquidityAuditFreshness( market: Pick, ): string { const at = resolveHoLiquidityGeneratedAt(market) if (!at) return 'Fineract audit pending' const ageMs = Date.now() - new Date(at).getTime() if (!Number.isFinite(ageMs) || ageMs < 0) return 'Fineract audit pending' const ageMin = Math.round(ageMs / 60_000) if (ageMin < 1) return 'Fineract audit · just now' if (ageMin < 60) return `Fineract audit · ${ageMin}m ago` const ageH = Math.round(ageMin / 60) return `Fineract audit · ${ageH}h ago` } export function formatCashInBankLabel( value: number | undefined, audit: TokenAggregationCashInBankAudit | undefined, supplyNostroMatch?: TokenAggregationSupplyNostroMatch, ): string { if (value == null) return 'Cash-in-bank unavailable' const supplyLabel = formatSupplyNostroMatchLabel(supplyNostroMatch) return `${formatUsd(value)} (${formatCashInBankAuditLabel(audit)} · ${supplyLabel})` } export function estimateTokenBalanceUsd( rawAmount: string, decimals: number, priceUsd: number | undefined, ): number | undefined { if (priceUsd == null || !(priceUsd > 0) || !rawAmount) return undefined try { const raw = BigInt(rawAmount) if (raw <= 0n) return 0 const scale = 10n ** BigInt(decimals) const whole = raw / scale const fraction = raw % scale const amount = Number(whole) + Number(fraction) / Number(scale) if (!Number.isFinite(amount)) return undefined return amount * priceUsd } catch { return undefined } }