Skip to content

Commit 81362ac

Browse files
authored
feat: added DebugHistory panel (#671)
1 parent 01f1cec commit 81362ac

File tree

10 files changed

+288
-5
lines changed

10 files changed

+288
-5
lines changed

packages/nextjs/app/debug/_components/DebugContracts.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const ContractUI = dynamic(
1414

1515
import { ContractName } from "~~/utils/scaffold-stark/contract";
1616
import { getAllContracts } from "~~/utils/scaffold-stark/contractsData";
17-
import { Abi } from "@starknet-react/core";
1817

1918
const selectedContractStorageKey = "scaffoldStark2.selectedContract";
2019
const contractsData = getAllContracts();

packages/nextjs/app/debug/_components/contract/ContractUI.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "~~/utils/scaffold-stark/contract";
1414
import { ContractVariables } from "./ContractVariables";
1515
import { ClassHash } from "~~/components/scaffold-stark/ClassHash";
16+
import DebugHistory from "./history/DebugHistory";
1617

1718
const ContractWriteMethods = dynamic(() =>
1819
import("./ContractWriteMethods").then((mod) => mod.ContractWriteMethods),
@@ -72,9 +73,9 @@ export const ContractUI = ({
7273

7374
return (
7475
<div
75-
className={`grid grid-cols-1 lg:grid-cols-6 px-6 lg:px-10 lg:gap-12 w-full max-w-7xl my-0 ${className}`}
76+
className={`grid grid-cols-1 lg:grid-cols-6 px-6 lg:px-10 lg:gap-12 w-full max-w-7xl my-0 mx-auto ${className}`}
7677
>
77-
<div className="col-span-5 grid grid-cols-1 lg:grid-cols-3 gap-8 lg:gap-10">
78+
<div className="col-span-6 grid grid-cols-1 lg:grid-cols-4 gap-8 lg:gap-10">
7879
<div className="col-span-1 flex flex-col">
7980
<div className="bg-transparent border-gradient rounded-[5px] px-6 lg:px-8 mb-6 space-y-1 py-4">
8081
<div className="flex">
@@ -109,7 +110,7 @@ export const ContractUI = ({
109110
</div>
110111
</div>
111112

112-
<div className="col-span-1 lg:col-span-2 flex flex-col gap-6">
113+
<div className="col-span-1 lg:col-span-2 flex flex-col gap-6 min-w-0">
113114
<div className="tabs tabs-box border border-[#8A45FC] rounded-[5px] bg-transparent">
114115
{tabs.map((tab) => (
115116
<a
@@ -136,6 +137,10 @@ export const ContractUI = ({
136137
</div>
137138
</div>
138139
</div>
140+
141+
<div className="xl:col-span-1 min-w-0">
142+
<DebugHistory />
143+
</div>
139144
</div>
140145
</div>
141146
);

packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { BlockNumber } from "starknet";
1818
import { useContract, useReadContract } from "@starknet-react/core";
1919
import { ContractInput } from "./ContractInput";
2020
import { isValidContractArgs } from "~~/utils/scaffold-stark/common";
21+
import { addHistory } from "~~/services/store/history";
2122

2223
type ReadOnlyFunctionFormProps = {
2324
contractAddress: Address;
@@ -102,6 +103,18 @@ export const ReadOnlyFunctionForm = ({
102103
}
103104

104105
refetch();
106+
try {
107+
const inputStr = JSON.stringify(newInputValue);
108+
// Optimistically log a read call as success; real error will be captured via useReadContract error effect
109+
addHistory(contractAddress, {
110+
txHash: undefined,
111+
functionName: abiFunction.name,
112+
timestamp: Date.now(),
113+
status: "success",
114+
message: "Read executed",
115+
input: inputStr,
116+
});
117+
} catch {}
105118
};
106119

107120
return (

packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { InvokeTransactionReceiptResponse } from "starknet";
2121
import { TxReceipt } from "./TxReceipt";
2222
import { useTransactor } from "~~/hooks/scaffold-stark";
2323
import { useAccount } from "~~/hooks/useAccount";
24+
import { addHistory } from "~~/services/store/history";
25+
import { safeStringify } from "~~/utils/scaffold-stark/common";
2426

2527
type WriteOnlyFunctionFormProps = {
2628
abi: Abi;
@@ -76,7 +78,7 @@ export const WriteOnlyFunctionForm = ({
7678

7779
const handleWrite = async () => {
7880
try {
79-
await writeTransaction(
81+
const txHash = await writeTransaction(
8082
!!contractInstance
8183
? [
8284
contractInstance.populate(
@@ -86,6 +88,18 @@ export const WriteOnlyFunctionForm = ({
8688
]
8789
: [],
8890
);
91+
try {
92+
const inputStr = safeStringify(getArgsAsStringInputFromForm(form));
93+
const message = "Transaction sent";
94+
addHistory(contractAddress, {
95+
txHash: typeof txHash === "string" ? txHash : undefined,
96+
functionName: abiFunction.name,
97+
timestamp: Date.now(),
98+
status: "success",
99+
message,
100+
input: inputStr,
101+
});
102+
} catch {}
89103
} catch (e: any) {
90104
const errorPattern = /Contract (.*?)"}/;
91105
const match = errorPattern.exec(e.message);
@@ -95,6 +109,18 @@ export const WriteOnlyFunctionForm = ({
95109
"⚡️ ~ file: WriteOnlyFunctionForm.tsx:handleWrite ~ error",
96110
message,
97111
);
112+
113+
try {
114+
const inputStr = safeStringify(getArgsAsStringInputFromForm(form));
115+
addHistory(contractAddress, {
116+
txHash: undefined,
117+
functionName: abiFunction.name,
118+
timestamp: Date.now(),
119+
status: "error",
120+
message,
121+
input: inputStr,
122+
});
123+
} catch {}
98124
}
99125
};
100126

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"use client";
2+
import React, { useMemo, useState } from "react";
3+
import Image from "next/image";
4+
import { formatTimestamp } from "~~/utils/scaffold-stark/common";
5+
import { useLocalStorage } from "usehooks-ts";
6+
import { useHistoryStore, HistoryEntry } from "~~/services/store/history";
7+
import { ContractName } from "~~/utils/scaffold-stark/contract";
8+
import { getAllContracts } from "~~/utils/scaffold-stark/contractsData";
9+
import HistoryModal from "./HistoryModal";
10+
import { useTheme } from "next-themes";
11+
12+
const contractsData = getAllContracts();
13+
const contractNames = Object.keys(contractsData) as ContractName[];
14+
15+
export default function DebugHistory() {
16+
const [selectedContract] = useLocalStorage<ContractName>(
17+
"scaffoldStark2.selectedContract",
18+
contractNames[0],
19+
{ initializeWithValue: false },
20+
);
21+
const historyByContract = useHistoryStore((s) => s.historyByContract);
22+
const selectedAddress = contractsData[selectedContract]?.address as string;
23+
const entries = useMemo(
24+
() => historyByContract[selectedAddress] || [],
25+
[historyByContract, selectedAddress],
26+
);
27+
const [openEntry, setOpenEntry] = useState<HistoryEntry | null>(null);
28+
29+
const { theme } = useTheme();
30+
const isDarkMode = theme === "dark";
31+
32+
const formatted = useMemo(
33+
() =>
34+
entries.map((e) => ({
35+
...e,
36+
ts: new Date(e.timestamp),
37+
})),
38+
[entries],
39+
);
40+
41+
const formatDate = (ts: number) => formatTimestamp(ts);
42+
43+
const StatusIcon = ({ status }: { status: HistoryEntry["status"] }) => (
44+
<Image
45+
src={status === "success" ? "/success-icon.svg" : "/fail-icon.svg"}
46+
alt={status}
47+
width={20}
48+
height={20}
49+
/>
50+
);
51+
52+
return (
53+
<div className="h-full max-h-[650px] w-full xl:w-[400px] space-y-4">
54+
<div className="tab h-10 w-full xl:w-1/3 tab-active bg-[#8A45FC]! rounded-[5px] text-white!">
55+
History
56+
</div>
57+
<div className="border-gradient rounded-[5px] h-full w-full overflow-y-auto">
58+
<div className="flex flex-col">
59+
{formatted.length === 0 ? (
60+
<div className="p-4 text-sm text-neutral">No history yet.</div>
61+
) : (
62+
formatted.map((e, idx) => (
63+
<button
64+
key={e.txHash ?? `${e.functionName}-${e.timestamp}`}
65+
className={`w-full flex items-center justify-between py-3 px-3 text-left border-b ${isDarkMode ? "bg-[#0000002E] border-b-[#ffffff4f]" : "bg-[#ffffff0f] border-b-black/40"}`}
66+
onClick={() => setOpenEntry(e)}
67+
>
68+
<span className="truncate mr-2 text-[#4DB4FF]">
69+
{e.functionName}
70+
</span>
71+
<div className="flex items-center gap-3">
72+
<span className="text-xs text-neutral">
73+
{formatDate(e.timestamp)}
74+
</span>
75+
<StatusIcon status={e.status} />
76+
</div>
77+
</button>
78+
))
79+
)}
80+
</div>
81+
</div>
82+
83+
{openEntry && (
84+
<HistoryModal entry={openEntry} onClose={() => setOpenEntry(null)} />
85+
)}
86+
</div>
87+
);
88+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"use client";
2+
import React from "react";
3+
import Image from "next/image";
4+
import { useTheme } from "next-themes";
5+
import { HistoryEntry } from "~~/services/store/history";
6+
import { formatTimestamp } from "~~/utils/scaffold-stark/common";
7+
8+
export default function HistoryModal({
9+
entry,
10+
onClose,
11+
}: {
12+
entry: HistoryEntry;
13+
onClose: () => void;
14+
}) {
15+
const { theme } = useTheme();
16+
const isDarkMode = theme === "dark";
17+
const formatDate = (ts: number) => formatTimestamp(ts);
18+
19+
const isSuccess = entry.status === "success";
20+
const chipClasses = isSuccess
21+
? "bg-green-500/20 text-green-400"
22+
: "bg-red-500/20 text-red-400";
23+
const iconSrc = isSuccess ? "/success-icon.svg" : "/fail-icon.svg";
24+
const sectionTitle = isSuccess ? "Message" : "Receipt";
25+
const sectionBg = isSuccess ? "bg-green-900/30" : "bg-red-900/30";
26+
const chipLabel = isSuccess ? "Success" : "Fail";
27+
28+
return (
29+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
30+
<div className="absolute inset-0 backdrop-blur-xs" onClick={onClose} />
31+
<div
32+
className={`relative z-10 w-full max-w-2xl max-h-[90vh] rounded-[5px] border border-[#8A45FC] p-4 flex flex-col ${isDarkMode ? "bg-[#0C1023]" : "bg-base-100"}`}
33+
>
34+
<div className="flex items-center justify-between mb-2 flex-shrink-0">
35+
<div className="flex items-center gap-2">
36+
<h3 className="m-0 font-semibold">{entry.functionName}</h3>
37+
<span
38+
className={`inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded ${chipClasses}`}
39+
>
40+
<Image src={iconSrc} alt={chipLabel} width={16} height={16} />
41+
{chipLabel}
42+
</span>
43+
</div>
44+
<button className="btn btn-ghost btn-xs" onClick={onClose}>
45+
46+
</button>
47+
</div>
48+
49+
<div className="flex-1 overflow-y-auto space-y-3">
50+
<div className="bg-base-200 text-sm px-4 py-2 rounded">
51+
<div className="flex items-center justify-between">
52+
<span>Your input</span>
53+
<span className="text-xs">{formatDate(entry.timestamp)}</span>
54+
</div>
55+
<pre className="m-0 whitespace-pre-wrap max-h-32 overflow-y-auto">
56+
{entry.input || "[code here]"}
57+
</pre>
58+
</div>
59+
60+
<div className={`text-sm px-4 py-2 rounded ${sectionBg}`}>
61+
<p className="font-bold m-0 mb-1">{sectionTitle}</p>
62+
<pre className="whitespace-pre-wrap break-words m-0 max-h-64 overflow-y-auto">
63+
{entry.message}
64+
</pre>
65+
</div>
66+
</div>
67+
</div>
68+
</div>
69+
);
70+
}
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { create } from "zustand";
2+
import { persist, createJSONStorage } from "zustand/middleware";
3+
4+
export type HistoryStatus = "success" | "error";
5+
export type HistoryEntry = {
6+
txHash?: string;
7+
functionName: string;
8+
timestamp: number;
9+
status: HistoryStatus;
10+
message: string;
11+
input?: string;
12+
};
13+
14+
type HistoryState = {
15+
historyByContract: Record<string, HistoryEntry[]>; // key: contractAddress
16+
addHistory: (contractAddress: string, entry: HistoryEntry) => void;
17+
clearHistory: (contractAddress: string) => void;
18+
};
19+
20+
export const useHistoryStore = create<HistoryState>()(
21+
persist(
22+
(set) => ({
23+
historyByContract: {},
24+
addHistory: (contractAddress: string, entry: HistoryEntry) =>
25+
set((state) => {
26+
const current = state.historyByContract[contractAddress] || [];
27+
return {
28+
historyByContract: {
29+
...state.historyByContract,
30+
[contractAddress]: [entry, ...current].slice(0, 200),
31+
},
32+
} as Partial<HistoryState> as HistoryState;
33+
}),
34+
clearHistory: (contractAddress: string) =>
35+
set((state) => {
36+
const copy = { ...state.historyByContract };
37+
delete copy[contractAddress];
38+
return {
39+
historyByContract: copy,
40+
} as Partial<HistoryState> as HistoryState;
41+
}),
42+
}),
43+
{
44+
name: "scaffoldStark2.history",
45+
storage: createJSONStorage(() => sessionStorage),
46+
},
47+
),
48+
);
49+
50+
// Convenience helpers for non-hook contexts
51+
export const addHistory = (contractAddress: string, entry: HistoryEntry) =>
52+
useHistoryStore.getState().addHistory(contractAddress, entry);
53+
export const clearHistory = (contractAddress: string) =>
54+
useHistoryStore.getState().clearHistory(contractAddress);

packages/nextjs/utils/scaffold-stark/common.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,25 @@ export function isValidContractArgs(
3737
args.every((arg) => arg !== undefined && arg !== null && arg !== "")
3838
);
3939
}
40+
41+
// Safely stringify objects that might contain bigint or complex values
42+
export function safeStringify(value: unknown): string {
43+
try {
44+
return JSON.stringify(value, (_key, val) =>
45+
typeof val === "bigint" ? val.toString() : val,
46+
);
47+
} catch {
48+
return String(value);
49+
}
50+
}
51+
52+
export function formatTimestamp(ts: number): string {
53+
return new Intl.DateTimeFormat(undefined, {
54+
month: "short",
55+
day: "2-digit",
56+
year: "numeric",
57+
hour: "2-digit",
58+
minute: "2-digit",
59+
hour12: false,
60+
}).format(new Date(ts));
61+
}

0 commit comments

Comments
 (0)