From 94f7fed569a2f4b0125f1071ee4ae13ee623837d Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Sun, 28 Jan 2024 22:01:53 -0800 Subject: [PATCH] event listening for react & lots of fixes --- .../thirdweb/src/abi/resolveAbiFunction.ts | 14 --- .../thirdweb/src/event/actions/get-events.ts | 54 +++++++++++ .../thirdweb/src/event/actions/resolve-abi.ts | 71 +++++++++++++++ .../src/event/actions/watch-events.ts | 68 ++++++++++++++ packages/thirdweb/src/event/actions/watch.ts | 80 ---------------- packages/thirdweb/src/event/event.ts | 9 ++ packages/thirdweb/src/event/index.ts | 3 +- packages/thirdweb/src/gas/fee-data.ts | 4 +- .../contract}/useEstimate.ts | 6 +- .../contract}/useRead.ts | 12 +-- .../contract}/useSend.ts | 6 +- .../contract}/useWaitForReceipt.ts | 4 +- .../hooks/contract/useWatchContractEvents.ts | 42 +++++++++ .../react/hooks/rpc/useWatchBlockNumber.ts | 29 ++++++ packages/thirdweb/src/react/index.tsx | 13 ++- packages/thirdweb/src/rpc/index.ts | 2 +- packages/thirdweb/src/rpc/rpc.ts | 28 +++--- .../{blockNumber.ts => watchBlockNumber.ts} | 91 ++++++++++--------- .../src/transaction/actions/estimate-gas.ts | 4 +- .../thirdweb/src/transaction/actions/read.ts | 4 +- .../src/transaction/actions/resolve-abi.ts | 7 +- .../transaction/actions/send-transaction.ts | 4 +- .../actions/wait-for-tx-receipt.ts | 4 +- .../thirdweb/src/transaction/transaction.ts | 9 ++ packages/thirdweb/src/wallets/private-key.ts | 3 +- 25 files changed, 381 insertions(+), 190 deletions(-) delete mode 100644 packages/thirdweb/src/abi/resolveAbiFunction.ts create mode 100644 packages/thirdweb/src/event/actions/get-events.ts create mode 100644 packages/thirdweb/src/event/actions/resolve-abi.ts create mode 100644 packages/thirdweb/src/event/actions/watch-events.ts delete mode 100644 packages/thirdweb/src/event/actions/watch.ts rename packages/thirdweb/src/react/{contract-hooks => hooks/contract}/useEstimate.ts (67%) rename packages/thirdweb/src/react/{contract-hooks => hooks/contract}/useRead.ts (89%) rename packages/thirdweb/src/react/{contract-hooks => hooks/contract}/useSend.ts (69%) rename packages/thirdweb/src/react/{contract-hooks => hooks/contract}/useWaitForReceipt.ts (85%) create mode 100644 packages/thirdweb/src/react/hooks/contract/useWatchContractEvents.ts create mode 100644 packages/thirdweb/src/react/hooks/rpc/useWatchBlockNumber.ts rename packages/thirdweb/src/rpc/{blockNumber.ts => watchBlockNumber.ts} (61%) diff --git a/packages/thirdweb/src/abi/resolveAbiFunction.ts b/packages/thirdweb/src/abi/resolveAbiFunction.ts deleted file mode 100644 index 269d4b55e80..00000000000 --- a/packages/thirdweb/src/abi/resolveAbiFunction.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AbiFunction } from "abitype"; - -export type MethodType = AbiFunction | string; - -// helpers - -export function isAbiFunction(item: unknown): item is AbiFunction { - return !!( - item && - typeof item === "object" && - "name" in item && - "type" in item - ); -} diff --git a/packages/thirdweb/src/event/actions/get-events.ts b/packages/thirdweb/src/event/actions/get-events.ts new file mode 100644 index 00000000000..95dd0d7c757 --- /dev/null +++ b/packages/thirdweb/src/event/actions/get-events.ts @@ -0,0 +1,54 @@ +import type { Abi, AbiEvent } from "abitype"; +import type { BlockTag } from "viem"; +import { eth_getLogs, getRpcClient } from "../../rpc/index.js"; +import { resolveAbi } from "./resolve-abi.js"; +import type { ContractEvent } from "../event.js"; +import { + resolveContractAbi, + type ThirdwebContract, +} from "../../contract/index.js"; + +type GetContractEventsOptions< + abi extends Abi, + abiEvent extends AbiEvent, + contractEvent extends ContractEvent, + fBlock extends bigint | BlockTag, + tBlock extends bigint | BlockTag, +> = { + contract: ThirdwebContract; + events?: contractEvent[]; + fromBlock?: fBlock; + toBlock?: tBlock; +}; + +export async function getContractEvents< + const abi extends Abi, + const abiEvent extends AbiEvent, + const contractEvent extends ContractEvent, + const fBlock extends bigint | BlockTag, + const tBlock extends bigint | BlockTag, +>( + options: GetContractEventsOptions< + abi, + abiEvent, + contractEvent, + fBlock, + tBlock + >, +) { + const rpcRequest = getRpcClient(options.contract); + const parsedEvents = await (options.events + ? Promise.all(options.events.map((e) => resolveAbi(e))) + : // if we don't have events passed then resolve the abi for the contract -> all events! + (resolveContractAbi(options.contract).then((abi) => + abi.filter((item) => item.type === "event"), + ) as Promise)); + + // @ts-expect-error - fromBlock and toBlock ARE allowed to be undefined + return await eth_getLogs(rpcRequest, { + fromBlock: options.fromBlock, + toBlock: options.toBlock, + address: options.contract.address, + events: parsedEvents, + }); +} diff --git a/packages/thirdweb/src/event/actions/resolve-abi.ts b/packages/thirdweb/src/event/actions/resolve-abi.ts new file mode 100644 index 00000000000..0629f4352d0 --- /dev/null +++ b/packages/thirdweb/src/event/actions/resolve-abi.ts @@ -0,0 +1,71 @@ +import { parseAbiItem, type Abi, type AbiEvent } from "abitype"; +import { + isAbiEvent, + type ContractEvent, + type ContractEventInput, +} from "../event.js"; +import type { ParseEvent } from "../../abi/types.js"; + +const ABI_FN_RESOLUTION_CACHE = new WeakMap< + ContractEvent, + Promise +>(); + +export function resolveAbi( + contractEvent: ContractEventInput, +): Promise> { + if (ABI_FN_RESOLUTION_CACHE.has(contractEvent as ContractEvent)) { + return ABI_FN_RESOLUTION_CACHE.get( + contractEvent as ContractEvent, + ) as Promise>; + } + const prom = (async () => { + if (isAbiEvent(contractEvent.event)) { + return contractEvent.event as ParseEvent; + } + // if the method starts with the string `event ` we always will want to try to parse it + if (contractEvent.event.startsWith("event ")) { + const abiItem = parseAbiItem(contractEvent.event); + if (abiItem.type === "event") { + return abiItem as ParseEvent; + } + throw new Error(`"method" passed is not of type "function"`); + } + // check if we have a "abi" on the contract + if (contractEvent.contract.abi && contractEvent.contract.abi?.length > 0) { + // extract the abiEv from it + const abiEv = contractEvent.contract.abi?.find( + (item) => item.type === "event" && item.name === contractEvent.event, + ); + // if we were able to find it -> return it + if (isAbiEvent(abiEv)) { + return abiEv as ParseEvent; + } + } + + // if we get here we need to async resolve the ABI and try to find the method on there + const { resolveContractAbi } = await import( + "../../contract/actions/resolve-abi.js" + ); + + const abi = await resolveContractAbi(contractEvent.contract); + // we try to find the abiEv in the abi + const abiEv = abi.find((item) => { + // if the item is not an event we can ignore it + if (item.type !== "event") { + return false; + } + // if the item is a function we can compare the name + return item.name === contractEvent.event; + }) as ParseEvent | undefined; + + if (!abiEv) { + throw new Error( + `could not find event with name ${contractEvent.event} in abi`, + ); + } + return abiEv; + })(); + ABI_FN_RESOLUTION_CACHE.set(contractEvent as ContractEvent, prom); + return prom; +} diff --git a/packages/thirdweb/src/event/actions/watch-events.ts b/packages/thirdweb/src/event/actions/watch-events.ts new file mode 100644 index 00000000000..d87f38ead75 --- /dev/null +++ b/packages/thirdweb/src/event/actions/watch-events.ts @@ -0,0 +1,68 @@ +import type { Abi, AbiEvent } from "abitype"; +import type { GetLogsReturnType } from "viem"; +import { + eth_getLogs, + getRpcClient, + watchBlockNumber, +} from "../../rpc/index.js"; +import { resolveAbi } from "./resolve-abi.js"; +import type { ContractEvent } from "../event.js"; +import { + resolveContractAbi, + type ThirdwebContract, +} from "../../contract/index.js"; + +export type WatchContractEventsOptions< + abi extends Abi, + abiEvent extends AbiEvent, + contractEvent extends ContractEvent, +> = { + onLogs: ( + logs: GetLogsReturnType, + ) => void | undefined; + contract: ThirdwebContract; + events?: contractEvent[] | undefined; +}; + +export function watchContractEvents< + const abi extends Abi, + const abiEvent extends AbiEvent, + const contractEvent extends ContractEvent, +>(options: WatchContractEventsOptions) { + const rpcRequest = getRpcClient(options.contract); + const resolveAbiPromise = options.events + ? Promise.all(options.events.map((e) => resolveAbi(e))) + : // if we don't have events passed then resolve the abi for the contract -> all events! + (resolveContractAbi(options.contract).then((abi) => + abi.filter((item) => item.type === "event"), + ) as Promise); + + // returning this returns the underlying "unwatch" function + return watchBlockNumber({ + ...options.contract, + onNewBlockNumber: async (blockNumber) => { + const parsedEvents = await resolveAbiPromise; + + const logs = (await eth_getLogs(rpcRequest, { + // onNewBlockNumber fires exactly once per block + // => we want to get the logs for the block that just happened + // fromBlock is inclusive + fromBlock: blockNumber, + // toBlock is exclusive + toBlock: blockNumber, + address: options.contract.address, + events: parsedEvents, + })) as GetLogsReturnType< + undefined, + abiEvent[], + undefined, + bigint, + bigint + >; + // if there were any logs associated with our event(s) + if (logs.length) { + options.onLogs(logs); + } + }, + }); +} diff --git a/packages/thirdweb/src/event/actions/watch.ts b/packages/thirdweb/src/event/actions/watch.ts deleted file mode 100644 index 59f09f4ee88..00000000000 --- a/packages/thirdweb/src/event/actions/watch.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - type Abi, - type AbiEvent, - type ExtractAbiEventNames, - parseAbiItem, -} from "abitype"; -import { type ContractEventInput } from "../event.js"; -import { type GetLogsReturnType } from "viem"; -import { eth_blockNumber, eth_getLogs, getRpcClient } from "../../rpc/index.js"; -import type { ParseEvent } from "../../abi/types.js"; - -type WatchOptions< - abi extends Abi, - // if an abi has been passed into the contract, restrict the event to event names of the abi - event extends abi extends { length: 0 } - ? AbiEvent | string - : ExtractAbiEventNames, -> = { - onLogs: ( - logs: GetLogsReturnType< - ParseEvent, - [ParseEvent], - undefined, - bigint, - bigint - >, - ) => void | undefined; -} & ContractEventInput; - -export function watch< - const abi extends Abi, - // if an abi has been passed into the contract, restrict the event to event names of the abi - const event extends abi extends { length: 0 } - ? AbiEvent | string - : ExtractAbiEventNames, ->(options: WatchOptions) { - const rpcRequest = getRpcClient(options.contract.client, { - chainId: options.contract.chainId, - }); - - let lastBlock = 0n; - const parsedEvent: ParseEvent = - typeof options.event === "string" - ? (parseAbiItem(options.event as string) as ParseEvent) - : (options.event as ParseEvent); - if (parsedEvent.type !== "event") { - throw new Error("Expected event"); - } - eth_blockNumber(rpcRequest).then((x) => { - lastBlock = x; - }); - - const interval = setInterval(async function () { - const newBlock = await eth_blockNumber(rpcRequest); - - if (lastBlock === 0n) { - lastBlock = newBlock; - } else if (newBlock > lastBlock) { - const logs = await eth_getLogs(rpcRequest, { - fromBlock: lastBlock, - toBlock: newBlock, - address: options.contract.address, - event: parsedEvent, - // @ts-expect-error - missing | undefined in type - args: options.params, - }); - if (logs.length) { - // @ts-expect-error - this works fine - options.onLogs(logs); - } - - lastBlock = newBlock; - } - }, 5000); - - // return the unsubscribe function - return function () { - clearInterval(interval); - }; -} diff --git a/packages/thirdweb/src/event/event.ts b/packages/thirdweb/src/event/event.ts index 4c1ff02dfaf..3b5566eb318 100644 --- a/packages/thirdweb/src/event/event.ts +++ b/packages/thirdweb/src/event/event.ts @@ -35,3 +35,12 @@ export function contractEvent< >(options: ContractEventInput) { return options as unknown as ContractEvent>; } + +export function isAbiEvent(item: unknown): item is AbiEvent { + return !!( + item && + typeof item === "object" && + "type" in item && + item.type === "event" + ); +} diff --git a/packages/thirdweb/src/event/index.ts b/packages/thirdweb/src/event/index.ts index 0981b137d87..bb33dfaf422 100644 --- a/packages/thirdweb/src/event/index.ts +++ b/packages/thirdweb/src/event/index.ts @@ -4,4 +4,5 @@ export { type ContractEventInput, } from "./event.js"; -export { watch } from "./actions/watch.js"; +export { watchContractEvents } from "./actions/watch-events.js"; +export { getContractEvents } from "./actions/get-events.js"; diff --git a/packages/thirdweb/src/gas/fee-data.ts b/packages/thirdweb/src/gas/fee-data.ts index f1563b63da8..22b71fc88ca 100644 --- a/packages/thirdweb/src/gas/fee-data.ts +++ b/packages/thirdweb/src/gas/fee-data.ts @@ -45,7 +45,7 @@ export async function getDynamicFeeData( let maxFeePerGas: null | bigint = null; let maxPriorityFeePerGas_: null | bigint = null; - const rpcRequest = getRpcClient(client, { chainId }); + const rpcRequest = getRpcClient({ client, chainId }); const [block, maxPriorityFeePerGas] = await Promise.all([ eth_getBlockByNumber(rpcRequest, { blockTag: "latest" }), @@ -103,7 +103,7 @@ export async function getGasPrice( client: ThirdwebClient, chainId: number, ): Promise { - const rpcClient = getRpcClient(client, { chainId }); + const rpcClient = getRpcClient({ client, chainId }); const gasPrice_ = await eth_gasPrice(rpcClient); const maxGasPrice = 300n; // 300 gwei const extraTip = (gasPrice_ / BigInt(100)) * BigInt(10); diff --git a/packages/thirdweb/src/react/contract-hooks/useEstimate.ts b/packages/thirdweb/src/react/hooks/contract/useEstimate.ts similarity index 67% rename from packages/thirdweb/src/react/contract-hooks/useEstimate.ts rename to packages/thirdweb/src/react/hooks/contract/useEstimate.ts index a2f56180a46..40e6d1776ff 100644 --- a/packages/thirdweb/src/react/contract-hooks/useEstimate.ts +++ b/packages/thirdweb/src/react/hooks/contract/useEstimate.ts @@ -1,8 +1,8 @@ import type { AbiFunction } from "abitype"; import { useMutation, type UseMutationResult } from "@tanstack/react-query"; -import { estimateGas } from "../../transaction/index.js"; -import type { Transaction } from "../../transaction/transaction.js"; -import { useActiveWallet } from "../providers/wallet-provider.js"; +import { estimateGas } from "../../../transaction/index.js"; +import type { Transaction } from "../../../transaction/transaction.js"; +import { useActiveWallet } from "../../providers/wallet-provider.js"; export function useEstimateGas< const abiFn extends AbiFunction, diff --git a/packages/thirdweb/src/react/contract-hooks/useRead.ts b/packages/thirdweb/src/react/hooks/contract/useRead.ts similarity index 89% rename from packages/thirdweb/src/react/contract-hooks/useRead.ts rename to packages/thirdweb/src/react/hooks/contract/useRead.ts index 6a6b8a77a8a..be94ba81b21 100644 --- a/packages/thirdweb/src/react/contract-hooks/useRead.ts +++ b/packages/thirdweb/src/react/hooks/contract/useRead.ts @@ -4,19 +4,19 @@ import { type UseQueryOptions, } from "@tanstack/react-query"; import type { Abi, AbiFunction, ExtractAbiFunctionNames } from "abitype"; -import type { ParseMethod } from "../../abi/types.js"; -import type { ReadOutputs } from "../../transaction/actions/read.js"; -import { read } from "../../transaction/index.js"; +import type { ParseMethod } from "../../../abi/types.js"; +import type { ReadOutputs } from "../../../transaction/actions/read.js"; +import { read } from "../../../transaction/index.js"; import { type TransactionInput, type TxOpts, -} from "../../transaction/transaction.js"; +} from "../../../transaction/transaction.js"; import { getExtensionId, isReadExtension, type ReadExtension, -} from "../../utils/extension.js"; -import { stringify } from "../../utils/json.js"; +} from "../../../utils/extension.js"; +import { stringify } from "../../../utils/json.js"; type PickedQueryOptions = Pick; diff --git a/packages/thirdweb/src/react/contract-hooks/useSend.ts b/packages/thirdweb/src/react/hooks/contract/useSend.ts similarity index 69% rename from packages/thirdweb/src/react/contract-hooks/useSend.ts rename to packages/thirdweb/src/react/hooks/contract/useSend.ts index 0f934b10a12..8f1e9e5118b 100644 --- a/packages/thirdweb/src/react/contract-hooks/useSend.ts +++ b/packages/thirdweb/src/react/hooks/contract/useSend.ts @@ -1,9 +1,9 @@ import type { AbiFunction } from "abitype"; import { useMutation, type UseMutationResult } from "@tanstack/react-query"; -import type { Transaction } from "../../transaction/transaction.js"; -import { useActiveWallet } from "../providers/wallet-provider.js"; +import type { Transaction } from "../../../transaction/transaction.js"; +import { useActiveWallet } from "../../providers/wallet-provider.js"; import type { Hex } from "viem"; -import { sendTransaction } from "../../transaction/actions/send-transaction.js"; +import { sendTransaction } from "../../../transaction/actions/send-transaction.js"; export function useSendTransaction< const abiFn extends AbiFunction, diff --git a/packages/thirdweb/src/react/contract-hooks/useWaitForReceipt.ts b/packages/thirdweb/src/react/hooks/contract/useWaitForReceipt.ts similarity index 85% rename from packages/thirdweb/src/react/contract-hooks/useWaitForReceipt.ts rename to packages/thirdweb/src/react/hooks/contract/useWaitForReceipt.ts index b809a48f367..4ffc6ba0e09 100644 --- a/packages/thirdweb/src/react/contract-hooks/useWaitForReceipt.ts +++ b/packages/thirdweb/src/react/hooks/contract/useWaitForReceipt.ts @@ -1,7 +1,7 @@ import type { Abi } from "abitype"; -import type { ThirdwebContract } from "../../contract/index.js"; +import type { ThirdwebContract } from "../../../contract/index.js"; import { useQuery, type UseQueryResult } from "@tanstack/react-query"; -import { waitForReceipt } from "../../transaction/index.js"; +import { waitForReceipt } from "../../../transaction/index.js"; import type { TransactionReceipt } from "viem"; export function useWaitForReceipt({ diff --git a/packages/thirdweb/src/react/hooks/contract/useWatchContractEvents.ts b/packages/thirdweb/src/react/hooks/contract/useWatchContractEvents.ts new file mode 100644 index 00000000000..60c10760ef8 --- /dev/null +++ b/packages/thirdweb/src/react/hooks/contract/useWatchContractEvents.ts @@ -0,0 +1,42 @@ +import { useState, useEffect } from "react"; +import type { Abi, AbiEvent } from "abitype"; +import { + watchContractEvents, + type ContractEvent, +} from "../../../event/index.js"; +import type { WatchContractEventsOptions } from "../../../event/actions/watch-events.js"; +import type { GetLogsReturnType } from "viem"; + +export function useWatchContractEvents< + const abi extends Abi, + const abiEvent extends AbiEvent, + const contractEvent extends ContractEvent, +>({ + contract, + events, + limit = 1000, + enabled = true, +}: Omit, "onLogs"> & { + limit?: number; + enabled?: boolean; +}) { + const [logs, setLogs] = useState< + GetLogsReturnType + >([]); + + useEffect(() => { + if (!enabled) { + // don't watch if not enabled + return; + } + return watchContractEvents({ + contract, + onLogs: (logs_) => { + setLogs((oldLogs) => [...oldLogs, ...logs_].slice(-limit)); + }, + events, + }); + }, [contract, enabled, events, limit]); + + return logs; +} diff --git a/packages/thirdweb/src/react/hooks/rpc/useWatchBlockNumber.ts b/packages/thirdweb/src/react/hooks/rpc/useWatchBlockNumber.ts new file mode 100644 index 00000000000..6234d81a6b0 --- /dev/null +++ b/packages/thirdweb/src/react/hooks/rpc/useWatchBlockNumber.ts @@ -0,0 +1,29 @@ +import { useState, useEffect } from "react"; +import type { ThirdwebClient } from "../../../index.js"; +import { watchBlockNumber } from "../../../rpc/watchBlockNumber.js"; + +export function useWatchBlockNumber({ + client, + chainId, + enabled = true, +}: { + client: ThirdwebClient; + chainId: number; + enabled?: boolean; +}) { + const [blockNumber, setBlockNumber] = useState(undefined); + + useEffect(() => { + if (!enabled) { + // don't watch if not enabled + return; + } + return watchBlockNumber({ + client, + chainId, + onNewBlockNumber: setBlockNumber, + }); + }, [client, chainId, enabled]); + + return blockNumber; +} diff --git a/packages/thirdweb/src/react/index.tsx b/packages/thirdweb/src/react/index.tsx index 77042ad9956..755f01ae769 100644 --- a/packages/thirdweb/src/react/index.tsx +++ b/packages/thirdweb/src/react/index.tsx @@ -9,7 +9,12 @@ export { WallerProvider, } from "./providers/wallet-provider.js"; -export { useRead } from "./contract-hooks/useRead.js"; -export { useSendTransaction } from "./contract-hooks/useSend.js"; -export { useEstimateGas } from "./contract-hooks/useEstimate.js"; -export { useWaitForReceipt } from "./contract-hooks/useWaitForReceipt.js"; +// contract related +export { useRead } from "./hooks/contract/useRead.js"; +export { useSendTransaction } from "./hooks/contract/useSend.js"; +export { useEstimateGas } from "./hooks/contract/useEstimate.js"; +export { useWaitForReceipt } from "./hooks/contract/useWaitForReceipt.js"; +export { useWatchContractEvents } from "./hooks/contract/useWatchContractEvents.js"; + +// rpc related +export { useWatchBlockNumber } from "./hooks/rpc/useWatchBlockNumber.js"; diff --git a/packages/thirdweb/src/rpc/index.ts b/packages/thirdweb/src/rpc/index.ts index c7ef895558f..34955964a79 100644 --- a/packages/thirdweb/src/rpc/index.ts +++ b/packages/thirdweb/src/rpc/index.ts @@ -1,7 +1,7 @@ export { getRpcClient } from "./rpc.js"; // blockNumber watcher -export { watchBlockNumber } from "./blockNumber.js"; +export { watchBlockNumber } from "./watchBlockNumber.js"; // all the actions export { eth_gasPrice } from "./actions/eth_gasPrice.js"; diff --git a/packages/thirdweb/src/rpc/rpc.ts b/packages/thirdweb/src/rpc/rpc.ts index 57644d9dbfa..731e2a101e1 100644 --- a/packages/thirdweb/src/rpc/rpc.ts +++ b/packages/thirdweb/src/rpc/rpc.ts @@ -1,6 +1,6 @@ import type { ThirdwebClient } from "../client/client.js"; import { stringify } from "../utils/json.js"; -import type { EIP1193RequestFn, EIP1474Methods, RpcSchema } from "viem"; +import type { EIP1193RequestFn, EIP1474Methods } from "viem"; type SuccessResult = { method?: never; @@ -49,6 +49,7 @@ export type RpcResponse = { const RPC_CLIENT_MAP = new WeakMap(); type RPCOptions = { + readonly client: ThirdwebClient; readonly chainId: number; }; @@ -69,22 +70,17 @@ const DEFAULT_MAX_BATCH_SIZE = 100; // default to no timeout (next tick) const DEFAULT_BATCH_TIMEOUT_MS = 0; -export function getRpcClient< - rpcSchema extends RpcSchema | undefined = undefined, ->( - client: ThirdwebClient, +export function getRpcClient( options: RPCOptions, -): EIP1193RequestFn { - const rpcClientMap = getRpcClientMap(client); +): EIP1193RequestFn { + const rpcClientMap = getRpcClientMap(options.client); if (rpcClientMap.has(options.chainId)) { - return rpcClientMap.get(options.chainId) as EIP1193RequestFn< - rpcSchema extends undefined ? EIP1474Methods : rpcSchema - >; + return rpcClientMap.get( + options.chainId, + ) as EIP1193RequestFn; } - const rpcClient: EIP1193RequestFn< - rpcSchema extends undefined ? EIP1474Methods : rpcSchema - > = (function () { + const rpcClient: EIP1193RequestFn = (function () { // inflight requests const inflightRequests = new Map>(); let pendingBatch: Array<{ @@ -118,7 +114,7 @@ export function getRpcClient< // reset pendingBatch to empty pendingBatch = []; - fetchRpc(client, { + fetchRpc(options.client, { requests: activeBatch.map((inflight) => inflight.request), chainId: options.chainId, }) @@ -186,9 +182,7 @@ export function getRpcClient< })(); rpcClientMap.set(options.chainId, rpcClient); - return rpcClient as EIP1193RequestFn< - rpcSchema extends undefined ? EIP1474Methods : rpcSchema - >; + return rpcClient as EIP1193RequestFn; } type FetchRpcOptions = { diff --git a/packages/thirdweb/src/rpc/blockNumber.ts b/packages/thirdweb/src/rpc/watchBlockNumber.ts similarity index 61% rename from packages/thirdweb/src/rpc/blockNumber.ts rename to packages/thirdweb/src/rpc/watchBlockNumber.ts index 04346737d33..9d41ca51fe5 100644 --- a/packages/thirdweb/src/rpc/blockNumber.ts +++ b/packages/thirdweb/src/rpc/watchBlockNumber.ts @@ -7,10 +7,8 @@ const DEFAULT_POLL_DELAY = 1000; // 1 second const MIN_POLL_DELAY = 100; // 100 milliseconds const SLIDING_WINDOW_SIZE = 10; // always keep track of the last 10 block times -const SLIDING_WINDOWS = new Map(); // chainId -> [blockTime, blockTime, ...] -function getAverageBlockTime(chainId: number): number { - const blockTimes = SLIDING_WINDOWS.get(chainId) ?? []; +function getAverageBlockTime(blockTimes: number[]): number { // left-pad the blocktimes Array with the DEFAULT_POLL_DELAY while (blockTimes.length < SLIDING_WINDOW_SIZE) { blockTimes.unshift(DEFAULT_POLL_DELAY); @@ -30,24 +28,23 @@ function getAverageBlockTime(chainId: number): number { return sum / blockTimes.length; } -const SUBSCRIPTIONS = new Map void>>(); - -export function watchBlockNumber(opts: { - client: ThirdwebClient; - chainId: number; - onNewBlockNumber: (blockNumber: bigint) => void; - overPollRatio?: number; -}) { - // ignore that there could be multiple pollers for the same chainId etc +function createBlockNumberPoller( + client: ThirdwebClient, + chainId: number, + overPollRatio?: number, +) { + let subscribers: Array<(blockNumber: bigint) => void> = []; + let blockTimesWindow: number[] = []; + let isActive = false; let lastBlockNumber: bigint | undefined; let lastBlockAt: number | undefined; - const rpcRequest = getRpcClient(opts.client, { chainId: opts.chainId }); + const rpcRequest = getRpcClient({ client, chainId }); async function poll() { // stop polling if there are no more subscriptions - if (!SUBSCRIPTIONS.get(opts.chainId)?.length) { + if (!isActive) { return; } const blockNumber = await eth_blockNumber(rpcRequest); @@ -66,49 +63,59 @@ export function watchBlockNumber(opts: { if (lastBlockAt) { // if we skipped a block we need to adjust the block time down to that level const blockTime = (currentTime - lastBlockAt) / newBlockNumbers.length; - const blockTimes = SLIDING_WINDOWS.get(opts.chainId) ?? []; - blockTimes.push(blockTime); - SLIDING_WINDOWS.set( - opts.chainId, - blockTimes.slice(-SLIDING_WINDOW_SIZE), - ); + + blockTimesWindow.push(blockTime); + blockTimesWindow = blockTimesWindow.slice(-SLIDING_WINDOW_SIZE); } lastBlockAt = currentTime; newBlockNumbers.forEach((b) => { - opts.onNewBlockNumber(b); + subscribers.forEach((fn) => fn(b)); }); } - const currentApproximateBlockTime = getAverageBlockTime(opts.chainId); + const currentApproximateBlockTime = getAverageBlockTime(blockTimesWindow); // sleep for the average block time for this chain (divided by the overPollRatio, which defaults to 2) - await sleep(currentApproximateBlockTime / (opts.overPollRatio ?? 2)); + await sleep(currentApproximateBlockTime / (overPollRatio ?? 2)); // poll again poll(); } - // setup the subscription - const currentSubscriptions = SUBSCRIPTIONS.get(opts.chainId) ?? []; - - SUBSCRIPTIONS.set(opts.chainId, [ - ...currentSubscriptions, - opts.onNewBlockNumber, - ]); - // if there were no subscriptions, start polling (we just added one) - if (currentSubscriptions.length === 0) { - poll(); - } + return function (callBack: (blockNumber: bigint) => void) { + subscribers.push(callBack); + if (!isActive) { + isActive = true; + poll(); + } - return function () { - // remove the subscription - SUBSCRIPTIONS.set( - opts.chainId, - (SUBSCRIPTIONS.get(opts.chainId) ?? []).filter( - (fn) => fn !== opts.onNewBlockNumber, - ), - ); + return function unSubscribe() { + subscribers = subscribers.filter((fn) => fn !== callBack); + if (subscribers.length === 0) { + isActive = false; + } + }; }; } function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } + +const existingPollers = new Map< + number, + ReturnType +>(); + +export function watchBlockNumber(opts: { + client: ThirdwebClient; + chainId: number; + onNewBlockNumber: (blockNumber: bigint) => void; + overPollRatio?: number; +}) { + const { client, chainId, onNewBlockNumber, overPollRatio } = opts; + let poller = existingPollers.get(chainId); + if (!poller) { + poller = createBlockNumberPoller(client, chainId, overPollRatio); + existingPollers.set(chainId, poller); + } + return poller(onNewBlockNumber); +} diff --git a/packages/thirdweb/src/transaction/actions/estimate-gas.ts b/packages/thirdweb/src/transaction/actions/estimate-gas.ts index 802553b405b..7af3063e87c 100644 --- a/packages/thirdweb/src/transaction/actions/estimate-gas.ts +++ b/packages/thirdweb/src/transaction/actions/estimate-gas.ts @@ -10,9 +10,7 @@ export async function estimateGas( tx: Transaction, options?: { from?: string }, ): Promise { - const rpcRequest = getRpcClient(tx.contract.client, { - chainId: tx.contract.chainId, - }); + const rpcRequest = getRpcClient(tx.contract); const [gasOverrides, encodedData] = await Promise.all([ getDefaultGasOverrides(tx.contract.client, tx.contract.chainId), diff --git a/packages/thirdweb/src/transaction/actions/read.ts b/packages/thirdweb/src/transaction/actions/read.ts index 3ccf8059776..1731e3fd6d2 100644 --- a/packages/thirdweb/src/transaction/actions/read.ts +++ b/packages/thirdweb/src/transaction/actions/read.ts @@ -44,9 +44,7 @@ export async function readTx( throw new Error("Unable to resolve ABI"); } - const rpcRequest = getRpcClient(tx.contract.client, { - chainId: tx.contract.chainId, - }); + const rpcRequest = getRpcClient(tx.contract); const result = await eth_call(rpcRequest, { data: encodedData, to: tx.contract.address, diff --git a/packages/thirdweb/src/transaction/actions/resolve-abi.ts b/packages/thirdweb/src/transaction/actions/resolve-abi.ts index d20bd4199c1..565ed2ad83f 100644 --- a/packages/thirdweb/src/transaction/actions/resolve-abi.ts +++ b/packages/thirdweb/src/transaction/actions/resolve-abi.ts @@ -1,6 +1,9 @@ import { parseAbiItem, type AbiFunction, type Abi } from "abitype"; -import type { Transaction, TransactionInput } from "../transaction.js"; -import { isAbiFunction } from "../../abi/resolveAbiFunction.js"; +import { + isAbiFunction, + type Transaction, + type TransactionInput, +} from "../transaction.js"; import type { ParseMethod } from "../../abi/types.js"; const ABI_FN_RESOLUTION_CACHE = new WeakMap< diff --git a/packages/thirdweb/src/transaction/actions/send-transaction.ts b/packages/thirdweb/src/transaction/actions/send-transaction.ts index ef915f4580d..76672306146 100644 --- a/packages/thirdweb/src/transaction/actions/send-transaction.ts +++ b/packages/thirdweb/src/transaction/actions/send-transaction.ts @@ -11,9 +11,7 @@ export async function sendTransaction< throw new Error("not connected"); } const { getRpcClient } = await import("../../rpc/index.js"); - const rpcRequest = getRpcClient(tx.contract.client, { - chainId: tx.contract.chainId, - }); + const rpcRequest = getRpcClient(tx.contract); const [getDefaultGasOverrides, encode, eth_getTransactionCount, estimateGas] = await Promise.all([ diff --git a/packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts b/packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts index f4f0b6f41b8..945f6cd2e41 100644 --- a/packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts +++ b/packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts @@ -31,9 +31,7 @@ export function waitForReceipt({ ); } - const request = getRpcClient(contract.client, { - chainId: contract.chainId, - }); + const request = getRpcClient(contract); // start at -1 because the first block doesn't count let blocksWaited = -1; diff --git a/packages/thirdweb/src/transaction/transaction.ts b/packages/thirdweb/src/transaction/transaction.ts index 05d1aba0490..c0f57534480 100644 --- a/packages/thirdweb/src/transaction/transaction.ts +++ b/packages/thirdweb/src/transaction/transaction.ts @@ -54,3 +54,12 @@ export function isTxOpts(value: unknown): value is TxOpts { typeof value.contract.address === "string" ); } + +export function isAbiFunction(item: unknown): item is AbiFunction { + return !!( + item && + typeof item === "object" && + "type" in item && + item.type === "function" + ); +} diff --git a/packages/thirdweb/src/wallets/private-key.ts b/packages/thirdweb/src/wallets/private-key.ts index 05429b14c4c..70ff0449aa9 100644 --- a/packages/thirdweb/src/wallets/private-key.ts +++ b/packages/thirdweb/src/wallets/private-key.ts @@ -73,7 +73,8 @@ class PrivateKeyWallet implements IWallet { throw new Error("not connected"); } - const rpcRequest = getRpcClient(this.client, { + const rpcRequest = getRpcClient({ + client: this.client, chainId: tx.chainId, });