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>
This commit is contained in:
81
frontend/src/components/common/HoBackingSummaryCard.tsx
Normal file
81
frontend/src/components/common/HoBackingSummaryCard.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import type { TokenLiquidityMarketView } from '@/utils/tokenMarket'
|
||||
import {
|
||||
formatCashInBankLabel,
|
||||
formatHoLiquidityAuditFreshness,
|
||||
formatHoReserveCoverageLabel,
|
||||
formatSupplyNostroMatchLabel,
|
||||
formatTotalSupplyHoBackingLabel,
|
||||
resolveDisplayedCashInBankUsd,
|
||||
} from '@/utils/tokenMarket'
|
||||
import { formatUsdValue } from '@/utils/displayCurrency'
|
||||
|
||||
type HoBackingSummaryCardProps = {
|
||||
market: TokenLiquidityMarketView & { totalSupplyUsd?: number }
|
||||
}
|
||||
|
||||
export default function HoBackingSummaryCard({ market }: HoBackingSummaryCardProps) {
|
||||
const coverage = market.hoReserveCoverage
|
||||
const nostroMatch = market.supplyNostroMatch
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
HO backing & audit
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">{formatHoLiquidityAuditFreshness(market)}</div>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<div className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="font-semibold text-gray-900 dark:text-white">Nostro settlement (GL 13010)</div>
|
||||
<div>
|
||||
{formatCashInBankLabel(
|
||||
resolveDisplayedCashInBankUsd(market),
|
||||
market.cashInBankAudit,
|
||||
market.supplyNostroMatch,
|
||||
)}
|
||||
</div>
|
||||
{nostroMatch ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EntityBadge
|
||||
label={formatSupplyNostroMatchLabel(nostroMatch)}
|
||||
tone={nostroMatch.status === 'matched' ? 'success' : nostroMatch.status === 'break' ? 'warning' : 'neutral'}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{market.totalSupplyUsd != null ? (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatTotalSupplyHoBackingLabel(market.totalSupplyUsd, market.supplyNostroMatch)}
|
||||
</div>
|
||||
) : null}
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Per-token supply vs 1.2× external nostro cash. Large c* supply can break while reserve economics remain sufficient.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="font-semibold text-gray-900 dark:text-white">Reserve coverage (GL 1000+1050)</div>
|
||||
{coverage ? (
|
||||
<>
|
||||
<div>{formatHoReserveCoverageLabel(coverage)}</div>
|
||||
<EntityBadge
|
||||
label={coverage.status === 'sufficient' ? 'Reserve sufficient' : coverage.status}
|
||||
tone={coverage.status === 'sufficient' ? 'success' : coverage.status === 'insufficient' ? 'warning' : 'neutral'}
|
||||
/>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Reserve assets {formatUsdValue(coverage.reserveAssetsUsd)} · required{' '}
|
||||
{formatUsdValue(coverage.requiredReserveUsd)} · aggregate issued c*{' '}
|
||||
{formatUsdValue(coverage.issuedCStarUsd)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-gray-500">Reserve coverage unavailable</div>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
M0 reserve stock vs 1.2× aggregate Chain 138 issued c*. Not correspondent nostro cash and not GL 2100 M1 book money.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
resolveHomePriceFeedAddresses,
|
||||
} from '@/utils/featuredTokens'
|
||||
import { createVisibilityAwarePoller } from '@/utils/visibilityRefresh'
|
||||
import TokenLiquidityStack from '@/components/wallet/TokenLiquidityStack'
|
||||
|
||||
type HomeStats = ExplorerStats
|
||||
|
||||
@@ -933,13 +934,12 @@ export default function Home({
|
||||
{formatUsd(token.market?.priceUsd)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Liq {formatUsd(token.market?.liquidityUsd)}
|
||||
<div className="mt-1">
|
||||
<TokenLiquidityStack market={token.market ?? {}} />
|
||||
{token.market?.lastUpdated ? (
|
||||
<>
|
||||
{' '}
|
||||
· <ClientRelativeTime value={token.market.lastUpdated} suffix=" ago" />
|
||||
</>
|
||||
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
<ClientRelativeTime value={token.market.lastUpdated} suffix=" ago" />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
52
frontend/src/components/wallet/TokenLiquidityStack.tsx
Normal file
52
frontend/src/components/wallet/TokenLiquidityStack.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { TokenAggregationHoReserveCoverage } from '@/services/api/tokenAggregation'
|
||||
import type { TokenLiquidityMarketView } from '@/utils/tokenMarket'
|
||||
import {
|
||||
formatCashInBankAuditLabel,
|
||||
formatCashInBankLabel,
|
||||
formatCashLiquidityLabel,
|
||||
formatHoLiquidityAuditFreshness,
|
||||
formatHoReserveCoverageLabel,
|
||||
resolveDisplayedCashInBankUsd,
|
||||
resolveDisplayedCashLiquidityUsd,
|
||||
} from '@/utils/tokenMarket'
|
||||
|
||||
type TokenLiquidityStackProps = {
|
||||
market: TokenLiquidityMarketView
|
||||
className?: string
|
||||
/** Include supply nostro detail on the cash-in-bank line (default false for compact stack). */
|
||||
verboseCashInBank?: boolean
|
||||
}
|
||||
|
||||
export default function TokenLiquidityStack({
|
||||
market,
|
||||
className = 'space-y-1 text-xs text-gray-500 dark:text-gray-400',
|
||||
verboseCashInBank = false,
|
||||
}: TokenLiquidityStackProps) {
|
||||
const cashInBankUsd = resolveDisplayedCashInBankUsd(market)
|
||||
const coverage = market.hoReserveCoverage
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div>Cash liq. {formatCashLiquidityLabel(resolveDisplayedCashLiquidityUsd(market))}</div>
|
||||
<div>
|
||||
Cash-in-bank{' '}
|
||||
{verboseCashInBank
|
||||
? formatCashInBankLabel(cashInBankUsd, market.cashInBankAudit, market.supplyNostroMatch)
|
||||
: cashInBankUsd != null
|
||||
? `${formatCashInBankLabel(cashInBankUsd, market.cashInBankAudit)} · GL 13010 nostro`
|
||||
: 'Cash-in-bank unavailable'}
|
||||
</div>
|
||||
{coverage ? (
|
||||
<div title="OMNL Fineract book reserve assets (M0), not correspondent nostro cash.">
|
||||
Reserve coverage {formatHoReserveCoverageLabel(coverage)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-[11px] opacity-80">{formatHoLiquidityAuditFreshness(market)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function formatHoReserveCoverageShort(coverage: TokenAggregationHoReserveCoverage | undefined): string {
|
||||
if (!coverage) return 'Reserve coverage unavailable'
|
||||
return formatHoReserveCoverageLabel(coverage)
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
import TokenLiquidityStack from '@/components/wallet/TokenLiquidityStack'
|
||||
import type { FundedWalletTokenRow } from '@/utils/walletFundedTokenListing'
|
||||
import {
|
||||
formatFundedRowBalanceUsd,
|
||||
formatFundedRowUnitPrice,
|
||||
} from '@/utils/walletFundedTokenListing'
|
||||
|
||||
function formatLiquidityUsd(value: number | undefined): string {
|
||||
if (value == null || !Number.isFinite(value)) return '—'
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: value >= 100 ? 0 : 2,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
type WalletFundedTokenListingProps = {
|
||||
rows: FundedWalletTokenRow[]
|
||||
walletAddress?: string | null
|
||||
@@ -34,7 +26,7 @@ export default function WalletFundedTokenListing({
|
||||
<div className="text-sm font-semibold text-gray-900 dark:text-white">Funded Chain 138 holdings</div>
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Native ETH appears first with the canonical ETH logo. ERC-20 rows use explorer market-batch pricing (unit price,
|
||||
balance USD, visible liquidity). MetaMask may still show "-" for custom tokens until upstream price
|
||||
balance USD, cash pool liquidity). MetaMask may still show "-" for custom tokens until upstream price
|
||||
providers list Chain 138 assets.
|
||||
</p>
|
||||
|
||||
@@ -65,7 +57,7 @@ export default function WalletFundedTokenListing({
|
||||
<th className="px-2 py-2 font-semibold">Balance</th>
|
||||
<th className="px-2 py-2 font-semibold">Unit price</th>
|
||||
<th className="px-2 py-2 font-semibold">Balance USD</th>
|
||||
<th className="px-2 py-2 font-semibold">Liquidity</th>
|
||||
<th className="px-2 py-2 font-semibold">Cash & reserve</th>
|
||||
<th className="px-2 py-2 font-semibold">MetaMask</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -101,7 +93,9 @@ export default function WalletFundedTokenListing({
|
||||
<td className="px-2 py-3 font-medium text-gray-900 dark:text-white">{row.balanceLabel}</td>
|
||||
<td className="px-2 py-3 text-gray-700 dark:text-gray-300">{formatFundedRowUnitPrice(row)}</td>
|
||||
<td className="px-2 py-3 text-gray-700 dark:text-gray-300">{formatFundedRowBalanceUsd(row)}</td>
|
||||
<td className="px-2 py-3 text-gray-700 dark:text-gray-300">{formatLiquidityUsd(row.liquidityUsd)}</td>
|
||||
<td className="px-2 py-3 text-gray-700 dark:text-gray-300">
|
||||
<TokenLiquidityStack market={row} />
|
||||
</td>
|
||||
<td className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400">
|
||||
{row.kind === 'native' ? 'Automatic on Chain 138' : 'EIP-747 add'}
|
||||
</td>
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
buildCanonicalAddressSet,
|
||||
isCanonicalTokenAddress,
|
||||
} from '@/utils/canonicalTokens'
|
||||
import TokenLiquidityStack from '@/components/wallet/TokenLiquidityStack'
|
||||
import {
|
||||
estimateTokenBalanceUsd,
|
||||
formatBalanceUsdLabel,
|
||||
@@ -536,9 +537,7 @@ export default function AddressDetailPage({ serverRegistryLabel = null }: { serv
|
||||
return (
|
||||
<div className="space-y-1 text-sm">
|
||||
<div>{formatUsd(market.priceUsd)}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Liq. {formatUsd(market.liquidityUsd)}
|
||||
</div>
|
||||
<TokenLiquidityStack market={market} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -637,9 +636,7 @@ export default function AddressDetailPage({ serverRegistryLabel = null }: { serv
|
||||
return (
|
||||
<div className="space-y-1 text-sm">
|
||||
<div>{formatUsd(market.priceUsd)}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Liq. {formatUsd(market.liquidityUsd)}
|
||||
</div>
|
||||
<TokenLiquidityStack market={market} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ import { fetchPublicJson } from '@/utils/publicExplorer'
|
||||
import { fetchTokenListForSurface } from '@/services/api/tokenListSurfaces'
|
||||
import { useUiMode } from '@/components/common/UiModeContext'
|
||||
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
|
||||
import TokenLiquidityStack from '@/components/wallet/TokenLiquidityStack'
|
||||
import {
|
||||
externalChainExplorerUrl,
|
||||
fetchCwRegistry,
|
||||
@@ -721,9 +722,9 @@ export default function SearchPage({
|
||||
<Address address={result.data.address} truncate showCopy={false} />
|
||||
{market ? (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<span>Live price: {formatUsd(market.priceUsd)}</span>
|
||||
<span>Visible liquidity: {formatUsd(market.liquidityUsd)}</span>
|
||||
<TokenLiquidityStack market={market} className="space-y-0.5 text-sm text-gray-500 dark:text-gray-400" />
|
||||
</div>
|
||||
<MarketEvidenceNote lastUpdated={market.lastUpdated} compact />
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,9 @@ import PostureBadge from '@/components/common/PostureBadge'
|
||||
import GruStandardsCard from '@/components/common/GruStandardsCard'
|
||||
import TokenSigningSurfaceCard from '@/components/common/TokenSigningSurfaceCard'
|
||||
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
|
||||
import HoBackingSummaryCard from '@/components/common/HoBackingSummaryCard'
|
||||
import AddTokenToWalletButton from '@/components/wallet/AddTokenToWalletButton'
|
||||
import TokenLiquidityStack from '@/components/wallet/TokenLiquidityStack'
|
||||
import PaginationControls from '@/components/common/PaginationControls'
|
||||
import SectionTabs, { TabPanel, type SectionTab } from '@/components/common/SectionTabs'
|
||||
import { useDetailTabQuery } from '@/utils/useDetailTabQuery'
|
||||
@@ -25,7 +27,14 @@ import { useDisplayCurrency } from '@/components/common/DisplayCurrencyContext'
|
||||
import { getGruStandardsProfileSafe, type GruStandardsProfile } from '@/services/api/gru'
|
||||
import { getGruExplorerMetadata } from '@/services/api/gruExplorerData'
|
||||
import { contractsApi, type ContractProfile } from '@/services/api/contracts'
|
||||
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
|
||||
import { formatUsdValue, getSecondaryDisplayValue } from '@/utils/displayCurrency'
|
||||
import {
|
||||
formatCashInBankLabel,
|
||||
formatHoReserveCoverageLabel,
|
||||
formatTotalSupplyHoBackingLabel,
|
||||
resolveDisplayedCashInBankUsd,
|
||||
} from '@/utils/tokenMarket'
|
||||
|
||||
function isValidAddress(value: string) {
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(value)
|
||||
@@ -59,6 +68,7 @@ export default function TokenDetailPage() {
|
||||
const [pools, setPools] = useState<MissionControlLiquidityPool[]>([])
|
||||
const [gruProfile, setGruProfile] = useState<GruStandardsProfile | null>(null)
|
||||
const [contractProfile, setContractProfile] = useState<ContractProfile | null>(null)
|
||||
const [aggregationMarket, setAggregationMarket] = useState<TokenAggregationTokenSnapshot | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [holderPage, setHolderPage] = useState(1)
|
||||
const [transferPage, setTransferPage] = useState(1)
|
||||
@@ -68,13 +78,14 @@ export default function TokenDetailPage() {
|
||||
const loadToken = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [tokenResult, provenanceResult, holdersResult, transfersResult, poolsResult, contractResult] = await Promise.all([
|
||||
const [tokenResult, provenanceResult, holdersResult, transfersResult, poolsResult, contractResult, aggregationResult] = await Promise.all([
|
||||
tokensApi.getSafe(address),
|
||||
tokensApi.getProvenanceSafe(address),
|
||||
tokensApi.getHoldersSafe(address, 1, 10),
|
||||
tokensApi.getTransfersSafe(address, 1, 10),
|
||||
tokensApi.getRelatedPoolsSafe(address),
|
||||
contractsApi.getProfileSafe(address),
|
||||
tokenAggregationApi.getTokenSafe(138, address),
|
||||
])
|
||||
|
||||
setToken(tokenResult.ok ? tokenResult.data : null)
|
||||
@@ -82,6 +93,7 @@ export default function TokenDetailPage() {
|
||||
setHolders(holdersResult.ok ? holdersResult.data : [])
|
||||
setTransfers(transfersResult.ok ? transfersResult.data : [])
|
||||
setPools(poolsResult.ok ? poolsResult.data : [])
|
||||
setAggregationMarket(aggregationResult.ok ? aggregationResult.data : null)
|
||||
const resolvedContractProfile = contractResult.ok ? contractResult.data : null
|
||||
setContractProfile(resolvedContractProfile)
|
||||
if (tokenResult.ok && tokenResult.data) {
|
||||
@@ -103,6 +115,7 @@ export default function TokenDetailPage() {
|
||||
setPools([])
|
||||
setGruProfile(null)
|
||||
setContractProfile(null)
|
||||
setAggregationMarket(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -117,6 +130,7 @@ export default function TokenDetailPage() {
|
||||
setLoading(false)
|
||||
setToken(null)
|
||||
setContractProfile(null)
|
||||
setAggregationMarket(null)
|
||||
return
|
||||
}
|
||||
void loadToken()
|
||||
@@ -196,6 +210,7 @@ export default function TokenDetailPage() {
|
||||
() => getGruExplorerMetadata({ address: token?.address || address, symbol: token?.symbol }),
|
||||
[address, token?.address, token?.symbol],
|
||||
)
|
||||
const aggregationMarketData = aggregationMarket?.market ?? null
|
||||
const tabs = useMemo<SectionTab<'intelligence' | 'standards' | 'holders' | 'transfers' | 'liquidity'>[]>(
|
||||
() => [
|
||||
{ id: 'intelligence', label: 'Intelligence' },
|
||||
@@ -441,9 +456,53 @@ export default function TokenDetailPage() {
|
||||
</DetailRow>
|
||||
{token.total_supply && (
|
||||
<DetailRow label="Total Supply">
|
||||
{renderAmountWithDisplayCurrency(token.total_supply, token.decimals, token.symbol)}
|
||||
<div className="space-y-1">
|
||||
<div>{renderAmountWithDisplayCurrency(token.total_supply, token.decimals, token.symbol)}</div>
|
||||
{aggregationMarketData?.totalSupplyUsd != null ? (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatTotalSupplyHoBackingLabel(
|
||||
aggregationMarketData.totalSupplyUsd,
|
||||
aggregationMarketData.supplyNostroMatch,
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DetailRow>
|
||||
)}
|
||||
{aggregationMarketData?.hoReserveCoverage ? (
|
||||
<DetailRow label="HO reserve coverage">
|
||||
<div className="space-y-1 text-sm">
|
||||
<div>{formatHoReserveCoverageLabel(aggregationMarketData.hoReserveCoverage)}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Aggregate issued c* vs HO GL {aggregationMarketData.hoReserveCoverage.reserveAssetGlCodes?.join('+') ?? '1000+1050'} reserve assets (M0). Not correspondent nostro cash.
|
||||
</div>
|
||||
{aggregationMarketData.hoReserveCoverage.reserveAssetsUsd != null ? (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Reserve assets {formatUsdValue(aggregationMarketData.hoReserveCoverage.reserveAssetsUsd)} · required{' '}
|
||||
{formatUsdValue(aggregationMarketData.hoReserveCoverage.requiredReserveUsd)} · issued c*{' '}
|
||||
{formatUsdValue(aggregationMarketData.hoReserveCoverage.issuedCStarUsd)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DetailRow>
|
||||
) : null}
|
||||
{aggregationMarketData?.cashInBankUsd != null ? (
|
||||
<DetailRow label="HO cash-in-bank">
|
||||
<div className="space-y-1 text-sm">
|
||||
<div>
|
||||
{formatCashInBankLabel(
|
||||
resolveDisplayedCashInBankUsd(aggregationMarketData),
|
||||
aggregationMarketData.cashInBankAudit,
|
||||
aggregationMarketData.supplyNostroMatch,
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Head Office external nostro (GL 13010). Total supply USD should equal{' '}
|
||||
{aggregationMarketData.supplyNostroMatch?.multiplier ?? 1.2}× this amount.
|
||||
</div>
|
||||
</div>
|
||||
</DetailRow>
|
||||
) : null}
|
||||
{token.holders != null && (
|
||||
<DetailRow label="Holders">{formatInteger(token.holders)}</DetailRow>
|
||||
)}
|
||||
@@ -476,6 +535,11 @@ export default function TokenDetailPage() {
|
||||
/>
|
||||
|
||||
<TabPanel idPrefix={TOKEN_DETAIL_TABS_ID} tabId="intelligence" activeTab={activeTab}>
|
||||
{aggregationMarketData?.hoReserveCoverage || aggregationMarketData?.cashInBankUsd != null ? (
|
||||
<div className="mb-6">
|
||||
<HoBackingSummaryCard market={aggregationMarketData} />
|
||||
</div>
|
||||
) : null}
|
||||
<Card title="Token Intelligence">
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
@@ -485,6 +549,15 @@ export default function TokenDetailPage() {
|
||||
<div>24h volume: {formatUsd(token.volume_24h)}</div>
|
||||
<div>Market cap: {formatUsd(token.circulating_market_cap)}</div>
|
||||
<div>Visible liquidity: {formatUsd(token.liquidity_usd)}</div>
|
||||
<TokenLiquidityStack market={aggregationMarketData ?? {}} verboseCashInBank />
|
||||
{aggregationMarketData?.supplyNostroMatch ? (
|
||||
<div>
|
||||
{formatTotalSupplyHoBackingLabel(
|
||||
aggregationMarketData.totalSupplyUsd,
|
||||
aggregationMarketData.supplyNostroMatch,
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div>Valuation source: {token.price_source === 'token-aggregation' ? 'live token aggregation' : token.price_source || 'unavailable'}</div>
|
||||
<div>Market snapshot: {token.market_updated_at ? formatTimestamp(token.market_updated_at) : 'Unavailable'}</div>
|
||||
<MarketEvidenceNote
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
sortIndexedTokensCanonicalFirst,
|
||||
} from '@/utils/canonicalTokens'
|
||||
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
|
||||
import TokenLiquidityStack from '@/components/wallet/TokenLiquidityStack'
|
||||
import { fetchTokenListForSurface } from '@/services/api/tokenListSurfaces'
|
||||
import { selectCuratedFeaturedTokens } from '@/utils/featuredTokens'
|
||||
import { formatInteger } from '@/utils/format'
|
||||
@@ -282,8 +283,10 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
|
||||
<div className="mt-1 font-semibold text-gray-950 dark:text-white">{formatUsd(market.priceUsd)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-gray-50 px-3 py-2 dark:bg-gray-950/60">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Liquidity</div>
|
||||
<div className="mt-1 font-semibold text-gray-950 dark:text-white">{formatUsd(market.liquidityUsd)}</div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Cash & reserve</div>
|
||||
<div className="mt-1">
|
||||
<TokenLiquidityStack market={market} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<MarketEvidenceNote lastUpdated={market.lastUpdated} compact />
|
||||
|
||||
@@ -1,14 +1,66 @@
|
||||
import { resolveExplorerApiBase } from '../../../libs/frontend-api-client/api-base'
|
||||
|
||||
export type TokenAggregationHoReserveCoverageStatus = 'sufficient' | 'insufficient' | 'pending' | 'unavailable'
|
||||
|
||||
export interface TokenAggregationHoReserveCoverage {
|
||||
status: TokenAggregationHoReserveCoverageStatus
|
||||
multiplier: number
|
||||
headOfficeId: number
|
||||
reserveAssetGlCodes: string[]
|
||||
reserveAssetsUsd: number
|
||||
issuedCStarUsd: number
|
||||
requiredReserveUsd: number
|
||||
coverageRatio: number
|
||||
formula: string
|
||||
policyId: string
|
||||
policyRef: string
|
||||
generatedAt: string
|
||||
tokenCount?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface TokenAggregationMarketSnapshot {
|
||||
priceUsd?: number
|
||||
volume24h?: number
|
||||
liquidityUsd?: number
|
||||
cashLiquidityUsd?: number
|
||||
cashInBankUsd?: number
|
||||
cashInBankAudit?: TokenAggregationCashInBankAudit
|
||||
totalSupplyUsd?: number
|
||||
supplyNostroMatch?: TokenAggregationSupplyNostroMatch
|
||||
hoReserveCoverage?: TokenAggregationHoReserveCoverage
|
||||
lastUpdated?: string | null
|
||||
pricingKind?: 'spot' | 'lp-share'
|
||||
isCanonicalClone?: boolean
|
||||
}
|
||||
|
||||
export type TokenAggregationSupplyNostroMatchStatus = 'matched' | 'break' | 'pending' | 'unavailable'
|
||||
|
||||
export interface TokenAggregationSupplyNostroMatch {
|
||||
status: TokenAggregationSupplyNostroMatchStatus
|
||||
multiplier: number
|
||||
toleranceBps: number
|
||||
headOfficeId?: number
|
||||
externalNostroGlCode?: string
|
||||
formula?: string
|
||||
totalSupplyUsd?: number
|
||||
externalNostroCashUsd?: number
|
||||
expectedSupplyUsd?: number
|
||||
deltaUsd?: number
|
||||
deltaBps?: number
|
||||
}
|
||||
|
||||
export type TokenAggregationCashInBankAuditStatus = 'aligned' | 'breaks' | 'pending' | 'unavailable'
|
||||
|
||||
export interface TokenAggregationCashInBankAudit {
|
||||
status: TokenAggregationCashInBankAuditStatus
|
||||
breaksCount: number
|
||||
tripleStateAligned?: boolean
|
||||
policyId: string
|
||||
policyRef: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface TokenAggregationTokenSnapshot {
|
||||
chainId: number
|
||||
address: string
|
||||
@@ -79,6 +131,12 @@ interface RawTokenMarketBatchResponse {
|
||||
decimals?: number | string | null
|
||||
priceUsd?: number | string | null
|
||||
liquidityUsd?: number | string | null
|
||||
cashLiquidityUsd?: number | string | null
|
||||
cashInBankUsd?: number | string | null
|
||||
cashInBankAudit?: TokenAggregationCashInBankAudit | null
|
||||
totalSupplyUsd?: number | string | null
|
||||
supplyNostroMatch?: TokenAggregationSupplyNostroMatch | null
|
||||
hoReserveCoverage?: TokenAggregationHoReserveCoverage | null
|
||||
volume24h?: number | string | null
|
||||
lastUpdated?: string | null
|
||||
source?: string | null
|
||||
@@ -99,6 +157,12 @@ interface RawTokenAggregationTokenResponse {
|
||||
priceUsd?: number | string | null
|
||||
volume24h?: number | string | null
|
||||
liquidityUsd?: number | string | null
|
||||
cashLiquidityUsd?: number | string | null
|
||||
cashInBankUsd?: number | string | null
|
||||
cashInBankAudit?: TokenAggregationCashInBankAudit | null
|
||||
totalSupplyUsd?: number | string | null
|
||||
supplyNostroMatch?: TokenAggregationSupplyNostroMatch | null
|
||||
hoReserveCoverage?: TokenAggregationHoReserveCoverage | null
|
||||
lastUpdated?: string | null
|
||||
} | null
|
||||
} | null
|
||||
@@ -179,6 +243,9 @@ function normalizeTokenSnapshot(raw: RawTokenAggregationTokenResponse): TokenAgg
|
||||
decimals,
|
||||
priceUsd,
|
||||
)
|
||||
const cashLiquidityUsd = toNumber(token.market?.cashLiquidityUsd)
|
||||
const cashInBankUsd = toNumber(token.market?.cashInBankUsd)
|
||||
const totalSupplyUsd = toNumber(token.market?.totalSupplyUsd)
|
||||
|
||||
return {
|
||||
chainId: toNumber(token.chainId) ?? 138,
|
||||
@@ -192,6 +259,12 @@ function normalizeTokenSnapshot(raw: RawTokenAggregationTokenResponse): TokenAgg
|
||||
priceUsd,
|
||||
volume24h: toNumber(token.market.volume24h),
|
||||
liquidityUsd,
|
||||
cashLiquidityUsd,
|
||||
cashInBankUsd,
|
||||
cashInBankAudit: token.market.cashInBankAudit || undefined,
|
||||
totalSupplyUsd,
|
||||
supplyNostroMatch: token.market.supplyNostroMatch || undefined,
|
||||
hoReserveCoverage: token.market.hoReserveCoverage || undefined,
|
||||
lastUpdated: token.market.lastUpdated || null,
|
||||
}
|
||||
: null,
|
||||
@@ -263,16 +336,25 @@ export const tokenAggregationApi = {
|
||||
if (!snapshot.address) return []
|
||||
const priceUsd = toNumber(snapshot.priceUsd)
|
||||
const liquidityUsd = toNumber(snapshot.liquidityUsd)
|
||||
const cashLiquidityUsd = toNumber(snapshot.cashLiquidityUsd)
|
||||
const cashInBankUsd = toNumber(snapshot.cashInBankUsd)
|
||||
const totalSupplyUsd = toNumber(snapshot.totalSupplyUsd)
|
||||
const entry: TokenAggregationTokenSnapshot = {
|
||||
chainId,
|
||||
address: snapshot.address,
|
||||
name: snapshot.name || undefined,
|
||||
symbol: snapshot.symbol || undefined,
|
||||
decimals: toNumber(snapshot.decimals),
|
||||
market: priceUsd != null || liquidityUsd != null || snapshot.pricingKind
|
||||
market: priceUsd != null || liquidityUsd != null || cashLiquidityUsd != null || cashInBankUsd != null || totalSupplyUsd != null || snapshot.hoReserveCoverage || snapshot.pricingKind
|
||||
? {
|
||||
priceUsd,
|
||||
liquidityUsd,
|
||||
cashLiquidityUsd,
|
||||
cashInBankUsd,
|
||||
cashInBankAudit: snapshot.cashInBankAudit || undefined,
|
||||
totalSupplyUsd,
|
||||
supplyNostroMatch: snapshot.supplyNostroMatch || undefined,
|
||||
hoReserveCoverage: snapshot.hoReserveCoverage || undefined,
|
||||
volume24h: toNumber(snapshot.volume24h),
|
||||
lastUpdated: snapshot.lastUpdated || null,
|
||||
pricingKind: snapshot.pricingKind || undefined,
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
import type { AddressTokenBalance } from '@/services/api/addresses'
|
||||
import type { TokenAggregationMarketSnapshot, TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
|
||||
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()
|
||||
@@ -63,6 +85,13 @@ export function resolveTokenMarketForBalance(
|
||||
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,
|
||||
@@ -73,6 +102,13 @@ export function resolveTokenMarketForBalance(
|
||||
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,
|
||||
@@ -82,12 +118,146 @@ export function resolveTokenMarketForBalance(
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getNativeAssetMarketSafe, estimateNativeUsdValue } from '@/services/api/nativeAssetPricing'
|
||||
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
|
||||
import { tokenAggregationApi, type TokenAggregationCashInBankAudit, type TokenAggregationHoReserveCoverage, type TokenAggregationSupplyNostroMatch, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
|
||||
import { formatTokenAmount } from '@/utils/format'
|
||||
import { estimateTokenBalanceUsd } from '@/utils/tokenMarket'
|
||||
import {
|
||||
@@ -33,6 +33,11 @@ export type FundedWalletTokenRow = {
|
||||
priceUsd?: number
|
||||
balanceUsd?: number
|
||||
liquidityUsd?: number
|
||||
cashLiquidityUsd?: number
|
||||
cashInBankUsd?: number
|
||||
cashInBankAudit?: TokenAggregationCashInBankAudit
|
||||
supplyNostroMatch?: TokenAggregationSupplyNostroMatch
|
||||
hoReserveCoverage?: TokenAggregationHoReserveCoverage
|
||||
pricingKind?: string
|
||||
marketSource?: string
|
||||
metaMaskAddable: boolean
|
||||
@@ -147,6 +152,11 @@ export async function enrichFundedWalletTokenRowsWithMarket(
|
||||
priceUsd: nativePriceUsd,
|
||||
balanceUsd: balanceUsdText != null ? Number(balanceUsdText) : undefined,
|
||||
liquidityUsd: nativeMarket.data?.market?.liquidityUsd,
|
||||
cashLiquidityUsd: nativeMarket.data?.market?.cashLiquidityUsd,
|
||||
cashInBankUsd: nativeMarket.data?.market?.cashInBankUsd,
|
||||
cashInBankAudit: nativeMarket.data?.market?.cashInBankAudit,
|
||||
supplyNostroMatch: nativeMarket.data?.market?.supplyNostroMatch,
|
||||
hoReserveCoverage: nativeMarket.data?.market?.hoReserveCoverage,
|
||||
pricingKind: nativeMarket.data?.market?.pricingKind,
|
||||
marketSource: nativeMarket.ok && nativeMarket.data?.market ? 'token-aggregation' : undefined,
|
||||
}
|
||||
@@ -161,6 +171,11 @@ export async function enrichFundedWalletTokenRowsWithMarket(
|
||||
priceUsd,
|
||||
balanceUsd,
|
||||
liquidityUsd: market?.liquidityUsd,
|
||||
cashLiquidityUsd: market?.cashLiquidityUsd,
|
||||
cashInBankUsd: market?.cashInBankUsd,
|
||||
cashInBankAudit: market?.cashInBankAudit,
|
||||
supplyNostroMatch: market?.supplyNostroMatch,
|
||||
hoReserveCoverage: market?.hoReserveCoverage,
|
||||
pricingKind: market?.pricingKind,
|
||||
marketSource: market ? 'token-aggregation' : undefined,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user