'use client' import { FormEvent, useMemo, useState } from 'react' import clsx from 'clsx' import { Card } from '@/libs/frontend-ui-primitives' import EntityBadge from '@/components/common/EntityBadge' import { getExplorerApiBase } from '@/services/api/blockscout' import type { ContractProfile, ContractSourceFile } from '@/services/api/contracts' interface ContractCodeWorkspaceProps { address: string profile: ContractProfile } interface OutlineEntry { type: 'contract' | 'interface' | 'library' | 'function' | 'event' | 'error' name: string line: number } const QUICK_PROMPTS = [ 'What does this contract do?', 'What are the functions available in this contract?', 'Which functions can change state or move funds?', 'Who has special permissions or control in this contract?', 'What are potential risks or red flags in this contract?', ] as const function makeFallbackSourceFile(profile: ContractProfile): ContractSourceFile | null { if (!profile.source_code_preview && !profile.abi_full && !profile.abi) return null return { path: profile.contract_name ? `${profile.contract_name}.sol` : 'Contract.sol', content: profile.source_code_full || profile.source_code_preview || profile.abi_full || profile.abi || '', } } function parseOutline(content: string): OutlineEntry[] { const entries: OutlineEntry[] = [] content.split('\n').forEach((line, index) => { const lineNumber = index + 1 const typeMatch = line.match(/^\s*(?:abstract\s+)?(contract|interface|library)\s+([A-Za-z_][A-Za-z0-9_]*)/) if (typeMatch) { entries.push({ type: typeMatch[1] as OutlineEntry['type'], name: typeMatch[2], line: lineNumber, }) return } const memberMatch = line.match(/^\s*(function|event|error)\s+([A-Za-z_][A-Za-z0-9_]*)/) if (memberMatch) { entries.push({ type: memberMatch[1] as OutlineEntry['type'], name: memberMatch[2], line: lineNumber, }) } }) return entries } function sourceExcerptForPrompt(files: ContractSourceFile[]): string { return files .slice(0, 4) .map((file) => `File: ${file.path}\n${file.content.slice(0, 2600)}`) .join('\n\n') .slice(0, 5200) } export default function ContractCodeWorkspace({ address, profile }: ContractCodeWorkspaceProps) { const files = useMemo(() => { const normalized = profile.source_files?.length ? profile.source_files : [] const fallback = makeFallbackSourceFile(profile) return normalized.length > 0 ? normalized : fallback ? [fallback] : [] }, [profile]) const [activeTab, setActiveTab] = useState<'source' | 'reader'>('source') const [activePath, setActivePath] = useState(files[0]?.path || '') const [prompt, setPrompt] = useState('What does this contract do?') const [model, setModel] = useState('Explorer AI') const [saveHistory, setSaveHistory] = useState(false) const [submitting, setSubmitting] = useState(false) const [readerAnswer, setReaderAnswer] = useState('') const [readerError, setReaderError] = useState('') const [expanded, setExpanded] = useState(false) const activeFile = files.find((file) => file.path === activePath) || files[0] const outline = useMemo(() => parseOutline(activeFile?.content || ''), [activeFile?.content]) const sourceLines = useMemo(() => (activeFile?.content || '').split('\n'), [activeFile?.content]) const selectedFiles = files const sourceAvailable = files.length > 0 && Boolean(activeFile?.content) const handleCopySource = async () => { if (!activeFile?.content || typeof navigator === 'undefined') return await navigator.clipboard?.writeText(activeFile.content) } const handleCopyLink = async () => { if (typeof navigator === 'undefined' || typeof window === 'undefined') return await navigator.clipboard?.writeText(`${window.location.href.split('#')[0]}#contract-source`) } const askReader = async (question: string) => { const trimmed = question.trim() if (!trimmed || submitting) return setPrompt(trimmed) setReaderError('') setReaderAnswer('') setSubmitting(true) try { const context = [ `Contract address: ${address}`, profile.contract_name ? `Contract name: ${profile.contract_name}` : '', profile.compiler_version ? `Compiler: ${profile.compiler_version}` : '', profile.license_type ? `License: ${profile.license_type}` : '', profile.proxy_type ? `Proxy type: ${profile.proxy_type}` : '', `Read methods: ${profile.read_methods.map((method) => method.signature).slice(0, 24).join(', ') || 'none reported'}`, `Write methods: ${profile.write_methods.map((method) => method.signature).slice(0, 24).join(', ') || 'none reported'}`, sourceAvailable ? `Verified source excerpts:\n${sourceExcerptForPrompt(selectedFiles)}` : 'Verified source text is not available.', ].filter(Boolean).join('\n') const response = await fetch(`${getExplorerApiBase()}/api/v1/ai/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [ { role: 'user', content: `${trimmed}\n\nUse this contract context and answer concisely. Do not invent behavior that is not supported by the ABI or source.\n\n${context}`, }, ], pageContext: { path: `/addresses/${address}`, view: 'contract-code-reader', address, }, }), }) const payload = await response.json().catch(() => null) if (!response.ok) { throw new Error(payload?.error?.message || `AI reader returned HTTP ${response.status}`) } setReaderAnswer(String(payload?.reply || payload?.message?.content || 'No answer returned.')) } catch (error) { setReaderError(error instanceof Error ? error.message : 'Code Reader is temporarily unavailable.') } finally { setSubmitting(false) } } const handleSubmit = async (event: FormEvent) => { event.preventDefault() await askReader(prompt) } if (!sourceAvailable && !profile.abi_available) { return null } return (
{profile.source_verified ? : null} {profile.abi_available ? : null} {profile.compiler_version ? : null}
{activeTab === 'source' ? (
{activeFile?.path || 'Source'}
{sourceLines.length} lines
                  
                    {sourceLines.map((line, index) => (
                      
                        {index + 1}
                        {line || ' '}
                      
                    ))}
                  
                
) : (
File Browser
{files.map((file) => ( ))}
Prompt