diff --git a/.changeset/great-chairs-find.md b/.changeset/great-chairs-find.md new file mode 100644 index 00000000000..fee1ea7983c --- /dev/null +++ b/.changeset/great-chairs-find.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +More efficient multi event querying when using indexer diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useActivity.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useActivity.ts index 13b04fe4570..1795a5f2e21 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useActivity.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useActivity.ts @@ -1,12 +1,6 @@ -import { type AbiEvent, formatAbiItem } from "abitype"; import { useMemo } from "react"; -import { - type PreparedEvent, - type ThirdwebContract, - prepareEvent, -} from "thirdweb"; +import type { ThirdwebContract } from "thirdweb"; import { useContractEvents } from "thirdweb/react"; -import { useResolveContractAbi } from "./useResolveContractAbi"; export interface InternalTransaction { transactionHash: string; @@ -20,20 +14,8 @@ export interface InternalTransaction { } export function useActivity(contract: ThirdwebContract, autoUpdate?: boolean) { - const abiQuery = useResolveContractAbi(contract); - - // Get all the Prepare Events from the contract abis - const events: PreparedEvent[] = useMemo(() => { - const eventsItems = (abiQuery.data || []).filter((o) => o.type === "event"); - const eventSignatures = eventsItems.map((event) => formatAbiItem(event)); - return eventSignatures.map((signature) => - prepareEvent({ signature: signature as `event ${string}` }), - ); - }, [abiQuery.data]); - const eventsQuery = useContractEvents({ contract, - events, blockRange: 20000, watch: autoUpdate, }); @@ -42,26 +24,30 @@ export function useActivity(contract: ThirdwebContract, autoUpdate?: boolean) { if (!eventsQuery.data) { return []; } - const obj = eventsQuery.data.slice(0, 100).reduce( - (acc, curr) => { - const internalTx = acc[curr.transactionHash]; - if (internalTx) { - internalTx.events.push(curr); - internalTx.events.sort((a, b) => b.logIndex - a.logIndex); - if (internalTx.blockNumber > curr.blockNumber) { - internalTx.blockNumber = curr.blockNumber; + const obj = eventsQuery.data + .slice() + .reverse() + .slice(0, 100) + .reduce( + (acc, curr) => { + const internalTx = acc[curr.transactionHash]; + if (internalTx) { + internalTx.events.push(curr); + internalTx.events.sort((a, b) => b.logIndex - a.logIndex); + if (internalTx.blockNumber > curr.blockNumber) { + internalTx.blockNumber = curr.blockNumber; + } + } else { + acc[curr.transactionHash] = { + transactionHash: curr.transactionHash, + blockNumber: curr.blockNumber, + events: [curr], + }; } - } else { - acc[curr.transactionHash] = { - transactionHash: curr.transactionHash, - blockNumber: curr.blockNumber, - events: [curr], - }; - } - return acc; - }, - {} as Record, - ); + return acc; + }, + {} as Record, + ); return Object.values(obj).sort((a, b) => a.blockNumber > b.blockNumber ? -1 : 1, ); diff --git a/packages/thirdweb/src/event/actions/get-events.ts b/packages/thirdweb/src/event/actions/get-events.ts index 3491c4d4f1d..52fd4292676 100644 --- a/packages/thirdweb/src/event/actions/get-events.ts +++ b/packages/thirdweb/src/event/actions/get-events.ts @@ -7,7 +7,10 @@ import type { import { type Log, formatLog } from "viem"; import { resolveContractAbi } from "../../contract/actions/resolve-abi.js"; import type { ThirdwebContract } from "../../contract/contract.js"; -import { getContractEvents as getContractEventsInsight } from "../../insight/get-events.js"; +import { + type ContractEvent, + getContractEvents as getContractEventsInsight, +} from "../../insight/get-events.js"; import { eth_blockNumber } from "../../rpc/actions/eth_blockNumber.js"; import { type GetLogsBlockParams, @@ -156,6 +159,28 @@ export async function getContractEvents< // if we have an abi on the contract, we can encode the topics with it if (!events?.length && !!contract) { + if (useIndexer) { + // fetch all events from the indexer, no need to get events from ABI + const events = await getContractEventsInsight({ + client: contract.client, + chains: [contract.chain], + contractAddress: contract.address, + decodeLogs: true, + queryOptions: { + limit: 500, + filter_block_hash: restParams.blockHash, + filter_block_number_gte: restParams.fromBlock, + filter_block_number_lte: restParams.toBlock, + }, + }).catch(() => { + // chain might not support indexer + return null; + }); + if (events) { + return toLog(events) as GetContractEventsResult; + } + } + // if we have a contract *WITH* an abi we can use that if (contract.abi?.length) { // @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: { }, }); + return toLog(r); +} + +function toLog(r: ContractEvent[]) { const cleanedEventData = r.map((tx) => ({ chainId: tx.chain_id, blockNumber: numberToHex(Number(tx.block_number)), @@ -257,7 +286,18 @@ async function getLogsFromInsight(options: { address: tx.address, data: tx.data as Hex, topics: tx.topics as [`0x${string}`, ...`0x${string}`[]] | [] | undefined, + ...(tx.decoded + ? { + eventName: tx.decoded.name, + args: { + ...tx.decoded.indexed_params, + ...tx.decoded.non_indexed_params, + }, + } + : {}), })); - return cleanedEventData.map((e) => formatLog(e)); + return cleanedEventData + .map((e) => formatLog(e)) + .sort((a, b) => Number((a.blockNumber ?? 0n) - (b.blockNumber ?? 0n))); } diff --git a/packages/thirdweb/src/event/actions/watch-events.ts b/packages/thirdweb/src/event/actions/watch-events.ts index b12010a1863..ca703e9e068 100644 --- a/packages/thirdweb/src/event/actions/watch-events.ts +++ b/packages/thirdweb/src/event/actions/watch-events.ts @@ -77,6 +77,7 @@ export function watchContractEvents< fromBlock: blockNumber, // toBlock is inclusive toBlock: blockNumber, + useIndexer: false, }), { retries: 3, diff --git a/packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts b/packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts index c5af41221a0..074f9a4f31a 100644 --- a/packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts +++ b/packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts @@ -58,7 +58,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFT", () => { includeOwner: true, }); expect(nft.metadata.name).toBe("Doodle #1"); - expect(nft.owner).toBe("0xbe9936fcfc50666f5425fde4a9decc59cef73b24"); + expect(nft.owner).toBe("0xbE9936FCFC50666f5425FDE4A9decC59cEF73b24"); expect(nft).toMatchInlineSnapshot(` { "chainId": 1, @@ -90,12 +90,9 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFT", () => { "image": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", "image_url": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", "name": "Doodle #1", - "owner_addresses": [ - "0xbe9936fcfc50666f5425fde4a9decc59cef73b24", - ], "uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", }, - "owner": "0xbe9936fcfc50666f5425fde4a9decc59cef73b24", + "owner": "0xbE9936FCFC50666f5425FDE4A9decC59cEF73b24", "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", "tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", "type": "ERC721", diff --git a/packages/thirdweb/src/extensions/erc721/read/getOwnedNFTs.test.ts b/packages/thirdweb/src/extensions/erc721/read/getOwnedNFTs.test.ts index bf44630c0fa..16c51054311 100644 --- a/packages/thirdweb/src/extensions/erc721/read/getOwnedNFTs.test.ts +++ b/packages/thirdweb/src/extensions/erc721/read/getOwnedNFTs.test.ts @@ -13,6 +13,9 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getOwnedNFTs", () => { owner, }); expect(nfts.length).greaterThan(0); + for (const item of nfts) { + expect(item.owner).toBe(owner); + } }); it("should detect ownership functions using indexer", async () => { diff --git a/packages/thirdweb/src/insight/get-nfts.ts b/packages/thirdweb/src/insight/get-nfts.ts index 6592991dc97..6bf5d017b90 100644 --- a/packages/thirdweb/src/insight/get-nfts.ts +++ b/packages/thirdweb/src/insight/get-nfts.ts @@ -11,6 +11,7 @@ import type { NFT } from "../utils/nft/parseNft.js"; import { getCachedChain } from "../chains/utils.js"; import { getContract } from "../contract/contract.js"; +import { getAddress } from "../utils/address.js"; type OwnedNFT = GetV1NftsResponse["data"][number]; type ContractNFT = GetV1NftsByContractAddressResponse["data"][number]; @@ -268,21 +269,25 @@ async function transformNFTModel( token_type, ...rest } = nft; + + let metadataToUse = rest; + let owners: string[] | undefined = ownerAddress + ? [getAddress(ownerAddress)] + : undefined; + + if ("owner_addresses" in rest) { + const { owner_addresses, ...restWithoutOwnerAddresses } = rest; + metadataToUse = restWithoutOwnerAddresses; + owners = owners ?? owner_addresses?.map((o) => getAddress(o)); + } + const metadata = replaceIPFSGatewayRecursively({ uri: nft.metadata_url ?? "", image: nft.image_url, attributes: nft.extra_metadata?.attributes ?? undefined, - ...rest, + ...metadataToUse, }); - // replace the ipfs gateway with the ipfs gateway from the client recusively for each key in the metadata object - - const owner_addresses = ownerAddress - ? [ownerAddress] - : "owner_addresses" in nft - ? nft.owner_addresses - : undefined; - if (contract?.type === "erc1155") { // TODO (insight): this needs to be added in the API const supply = await totalSupply({ @@ -298,7 +303,7 @@ async function transformNFTModel( tokenId: BigInt(token_id), tokenUri: replaceIPFSGateway(metadata_url) ?? "", type: "ERC1155", - owner: owner_addresses?.[0], + owner: owners?.[0], tokenAddress: contract?.address ?? "", chainId: contract?.chain_id ?? 0, supply: supply, @@ -307,7 +312,7 @@ async function transformNFTModel( parsedNft = parseNFT(metadata, { tokenId: BigInt(token_id), type: "ERC721", - owner: owner_addresses?.[0], + owner: owners?.[0], tokenUri: replaceIPFSGateway(metadata_url) ?? "", tokenAddress: contract?.address ?? "", chainId: contract?.chain_id ?? 0, diff --git a/packages/thirdweb/src/wallets/smart/lib/paymaster.ts b/packages/thirdweb/src/wallets/smart/lib/paymaster.ts index 5b365173089..8c91ecbd233 100644 --- a/packages/thirdweb/src/wallets/smart/lib/paymaster.ts +++ b/packages/thirdweb/src/wallets/smart/lib/paymaster.ts @@ -65,19 +65,15 @@ export async function getPaymasterAndData(args: { headers, body: stringify(body), }); - const res = await response.json(); if (!response.ok) { - const error = res.error || response.statusText; - const code = res.code || "UNKNOWN"; + const error = (await response.text()) || response.statusText; - throw new Error( - `Paymaster error: ${error} -Status: ${response.status} -Code: ${code}`, - ); + throw new Error(`Paymaster error: ${response.status} - ${error}`); } + const res = await response.json(); + if (res.result) { // some paymasters return a string, some return an object with more data if (typeof res.result === "string") {