From bc742b2f25d2df7839f4df8f13c7b311eaf874a7 Mon Sep 17 00:00:00 2001 From: tu11aa Date: Tue, 31 Mar 2026 15:25:15 +0700 Subject: [PATCH 1/3] feat: enhance debug interaction logs with search, filters, and export Add richer logging (call type, duration, decoded results, error details), search by function name, filter chips (Read/Write/Success/Error), JSON/CSV export, clear history, and improved modal with copy-to-clipboard. --- .../contract/ReadOnlyFunctionForm.tsx | 47 ++++- .../contract/WriteOnlyFunctionForm.tsx | 6 + .../contract/history/DebugHistory.tsx | 194 ++++++++++++++++-- .../contract/history/HistoryModal.tsx | 89 +++++++- packages/nextjs/services/store/history.ts | 5 + 5 files changed, 308 insertions(+), 33 deletions(-) diff --git a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx index 660fd54ce..2c349e7f9 100644 --- a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx +++ b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx @@ -81,7 +81,7 @@ export const ReadOnlyFunctionForm = ({ ); }); - const handleRead = () => { + const handleRead = async () => { const newInputValue = getArgsAsStringInputFromForm(form); const expectedArgCount = abiFunction.inputs.length; @@ -102,18 +102,43 @@ export const ReadOnlyFunctionForm = ({ lastForm.current = form; } - refetch(); + const startTime = Date.now(); + const { data: refetchData, error: refetchError } = await refetch(); + const duration = Date.now() - startTime; + try { const inputStr = JSON.stringify(newInputValue); - // Optimistically log a read call as success; real error will be captured via useReadContract error effect - addHistory(contractAddress, { - txHash: undefined, - functionName: abiFunction.name, - timestamp: Date.now(), - status: "success", - message: "Read executed", - input: inputStr, - }); + if (refetchError) { + addHistory(contractAddress, { + txHash: undefined, + functionName: abiFunction.name, + callType: "read", + timestamp: Date.now(), + status: "error", + message: refetchError.message, + input: inputStr, + duration, + errorDetails: refetchError.stack || refetchError.message, + }); + } else { + const decodedResult = decodeContractResponse({ + resp: refetchData, + abi, + functionOutputs: abiFunction?.outputs, + asText: true, + }); + addHistory(contractAddress, { + txHash: undefined, + functionName: abiFunction.name, + callType: "read", + timestamp: Date.now(), + status: "success", + message: "Read executed", + input: inputStr, + decodedResult: decodedResult ?? undefined, + duration, + }); + } } catch {} }; diff --git a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx index a1335fe5c..d9c510c8b 100644 --- a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx +++ b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx @@ -77,6 +77,7 @@ export const WriteOnlyFunctionForm = ({ }, [error]); const handleWrite = async () => { + const startTime = Date.now(); try { const txHash = await writeTransaction( !!contractInstance @@ -94,10 +95,12 @@ export const WriteOnlyFunctionForm = ({ addHistory(contractAddress, { txHash: typeof txHash === "string" ? txHash : undefined, functionName: abiFunction.name, + callType: "write", timestamp: Date.now(), status: "success", message, input: inputStr, + duration: Date.now() - startTime, }); } catch {} } catch (e: any) { @@ -115,10 +118,13 @@ export const WriteOnlyFunctionForm = ({ addHistory(contractAddress, { txHash: undefined, functionName: abiFunction.name, + callType: "write", timestamp: Date.now(), status: "error", message, input: inputStr, + duration: Date.now() - startTime, + errorDetails: e.stack || e.message, }); } catch {} } diff --git a/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx b/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx index da2e9fe66..1a6f948e7 100644 --- a/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx +++ b/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx @@ -12,6 +12,44 @@ import { useTheme } from "next-themes"; const contractsData = getAllContracts(); const contractNames = Object.keys(contractsData) as ContractName[]; +type FilterChip = "All" | "Read" | "Write" | "Success" | "Error"; + +function downloadFile(content: string, filename: string, mimeType: string) { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = filename; + anchor.click(); + URL.revokeObjectURL(url); +} + +function entriesToCSV(entries: HistoryEntry[]): string { + const headers = + "timestamp,callType,functionName,status,txHash,input,message,decodedResult,gasUsed,duration,errorDetails"; + const escape = (val: string | number | undefined) => { + if (val === undefined || val === null) return ""; + const str = String(val).replace(/"/g, '""'); + return `"${str}"`; + }; + const rows = entries.map((e) => + [ + escape(e.timestamp), + escape(e.callType), + escape(e.functionName), + escape(e.status), + escape(e.txHash), + escape(e.input), + escape(e.message), + escape(e.decodedResult), + escape(e.gasUsed), + escape(e.duration), + escape(e.errorDetails), + ].join(","), + ); + return [headers, ...rows].join("\n"); +} + export default function DebugHistory() { const [selectedContract] = useLocalStorage( "scaffoldStark2.selectedContract", @@ -19,24 +57,53 @@ export default function DebugHistory() { { initializeWithValue: false }, ); const historyByContract = useHistoryStore((s) => s.historyByContract); - const selectedAddress = contractsData[selectedContract]?.address as string; + const clearHistory = useHistoryStore((s) => s.clearHistory); + const selectedAddress = (contractsData as Record)[selectedContract as string]?.address as string; const entries = useMemo( () => historyByContract[selectedAddress] || [], [historyByContract, selectedAddress], ); const [openEntry, setOpenEntry] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [activeFilters, setActiveFilters] = useState([]); const { theme } = useTheme(); const isDarkMode = theme === "dark"; - const formatted = useMemo( - () => - entries.map((e) => ({ - ...e, - ts: new Date(e.timestamp), - })), - [entries], - ); + const toggleFilter = (chip: FilterChip) => { + if (chip === "All") { + setActiveFilters([]); + return; + } + setActiveFilters((prev) => + prev.includes(chip) ? prev.filter((f) => f !== chip) : [...prev, chip], + ); + }; + + const formatted = useMemo(() => { + let result = entries.map((e) => ({ ...e, ts: new Date(e.timestamp) })); + + if (searchTerm.trim()) { + const lower = searchTerm.toLowerCase(); + result = result.filter((e) => + e.functionName.toLowerCase().includes(lower), + ); + } + + if (activeFilters.length > 0) { + result = result.filter((e) => { + return activeFilters.some((f) => { + if (f === "Read") return e.callType === "read"; + if (f === "Write") return e.callType === "write"; + if (f === "Success") return e.status === "success"; + if (f === "Error") return e.status === "error"; + return true; + }); + }); + } + + return result; + }, [entries, searchTerm, activeFilters]); const formatDate = (ts: number) => formatTimestamp(ts); @@ -49,29 +116,124 @@ export default function DebugHistory() { /> ); + const chips: FilterChip[] = ["All", "Read", "Write", "Success", "Error"]; + + const handleExportJSON = () => { + const raw = formatted.map(({ ts: _ts, ...rest }) => rest); + downloadFile( + JSON.stringify(raw, null, 2), + "debug-history.json", + "application/json", + ); + }; + + const handleExportCSV = () => { + const raw = formatted.map(({ ts: _ts, ...rest }) => rest); + downloadFile(entriesToCSV(raw), "debug-history.csv", "text/csv"); + }; + + const handleClear = () => { + if (selectedAddress) clearHistory(selectedAddress); + }; + return (
History
+ + {/* Search bar */} + setSearchTerm(e.target.value)} + /> + + {/* Filter chips */} +
+ {chips.map((chip) => { + const isActive = + chip === "All" ? activeFilters.length === 0 : activeFilters.includes(chip); + return ( + + ); + })} +
+ + {/* Toolbar: export + clear */} +
+ + + +
+
{formatted.length === 0 ? (
No history yet.
) : ( - formatted.map((e, idx) => ( + formatted.map((e) => ( diff --git a/packages/nextjs/app/debug/_components/contract/history/HistoryModal.tsx b/packages/nextjs/app/debug/_components/contract/history/HistoryModal.tsx index 6be35b33e..c4d2e8cbc 100644 --- a/packages/nextjs/app/debug/_components/contract/history/HistoryModal.tsx +++ b/packages/nextjs/app/debug/_components/contract/history/HistoryModal.tsx @@ -1,5 +1,5 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import Image from "next/image"; import { useTheme } from "next-themes"; import { HistoryEntry } from "~~/services/store/history"; @@ -15,6 +15,7 @@ export default function HistoryModal({ const { theme } = useTheme(); const isDarkMode = theme === "dark"; const formatDate = (ts: number) => formatTimestamp(ts); + const [copied, setCopied] = useState(false); const isSuccess = entry.status === "success"; const chipClasses = isSuccess @@ -25,28 +26,79 @@ export default function HistoryModal({ const sectionBg = isSuccess ? "bg-green-900/30" : "bg-red-900/30"; const chipLabel = isSuccess ? "Success" : "Fail"; + const callTypeBadgeClasses = + entry.callType === "read" + ? "bg-blue-500/20 text-blue-400" + : "bg-amber-500/20 text-amber-400"; + const callTypeLabel = entry.callType === "read" ? "Read" : "Write"; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(JSON.stringify(entry, null, 2)); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + // clipboard access denied — silently ignore + } + }; + return (
-
-
+ {/* Header row */} +
+

{entry.functionName}

+ + {/* Status chip */} {chipLabel} {chipLabel} + + {/* Call type badge */} + + {callTypeLabel} + + + {/* Gas used */} + {entry.gasUsed && ( + + Gas: {entry.gasUsed} + + )} + + {/* Duration */} + {entry.duration !== undefined && ( + + Duration: {entry.duration}ms + + )} +
+ +
+ +
-
+ {/* Input section */}
Your input @@ -57,12 +109,37 @@ export default function HistoryModal({
+ {/* Message / Receipt section */}

{sectionTitle}

               {entry.message}
             
+ + {/* Decoded result section */} + {entry.decodedResult && ( +
+

+ Decoded Result +

+
+                {entry.decodedResult}
+              
+
+ )} + + {/* Error details collapsible section */} + {entry.errorDetails && ( +
+ + Error Details + +
+                {entry.errorDetails}
+              
+
+ )}
diff --git a/packages/nextjs/services/store/history.ts b/packages/nextjs/services/store/history.ts index eb7451220..eb74c2be0 100644 --- a/packages/nextjs/services/store/history.ts +++ b/packages/nextjs/services/store/history.ts @@ -5,10 +5,15 @@ export type HistoryStatus = "success" | "error"; export type HistoryEntry = { txHash?: string; functionName: string; + callType: "read" | "write"; timestamp: number; status: HistoryStatus; message: string; input?: string; + decodedResult?: string; + gasUsed?: string; + duration?: number; + errorDetails?: string; }; type HistoryState = { From 622ade0aadd719b89564d5687a11ac899844ea4b Mon Sep 17 00:00:00 2001 From: tu11aa Date: Tue, 31 Mar 2026 16:10:02 +0700 Subject: [PATCH 2/3] fix: add error logging in catch blocks and improve history pane UI consistency - Add console.warn in empty catch blocks for history logging failures - Restyle history pane to match Read/Write panel (purple border card, tabs-box header) - Use consistent rounded-[5px] borders, bg-component, and border-[#8A45FC] - Improve search bar, filter chips, and toolbar styling for dark/light themes - Extract StatusIcon outside component body to avoid re-renders - Clean up modal layout with meta info row and consistent section styling --- .../contract/ReadOnlyFunctionForm.tsx | 4 +- .../contract/WriteOnlyFunctionForm.tsx | 8 +- .../contract/history/DebugHistory.tsx | 175 ++++++++++-------- .../contract/history/HistoryModal.tsx | 84 +++++---- 4 files changed, 154 insertions(+), 117 deletions(-) diff --git a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx index 2c349e7f9..7af195856 100644 --- a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx +++ b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx @@ -139,7 +139,9 @@ export const ReadOnlyFunctionForm = ({ duration, }); } - } catch {} + } catch (e) { + console.warn("Failed to log read history:", e); + } }; return ( diff --git a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx index d9c510c8b..23096f75b 100644 --- a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx +++ b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx @@ -102,7 +102,9 @@ export const WriteOnlyFunctionForm = ({ input: inputStr, duration: Date.now() - startTime, }); - } catch {} + } catch (histErr) { + console.warn("Failed to log write history:", histErr); + } } catch (e: any) { const errorPattern = /Contract (.*?)"}/; const match = errorPattern.exec(e.message); @@ -126,7 +128,9 @@ export const WriteOnlyFunctionForm = ({ duration: Date.now() - startTime, errorDetails: e.stack || e.message, }); - } catch {} + } catch (histErr) { + console.warn("Failed to log write history:", histErr); + } } }; diff --git a/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx b/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx index 1a6f948e7..20f11689b 100644 --- a/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx +++ b/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx @@ -50,6 +50,15 @@ function entriesToCSV(entries: HistoryEntry[]): string { return [headers, ...rows].join("\n"); } +const StatusIcon = ({ status }: { status: HistoryEntry["status"] }) => ( + {status} +); + export default function DebugHistory() { const [selectedContract] = useLocalStorage( "scaffoldStark2.selectedContract", @@ -58,7 +67,9 @@ export default function DebugHistory() { ); const historyByContract = useHistoryStore((s) => s.historyByContract); const clearHistory = useHistoryStore((s) => s.clearHistory); - const selectedAddress = (contractsData as Record)[selectedContract as string]?.address as string; + const selectedAddress = ( + contractsData as Record + )[selectedContract as string]?.address as string; const entries = useMemo( () => historyByContract[selectedAddress] || [], [historyByContract, selectedAddress], @@ -107,15 +118,6 @@ export default function DebugHistory() { const formatDate = (ts: number) => formatTimestamp(ts); - const StatusIcon = ({ status }: { status: HistoryEntry["status"] }) => ( - {status} - ); - const chips: FilterChip[] = ["All", "Read", "Write", "Success", "Error"]; const handleExportJSON = () => { @@ -137,81 +139,104 @@ export default function DebugHistory() { }; return ( -
-
- History +
+ {/* Tab header — matches Read/Write tab bar */} + - {/* Search bar */} - setSearchTerm(e.target.value)} - /> - - {/* Filter chips */} -
- {chips.map((chip) => { - const isActive = - chip === "All" ? activeFilters.length === 0 : activeFilters.includes(chip); - return ( + {/* Content card — matches the contract methods card */} +
+ {/* Search bar */} +
+ setSearchTerm(e.target.value)} + /> +
+ + {/* Filter chips + toolbar */} +
+
+ {chips.map((chip) => { + const isActive = + chip === "All" + ? activeFilters.length === 0 + : activeFilters.includes(chip); + return ( + + ); + })} +
+ + {/* Export / Clear toolbar */} +
- ); - })} -
- - {/* Toolbar: export + clear */} -
- - - -
+ | + + +
+
-
-
+ {/* History entries list */} +
{formatted.length === 0 ? ( -
No history yet.
+
+ No history yet. +
) : ( formatted.map((e) => ( -
+ {/* Meta info row */} +
+ {formatDate(entry.timestamp)} + {entry.duration !== undefined && {entry.duration}ms} + {entry.gasUsed && Gas: {entry.gasUsed}} + {entry.txHash && ( + + Tx: {entry.txHash} + + )} +
+
{/* Input section */} -
-
- Your input - {formatDate(entry.timestamp)} -
-
-              {entry.input || "[code here]"}
+          
+

+ Input +

+
+              {entry.input || "—"}
             
{/* Message / Receipt section */} -
-

{sectionTitle}

-
+          
+

+ {sectionTitle} +

+
               {entry.message}
             
{/* Decoded result section */} {entry.decodedResult && ( -
-

+

+

Decoded Result

-
+              
                 {entry.decodedResult}
               
@@ -131,11 +137,11 @@ export default function HistoryModal({ {/* Error details collapsible section */} {entry.errorDetails && ( -
- +
+ Error Details -
+              
                 {entry.errorDetails}
               
From 1f58a5a1b3fbadbe64461d3dd757a9f4ea04eb80 Mon Sep 17 00:00:00 2001 From: tu11aa Date: Tue, 31 Mar 2026 16:12:06 +0700 Subject: [PATCH 3/3] fix: use heroicons for read/write call type badges Replace R/W text with EyeIcon (read) and PencilSquareIcon (write) from @heroicons/react for better visual clarity. --- .../_components/contract/history/DebugHistory.tsx | 9 +++++++-- .../_components/contract/history/HistoryModal.tsx | 12 ++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx b/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx index 20f11689b..57988ca9c 100644 --- a/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx +++ b/packages/nextjs/app/debug/_components/contract/history/DebugHistory.tsx @@ -8,6 +8,7 @@ import { ContractName } from "~~/utils/scaffold-stark/contract"; import { getAllContracts } from "~~/utils/scaffold-stark/contractsData"; import HistoryModal from "./HistoryModal"; import { useTheme } from "next-themes"; +import { EyeIcon, PencilSquareIcon } from "@heroicons/react/24/outline"; const contractsData = getAllContracts(); const contractNames = Object.keys(contractsData) as ContractName[]; @@ -236,13 +237,17 @@ export default function DebugHistory() { >
- {e.callType === "read" ? "R" : "W"} + {e.callType === "read" ? ( + + ) : ( + + )} {e.functionName} diff --git a/packages/nextjs/app/debug/_components/contract/history/HistoryModal.tsx b/packages/nextjs/app/debug/_components/contract/history/HistoryModal.tsx index ec697cd0f..2854ec476 100644 --- a/packages/nextjs/app/debug/_components/contract/history/HistoryModal.tsx +++ b/packages/nextjs/app/debug/_components/contract/history/HistoryModal.tsx @@ -4,6 +4,7 @@ import Image from "next/image"; import { useTheme } from "next-themes"; import { HistoryEntry } from "~~/services/store/history"; import { formatTimestamp } from "~~/utils/scaffold-stark/common"; +import { EyeIcon, PencilSquareIcon } from "@heroicons/react/24/outline"; export default function HistoryModal({ entry, @@ -51,9 +52,7 @@ export default function HistoryModal({ {/* Header row */}
-

- {entry.functionName} -

+

{entry.functionName}

{/* Status chip */} + {entry.callType === "read" ? ( + + ) : ( + + )} {callTypeLabel}