diff --git a/apps/explorer/src/comps/Contract.tsx b/apps/explorer/src/comps/Contract.tsx index e236f88cf..3f72de109 100644 --- a/apps/explorer/src/comps/Contract.tsx +++ b/apps/explorer/src/comps/Contract.tsx @@ -1,8 +1,10 @@ +import { useQuery } from '@tanstack/react-query' import type { Address } from 'ox' import * as React from 'react' import type { Abi } from 'viem' import { Link } from '@tanstack/react-router' import { useBytecode, usePublicClient } from 'wagmi' +import { Address as AddressComp } from '#comps/Address.tsx' import { ConnectWallet } from '#comps/ConnectWallet.tsx' import { AbiViewer } from '#comps/ContractAbi.tsx' import { ContractReader } from '#comps/ContractReader.tsx' @@ -12,6 +14,7 @@ import { cx } from '#lib/css' import { ellipsis } from '#lib/chars.ts' import type { ContractSource } from '#lib/domain/contract-source.ts' import { autoloadAbi, getContractAbi } from '#lib/domain/contracts.ts' +import { getApiUrl } from '#lib/env.ts' import { detectProxy, type ProxyInfo, @@ -35,23 +38,70 @@ function proxyTypeUrl(type: ProxyType | undefined): string { return type ? proxyTypeUrls[type] : proxyTypeUrls['EIP-1967'] } +function formatDate(timestamp: number): string { + return new Date(timestamp * 1000).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }) +} + /** * Contract tab content - shows ABI and Source */ -export function ContractTabContent(props: { - address: Address.Address - abi?: Abi - docsUrl?: string - source?: ContractSource -}) { +export function ContractTabContent( + props: ContractTabContent.Props, +): React.JSX.Element { const { address, docsUrl, source } = props const isTip20 = isTip20Address(address) const { copy: copyAbi, notifying: copiedAbi } = useCopy({ timeout: 2_000 }) + const [deploymentExpanded, setDeploymentExpanded] = React.useState(true) const [abiExpanded, setAbiExpanded] = React.useState(false) const abi = props.abi ?? getContractAbi(address) + const { data: metadataData } = useQuery({ + queryKey: ['address-metadata', address], + queryFn: async () => { + const url = getApiUrl(`/api/address/metadata/${address}`) + const response = await fetch(url) + if (!response.ok) { + return { + createdTimestamp: null, + createdTxHash: null, + createdBy: null, + } as const + } + return response.json() + }, + }) + + const { data: contractCreationData } = + useQuery({ + queryKey: ['contract-creation', address], + queryFn: async () => { + const url = getApiUrl(`/api/contract/creation/${address}`) + const response = await fetch(url) + return response.json() as Promise + }, + enabled: !metadataData?.createdTxHash || !metadataData?.createdBy, + staleTime: 60_000, + }) + + const createdTimestamp = + metadataData?.createdTimestamp ?? + (contractCreationData?.creation?.timestamp + ? Number(contractCreationData.creation.timestamp) + : null) + const createdTxHash = + metadataData?.createdTxHash ?? contractCreationData?.creation?.hash ?? null + const createdBy = + metadataData?.createdBy ?? contractCreationData?.creation?.from ?? null + const hasDeploymentInfo = Boolean( + createdTimestamp || createdTxHash || createdBy, + ) + const handleCopyAbi = React.useCallback(() => { if (!abi) return void copyAbi(JSON.stringify(abi, null, 2)) @@ -102,9 +152,46 @@ export function ContractTabContent(props: { {/* Source Section */} {source && } + {/* Deployment Section */} + {hasDeploymentInfo && ( + setDeploymentExpanded(!deploymentExpanded)} + > +
+ + {createdBy && ( +
+ Created By + +
+ )} + {createdTxHash && ( +
+ Creation Tx + + {createdTxHash.slice(0, 10)}…{createdTxHash.slice(-8)} + +
+ )} +
+
+ )} + {/* ABI Section */} ABI} expanded={abiExpanded} onToggle={() => setAbiExpanded(!abiExpanded)} @@ -152,6 +239,30 @@ export function ContractTabContent(props: { ) } +export declare namespace ContractTabContent { + type Props = { + address: Address.Address + abi?: Abi | undefined + docsUrl?: string | undefined + source?: ContractSource | undefined + } + + type MetadataData = { + createdTimestamp: number | null + createdTxHash: `0x${string}` | null + createdBy: Address.Address | null + } + + type CreationResponse = { + creation: { + timestamp: string + hash: `0x${string}` | null + from: Address.Address | null + } | null + error: string | null + } +} + /** * Collapsible section component */ @@ -199,6 +310,20 @@ export function CollapsibleSection(props: { ) } +function DeploymentRow(props: { + label: string + value: string | undefined +}): React.JSX.Element { + return ( +
+ {props.label} + + {props.value ?? } + +
+ ) +} + /** * Bytecode section - shows raw bytecode */