fix(explorer): normalize token market liquidityUsd client-side
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 13s
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:
@@ -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\"}"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user