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:
@@ -12,6 +12,11 @@ interface TableProps<T> {
|
||||
data: T[]
|
||||
className?: string
|
||||
emptyMessage?: string
|
||||
/**
|
||||
* responsive: stacked cards below `md`, table at md+.
|
||||
* tabular: always use columnar HTML table (holder lists, dense numeric tables).
|
||||
*/
|
||||
layout?: 'responsive' | 'tabular'
|
||||
/** Stable key for each row (e.g. row => row.id or row => row.hash). Falls back to index if not provided. */
|
||||
keyExtractor?: (row: T) => string | number
|
||||
}
|
||||
@@ -21,6 +26,7 @@ export function Table<T>({
|
||||
data,
|
||||
className,
|
||||
emptyMessage = 'No data available right now.',
|
||||
layout = 'responsive',
|
||||
keyExtractor,
|
||||
}: TableProps<T>) {
|
||||
if (data.length === 0) {
|
||||
@@ -36,9 +42,12 @@ export function Table<T>({
|
||||
)
|
||||
}
|
||||
|
||||
const stackedClass = layout === 'tabular' ? 'hidden' : 'grid gap-3 md:hidden'
|
||||
const tableWrapperClass = layout === 'tabular' ? 'overflow-x-auto' : 'hidden overflow-x-auto md:block'
|
||||
|
||||
return (
|
||||
<div className={clsx('space-y-3', className)}>
|
||||
<div className="grid gap-3 md:hidden">
|
||||
<div className={stackedClass}>
|
||||
{data.map((row, rowIndex) => (
|
||||
<div
|
||||
key={keyExtractor ? keyExtractor(row) : rowIndex}
|
||||
@@ -60,7 +69,7 @@ export function Table<T>({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden overflow-x-auto md:block">
|
||||
<div className={tableWrapperClass}>
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -11,10 +11,12 @@ 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 TokenSigningSurfaceCard from '@/components/common/TokenSigningSurfaceCard'
|
||||
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
|
||||
import { formatTokenAmount, formatTimestamp } from '@/utils/format'
|
||||
import { getGruStandardsProfileSafe, type GruStandardsProfile } from '@/services/api/gru'
|
||||
import { getGruExplorerMetadata } from '@/services/api/gruExplorerData'
|
||||
import { contractsApi, type ContractProfile } from '@/services/api/contracts'
|
||||
|
||||
function isValidAddress(value: string) {
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(value)
|
||||
@@ -50,17 +52,19 @@ export default function TokenDetailPage() {
|
||||
const [transfers, setTransfers] = useState<AddressTokenTransfer[]>([])
|
||||
const [pools, setPools] = useState<MissionControlLiquidityPool[]>([])
|
||||
const [gruProfile, setGruProfile] = useState<GruStandardsProfile | null>(null)
|
||||
const [contractProfile, setContractProfile] = useState<ContractProfile | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const loadToken = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [tokenResult, provenanceResult, holdersResult, transfersResult, poolsResult] = await Promise.all([
|
||||
const [tokenResult, provenanceResult, holdersResult, transfersResult, poolsResult, contractResult] = await Promise.all([
|
||||
tokensApi.getSafe(address),
|
||||
tokensApi.getProvenanceSafe(address),
|
||||
tokensApi.getHoldersSafe(address, 1, 10),
|
||||
tokensApi.getTransfersSafe(address, 1, 10),
|
||||
tokensApi.getRelatedPoolsSafe(address),
|
||||
contractsApi.getProfileSafe(address),
|
||||
])
|
||||
|
||||
setToken(tokenResult.ok ? tokenResult.data : null)
|
||||
@@ -68,11 +72,14 @@ export default function TokenDetailPage() {
|
||||
setHolders(holdersResult.ok ? holdersResult.data : [])
|
||||
setTransfers(transfersResult.ok ? transfersResult.data : [])
|
||||
setPools(poolsResult.ok ? poolsResult.data : [])
|
||||
const resolvedContractProfile = contractResult.ok ? contractResult.data : null
|
||||
setContractProfile(resolvedContractProfile)
|
||||
if (tokenResult.ok && tokenResult.data) {
|
||||
const gruResult = await getGruStandardsProfileSafe({
|
||||
address,
|
||||
symbol: tokenResult.data.symbol,
|
||||
tags: provenanceResult.ok ? provenanceResult.data?.tags || [] : [],
|
||||
contractProfile: resolvedContractProfile,
|
||||
})
|
||||
setGruProfile(gruResult.ok ? gruResult.data : null)
|
||||
} else {
|
||||
@@ -85,6 +92,7 @@ export default function TokenDetailPage() {
|
||||
setTransfers([])
|
||||
setPools([])
|
||||
setGruProfile(null)
|
||||
setContractProfile(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -98,6 +106,7 @@ export default function TokenDetailPage() {
|
||||
if (!isValidTokenAddress) {
|
||||
setLoading(false)
|
||||
setToken(null)
|
||||
setContractProfile(null)
|
||||
return
|
||||
}
|
||||
void loadToken()
|
||||
@@ -274,7 +283,7 @@ export default function TokenDetailPage() {
|
||||
<div className="container mx-auto px-4 py-6 sm:py-8">
|
||||
<PageIntro
|
||||
eyebrow="Token Detail"
|
||||
title={token?.symbol || token?.name || 'Token'}
|
||||
title={token?.symbol || token?.name || provenance?.symbol || provenance?.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' },
|
||||
@@ -303,15 +312,20 @@ export default function TokenDetailPage() {
|
||||
<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>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Token details were not found for this address from the token index APIs. If this is a contract, verification metadata and ERC-5267 may still be available below.
|
||||
</p>
|
||||
</Card>
|
||||
<TokenSigningSurfaceCard address={address} contractProfile={contractProfile} />
|
||||
</div>
|
||||
) : (
|
||||
<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="Name">{token.name || provenance?.name || 'Unknown'}</DetailRow>
|
||||
<DetailRow label="Symbol">{token.symbol || provenance?.symbol || 'Unknown'}</DetailRow>
|
||||
<DetailRow label="Address">
|
||||
<Address address={token.address} />
|
||||
</DetailRow>
|
||||
@@ -342,6 +356,8 @@ export default function TokenDetailPage() {
|
||||
</dl>
|
||||
</Card>
|
||||
|
||||
<TokenSigningSurfaceCard address={token.address} contractProfile={contractProfile} />
|
||||
|
||||
<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">
|
||||
@@ -469,6 +485,7 @@ export default function TokenDetailPage() {
|
||||
|
||||
<Card title="Top Holders">
|
||||
<Table
|
||||
layout="tabular"
|
||||
columns={holderColumns}
|
||||
data={holders}
|
||||
emptyMessage="No holder data was available for this token."
|
||||
|
||||
148
frontend/src/services/api/eip712Domain.ts
Normal file
148
frontend/src/services/api/eip712Domain.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* ERC-5267 — read eip712Domain() via eth_call and decode the standard return tuple:
|
||||
* (bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions)
|
||||
*/
|
||||
|
||||
export interface DecodedEip712Domain {
|
||||
fields: string
|
||||
name: string
|
||||
version: string
|
||||
chainId: string
|
||||
verifyingContract: string
|
||||
salt: string
|
||||
extensionsSummary: string
|
||||
}
|
||||
|
||||
const EIP712_DOMAIN_SELECTOR = '84b0196e'
|
||||
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
const normalized = hex.replace(/^0x/i, '')
|
||||
if (normalized.length % 2 !== 0) return new Uint8Array()
|
||||
const out = new Uint8Array(normalized.length / 2)
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
out[i] = parseInt(normalized.slice(i * 2, i * 2 + 2), 16)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function readWord(bytes: Uint8Array, wordIndex: number): Uint8Array {
|
||||
const start = wordIndex * 32
|
||||
return bytes.subarray(start, start + 32)
|
||||
}
|
||||
|
||||
function wordToBigInt(word: Uint8Array): bigint {
|
||||
let v = 0n
|
||||
for (let i = 0; i < word.length; i++) {
|
||||
v = (v << 8n) + BigInt(word[i] || 0)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
function readBytes1(word: Uint8Array): string {
|
||||
return `0x${(word[31] ?? 0).toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function readAddress(word: Uint8Array): string {
|
||||
const slice = word.subarray(12, 32)
|
||||
return `0x${Array.from(slice)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')}`
|
||||
}
|
||||
|
||||
function readBytes32(word: Uint8Array): string {
|
||||
return `0x${Array.from(word)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')}`
|
||||
}
|
||||
|
||||
function readAbiString(bytes: Uint8Array, offset: number): { value: string; next: number } {
|
||||
if (offset + 32 > bytes.length) return { value: '', next: offset }
|
||||
const len = Number(wordToBigInt(bytes.subarray(offset, offset + 32)))
|
||||
const start = offset + 32
|
||||
const end = start + len
|
||||
if (end > bytes.length) return { value: '', next: offset }
|
||||
const raw = bytes.subarray(start, end)
|
||||
const value = new TextDecoder().decode(raw).replace(/\u0000+$/g, '')
|
||||
const padded = Math.ceil(len / 32) * 32
|
||||
return { value, next: start + padded }
|
||||
}
|
||||
|
||||
function readUint256Array(bytes: Uint8Array, offset: number): { summary: string; next: number } {
|
||||
if (offset + 32 > bytes.length) return { summary: '[]', next: offset }
|
||||
const n = Number(wordToBigInt(bytes.subarray(offset, offset + 32)))
|
||||
if (!Number.isFinite(n) || n < 0 || n > 256) {
|
||||
return { summary: `(length ${n})`, next: offset + 32 }
|
||||
}
|
||||
let pos = offset + 32
|
||||
const parts: string[] = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (pos + 32 > bytes.length) break
|
||||
parts.push(wordToBigInt(bytes.subarray(pos, pos + 32)).toString())
|
||||
pos += 32
|
||||
}
|
||||
const summary = n === 0 ? '[]' : `[${parts.join(', ')}]`
|
||||
return { summary, next: pos }
|
||||
}
|
||||
|
||||
export function decodeEip712DomainReturnData(resultHex: string): DecodedEip712Domain | null {
|
||||
const bytes = hexToBytes(resultHex)
|
||||
if (bytes.length < 32 * 7) return null
|
||||
|
||||
const fields = readBytes1(readWord(bytes, 0))
|
||||
const nameOff = Number(wordToBigInt(readWord(bytes, 1)))
|
||||
const versionOff = Number(wordToBigInt(readWord(bytes, 2)))
|
||||
const chainId = wordToBigInt(readWord(bytes, 3)).toString()
|
||||
const verifyingContract = readAddress(readWord(bytes, 4))
|
||||
const salt = readBytes32(readWord(bytes, 5))
|
||||
const extOff = Number(wordToBigInt(readWord(bytes, 6)))
|
||||
|
||||
if (!Number.isFinite(nameOff) || nameOff < 0 || nameOff >= bytes.length) return null
|
||||
if (!Number.isFinite(versionOff) || versionOff < 0 || versionOff >= bytes.length) return null
|
||||
|
||||
const name = readAbiString(bytes, nameOff).value
|
||||
const version = readAbiString(bytes, versionOff).value
|
||||
const extensionsSummary =
|
||||
Number.isFinite(extOff) && extOff >= 0 && extOff < bytes.length ? readUint256Array(bytes, extOff).summary : '[]'
|
||||
|
||||
return {
|
||||
fields,
|
||||
name,
|
||||
version,
|
||||
chainId,
|
||||
verifyingContract,
|
||||
salt,
|
||||
extensionsSummary,
|
||||
}
|
||||
}
|
||||
|
||||
function getPublicRpcUrl(): string {
|
||||
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_RPC_URL_138) {
|
||||
return process.env.NEXT_PUBLIC_RPC_URL_138
|
||||
}
|
||||
return process.env.NEXT_PUBLIC_RPC_URL_138 || 'https://rpc-http-pub.d-bis.org'
|
||||
}
|
||||
|
||||
export async function fetchEip712DomainDecoded(address: string): Promise<DecodedEip712Domain | null> {
|
||||
const trimmed = address.trim()
|
||||
if (!/^0x[a-fA-F0-9]{40}$/.test(trimmed)) return null
|
||||
|
||||
const data = `0x${EIP712_DOMAIN_SELECTOR}`
|
||||
try {
|
||||
const response = await fetch(getPublicRpcUrl(), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'eth_call',
|
||||
params: [{ to: trimmed, data }, 'latest'],
|
||||
}),
|
||||
})
|
||||
if (!response.ok) return null
|
||||
const payload = (await response.json()) as { result?: string; error?: { message?: string } }
|
||||
if (payload.error?.message || !payload.result || payload.result === '0x') return null
|
||||
return decodeEip712DomainReturnData(payload.result)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user