2026-06-19 16:16:17 -07:00
|
|
|
|
import type { AddressTokenBalance } from '@/services/api/addresses'
|
2026-06-24 01:24:43 -07:00
|
|
|
|
import type { TokenAggregationCashInBankAudit, TokenAggregationHoReserveCoverage, TokenAggregationMarketSnapshot, TokenAggregationSupplyNostroMatch, TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
|
2026-06-19 16:16:17 -07:00
|
|
|
|
|
|
|
|
|
|
export interface ResolvedTokenMarket {
|
|
|
|
|
|
priceUsd?: number
|
|
|
|
|
|
liquidityUsd?: number
|
2026-06-24 01:24:43 -07:00
|
|
|
|
cashLiquidityUsd?: number
|
|
|
|
|
|
cashInBankUsd?: number
|
|
|
|
|
|
cashInBankAudit?: TokenAggregationCashInBankAudit
|
|
|
|
|
|
totalSupplyUsd?: number
|
|
|
|
|
|
supplyNostroMatch?: TokenAggregationSupplyNostroMatch
|
|
|
|
|
|
hoReserveCoverage?: TokenAggregationHoReserveCoverage
|
|
|
|
|
|
lastUpdated?: string | null
|
2026-06-19 16:16:17 -07:00
|
|
|
|
source?: 'token-aggregation' | 'blockscout' | 'derived'
|
|
|
|
|
|
pricingKind?: 'spot' | 'lp-share'
|
|
|
|
|
|
isCanonicalClone?: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 01:24:43 -07:00
|
|
|
|
/** 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 16:16:17 -07:00
|
|
|
|
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<AddressTokenBalance, 'token_address' | 'exchange_rate' | 'token_symbol'>,
|
|
|
|
|
|
tokenMarkets: Record<string, TokenAggregationTokenSnapshot>,
|
|
|
|
|
|
): 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,
|
2026-06-24 01:24:43 -07:00
|
|
|
|
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),
|
2026-06-19 16:16:17 -07:00
|
|
|
|
source: 'token-aggregation',
|
|
|
|
|
|
pricingKind,
|
|
|
|
|
|
isCanonicalClone,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (blockscoutPrice != null) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
priceUsd: blockscoutPrice,
|
|
|
|
|
|
liquidityUsd: aggregationMarket?.liquidityUsd,
|
2026-06-24 01:24:43 -07:00
|
|
|
|
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),
|
2026-06-19 16:16:17 -07:00
|
|
|
|
source: 'blockscout',
|
|
|
|
|
|
pricingKind,
|
|
|
|
|
|
isCanonicalClone,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
priceUsd: undefined,
|
|
|
|
|
|
liquidityUsd: aggregationMarket?.liquidityUsd,
|
2026-06-24 01:24:43 -07:00
|
|
|
|
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),
|
2026-06-19 16:16:17 -07:00
|
|
|
|
source: 'derived',
|
|
|
|
|
|
pricingKind,
|
|
|
|
|
|
isCanonicalClone,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 01:24:43 -07:00
|
|
|
|
/** Public-facing cash pool depth — never fall back to full DEX pool TVL. */
|
|
|
|
|
|
export function resolveDisplayedCashLiquidityUsd(
|
|
|
|
|
|
market: Pick<ResolvedTokenMarket, 'cashLiquidityUsd'>,
|
|
|
|
|
|
): 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<ResolvedTokenMarket, 'cashInBankUsd'>,
|
|
|
|
|
|
): 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<TokenLiquidityMarketView, 'hoReserveCoverage' | 'lastUpdated'>,
|
|
|
|
|
|
): 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<TokenLiquidityMarketView, 'hoReserveCoverage' | 'lastUpdated'>,
|
|
|
|
|
|
): 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})`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 16:16:17 -07:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|