Skip to content

Commit f1cd9c8

Browse files
authored
feat: Create UseScaffoldWatchContractEvent Hook (#584)
2 parents fef0dd3 + 6d7e601 commit f1cd9c8

3 files changed

Lines changed: 439 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { renderHook, act, waitFor } from "@testing-library/react";
2+
import { useScaffoldWatchContractEvent } from "../useScaffoldWatchContractEvent";
3+
import { useDeployedContractInfo } from "../useDeployedContractInfo";
4+
import { useTargetNetwork } from "../useTargetNetwork";
5+
import { useProvider } from "@starknet-react/core";
6+
import { RpcProvider } from "starknet";
7+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
8+
import { events as starknetEvents } from "starknet";
9+
import * as eventsData from "~~/utils/scaffold-stark/eventsData";
10+
import * as starknet from "starknet";
11+
12+
// Mock dependencies
13+
vi.mock("starknet", async () => {
14+
const actual: typeof import("starknet") = await vi.importActual("starknet");
15+
return {
16+
...actual,
17+
// replace only the `parseEvents` function, keep the rest intact
18+
events: {
19+
...actual.events,
20+
parseEvents: vi.fn(),
21+
getAbiEvents: vi.fn(),
22+
},
23+
};
24+
});
25+
26+
vi.mock("../useDeployedContractInfo", () => ({
27+
useDeployedContractInfo: vi.fn(),
28+
}));
29+
vi.mock("../useTargetNetwork", () => ({
30+
useTargetNetwork: vi.fn(),
31+
}));
32+
vi.mock("@starknet-react/core", () => ({
33+
useProvider: vi.fn(),
34+
}));
35+
36+
describe("useScaffoldWatchContractEvent", () => {
37+
const mockContractName = "MyContract";
38+
const mockEventName = "MyEvent";
39+
const mockTargetNetwork = {
40+
id: "testnet",
41+
rpcUrls: { public: { http: ["https://mock-rpc-url"] } },
42+
};
43+
44+
// A mock deployed contract with one event in its ABI
45+
const mockDeployedContractData = {
46+
address: "0x123",
47+
abi: [{ type: "event", name: "Module::MyEvent" }],
48+
};
49+
50+
// A mock event log returned by the provider
51+
const mockLog = {
52+
data: ["0x01"],
53+
keys: ["0xabc"],
54+
block_number: 1,
55+
block_hash: "0xblock",
56+
transaction_hash: "0xtx",
57+
from_address: "0xfrom",
58+
};
59+
60+
const fakeBlock = { block_hash: "0xblock" };
61+
const fakeTx = { transaction_hash: "0xtx" };
62+
const fakeReceipt = { transaction_hash: "0xtx" };
63+
64+
beforeEach(() => {
65+
// By default, simulate a loaded contract with the mock ABI
66+
// @ts-ignore
67+
(useDeployedContractInfo as vi.Mock).mockReturnValue({
68+
data: mockDeployedContractData,
69+
isLoading: false,
70+
});
71+
// @ts-ignore
72+
(useTargetNetwork as vi.Mock).mockReturnValue({
73+
targetNetwork: mockTargetNetwork,
74+
});
75+
// @ts-ignore
76+
(useProvider as vi.Mock).mockReturnValue({
77+
provider: {}, // Not used directly in hook logic
78+
});
79+
80+
// Mock the StarkNet RPC provider behavior
81+
RpcProvider.prototype.getBlockLatestAccepted = vi.fn().mockResolvedValue({
82+
block_number: 10,
83+
});
84+
RpcProvider.prototype.getEvents = vi.fn().mockResolvedValue({
85+
events: [mockLog],
86+
});
87+
88+
RpcProvider.prototype.getBlockWithTxHashes = vi
89+
.fn()
90+
.mockResolvedValue(fakeBlock);
91+
RpcProvider.prototype.getTransactionByHash = vi
92+
.fn()
93+
.mockResolvedValue(fakeTx);
94+
RpcProvider.prototype.getTransactionReceipt = vi
95+
.fn()
96+
.mockResolvedValue(fakeReceipt);
97+
98+
// spy on parseEvents & parseEventData
99+
vi.spyOn(starknet.events, "parseEvents").mockReturnValue([
100+
{ [mockEventName]: { foo: "bar" } },
101+
]);
102+
vi.spyOn(starknet.events, "getAbiEvents").mockReturnValue(
103+
// @ts-ignore
104+
(mockDeployedContractData.abi as any).filter((x) => x.type === "event"),
105+
);
106+
vi.spyOn(eventsData, "parseEventData").mockReturnValue({ parsed: true });
107+
});
108+
109+
afterEach(() => {
110+
vi.clearAllMocks();
111+
vi.useRealTimers();
112+
});
113+
114+
it("calls onLogs when events are fetched after mount", async () => {
115+
const onLogs = vi.fn();
116+
const { result } = renderHook(() =>
117+
useScaffoldWatchContractEvent({
118+
contractName: mockContractName as any,
119+
eventName: mockEventName as never,
120+
onLogs,
121+
}),
122+
);
123+
124+
// Initially, loading should be false (no operations yet)
125+
expect(result.current.isLoading).toBe(false);
126+
127+
// Wait for the effect to fetch events and call onLogs
128+
await waitFor(() => {
129+
expect(onLogs).toHaveBeenCalledWith(
130+
expect.objectContaining({
131+
type: "event", // or whatever your ABI’s `type` is
132+
args: { foo: "bar" },
133+
parsedArgs: { parsed: true },
134+
block: fakeBlock,
135+
transaction: fakeTx,
136+
receipt: fakeReceipt,
137+
}),
138+
);
139+
});
140+
141+
// After processing, loading should be false and no error should be set
142+
expect(result.current.isLoading).toBe(false);
143+
expect(result.current.error).toBeUndefined();
144+
});
145+
146+
it("does not call onLogs if no events are returned", async () => {
147+
// Simulate the provider returning an empty events array
148+
// @ts-ignore
149+
(RpcProvider.prototype.getEvents as vi.Mock).mockResolvedValueOnce({
150+
events: [],
151+
});
152+
153+
const onLogs = vi.fn();
154+
renderHook(() =>
155+
useScaffoldWatchContractEvent({
156+
contractName: mockContractName as any,
157+
eventName: mockEventName as never,
158+
onLogs,
159+
}),
160+
);
161+
162+
// Wait a short time to ensure the effect has run
163+
await waitFor(() => {
164+
// onLogs should not have been called since there are no events
165+
expect(onLogs).not.toHaveBeenCalled();
166+
});
167+
});
168+
169+
it("throws error if the event is not found in the contract ABI", () => {
170+
// @ts-ignore
171+
(useDeployedContractInfo as vi.Mock).mockReturnValue({
172+
data: { address: "0x123", abi: [] }, // empty ABI
173+
isLoading: false,
174+
});
175+
176+
expect(() =>
177+
renderHook(() =>
178+
useScaffoldWatchContractEvent({
179+
contractName: mockContractName as any,
180+
eventName: mockEventName as never,
181+
onLogs: () => {},
182+
}),
183+
),
184+
).toThrow(`Event ${mockEventName} not found in contract ABI`);
185+
});
186+
187+
it("throws error if multiple matching events are found in the ABI", () => {
188+
// @ts-ignore
189+
(useDeployedContractInfo as vi.Mock).mockReturnValue({
190+
data: {
191+
address: "0x123",
192+
abi: [
193+
{ type: "event", name: "Module::MyEvent" },
194+
{ type: "event", name: "Other::MyEvent" },
195+
],
196+
},
197+
isLoading: false,
198+
});
199+
200+
expect(() =>
201+
renderHook(() =>
202+
useScaffoldWatchContractEvent({
203+
contractName: mockContractName as any,
204+
eventName: mockEventName as never,
205+
onLogs: () => {},
206+
}),
207+
),
208+
).toThrow(/Ambiguous event/);
209+
});
210+
211+
it("sets an error if contract data is not found after loading", async () => {
212+
// @ts-ignore
213+
(useDeployedContractInfo as vi.Mock).mockReturnValue({
214+
data: undefined,
215+
isLoading: false,
216+
});
217+
const onLogs = vi.fn();
218+
const { result } = renderHook(() =>
219+
useScaffoldWatchContractEvent({
220+
contractName: mockContractName as any,
221+
eventName: mockEventName as never,
222+
onLogs,
223+
}),
224+
);
225+
226+
// Wait for the effect to detect the missing contract data and set the error
227+
await waitFor(() => expect(result.current.error).toBeDefined());
228+
expect(result.current.error).toBeInstanceOf(Error);
229+
expect(result.current.error?.message).toContain("Contract not found");
230+
expect(onLogs).not.toHaveBeenCalled();
231+
});
232+
});

0 commit comments

Comments
 (0)