fix(explorer): normalize token market liquidityUsd client-side
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 13s

- Mirror token-aggregation liquidity scaling in tokenAggregation API layer
- Tokens page and shared brand/layout tweaks
- deploy-live workflow adjustment

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-11 12:55:08 -07:00
parent d4f922c26e
commit 654933cb36
11 changed files with 121 additions and 88 deletions

View File

@@ -37,7 +37,8 @@ jobs:
if [ -z "$BRANCH" ] || [ "$BRANCH" = "HEAD" ]; then
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
fi
curl -sSf -X POST "${{ secrets.PHOENIX_DEPLOY_URL }}" \
curl -sSf --connect-timeout 10 --max-time 3600 \
-X POST "${{ secrets.PHOENIX_DEPLOY_URL }}" \
-H "Authorization: Bearer ${{ secrets.PHOENIX_DEPLOY_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"repo\":\"${{ gitea.repository }}\",\"sha\":\"${SHA}\",\"branch\":\"${BRANCH}\",\"target\":\"explorer-live\"}"

View File

@@ -11,12 +11,12 @@ export function Card({ children, className, title }: CardProps) {
return (
<div
className={clsx(
'rounded-xl bg-white p-4 shadow-md dark:bg-gray-800 sm:p-6',
'rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/70 sm:p-5',
className
)}
>
{title && (
<h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white sm:mb-4 sm:text-xl">
<h3 className="mb-3 text-base font-semibold text-gray-900 dark:text-white sm:text-lg">
{title}
</h3>
)}

View File

@@ -8,7 +8,7 @@ export default function BrandLockup({ compact = false }: { compact?: boolean })
<span
className={[
'block truncate font-semibold tracking-[-0.02em] text-gray-950 dark:text-white',
compact ? 'text-[1.45rem]' : 'text-[1.65rem]',
compact ? 'text-[1.2rem]' : 'text-[1.35rem]',
].join(' ')}
>
DBIS Explorer
@@ -16,7 +16,7 @@ export default function BrandLockup({ compact = false }: { compact?: boolean })
<span
className={[
'block truncate font-medium uppercase text-gray-500 dark:text-gray-400',
compact ? 'text-[0.72rem] tracking-[0.14em]' : 'text-[0.8rem] tracking-[0.12em]',
compact ? 'text-[0.64rem] tracking-[0.13em]' : 'text-[0.68rem] tracking-[0.12em]',
].join(' ')}
>
Chain 138 Explorer by DBIS

View File

@@ -1,9 +1,9 @@
export default function BrandMark({ size = 'default' }: { size?: 'default' | 'compact' }) {
const containerClassName =
size === 'compact'
? 'h-10 w-10 rounded-xl'
: 'h-11 w-11 rounded-2xl'
const iconClassName = size === 'compact' ? 'h-6 w-6' : 'h-7 w-7'
? 'h-9 w-9 rounded-lg'
: 'h-10 w-10 rounded-lg'
const iconClassName = size === 'compact' ? 'h-5 w-5' : 'h-6 w-6'
return (
<span

View File

@@ -27,7 +27,9 @@ export default function MarketEvidenceNote({
compact?: boolean
}) {
const freshness = lastUpdated ? `${formatRelativeAge(lastUpdated)} (${formatTimestamp(lastUpdated)})` : 'timestamp unavailable'
const text = `Source: ${formatSource(source)}. Updated: ${freshness}. Method: ${method}`
const text = compact
? `Updated ${freshness} · ${formatSource(source)}`
: `Source: ${formatSource(source)}. Updated: ${freshness}. Method: ${method}`
return (
<p className={`${compact ? 'mt-1' : 'mt-3'} text-xs leading-5 text-gray-500 dark:text-gray-400`}>

View File

@@ -703,10 +703,10 @@ export default function Navbar() {
<>
<header className="sticky top-0 z-40 border-b border-gray-200/90 bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/88 dark:border-gray-800 dark:bg-gray-950/92">
<div className="container mx-auto px-4">
<div className="flex min-h-[76px] items-center gap-4 lg:min-h-[84px]">
<div className="flex min-h-[60px] items-center gap-3 lg:min-h-[64px]">
<Link
href="/"
className="group inline-flex min-w-0 items-center gap-3 rounded-2xl py-2 pr-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-950"
className="group inline-flex min-w-0 items-center gap-2 rounded-lg py-1.5 pr-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-950"
onClick={() => setMobileMenuOpen(false)}
aria-label="Go to DBIS Explorer home"
>

View File

@@ -17,29 +17,33 @@ export default function PageIntro({
actions?: PageIntroAction[]
}) {
return (
<div className="mb-6 rounded-3xl border border-gray-200 bg-white/90 p-5 shadow-sm dark:border-gray-700 dark:bg-gray-800/80 sm:mb-8 sm:p-6">
{eyebrow ? (
<div className="mb-3 inline-flex rounded-full border border-sky-200 bg-sky-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-sky-700 dark:border-sky-900/60 dark:bg-sky-950/30 dark:text-sky-300">
{eyebrow}
<section className="mb-5 border-b border-gray-200 pb-5 dark:border-gray-800 sm:mb-6 sm:pb-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="min-w-0">
{eyebrow ? (
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-primary-700 dark:text-primary-300">
{eyebrow}
</div>
) : null}
<h1 className="text-2xl font-semibold tracking-normal text-gray-950 dark:text-white sm:text-3xl">{title}</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-400">
{description}
</p>
</div>
) : null}
<h1 className="text-3xl font-bold text-gray-900 dark:text-white sm:text-4xl">{title}</h1>
<p className="mt-3 max-w-4xl text-sm leading-7 text-gray-600 dark:text-gray-400 sm:text-base">
{description}
</p>
{actions.length > 0 ? (
<div className="mt-5 flex flex-wrap gap-3">
<div className="flex flex-wrap gap-2 lg:justify-end">
{actions.map((action) => (
<Link
key={`${action.href}-${action.label}`}
href={action.href}
className="rounded-full border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-600 dark:text-gray-200 dark:hover:text-primary-300"
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200 dark:hover:text-primary-300"
>
{action.label}
</Link>
))}
</div>
) : null}
</div>
</div>
</section>
)
}

View File

@@ -128,64 +128,28 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
]}
/>
<Card className="mb-6" title="Find a token">
<form onSubmit={handleSubmit} className="flex flex-col gap-3 md:flex-row">
<Card className="mb-5" title="Find a token">
<form onSubmit={handleSubmit} className="flex flex-col gap-2 sm:flex-row">
<input
type="text"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Token symbol, name, or contract address"
className="flex-1 rounded-lg border border-gray-300 px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500"
className="min-h-10 flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-950"
/>
<button
type="submit"
disabled={!query.trim()}
className="rounded-lg bg-primary-600 px-6 py-2 text-white hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-50"
className="min-h-10 rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-50"
>
Search
</button>
</form>
<p className="mt-3 text-sm text-gray-600 dark:text-gray-400">
Contract addresses open dedicated token detail pages with holders, transfers, provenance, and liquidity context.
</p>
</Card>
<div className="grid gap-6 lg:grid-cols-3">
<Card title="Curated Registry">
<p className="text-sm text-gray-600 dark:text-gray-400">
Review listed Chain 138 assets with provenance tags such as GRU, compliant, cW public-network, and reference asset before acting on a symbol match.
</p>
<div className="mt-4">
<Link href="/tokens" className="text-primary-600 hover:underline">
Browse curated tokens
</Link>
</div>
</Card>
<Card title="Wallet Discovery">
<p className="text-sm text-gray-600 dark:text-gray-400">
Add Chain 138 and supported token metadata to MetaMask directly from the explorer wallet tools.
</p>
<div className="mt-4">
<Link href="/wallet" className="text-primary-600 hover:underline">
Open wallet tools
</Link>
</div>
</Card>
<Card title="Liquidity Routes">
<p className="text-sm text-gray-600 dark:text-gray-400">
Review canonical PMM routes, partner payload templates, and token-routing examples for supported pools.
</p>
<div className="mt-4">
<Link href="/liquidity" className="text-primary-600 hover:underline">
Open liquidity access
</Link>
</div>
</Card>
</div>
<div className="mt-8">
<div className="mt-5">
<Card title="Curated Chain 138 tokens">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{featuredCuratedTokens
.filter((token): token is TokenListToken & { address: string } => typeof token.address === 'string' && token.address.trim().length > 0)
.map((token) => {
@@ -194,17 +158,29 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
<Link
key={token.address}
href={`/tokens/${token.address}`}
className="rounded-lg border border-gray-200 p-4 transition hover:border-primary-400 hover:shadow-sm dark:border-gray-700"
className="rounded-lg border border-gray-200 p-4 transition hover:border-primary-400 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-950/50"
>
<div className="text-sm font-semibold text-gray-900 dark:text-white">{token.symbol || token.name || 'Token'}</div>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-semibold text-gray-900 dark:text-white">{token.symbol || token.name || 'Token'}</div>
<p className="mt-1 line-clamp-2 text-sm leading-5 text-gray-600 dark:text-gray-400">
{token.name || 'Listed in the Chain 138 token registry.'}
</p>
</p>
</div>
</div>
{market ? (
<div className="mt-3 space-y-1 text-sm text-gray-700 dark:text-gray-300">
<div>Live price: {formatUsd(market.priceUsd)}</div>
<div>Visible liquidity: {formatUsd(market.liquidityUsd)}</div>
<div className="mt-3 grid grid-cols-2 gap-2 text-sm text-gray-700 dark:text-gray-300">
<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">Price</div>
<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>
<div className="col-span-2">
<MarketEvidenceNote lastUpdated={market.lastUpdated} compact />
</div>
</div>
) : null}
{token.tags && token.tags.length > 0 && (
@@ -221,17 +197,17 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
</Card>
</div>
<div className="mt-8">
<div className="mt-5">
<Card title="Common token searches">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{quickSearches.map((token) => (
<Link
key={token.label}
href={`/search?q=${encodeURIComponent(token.label)}`}
className="rounded-lg border border-gray-200 p-4 transition hover:border-primary-400 hover:shadow-sm dark:border-gray-700"
className="rounded-lg border border-gray-200 px-3 py-2 transition hover:border-primary-400 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-950/50"
>
<div className="text-sm font-semibold text-gray-900 dark:text-white">{token.label}</div>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">{token.description}</p>
<p className="mt-1 line-clamp-2 text-xs leading-5 text-gray-600 dark:text-gray-400">{token.description}</p>
</Link>
))}
</div>

View File

@@ -61,24 +61,76 @@ function toNumber(value: number | string | null | undefined): number | undefined
return undefined
}
function decimalStringToNumber(value: string | null | undefined, decimals: number | undefined): number | null {
if (!value || decimals == null || decimals < 0) return null
try {
const raw = BigInt(value)
if (raw <= 0n) return 0
const scale = 10n ** BigInt(decimals)
const whole = raw / scale
const fraction = raw % scale
const normalized = Number(whole) + Number(fraction) / Number(scale)
return Number.isFinite(normalized) ? normalized : null
} catch {
return null
}
}
function normalizePossiblyRawLiquidityUsd(
liquidityUsd: number | undefined,
totalSupply: string | null | undefined,
decimals: number | undefined,
priceUsd: number | undefined,
): number | undefined {
if (liquidityUsd == null || !(liquidityUsd > 0)) return liquidityUsd
if (!Number.isInteger(decimals) || decimals == null || decimals <= 0) return liquidityUsd
const supplyUnits = decimalStringToNumber(totalSupply, decimals)
const price = priceUsd != null && priceUsd > 0 ? priceUsd : 1
if (supplyUnits == null || supplyUnits <= 0) return liquidityUsd
const plausibleSupplyValue = supplyUnits * price
const normalizedLiquidity = liquidityUsd / 10 ** decimals
if (
Number.isFinite(plausibleSupplyValue) &&
Number.isFinite(normalizedLiquidity) &&
liquidityUsd > plausibleSupplyValue &&
normalizedLiquidity <= plausibleSupplyValue
) {
return normalizedLiquidity
}
return liquidityUsd
}
function normalizeTokenSnapshot(raw: RawTokenAggregationTokenResponse): TokenAggregationTokenSnapshot | null {
const token = raw.token
if (!token?.address) {
return null
}
const decimals = toNumber(token.decimals)
const priceUsd = toNumber(token.market?.priceUsd)
const liquidityUsd = normalizePossiblyRawLiquidityUsd(
toNumber(token.market?.liquidityUsd),
token.totalSupply,
decimals,
priceUsd,
)
return {
chainId: toNumber(token.chainId) ?? 138,
address: token.address,
name: token.name || undefined,
symbol: token.symbol || undefined,
decimals: toNumber(token.decimals),
decimals,
totalSupply: token.totalSupply || undefined,
market: token.market
? {
priceUsd: toNumber(token.market.priceUsd),
priceUsd,
volume24h: toNumber(token.market.volume24h),
liquidityUsd: toNumber(token.market.liquidityUsd),
liquidityUsd,
lastUpdated: token.market.lastUpdated || null,
}
: null,

View File

@@ -29,11 +29,11 @@ describe('tokensApi', () => {
symbol: 'cUSDT',
name: 'Tether USD (Compliant)',
decimals: 6,
totalSupply: '1000',
totalSupply: '928784229000000',
market: {
priceUsd: 1,
volume24h: 2500,
liquidityUsd: 500000,
liquidityUsd: 2270037545568.842,
lastUpdated: '2026-04-26T01:00:00.000Z',
},
},
@@ -82,7 +82,7 @@ describe('tokensApi', () => {
expect(token.data?.symbol).toBe('cUSDT')
expect(token.data?.exchange_rate).toBe(1)
expect(token.data?.volume_24h).toBe(2500)
expect(token.data?.liquidity_usd).toBe(500000)
expect(token.data?.liquidity_usd).toBe(2270037.545568842)
expect(token.data?.price_source).toBe('token-aggregation')
expect(holders.data[0].label).toBe('Treasury')
expect(transfers.data[0].token_symbol).toBe('cUSDT')

View File

@@ -18,11 +18,9 @@
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
background: #f8fafc;
}
.dark body {
background: #030712;
}