Files
explorer-monorepo/frontend/src/utils/tokenMarket.ts
defiQUG 391e84797f
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 42s
Validate Explorer / frontend (push) Failing after 57s
Validate Explorer / smoke-e2e (push) Has been skipped
Show HO cash liquidity, nostro, and reserve coverage in explorer wallets.
Add a three-line TokenLiquidityStack with Fineract audit freshness and a token-detail HoBackingSummaryCard fed by token-aggregation market-batch HO metrics.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 01:24:43 -07:00

280 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<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,
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<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})`
}
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
}
}