feat(explorer): token-list surfaces, homepage trim, and sprint smoke tests
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 14s

Unify wallet/catalog/extended token-list policy, add contract verification CTA,
trim the homepage dashboard with status strip and recent activity, and add Playwright smoke coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-22 20:22:45 -07:00
parent 0778c18e59
commit e3ec87c324
12 changed files with 446 additions and 244 deletions

View File

@@ -0,0 +1,44 @@
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
export const CONTRACT_VERIFICATION_GUIDE_URL =
'https://gitea.d-bis.org/d-bis/proxmox/src/branch/master/docs/08-monitoring/BLOCKSCOUT_VERIFICATION_GUIDE.md'
export const FORGE_VERIFY_COMMAND =
'source scripts/lib/load-project-env.sh && ./scripts/verify/run-contract-verification-with-proxy.sh'
interface ContractVerificationCalloutProps {
address: string
verified: boolean
}
export default function ContractVerificationCallout({ address, verified }: ContractVerificationCalloutProps) {
if (verified) {
return null
}
return (
<Card title="Verify & Publish Contract" className="mb-6">
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
This contract is not verified on the public explorer yet. Verified source improves read/write tooling,
ABI decoding, and auditability for{' '}
<span className="font-mono text-xs">{address}</span>.
</p>
<ul className="mt-4 list-disc space-y-2 pl-5 text-sm text-gray-700 dark:text-gray-300">
<li>
<strong>Forge batch (recommended):</strong>{' '}
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-900">{FORGE_VERIFY_COMMAND}</code>
</li>
<li>
<strong>Operator guide:</strong>{' '}
<Link href={CONTRACT_VERIFICATION_GUIDE_URL} className="text-primary-600 hover:underline" target="_blank" rel="noopener noreferrer">
Blockscout verification guide
</Link>
</li>
<li>
<strong>Native Blockscout UI (LAN):</strong> use contract verification on the VM Blockscout instance when the custom explorer UI does not expose the form.
</li>
</ul>
</Card>
)
}

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react'
import { Card } from '@/libs/frontend-ui-primitives/Card'
import { Address } from '@/libs/frontend-ui-primitives'
import Link from 'next/link'
import { blocksApi, type Block } from '@/services/api/blocks'
import {
@@ -129,6 +130,7 @@ export default function Home({
const [featuredPrices, setFeaturedPrices] = useState<TokenAggregationTokenSnapshot[]>([])
const [missionExpanded, setMissionExpanded] = useState(false)
const [relayExpanded, setRelayExpanded] = useState(false)
const [statsDetailsExpanded, setStatsDetailsExpanded] = useState(false)
const [relayPage, setRelayPage] = useState(1)
const [relayFeedState, setRelayFeedState] = useState<'connecting' | 'live' | 'fallback'>(
initialRelaySummary || initialBridgeStatus ? 'fallback' : 'connecting'
@@ -163,13 +165,13 @@ export default function Home({
}, [chainId])
const loadFeaturedPrices = useCallback(async () => {
const [curatedResult, reportResult] = await Promise.all([
tokensApi.listCuratedSafe(chainId),
const [catalogResult, reportResult] = await Promise.all([
tokensApi.listForSurface('catalog', chainId),
tokensApi.listReportSafe(chainId),
])
const addresses = resolveHomePriceFeedAddresses(
curatedResult.ok ? curatedResult.data : [],
catalogResult.ok ? catalogResult.data : [],
reportResult.ok ? reportResult.data : [],
)
@@ -751,9 +753,20 @@ export default function Home({
</Card>
)}
{(relaySummary || bridgeStatus || stats) && (
<div className="mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-gray-200 bg-white/80 px-4 py-3 text-sm shadow-sm dark:border-gray-800 dark:bg-gray-950/60">
<EntityBadge label={`block ${latestBlock != null ? latestBlock.toLocaleString() : 'unknown'}`} tone="info" />
{chainStatus?.status ? <EntityBadge label={`chain ${chainStatus.status}`} tone={chainStatus.status === 'operational' ? 'success' : 'warning'} /> : null}
{relaySummary ? <EntityBadge label={`${relayOperationalCount} relays ok`} tone="success" /> : null}
<EntityBadge label={relayFeedState === 'live' ? 'live feed' : relayFeedState === 'fallback' ? 'snapshot feed' : 'connecting'} tone={relayFeedState === 'live' ? 'success' : 'info'} />
<span className="text-gray-600 dark:text-gray-400">{latestTransactionAgeLabel}</span>
</div>
)}
{stats && (
<div className="mb-8 space-y-4">
<div className="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-4">
<div className="mb-6 space-y-4">
<p className="text-sm font-semibold text-gray-900 dark:text-white">Network overview</p>
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
{primaryMetricCards.map((card) => (
<Card key={card.label}>
<div className="text-sm text-gray-600 dark:text-gray-400">{card.label}</div>
@@ -763,48 +776,77 @@ export default function Home({
))}
</div>
<div className="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-3">
{activityMetricCards.map((card) => (
<Card key={card.label}>
<div className="text-sm text-gray-600 dark:text-gray-400">{card.label}</div>
<div className="text-xl font-bold sm:text-2xl">{card.value}</div>
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">{card.note}</div>
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">{card.detail}</div>
</Card>
))}
</div>
<button
type="button"
onClick={() => setStatsDetailsExpanded((current) => !current)}
className="text-sm font-semibold text-primary-600 hover:underline"
>
{statsDetailsExpanded ? 'Hide telemetry and freshness' : 'Show telemetry and freshness'}
</button>
{mode === 'guided' ? (
<div className="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-4">
{secondaryMetricCards.map((card) => (
<Card key={card.label}>
<div className="text-sm text-gray-600 dark:text-gray-400">{card.label}</div>
<div className="text-xl font-bold sm:text-2xl">{card.value}</div>
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">{card.note}</div>
</Card>
))}
</div>
) : (
<Card>
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<div className="text-sm font-semibold text-gray-900 dark:text-white">Telemetry Snapshot</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Secondary public stats in a denser expert layout.
</div>
</div>
<div className="grid flex-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
{statsDetailsExpanded ? (
<>
<div className="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-3">
{activityMetricCards.map((card) => (
<Card key={card.label}>
<div className="text-sm text-gray-600 dark:text-gray-400">{card.label}</div>
<div className="text-xl font-bold sm:text-2xl">{card.value}</div>
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">{card.note}</div>
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">{card.detail}</div>
</Card>
))}
</div>
{mode === 'guided' ? (
<div className="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-4">
{secondaryMetricCards.map((card) => (
<div key={card.label} className="rounded-2xl border border-gray-200 bg-gray-50/70 px-4 py-3 dark:border-gray-800 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{card.label}</div>
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{card.value}</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">{card.note}</div>
</div>
<Card key={card.label}>
<div className="text-sm text-gray-600 dark:text-gray-400">{card.label}</div>
<div className="text-xl font-bold sm:text-2xl">{card.value}</div>
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">{card.note}</div>
</Card>
))}
</div>
</div>
</Card>
)}
) : (
<Card>
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<div className="text-sm font-semibold text-gray-900 dark:text-white">Telemetry Snapshot</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Secondary public stats in a denser expert layout.
</div>
</div>
<div className="grid flex-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
{secondaryMetricCards.map((card) => (
<div key={card.label} className="rounded-2xl border border-gray-200 bg-gray-50/70 px-4 py-3 dark:border-gray-800 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{card.label}</div>
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{card.value}</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">{card.note}</div>
</div>
))}
</div>
</div>
</Card>
)}
<ActivityContextPanel
context={activityContext}
title="Freshness Interpretation"
compact
/>
<FreshnessTrustNote
className="mt-3"
context={activityContext}
stats={stats}
bridgeStatus={bridgeStatus}
scopeLabel={
mode === 'guided'
? 'Homepage status combines chain freshness, transaction visibility, and mission-control posture.'
: 'Homepage freshness view aligns chain, transaction, and mission-control posture.'
}
/>
</>
) : null}
</div>
)}
@@ -838,25 +880,6 @@ export default function Home({
</div>
) : null}
<div className="mb-8">
<ActivityContextPanel
context={activityContext}
title="Freshness Interpretation"
compact
/>
<FreshnessTrustNote
className="mt-3"
context={activityContext}
stats={stats}
bridgeStatus={bridgeStatus}
scopeLabel={
mode === 'guided'
? 'Homepage status combines chain freshness, transaction visibility, and mission-control posture.'
: 'Homepage freshness view aligns chain, transaction, and mission-control posture.'
}
/>
</div>
{!stats && (
<Card className="mb-8">
<p className="text-sm text-gray-600 dark:text-gray-400">
@@ -865,7 +888,46 @@ export default function Home({
</Card>
)}
<Card title="Recent Blocks">
<div className="mb-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
<Card title="Recent Transactions">
{recentTransactions.length === 0 ? (
<p className="text-sm text-gray-600 dark:text-gray-400">
Recent transactions are unavailable right now.
</p>
) : (
<div className="space-y-2">
{recentTransactions.map((transaction) => (
<div key={transaction.hash} className="flex flex-col gap-1.5 border-b border-gray-200 py-2 last:border-0 dark:border-gray-700 sm:flex-row sm:items-center sm:justify-between">
<div>
<Link href={`/transactions/${transaction.hash}`} className="text-primary-600 hover:underline">
{transaction.hash.slice(0, 10)}...{transaction.hash.slice(-8)}
</Link>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Block #{transaction.block_number} · from{' '}
<Address address={transaction.from_address} truncate showCopy={false} />
{transaction.to_address ? (
<>
{' '} <Address address={transaction.to_address} truncate showCopy={false} />
</>
) : null}
</div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 sm:text-right">
<div>{formatWeiAsEth(transaction.value, 4)}</div>
<div className="text-xs">{formatTimestamp(transaction.created_at)}</div>
</div>
</div>
))}
</div>
)}
<div className="mt-4">
<Link href="/transactions" className="text-primary-600 hover:underline">
View all transactions
</Link>
</div>
</Card>
<Card title="Recent Blocks">
{recentBlocks.length === 0 ? (
<p className="text-sm text-gray-600 dark:text-gray-400">
Recent blocks are unavailable right now.
@@ -903,120 +965,35 @@ export default function Home({
View all blocks
</Link>
</div>
</Card>
<div className="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
<Card title="Activity Pulse">
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
{mode === 'guided'
? 'A concise public view of chain activity, index coverage, and recent execution patterns.'
: 'Public chain activity and index posture.'}
</p>
<div className="mt-4 grid gap-3 sm: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">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Latest Daily Volume</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{latestTrendPoint ? latestTrendPoint.transaction_count.toLocaleString() : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">{latestTrendPoint?.date || 'Trend feed unavailable'}</div>
</div>
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Recent Success Rate</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{activitySnapshot ? `${Math.round(activitySnapshot.success_rate * 100)}%` : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{activitySnapshot ? `${activitySnapshot.sample_size} sampled transactions` : 'Recent activity snapshot unavailable'}
</div>
</div>
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Avg Recent Fee</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{activitySnapshot ? formatWeiAsEth(Math.round(activitySnapshot.average_fee_wei).toString(), 6) : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Average fee from the recent public sample.</div>
</div>
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Peak Charted Day</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{peakTrendPoint ? peakTrendPoint.transaction_count.toLocaleString() : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">{peakTrendPoint?.date || 'No trend data yet'}</div>
</div>
</div>
<div className="mt-4">
<Link href="/analytics" className="text-primary-600 hover:underline">
Open full analytics
</Link>
</div>
</Card>
<Card title="Explorer Shortcuts">
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
Go directly to the explorer surfaces that provide the strongest operational and discovery context.
</p>
<div className="mt-4 flex flex-wrap gap-3 text-sm">
<Link href="/search" className="text-primary-600 hover:underline">
Search
</Link>
<Link href="/transactions" className="text-primary-600 hover:underline">
Transactions
</Link>
<Link href="/tokens" className="text-primary-600 hover:underline">
Tokens
</Link>
<Link href="/addresses" className="text-primary-600 hover:underline">
Addresses
</Link>
<Link href="/analytics" className="text-primary-600 hover:underline">
Analytics
</Link>
</div>
</Card>
<Card title="Liquidity & Routes">
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
Explore the public Chain 138 DODO PMM liquidity mesh, the canonical route matrix, and the
partner payload endpoints exposed through the explorer.
</p>
<div className="mt-4">
<Link href="/routes" className="text-primary-600 hover:underline">
Open routes and liquidity
</Link>
</div>
</Card>
<Card title="Wallet & Token Discovery">
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
Add Chain 138, Ethereum Mainnet, and ALL Mainnet to MetaMask, then use the explorer token
list URL so supported tokens appear automatically.
</p>
<div className="mt-4">
<Link href="/wallet" className="text-primary-600 hover:underline">
Open wallet tools
</Link>
</div>
</Card>
<Card title="Bridge & Relay Monitoring">
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
Open the public bridge monitoring surface for relay status, mission-control links, bridge trace tooling,
and the visual command center entry points.
</p>
<div className="mt-4">
<Link href="/bridge" className="text-primary-600 hover:underline">
Open bridge monitoring
</Link>
</div>
</Card>
<Card title="Operations Hub">
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
Open the public operations surface for wrapped-asset references, analytics shortcuts, operator links,
system topology views, and other Chain 138 support tools.
</p>
<div className="mt-4">
<Link href="/operations" className="text-primary-600 hover:underline">
Open operations hub
</Link>
</div>
</Card>
</div>
<Card title="Quick links" className="mt-8">
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
Jump to the explorer surfaces used most often for discovery, liquidity, wallet setup, and bridge monitoring.
</p>
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<Link href="/search" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800">
Search
</Link>
<Link href="/tokens" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800">
Tokens
</Link>
<Link href="/wallet" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800">
Wallet & MetaMask
</Link>
<Link href="/routes" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800">
Liquidity & routes
</Link>
<Link href="/bridge" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800">
Bridge monitoring
</Link>
<Link href="/analytics" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800">
Analytics
</Link>
</div>
</Card>
</main>
)
}

View File

@@ -460,7 +460,7 @@ export function AddToMetaMask({
useEffect(() => {
let active = true
tokensApi.listCuratedSafe(138).then(({ ok, data }) => {
tokensApi.listForSurface('wallet', 138).then(({ ok, data }) => {
if (active) {
setCuratedTokens(ok ? (data as TokenListToken[]) : [])
}