Files
explorer-monorepo/frontend/src/pages/tokens/[address].tsx
defiQUG 0972178cc5 refactor: rename SolaceScanScout to Solace and update related configurations
- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation.
- Changed default base URL for Playwright tests and updated security headers to reflect the new branding.
- Enhanced README and API documentation to include new authentication endpoints and product access details.

This refactor aligns the project branding and improves clarity in the API documentation.
2026-04-10 12:52:17 -07:00

505 lines
23 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.
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Address, Card, Table } from '@/libs/frontend-ui-primitives'
import { tokensApi, type TokenHolder, type TokenProfile, type TokenProvenance } from '@/services/api/tokens'
import type { AddressTokenTransfer } from '@/services/api/addresses'
import type { MissionControlLiquidityPool } from '@/services/api/routes'
import PageIntro from '@/components/common/PageIntro'
import { DetailRow } from '@/components/common/DetailRow'
import EntityBadge from '@/components/common/EntityBadge'
import GruStandardsCard from '@/components/common/GruStandardsCard'
import { formatTokenAmount, formatTimestamp } from '@/utils/format'
import { getGruStandardsProfileSafe, type GruStandardsProfile } from '@/services/api/gru'
import { getGruExplorerMetadata } from '@/services/api/gruExplorerData'
function isValidAddress(value: string) {
return /^0x[a-fA-F0-9]{40}$/.test(value)
}
function toNumeric(value: string | number | null | undefined): number | null {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number(value)
if (Number.isFinite(parsed)) return parsed
}
return null
}
function formatUsd(value: string | number | null | undefined): string {
const numeric = toNumeric(value)
if (numeric == null) return 'Unavailable'
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: numeric >= 100 ? 0 : 2,
}).format(numeric)
}
export default function TokenDetailPage() {
const router = useRouter()
const address = typeof router.query.address === 'string' ? router.query.address : ''
const isValidTokenAddress = address !== '' && isValidAddress(address)
const [token, setToken] = useState<TokenProfile | null>(null)
const [provenance, setProvenance] = useState<TokenProvenance | null>(null)
const [holders, setHolders] = useState<TokenHolder[]>([])
const [transfers, setTransfers] = useState<AddressTokenTransfer[]>([])
const [pools, setPools] = useState<MissionControlLiquidityPool[]>([])
const [gruProfile, setGruProfile] = useState<GruStandardsProfile | null>(null)
const [loading, setLoading] = useState(true)
const loadToken = useCallback(async () => {
setLoading(true)
try {
const [tokenResult, provenanceResult, holdersResult, transfersResult, poolsResult] = await Promise.all([
tokensApi.getSafe(address),
tokensApi.getProvenanceSafe(address),
tokensApi.getHoldersSafe(address, 1, 10),
tokensApi.getTransfersSafe(address, 1, 10),
tokensApi.getRelatedPoolsSafe(address),
])
setToken(tokenResult.ok ? tokenResult.data : null)
setProvenance(provenanceResult.ok ? provenanceResult.data : null)
setHolders(holdersResult.ok ? holdersResult.data : [])
setTransfers(transfersResult.ok ? transfersResult.data : [])
setPools(poolsResult.ok ? poolsResult.data : [])
if (tokenResult.ok && tokenResult.data) {
const gruResult = await getGruStandardsProfileSafe({
address,
symbol: tokenResult.data.symbol,
tags: provenanceResult.ok ? provenanceResult.data?.tags || [] : [],
})
setGruProfile(gruResult.ok ? gruResult.data : null)
} else {
setGruProfile(null)
}
} catch {
setToken(null)
setProvenance(null)
setHolders([])
setTransfers([])
setPools([])
setGruProfile(null)
} finally {
setLoading(false)
}
}, [address])
useEffect(() => {
if (!router.isReady || !address) {
setLoading(router.isReady ? false : true)
return
}
if (!isValidTokenAddress) {
setLoading(false)
setToken(null)
return
}
void loadToken()
}, [address, isValidTokenAddress, loadToken, router.isReady])
const provenanceTags = useMemo(() => {
const tags = [...(provenance?.tags || [])]
if (provenance?.listed && !tags.includes('listed')) {
tags.unshift('listed')
}
return tags
}, [provenance])
const holderConcentration = useMemo(() => {
if (!token?.total_supply || holders.length === 0) return null
const topHolder = holders[0]
if (!topHolder) return null
const supply = BigInt(token.total_supply)
if (supply === 0n) return null
const topBalance = BigInt(topHolder.value || '0')
return Number((topBalance * 10000n) / supply) / 100
}, [holders, token?.total_supply])
const liquiditySummary = useMemo(() => {
const poolCount = pools.length
const totalTvl = pools.reduce((sum, pool) => sum + (pool.tvl || 0), 0)
return {
poolCount,
totalTvl,
}
}, [pools])
const transferFlowSummary = useMemo(() => {
const uniqueSenders = new Set(transfers.map((transfer) => transfer.from_address.toLowerCase())).size
const uniqueRecipients = new Set(transfers.map((transfer) => transfer.to_address.toLowerCase())).size
return {
sampleSize: transfers.length,
uniqueSenders,
uniqueRecipients,
}
}, [transfers])
const trustSummary = useMemo(() => {
const signals: string[] = []
if (provenance?.listed) signals.push('listed in the Chain 138 registry')
if (provenanceTags.includes('compliant')) signals.push('marked compliant')
if (provenanceTags.includes('bridge')) signals.push('bridge-linked asset')
if (liquiditySummary.poolCount > 0) signals.push(`${liquiditySummary.poolCount} related liquidity pool${liquiditySummary.poolCount === 1 ? '' : 's'}`)
if ((token?.holders || 0) > 0) signals.push(`${token?.holders?.toLocaleString()} indexed holders`)
return signals
}, [liquiditySummary.poolCount, provenance?.listed, provenanceTags, token?.holders])
const trustProfile = useMemo(() => {
if (provenance?.listed && provenanceTags.includes('compliant')) {
return {
label: 'high confidence',
tone: 'success' as const,
summary: 'Curated registry coverage plus standards and policy metadata make this one of the explorers better-understood assets.',
}
}
if (provenance?.listed || provenanceTags.includes('bridge') || liquiditySummary.poolCount > 0) {
return {
label: 'moderate confidence',
tone: 'info' as const,
summary: 'There are enough public signals to treat this as a known asset, but not enough to confuse that with blanket safety.',
}
}
return {
label: 'limited confidence',
tone: 'warning' as const,
summary: 'The explorer can see the token, but curated provenance and broader trust signals remain thin.',
}
}, [liquiditySummary.poolCount, provenance?.listed, provenanceTags])
const gruExplorerMetadata = useMemo(
() => getGruExplorerMetadata({ address: token?.address || address, symbol: token?.symbol }),
[address, token?.address, token?.symbol],
)
const holderColumns = [
{
header: 'Holder',
accessor: (holder: TokenHolder) => (
<Link href={`/addresses/${holder.address}`} className="text-primary-600 hover:underline">
{holder.label || <Address address={holder.address} truncate showCopy={false} />}
</Link>
),
},
{
header: 'Balance',
accessor: (holder: TokenHolder) => formatTokenAmount(holder.value, token?.decimals || holder.token_decimals, token?.symbol),
},
]
const transferColumns = [
{
header: 'Hash',
accessor: (transfer: AddressTokenTransfer) => (
<Link href={`/transactions/${transfer.transaction_hash}`} className="text-primary-600 hover:underline">
<Address address={transfer.transaction_hash} truncate showCopy={false} />
</Link>
),
},
{
header: 'Posture',
accessor: (transfer: AddressTokenTransfer) => {
const gruMetadata = getGruExplorerMetadata({
address: transfer.token_address,
symbol: transfer.token_symbol,
})
if (!gruMetadata) {
return <span className="text-gray-500 dark:text-gray-400">Generic token</span>
}
return (
<div className="flex flex-wrap gap-2">
<EntityBadge label="GRU" tone="success" />
{gruMetadata.x402Ready ? <EntityBadge label="x402 ready" tone="info" /> : null}
{gruMetadata.iso20022Ready ? <EntityBadge label="ISO-20022" tone="info" /> : null}
</div>
)
},
},
{
header: 'From',
accessor: (transfer: AddressTokenTransfer) => (
<Link href={`/addresses/${transfer.from_address}`} className="text-primary-600 hover:underline">
{transfer.from_label || <Address address={transfer.from_address} truncate showCopy={false} />}
</Link>
),
},
{
header: 'To',
accessor: (transfer: AddressTokenTransfer) => (
<Link href={`/addresses/${transfer.to_address}`} className="text-primary-600 hover:underline">
{transfer.to_label || <Address address={transfer.to_address} truncate showCopy={false} />}
</Link>
),
},
{
header: 'Amount',
accessor: (transfer: AddressTokenTransfer) => formatTokenAmount(transfer.value, transfer.token_decimals, transfer.token_symbol),
},
{
header: 'When',
accessor: (transfer: AddressTokenTransfer) => formatTimestamp(transfer.timestamp),
},
]
const poolColumns = [
{
header: 'Pool',
accessor: (pool: MissionControlLiquidityPool) => (
<Link href={`/addresses/${pool.address}`} className="text-primary-600 hover:underline">
<Address address={pool.address} truncate showCopy={false} />
</Link>
),
},
{
header: 'DEX',
accessor: (pool: MissionControlLiquidityPool) => pool.dex || 'Unknown',
},
{
header: 'Pair',
accessor: (pool: MissionControlLiquidityPool) => `${pool.token0?.symbol || 'Token 0'} / ${pool.token1?.symbol || 'Token 1'}`,
},
{
header: 'TVL',
accessor: (pool: MissionControlLiquidityPool) => pool.tvl != null ? `$${Math.round(pool.tvl).toLocaleString()}` : 'N/A',
},
]
return (
<div className="container mx-auto px-4 py-6 sm:py-8">
<PageIntro
eyebrow="Token Detail"
title={token?.symbol || token?.name || 'Token'}
description="Inspect token supply, holders, transfers, and liquidity context with the sort of composure one normally has to borrow from a better explorer."
actions={[
{ href: '/tokens', label: 'Token index' },
{ href: '/liquidity', label: 'Liquidity access' },
{ href: '/search', label: 'Explorer search' },
]}
/>
<div className="mb-6 flex flex-wrap gap-3 text-sm">
<Link href="/tokens" className="text-primary-600 hover:underline">
Back to tokens
</Link>
{address && (
<Link href={`/addresses/${address}`} className="text-primary-600 hover:underline">
Open contract address
</Link>
)}
</div>
{!router.isReady || loading ? (
<Card>
<p className="text-sm text-gray-600 dark:text-gray-400">Loading token...</p>
</Card>
) : !isValidTokenAddress ? (
<Card>
<p className="text-sm text-gray-600 dark:text-gray-400">Invalid token address. Please use a full 42-character 0x-prefixed address.</p>
</Card>
) : !token ? (
<Card>
<p className="text-sm text-gray-600 dark:text-gray-400">Token details were not found for this address.</p>
</Card>
) : (
<div className="space-y-6">
<Card title="Token Overview">
<dl className="space-y-4">
<DetailRow label="Name">{token.name || 'Unknown'}</DetailRow>
<DetailRow label="Symbol">{token.symbol || 'Unknown'}</DetailRow>
<DetailRow label="Address">
<Address address={token.address} />
</DetailRow>
<DetailRow label="Type">{token.type || 'Unknown'}</DetailRow>
<DetailRow label="Decimals">{token.decimals}</DetailRow>
{token.total_supply && (
<DetailRow label="Total Supply">
{formatTokenAmount(token.total_supply, token.decimals, token.symbol)}
</DetailRow>
)}
{token.holders != null && (
<DetailRow label="Holders">{token.holders.toLocaleString()}</DetailRow>
)}
<DetailRow label="Provenance" valueClassName="flex flex-wrap gap-2">
{provenanceTags.length > 0 ? provenanceTags.map((tag) => (
<EntityBadge key={tag} label={tag} />
)) : <span className="text-gray-500">No curated provenance metadata yet</span>}
</DetailRow>
<DetailRow label="Listing">
{provenance?.listed ? 'Listed in the Chain 138 token registry' : 'Not present in the curated Chain 138 token registry'}
</DetailRow>
<DetailRow label="Trust Posture">
<div className="space-y-2">
<EntityBadge label={trustProfile.label} tone={trustProfile.tone} />
<div className="text-sm text-gray-600 dark:text-gray-400">{trustProfile.summary}</div>
</div>
</DetailRow>
</dl>
</Card>
<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">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Market Context</div>
<div className="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<div>Indicative price: {formatUsd(token.exchange_rate)}</div>
<div>24h volume: {formatUsd(token.volume_24h)}</div>
<div>Market cap: {formatUsd(token.circulating_market_cap)}</div>
</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">Liquidity & Distribution</div>
<div className="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<div>Related pools: {liquiditySummary.poolCount.toLocaleString()}</div>
<div>Total visible TVL: {formatUsd(liquiditySummary.totalTvl)}</div>
<div>Largest visible holder: {holderConcentration != null ? `${holderConcentration}% of supply` : 'Unavailable'}</div>
</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">Transfer Activity</div>
<div className="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<div>Recent transfer sample: {transferFlowSummary.sampleSize.toLocaleString()}</div>
<div>Unique senders: {transferFlowSummary.uniqueSenders.toLocaleString()}</div>
<div>Unique recipients: {transferFlowSummary.uniqueRecipients.toLocaleString()}</div>
</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">Trust Signals</div>
<div className="mt-3 flex flex-wrap gap-2">
{trustSummary.length > 0 ? trustSummary.map((signal) => (
<EntityBadge key={signal} label={signal} tone="info" className="normal-case tracking-normal" />
)) : (
<span className="text-sm text-gray-600 dark:text-gray-400">No strong trust signals are available yet beyond the base token profile.</span>
)}
</div>
</div>
</div>
</Card>
{gruProfile ? <GruStandardsCard profile={gruProfile} /> : null}
{gruExplorerMetadata ? (
<Card title="x402 And ISO-20022 Posture">
<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">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">x402 readiness</div>
<div className="mt-3 flex flex-wrap gap-2">
<EntityBadge label={gruExplorerMetadata.x402Ready ? 'x402 ready' : 'x402 not ready'} tone={gruExplorerMetadata.x402Ready ? 'success' : 'warning'} />
{gruExplorerMetadata.x402PreferredVersion ? <EntityBadge label={`preferred ${gruExplorerMetadata.x402PreferredVersion}`} tone="info" /> : null}
{gruExplorerMetadata.canonicalForwardVersion ? <EntityBadge label={`forward ${gruExplorerMetadata.canonicalForwardVersion}`} tone="success" /> : null}
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
{gruExplorerMetadata.x402Ready
? 'This asset is modeled as payment-ready in the GRU explorer posture, meaning the preferred version exposes the signature and domain surfaces needed for x402-style settlement flows.'
: 'This asset is not currently marked as x402-ready in the local explorer intelligence layer.'}
</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">ISO-20022 and governance</div>
<div className="mt-3 flex flex-wrap gap-2">
<EntityBadge label={gruExplorerMetadata.iso20022Ready ? 'ISO-20022 aligned' : 'ISO-20022 unclear'} tone={gruExplorerMetadata.iso20022Ready ? 'success' : 'warning'} />
{gruExplorerMetadata.currencyCode ? <EntityBadge label={gruExplorerMetadata.currencyCode} tone="neutral" /> : null}
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
{gruExplorerMetadata.iso20022Ready
? 'The local GRU metadata for this asset treats it as part of the ISO-20022-aligned settlement model, with governance, supervision, disclosure, and reporting posture expected around the token surface.'
: 'The explorer does not currently have a strong ISO-20022 posture signal for this asset.'}
</div>
<div className="mt-3 flex flex-wrap gap-3 text-sm">
<Link href="/docs/gru" className="text-primary-600 hover:underline">
GRU guide
</Link>
<Link href="/docs/transaction-review" className="text-primary-600 hover:underline">
Transaction review guide
</Link>
<Link href="/docs" className="text-primary-600 hover:underline">
General docs
</Link>
</div>
</div>
</div>
</Card>
) : null}
{gruExplorerMetadata && gruExplorerMetadata.otherNetworks.length > 0 ? (
<Card title="Other Networks">
<div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
These are sibling representations or settlement counterparts for the same GRU asset family on other networks, drawn from the local transport and mapping posture used by this workspace.
</p>
<div className="space-y-3">
{gruExplorerMetadata.otherNetworks.map((network) => (
<div key={`${network.chainId}-${network.symbol}-${network.address}`} 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 gap-2">
<EntityBadge label={network.chainName} tone="neutral" className="normal-case tracking-normal" />
<EntityBadge label={network.symbol} tone="info" />
<EntityBadge label={`chain ${network.chainId}`} tone="warning" />
</div>
<div className="mt-3 break-all text-sm text-gray-900 dark:text-white">{network.address}</div>
{network.notes ? <div className="mt-2 text-sm text-gray-600 dark:text-gray-400">{network.notes}</div> : null}
<div className="mt-3 flex flex-wrap gap-3 text-sm">
{network.explorerUrl ? (
<a href={network.explorerUrl} target="_blank" rel="noopener noreferrer" className="text-primary-600 hover:underline">
Open network explorer
</a>
) : null}
<Link href={`/search?q=${encodeURIComponent(network.symbol)}`} className="text-primary-600 hover:underline">
Search symbol
</Link>
</div>
</div>
))}
</div>
</div>
</Card>
) : null}
<Card title="Top Holders">
<Table
columns={holderColumns}
data={holders}
emptyMessage="No holder data was available for this token."
keyExtractor={(holder) => holder.address}
/>
</Card>
<Card title="Recent Transfers">
{gruExplorerMetadata ? (
<div className="mb-4 flex flex-wrap items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
<span>
This token is tracked with GRU posture, so the transfer sample below can be read alongside its standards and transaction review guidance.
</span>
<Link href="/docs/gru" className="text-primary-600 hover:underline">
GRU guide
</Link>
<Link href="/docs/transaction-review" className="text-primary-600 hover:underline">
Transaction review guide
</Link>
</div>
) : null}
<Table
columns={transferColumns}
data={transfers}
emptyMessage="No recent token transfers were available."
keyExtractor={(transfer) => `${transfer.transaction_hash}-${transfer.value}-${transfer.from_address}`}
/>
</Card>
<Card title="Related Liquidity">
<Table
columns={poolColumns}
data={pools}
emptyMessage="No related liquidity pools were exposed through mission control for this token."
keyExtractor={(pool) => pool.address}
/>
</Card>
</div>
)}
</div>
)
}