Skip to content

Commit e6fce1f

Browse files
authored
Fix event duplication in useContractEvents (#2899)
1 parent 3c13fe4 commit e6fce1f

File tree

5 files changed

+94
-11
lines changed

5 files changed

+94
-11
lines changed

.changeset/tame-rivers-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Fixes useContractEvents duplicating events

packages/thirdweb/src/event/actions/watch-events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type WatchContractEventsOptions<
1616
> = Prettify<
1717
GetContractEventsOptionsDirect<abi, abiEvents, TStrict> & {
1818
onEvents: (events: ParseEventLogsResult<abiEvents, TStrict>) => void;
19+
latestBlockNumber?: bigint;
1920
}
2021
>;
2122

@@ -79,5 +80,6 @@ export function watchContractEvents<
7980
options.onEvents(logs);
8081
}
8182
},
83+
latestBlockNumber: options.latestBlockNumber,
8284
});
8385
}

packages/thirdweb/src/react/core/hooks/contract/useContractEvents.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
useQueryClient,
55
} from "@tanstack/react-query";
66
import type { Abi, AbiEvent } from "abitype";
7-
import { useEffect, useMemo } from "react";
7+
import { useEffect, useMemo, useRef } from "react";
88
import { getContractEvents } from "../../../../event/actions/get-events.js";
99
import type { ParseEventLogsResult } from "../../../../event/actions/parse-logs.js";
1010
import {
@@ -31,7 +31,13 @@ type UseContractEventsOptions<
3131
* @example
3232
* ```jsx
3333
* import { useContractEvents } from "thirdweb/react";
34-
* const contractEvents = useContractEvents({contract});
34+
* import { tokensClaimedEvent } from "thirdweb/extensions/erc721";
35+
*
36+
* const account = useActiveAccount();
37+
* const contractEvents = useContractEvents({
38+
* contract
39+
* events: [tokensClaimedEvent({ claimer: account?.address })],
40+
* });
3541
* ```
3642
* @contract
3743
*/
@@ -48,6 +54,7 @@ export function useContractEvents<
4854
enabled = true,
4955
watch = true,
5056
} = options;
57+
const latestBlockNumber = useRef<bigint>(); // We use this to keep track of the latest block number when new pollers are spawned
5158

5259
const queryClient = useQueryClient();
5360

@@ -70,6 +77,7 @@ export function useContractEvents<
7077
queryFn: async () => {
7178
const rpcRequest = getRpcClient(contract);
7279
const currentBlockNumber = await eth_blockNumber(rpcRequest);
80+
latestBlockNumber.current = currentBlockNumber;
7381
const initialEvents = await getContractEvents({
7482
contract,
7583
events: events,
@@ -85,17 +93,22 @@ export function useContractEvents<
8593
// don't watch if not enabled or if watch is false
8694
return;
8795
}
96+
8897
// the return is important here because it will unwatch the events
8998
return watchContractEvents<abi, abiEvents>({
9099
contract,
91100
onEvents: (newEvents) => {
101+
if (newEvents.length > 0 && newEvents[0]) {
102+
latestBlockNumber.current = newEvents[0].blockNumber; // Update the latest block number to avoid duplicate events if a new poller is spawned during this block
103+
}
92104
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix any
93-
queryClient.setQueryData(queryKey, (oldEvents: any = []) => {
94-
const newLogs = [...oldEvents, ...newEvents];
95-
return newLogs;
96-
});
105+
queryClient.setQueryData(queryKey, (oldEvents: any = []) => [
106+
...oldEvents,
107+
...newEvents,
108+
]);
97109
},
98110
events,
111+
latestBlockNumber: latestBlockNumber.current,
99112
});
100113
}, [contract, enabled, events, queryClient, queryKey, watch]);
101114

packages/thirdweb/src/rpc/watchBlockNumber.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,63 @@ describe.runIf(process.env.TW_SECRET_KEY)("watch block number", () => {
138138

139139
unwatch2();
140140
});
141+
142+
it("should re-start from latestBlockNumber if provided", async () => {
143+
const unwatch = watchBlockNumber({
144+
client: TEST_CLIENT,
145+
chain: baseSepolia,
146+
onNewBlockNumber,
147+
});
148+
149+
expect(onNewBlockNumber).toHaveBeenCalledTimes(0);
150+
151+
// wait for 5 seconds which should always be sufficient for a new block to be mined
152+
await wait(5000);
153+
154+
expect(onNewBlockNumber).toHaveBeenCalled();
155+
156+
const lastBlockNumber = onNewBlockNumber.mock.lastCall?.[0];
157+
158+
onNewBlockNumber.mockClear();
159+
160+
unwatch();
161+
162+
// wait for 5 seconds which should always be sufficient for a new block to be mined
163+
await wait(5000);
164+
165+
expect(onNewBlockNumber).toHaveBeenCalledTimes(0);
166+
167+
const unwatch2 = watchBlockNumber({
168+
client: TEST_CLIENT,
169+
chain: baseSepolia,
170+
onNewBlockNumber,
171+
latestBlockNumber: lastBlockNumber,
172+
});
173+
174+
// wait for 5 seconds which should always be sufficient for a new block to be mined
175+
await wait(5000);
176+
177+
expect(onNewBlockNumber).toHaveBeenCalled();
178+
179+
const firstBlockNumber = onNewBlockNumber.mock.calls[0]?.[0];
180+
181+
expect(firstBlockNumber).toEqual(lastBlockNumber + 1n);
182+
183+
unwatch2();
184+
});
185+
186+
it("should start from latestBlockNumber", async () => {
187+
const unwatch = watchBlockNumber({
188+
client: TEST_CLIENT,
189+
chain: baseSepolia,
190+
onNewBlockNumber,
191+
latestBlockNumber: 9342233n,
192+
});
193+
194+
await wait(500); // wait long enough to have called callback
195+
196+
expect(onNewBlockNumber.mock.calls[0]?.[0]).toEqual(9342234n);
197+
198+
unwatch();
199+
});
141200
});

packages/thirdweb/src/rpc/watchBlockNumber.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ function createBlockNumberPoller(
4949
async function poll() {
5050
// stop polling if there are no more subscriptions
5151
if (!isActive) {
52-
lastBlockNumber = undefined;
53-
lastBlockAt = undefined;
5452
return;
5553
}
5654
const blockNumber = await eth_blockNumber(rpcRequest);
@@ -100,10 +98,14 @@ function createBlockNumberPoller(
10098
}
10199

102100
// return the "subscribe" function
103-
return function subscribe(callBack: (blockNumber: bigint) => void) {
101+
return function subscribe(
102+
callBack: (blockNumber: bigint) => void,
103+
initialBlockNumber?: bigint,
104+
) {
104105
subscribers.push(callBack);
105106
// if we are currently not active -> start polling
106107
if (!isActive) {
108+
lastBlockNumber = initialBlockNumber;
107109
isActive = true;
108110
poll();
109111
}
@@ -140,6 +142,7 @@ export type WatchBlockNumberOptions = {
140142
chain: Chain;
141143
onNewBlockNumber: (blockNumber: bigint) => void;
142144
overPollRatio?: number;
145+
latestBlockNumber?: bigint;
143146
};
144147

145148
/**
@@ -163,7 +166,8 @@ export type WatchBlockNumberOptions = {
163166
* @rpc
164167
*/
165168
export function watchBlockNumber(opts: WatchBlockNumberOptions) {
166-
const { client, chain, onNewBlockNumber, overPollRatio } = opts;
169+
const { client, chain, onNewBlockNumber, overPollRatio, latestBlockNumber } =
170+
opts;
167171
const chainId = chain.id;
168172
// if we already have a poller for this chainId -> use it
169173
let poller = existingPollers.get(chainId);
@@ -174,5 +178,5 @@ export function watchBlockNumber(opts: WatchBlockNumberOptions) {
174178
existingPollers.set(chainId, poller);
175179
}
176180
// subscribe to the poller and return the unSubscribe function to the caller
177-
return poller(onNewBlockNumber);
181+
return poller(onNewBlockNumber, latestBlockNumber);
178182
}

0 commit comments

Comments
 (0)