feat(explorer): token signing surface card, ERC-5267 domain read, tabular top holders
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 15s
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 15s
- Add TokenSigningSurfaceCard: ABI flags, eip712Domain eth_call decode, verification metadata - Pass contract profile into GRU standards detection on token page - Table layout=tabular for Top Holders column layout at all breakpoints - Fallback provenance name/symbol; show signing card when token API empty - eip712Domain.ts: decode ERC-5267 tuple return data Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
217
frontend/src/components/common/TokenSigningSurfaceCard.tsx
Normal file
217
frontend/src/components/common/TokenSigningSurfaceCard.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import { DetailRow } from '@/components/common/DetailRow'
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import type { ContractProfile } from '@/services/api/contracts'
|
||||
import { fetchEip712DomainDecoded, type DecodedEip712Domain } from '@/services/api/eip712Domain'
|
||||
|
||||
function hasMethod(profile: ContractProfile | null | undefined, name: string): boolean {
|
||||
if (!profile) return false
|
||||
const all = [...(profile.read_methods || []), ...(profile.write_methods || [])]
|
||||
return all.some((m) => m.name === name)
|
||||
}
|
||||
|
||||
const ERC5267_EXPLANATION =
|
||||
'ERC-5267 defines eip712Domain() so wallets and relayers can discover the EIP-712 signing domain without guessing types or replay parameters.'
|
||||
|
||||
export default function TokenSigningSurfaceCard({
|
||||
address,
|
||||
contractProfile,
|
||||
}: {
|
||||
address: string
|
||||
contractProfile: ContractProfile | null
|
||||
}) {
|
||||
const [domain, setDomain] = useState<DecodedEip712Domain | null>(null)
|
||||
const [domainError, setDomainError] = useState<string | null>(null)
|
||||
|
||||
const abiHasEip712Domain = hasMethod(contractProfile, 'eip712Domain')
|
||||
|
||||
useEffect(() => {
|
||||
if (!abiHasEip712Domain) {
|
||||
setDomain(null)
|
||||
setDomainError(null)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
setDomainError(null)
|
||||
void (async () => {
|
||||
try {
|
||||
const decoded = await fetchEip712DomainDecoded(address)
|
||||
if (!cancelled) {
|
||||
setDomain(decoded)
|
||||
if (!decoded) setDomainError('eip712Domain() is present in the ABI but the live call did not return decodable data (proxy, revert, or RPC).')
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
setDomain(null)
|
||||
setDomainError(e instanceof Error ? e.message : 'Failed to read eip712Domain.')
|
||||
}
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [address, abiHasEip712Domain])
|
||||
|
||||
const standards = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'ERC-20',
|
||||
detected:
|
||||
hasMethod(contractProfile, 'name') ||
|
||||
hasMethod(contractProfile, 'symbol') ||
|
||||
hasMethod(contractProfile, 'decimals') ||
|
||||
hasMethod(contractProfile, 'totalSupply'),
|
||||
note: 'Standard fungible token interface expected by explorers and wallets.',
|
||||
},
|
||||
{
|
||||
id: 'EIP-712',
|
||||
detected: hasMethod(contractProfile, 'DOMAIN_SEPARATOR') || abiHasEip712Domain,
|
||||
note: 'Typed structured data hashing for signatures.',
|
||||
},
|
||||
{
|
||||
id: 'ERC-2612',
|
||||
detected: hasMethod(contractProfile, 'permit') || hasMethod(contractProfile, 'nonces'),
|
||||
note: 'Permit-style allowance via signature.',
|
||||
},
|
||||
{
|
||||
id: 'ERC-3009',
|
||||
detected:
|
||||
hasMethod(contractProfile, 'authorizationState') ||
|
||||
hasMethod(contractProfile, 'transferWithAuthorization') ||
|
||||
hasMethod(contractProfile, 'receiveWithAuthorization'),
|
||||
note: 'Transfer authorization without prior allowance.',
|
||||
},
|
||||
{
|
||||
id: 'ERC-5267',
|
||||
detected: abiHasEip712Domain,
|
||||
note: ERC5267_EXPLANATION,
|
||||
},
|
||||
],
|
||||
[contractProfile, abiHasEip712Domain],
|
||||
)
|
||||
|
||||
const verificationMeta = useMemo(() => {
|
||||
if (!contractProfile) return []
|
||||
const rows: { label: string; value: string }[] = []
|
||||
if (contractProfile.contract_name) rows.push({ label: 'Verified name', value: contractProfile.contract_name })
|
||||
if (contractProfile.compiler_version) rows.push({ label: 'Compiler', value: contractProfile.compiler_version })
|
||||
if (contractProfile.license_type) rows.push({ label: 'License', value: contractProfile.license_type })
|
||||
if (contractProfile.evm_version) rows.push({ label: 'EVM version', value: contractProfile.evm_version })
|
||||
if (contractProfile.optimization_enabled != null) {
|
||||
rows.push({
|
||||
label: 'Optimization',
|
||||
value: `${contractProfile.optimization_enabled ? 'On' : 'Off'}${contractProfile.optimization_runs != null ? ` · ${contractProfile.optimization_runs} runs` : ''}`,
|
||||
})
|
||||
}
|
||||
if (contractProfile.source_status_text) rows.push({ label: 'Source status', value: contractProfile.source_status_text })
|
||||
return rows
|
||||
}, [contractProfile])
|
||||
|
||||
if (!contractProfile) {
|
||||
return (
|
||||
<Card title="Signing surface & verification metadata">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Contract ABI and verification metadata were not available. Open the contract address page after Blockscout indexes this token, or verify the contract on the explorer.
|
||||
</p>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Signing surface & verification metadata">
|
||||
<dl className="space-y-4">
|
||||
<DetailRow label="ABI coverage" valueClassName="flex flex-wrap gap-2">
|
||||
<EntityBadge label={contractProfile.abi_available ? 'ABI available' : 'ABI unavailable'} tone={contractProfile.abi_available ? 'success' : 'warning'} />
|
||||
<EntityBadge label={contractProfile.source_verified ? 'Source verified' : 'Source not verified'} tone={contractProfile.source_verified ? 'success' : 'warning'} />
|
||||
</DetailRow>
|
||||
|
||||
<DetailRow label="ERC-5267 (EIP-712 domain introspection)" valueClassName="space-y-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<EntityBadge
|
||||
label={abiHasEip712Domain ? 'eip712Domain() in ABI' : 'eip712Domain() not in ABI'}
|
||||
tone={abiHasEip712Domain ? 'success' : 'warning'}
|
||||
/>
|
||||
{domain ? <EntityBadge label="Live domain decoded" tone="success" /> : null}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{ERC5267_EXPLANATION}</p>
|
||||
{domain ? (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm 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">Domain fields</div>
|
||||
<dl className="mt-2 space-y-1.5 text-gray-900 dark:text-white">
|
||||
<div><span className="text-gray-500 dark:text-gray-400">fields </span>{domain.fields}</div>
|
||||
<div><span className="text-gray-500 dark:text-gray-400">name </span>{domain.name || '—'}</div>
|
||||
<div><span className="text-gray-500 dark:text-gray-400">version </span>{domain.version || '—'}</div>
|
||||
<div><span className="text-gray-500 dark:text-gray-400">chainId </span>{domain.chainId}</div>
|
||||
<div className="break-all">
|
||||
<span className="text-gray-500 dark:text-gray-400">verifyingContract </span>
|
||||
{domain.verifyingContract}
|
||||
</div>
|
||||
<div className="break-all">
|
||||
<span className="text-gray-500 dark:text-gray-400">salt </span>
|
||||
{domain.salt}
|
||||
</div>
|
||||
<div className="break-all">
|
||||
<span className="text-gray-500 dark:text-gray-400">extensions </span>
|
||||
{domain.extensionsSummary}
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
) : abiHasEip712Domain && domainError ? (
|
||||
<p className="text-sm text-amber-700 dark:text-amber-300">{domainError}</p>
|
||||
) : !abiHasEip712Domain ? (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
This contract’s verified ABI does not expose eip712Domain(). ERC-5267 introspection is unavailable from the explorer surface until the implementation adds it.
|
||||
</p>
|
||||
) : null}
|
||||
</DetailRow>
|
||||
|
||||
<DetailRow label="Related interfaces" valueClassName="flex flex-wrap gap-2">
|
||||
{standards
|
||||
.filter((s) => s.id !== 'ERC-5267')
|
||||
.map((s) => (
|
||||
<EntityBadge
|
||||
key={s.id}
|
||||
label={`${s.id} ${s.detected ? 'detected' : 'not detected'}`}
|
||||
tone={s.detected ? 'success' : 'warning'}
|
||||
className="normal-case tracking-normal"
|
||||
/>
|
||||
))}
|
||||
</DetailRow>
|
||||
|
||||
{verificationMeta.length > 0 ? (
|
||||
<DetailRow label="Verification metadata">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{verificationMeta.map((row) => (
|
||||
<div key={row.label} className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm 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">{row.label}</div>
|
||||
<div className="mt-2 break-words text-gray-900 dark:text-white">{row.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DetailRow>
|
||||
) : (
|
||||
<DetailRow label="Verification metadata">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">No compiler or naming metadata was returned with this contract record.</span>
|
||||
</DetailRow>
|
||||
)}
|
||||
|
||||
<DetailRow label="Interpretation">
|
||||
<div className="space-y-3">
|
||||
{standards.map((s) => (
|
||||
<div key={s.id} className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium text-gray-900 dark:text-white">{s.id}</span>
|
||||
<EntityBadge label={s.detected ? 'detected' : 'not detected'} tone={s.detected ? 'success' : 'warning'} />
|
||||
</div>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-400">{s.note}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DetailRow>
|
||||
</dl>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user