Skip to content

Commit 33666aa

Browse files
[SDK] Optimize multi-event querying with indexer (#6822)
1 parent e526f75 commit 33666aa

File tree

8 files changed

+97
-64
lines changed

8 files changed

+97
-64
lines changed

.changeset/great-chairs-find.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+
More efficient multi event querying when using indexer

apps/dashboard/src/@3rdweb-sdk/react/hooks/useActivity.ts

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import { type AbiEvent, formatAbiItem } from "abitype";
21
import { useMemo } from "react";
3-
import {
4-
type PreparedEvent,
5-
type ThirdwebContract,
6-
prepareEvent,
7-
} from "thirdweb";
2+
import type { ThirdwebContract } from "thirdweb";
83
import { useContractEvents } from "thirdweb/react";
9-
import { useResolveContractAbi } from "./useResolveContractAbi";
104

115
export interface InternalTransaction {
126
transactionHash: string;
@@ -20,20 +14,8 @@ export interface InternalTransaction {
2014
}
2115

2216
export function useActivity(contract: ThirdwebContract, autoUpdate?: boolean) {
23-
const abiQuery = useResolveContractAbi(contract);
24-
25-
// Get all the Prepare Events from the contract abis
26-
const events: PreparedEvent<AbiEvent>[] = useMemo(() => {
27-
const eventsItems = (abiQuery.data || []).filter((o) => o.type === "event");
28-
const eventSignatures = eventsItems.map((event) => formatAbiItem(event));
29-
return eventSignatures.map((signature) =>
30-
prepareEvent({ signature: signature as `event ${string}` }),
31-
);
32-
}, [abiQuery.data]);
33-
3417
const eventsQuery = useContractEvents({
3518
contract,
36-
events,
3719
blockRange: 20000,
3820
watch: autoUpdate,
3921
});
@@ -42,26 +24,30 @@ export function useActivity(contract: ThirdwebContract, autoUpdate?: boolean) {
4224
if (!eventsQuery.data) {
4325
return [];
4426
}
45-
const obj = eventsQuery.data.slice(0, 100).reduce(
46-
(acc, curr) => {
47-
const internalTx = acc[curr.transactionHash];
48-
if (internalTx) {
49-
internalTx.events.push(curr);
50-
internalTx.events.sort((a, b) => b.logIndex - a.logIndex);
51-
if (internalTx.blockNumber > curr.blockNumber) {
52-
internalTx.blockNumber = curr.blockNumber;
27+
const obj = eventsQuery.data
28+
.slice()
29+
.reverse()
30+
.slice(0, 100)
31+
.reduce(
32+
(acc, curr) => {
33+
const internalTx = acc[curr.transactionHash];
34+
if (internalTx) {
35+
internalTx.events.push(curr);
36+
internalTx.events.sort((a, b) => b.logIndex - a.logIndex);
37+
if (internalTx.blockNumber > curr.blockNumber) {
38+
internalTx.blockNumber = curr.blockNumber;
39+
}
40+
} else {
41+
acc[curr.transactionHash] = {
42+
transactionHash: curr.transactionHash,
43+
blockNumber: curr.blockNumber,
44+
events: [curr],
45+
};
5346
}
54-
} else {
55-
acc[curr.transactionHash] = {
56-
transactionHash: curr.transactionHash,
57-
blockNumber: curr.blockNumber,
58-
events: [curr],
59-
};
60-
}
61-
return acc;
62-
},
63-
{} as Record<string, InternalTransaction>,
64-
);
47+
return acc;
48+
},
49+
{} as Record<string, InternalTransaction>,
50+
);
6551
return Object.values(obj).sort((a, b) =>
6652
a.blockNumber > b.blockNumber ? -1 : 1,
6753
);

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import type {
77
import { type Log, formatLog } from "viem";
88
import { resolveContractAbi } from "../../contract/actions/resolve-abi.js";
99
import type { ThirdwebContract } from "../../contract/contract.js";
10-
import { getContractEvents as getContractEventsInsight } from "../../insight/get-events.js";
10+
import {
11+
type ContractEvent,
12+
getContractEvents as getContractEventsInsight,
13+
} from "../../insight/get-events.js";
1114
import { eth_blockNumber } from "../../rpc/actions/eth_blockNumber.js";
1215
import {
1316
type GetLogsBlockParams,
@@ -156,6 +159,28 @@ export async function getContractEvents<
156159

157160
// if we have an abi on the contract, we can encode the topics with it
158161
if (!events?.length && !!contract) {
162+
if (useIndexer) {
163+
// fetch all events from the indexer, no need to get events from ABI
164+
const events = await getContractEventsInsight({
165+
client: contract.client,
166+
chains: [contract.chain],
167+
contractAddress: contract.address,
168+
decodeLogs: true,
169+
queryOptions: {
170+
limit: 500,
171+
filter_block_hash: restParams.blockHash,
172+
filter_block_number_gte: restParams.fromBlock,
173+
filter_block_number_lte: restParams.toBlock,
174+
},
175+
}).catch(() => {
176+
// chain might not support indexer
177+
return null;
178+
});
179+
if (events) {
180+
return toLog(events) as GetContractEventsResult<abiEvents, TStrict>;
181+
}
182+
}
183+
159184
// if we have a contract *WITH* an abi we can use that
160185
if (contract.abi?.length) {
161186
// @ts-expect-error - we can't make typescript happy here, but we know this is an abi event
@@ -246,6 +271,10 @@ async function getLogsFromInsight(options: {
246271
},
247272
});
248273

274+
return toLog(r);
275+
}
276+
277+
function toLog(r: ContractEvent[]) {
249278
const cleanedEventData = r.map((tx) => ({
250279
chainId: tx.chain_id,
251280
blockNumber: numberToHex(Number(tx.block_number)),
@@ -257,7 +286,18 @@ async function getLogsFromInsight(options: {
257286
address: tx.address,
258287
data: tx.data as Hex,
259288
topics: tx.topics as [`0x${string}`, ...`0x${string}`[]] | [] | undefined,
289+
...(tx.decoded
290+
? {
291+
eventName: tx.decoded.name,
292+
args: {
293+
...tx.decoded.indexed_params,
294+
...tx.decoded.non_indexed_params,
295+
},
296+
}
297+
: {}),
260298
}));
261299

262-
return cleanedEventData.map((e) => formatLog(e));
300+
return cleanedEventData
301+
.map((e) => formatLog(e))
302+
.sort((a, b) => Number((a.blockNumber ?? 0n) - (b.blockNumber ?? 0n)));
263303
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export function watchContractEvents<
7777
fromBlock: blockNumber,
7878
// toBlock is inclusive
7979
toBlock: blockNumber,
80+
useIndexer: false,
8081
}),
8182
{
8283
retries: 3,

packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFT", () => {
5858
includeOwner: true,
5959
});
6060
expect(nft.metadata.name).toBe("Doodle #1");
61-
expect(nft.owner).toBe("0xbe9936fcfc50666f5425fde4a9decc59cef73b24");
61+
expect(nft.owner).toBe("0xbE9936FCFC50666f5425FDE4A9decC59cEF73b24");
6262
expect(nft).toMatchInlineSnapshot(`
6363
{
6464
"chainId": 1,
@@ -90,12 +90,9 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFT", () => {
9090
"image": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9",
9191
"image_url": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9",
9292
"name": "Doodle #1",
93-
"owner_addresses": [
94-
"0xbe9936fcfc50666f5425fde4a9decc59cef73b24",
95-
],
9693
"uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1",
9794
},
98-
"owner": "0xbe9936fcfc50666f5425fde4a9decc59cef73b24",
95+
"owner": "0xbE9936FCFC50666f5425FDE4A9decC59cEF73b24",
9996
"tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
10097
"tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1",
10198
"type": "ERC721",

packages/thirdweb/src/extensions/erc721/read/getOwnedNFTs.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getOwnedNFTs", () => {
1313
owner,
1414
});
1515
expect(nfts.length).greaterThan(0);
16+
for (const item of nfts) {
17+
expect(item.owner).toBe(owner);
18+
}
1619
});
1720

1821
it("should detect ownership functions using indexer", async () => {

packages/thirdweb/src/insight/get-nfts.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { NFT } from "../utils/nft/parseNft.js";
1111

1212
import { getCachedChain } from "../chains/utils.js";
1313
import { getContract } from "../contract/contract.js";
14+
import { getAddress } from "../utils/address.js";
1415
type OwnedNFT = GetV1NftsResponse["data"][number];
1516
type ContractNFT = GetV1NftsByContractAddressResponse["data"][number];
1617

@@ -268,21 +269,25 @@ async function transformNFTModel(
268269
token_type,
269270
...rest
270271
} = nft;
272+
273+
let metadataToUse = rest;
274+
let owners: string[] | undefined = ownerAddress
275+
? [getAddress(ownerAddress)]
276+
: undefined;
277+
278+
if ("owner_addresses" in rest) {
279+
const { owner_addresses, ...restWithoutOwnerAddresses } = rest;
280+
metadataToUse = restWithoutOwnerAddresses;
281+
owners = owners ?? owner_addresses?.map((o) => getAddress(o));
282+
}
283+
271284
const metadata = replaceIPFSGatewayRecursively({
272285
uri: nft.metadata_url ?? "",
273286
image: nft.image_url,
274287
attributes: nft.extra_metadata?.attributes ?? undefined,
275-
...rest,
288+
...metadataToUse,
276289
});
277290

278-
// replace the ipfs gateway with the ipfs gateway from the client recusively for each key in the metadata object
279-
280-
const owner_addresses = ownerAddress
281-
? [ownerAddress]
282-
: "owner_addresses" in nft
283-
? nft.owner_addresses
284-
: undefined;
285-
286291
if (contract?.type === "erc1155") {
287292
// TODO (insight): this needs to be added in the API
288293
const supply = await totalSupply({
@@ -298,7 +303,7 @@ async function transformNFTModel(
298303
tokenId: BigInt(token_id),
299304
tokenUri: replaceIPFSGateway(metadata_url) ?? "",
300305
type: "ERC1155",
301-
owner: owner_addresses?.[0],
306+
owner: owners?.[0],
302307
tokenAddress: contract?.address ?? "",
303308
chainId: contract?.chain_id ?? 0,
304309
supply: supply,
@@ -307,7 +312,7 @@ async function transformNFTModel(
307312
parsedNft = parseNFT(metadata, {
308313
tokenId: BigInt(token_id),
309314
type: "ERC721",
310-
owner: owner_addresses?.[0],
315+
owner: owners?.[0],
311316
tokenUri: replaceIPFSGateway(metadata_url) ?? "",
312317
tokenAddress: contract?.address ?? "",
313318
chainId: contract?.chain_id ?? 0,

packages/thirdweb/src/wallets/smart/lib/paymaster.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,15 @@ export async function getPaymasterAndData(args: {
6565
headers,
6666
body: stringify(body),
6767
});
68-
const res = await response.json();
6968

7069
if (!response.ok) {
71-
const error = res.error || response.statusText;
72-
const code = res.code || "UNKNOWN";
70+
const error = (await response.text()) || response.statusText;
7371

74-
throw new Error(
75-
`Paymaster error: ${error}
76-
Status: ${response.status}
77-
Code: ${code}`,
78-
);
72+
throw new Error(`Paymaster error: ${response.status} - ${error}`);
7973
}
8074

75+
const res = await response.json();
76+
8177
if (res.result) {
8278
// some paymasters return a string, some return an object with more data
8379
if (typeof res.result === "string") {

0 commit comments

Comments
 (0)