Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/great-chairs-find.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

More efficient multi event querying when using indexer
62 changes: 24 additions & 38 deletions apps/dashboard/src/@3rdweb-sdk/react/hooks/useActivity.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<AbiEvent>[] = 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,
});
Expand All @@ -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<string, InternalTransaction>,
);
return acc;
},
{} as Record<string, InternalTransaction>,
);
return Object.values(obj).sort((a, b) =>
a.blockNumber > b.blockNumber ? -1 : 1,
);
Expand Down
44 changes: 42 additions & 2 deletions packages/thirdweb/src/event/actions/get-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<abiEvents, TStrict>;
}
}

// 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
Expand Down Expand Up @@ -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)),
Expand All @@ -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)));
}
1 change: 1 addition & 0 deletions packages/thirdweb/src/event/actions/watch-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export function watchContractEvents<
fromBlock: blockNumber,
// toBlock is inclusive
toBlock: blockNumber,
useIndexer: false,
}),
{
retries: 3,
Expand Down
7 changes: 2 additions & 5 deletions packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
27 changes: 16 additions & 11 deletions packages/thirdweb/src/insight/get-nfts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -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({
Expand All @@ -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,
Expand All @@ -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,
Expand Down
12 changes: 4 additions & 8 deletions packages/thirdweb/src/wallets/smart/lib/paymaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Loading