From 196163ffce343f1865188470fe4a429d2a26547f Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Sun, 28 Jan 2024 04:53:13 -0800 Subject: [PATCH] lots of updates --- packages/thirdweb/package.json | 8 + packages/thirdweb/src/adapters/ethers5.ts | 4 +- packages/thirdweb/src/adapters/ethers6.ts | 4 +- packages/thirdweb/src/contract/index.ts | 6 +- packages/thirdweb/src/event/actions/watch.ts | 133 ++-------- packages/thirdweb/src/event/event.ts | 4 +- .../src/extensions/erc721/read/getNFT.ts | 2 +- .../src/extensions/erc721/write/mintTo.ts | 2 +- packages/thirdweb/src/gas/fee-data.ts | 25 +- .../src/rpc/actions/eth_blockNumber.ts | 10 + packages/thirdweb/src/rpc/actions/eth_call.ts | 28 +++ .../src/rpc/actions/eth_estimateGas.ts | 17 ++ .../thirdweb/src/rpc/actions/eth_gasPrice.ts | 10 + .../src/rpc/actions/eth_getBlockByHash.ts | 39 +++ .../src/rpc/actions/eth_getBlockByNumber.ts | 60 +++++ .../thirdweb/src/rpc/actions/eth_getLogs.ts | 139 +++++++++++ .../rpc/actions/eth_getTransactionCount.ts | 18 ++ .../rpc/actions/eth_getTransactionReceipt.ts | 23 ++ .../rpc/actions/eth_maxPriorityFeePerGas.ts | 10 + .../src/rpc/actions/eth_sendRawTransaction.ts | 11 + packages/thirdweb/src/rpc/blockNumber.ts | 114 +++++++++ packages/thirdweb/src/rpc/index.ts | 204 ++-------------- packages/thirdweb/src/rpc/methods.ts | 67 ----- packages/thirdweb/src/rpc/rpc.ts | 228 ++++++++++++++++++ .../src/transaction/actions/estimate-gas.ts | 17 +- .../thirdweb/src/transaction/actions/read.ts | 16 +- .../transaction/actions/send-transaction.ts | 16 +- .../actions/wait-for-tx-receipt.ts | 65 +++-- packages/thirdweb/src/wallets/private-key.ts | 13 +- 29 files changed, 844 insertions(+), 449 deletions(-) create mode 100644 packages/thirdweb/src/rpc/actions/eth_blockNumber.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_call.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_estimateGas.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_gasPrice.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_getBlockByHash.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_getBlockByNumber.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_getLogs.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_getTransactionCount.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_getTransactionReceipt.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_maxPriorityFeePerGas.ts create mode 100644 packages/thirdweb/src/rpc/actions/eth_sendRawTransaction.ts create mode 100644 packages/thirdweb/src/rpc/blockNumber.ts delete mode 100644 packages/thirdweb/src/rpc/methods.ts create mode 100644 packages/thirdweb/src/rpc/rpc.ts diff --git a/packages/thirdweb/package.json b/packages/thirdweb/package.json index 5df8f15383e..90fc344d5c0 100644 --- a/packages/thirdweb/package.json +++ b/packages/thirdweb/package.json @@ -34,6 +34,11 @@ "import": "./esm/event/index.js", "default": "./cjs/event/index.js" }, + "./rpc": { + "types": "./types/rpc/index.d.ts", + "import": "./esm/rpc/index.js", + "default": "./cjs/rpc/index.js" + }, "./react": { "types": "./types/react/index.d.ts", "import": "./esm/react/index.js", @@ -72,6 +77,9 @@ "event": [ "./types/event/index.d.ts" ], + "rpc": [ + "./types/rpc/index.d.ts" + ], "storage": [ "./types/storage/index.d.ts" ], diff --git a/packages/thirdweb/src/adapters/ethers5.ts b/packages/thirdweb/src/adapters/ethers5.ts index 43d034b9d78..351894a9532 100644 --- a/packages/thirdweb/src/adapters/ethers5.ts +++ b/packages/thirdweb/src/adapters/ethers5.ts @@ -68,7 +68,7 @@ async function toEthersContract( return new ethers.Contract( twContract.address, JSON.stringify(twContract.abi), - toEthersProvider(ethers, twContract, twContract.chainId), + toEthersProvider(ethers, twContract.client, twContract.chainId), ); } @@ -81,7 +81,7 @@ async function toEthersContract( return new ethers.Contract( twContract.address, JSON.stringify(abi), - toEthersProvider(ethers, twContract, twContract.chainId), + toEthersProvider(ethers, twContract.client, twContract.chainId), ); } diff --git a/packages/thirdweb/src/adapters/ethers6.ts b/packages/thirdweb/src/adapters/ethers6.ts index bd66b91ad5b..3b86724c4e8 100644 --- a/packages/thirdweb/src/adapters/ethers6.ts +++ b/packages/thirdweb/src/adapters/ethers6.ts @@ -69,7 +69,7 @@ async function toEthersContract( return new ethers.Contract( twContract.address, JSON.stringify(twContract.abi), - toEthersProvider(ethers, twContract, twContract.chainId), + toEthersProvider(ethers, twContract.client, twContract.chainId), ); } @@ -82,7 +82,7 @@ async function toEthersContract( return new ethers.Contract( twContract.address, JSON.stringify(abi), - toEthersProvider(ethers, twContract, twContract.chainId), + toEthersProvider(ethers, twContract.client, twContract.chainId), ); } diff --git a/packages/thirdweb/src/contract/index.ts b/packages/thirdweb/src/contract/index.ts index ce873ec6dc9..80698ecbca4 100644 --- a/packages/thirdweb/src/contract/index.ts +++ b/packages/thirdweb/src/contract/index.ts @@ -8,7 +8,8 @@ export type ContractOptions = { readonly abi?: abi; }; -export type ThirdwebContract = ThirdwebClient & { +export type ThirdwebContract = { + readonly client: ThirdwebClient; readonly address: string; readonly chainId: number; readonly abi?: abi; @@ -23,8 +24,7 @@ export type ThirdwebContract = ThirdwebClient & { export function contract( options: ContractOptions, ): ThirdwebContract { - const { client, ...rest } = options; - return { ...client, ...rest } as const; + return options; } export { resolveContractAbi } from "./actions/resolve-abi.js"; diff --git a/packages/thirdweb/src/event/actions/watch.ts b/packages/thirdweb/src/event/actions/watch.ts index a0f372935f5..59f09f4ee88 100644 --- a/packages/thirdweb/src/event/actions/watch.ts +++ b/packages/thirdweb/src/event/actions/watch.ts @@ -1,26 +1,13 @@ import { - formatAbiItem, type Abi, type AbiEvent, type ExtractAbiEventNames, - type AbiParameter, - type AbiParameterToPrimitiveType, parseAbiItem, } from "abitype"; import { type ContractEventInput } from "../event.js"; -import { - hexToBigInt, - toHex, - type Log, - toEventSelector, - type Hex, - toBytes, - keccak256, - FilterTypeNotSupportedError, - encodeAbiParameters, -} from "viem"; +import { type GetLogsReturnType } from "viem"; +import { eth_blockNumber, eth_getLogs, getRpcClient } from "../../rpc/index.js"; import type { ParseEvent } from "../../abi/types.js"; -import { getRpcClient } from "../../rpc/index.js"; type WatchOptions< abi extends Abi, @@ -30,15 +17,12 @@ type WatchOptions< : ExtractAbiEventNames, > = { onLogs: ( - logs: Array< - Log< - bigint, - number, - boolean, - ParseEvent, - undefined, - abi extends { length: 0 } ? [ParseEvent] : abi - > + logs: GetLogsReturnType< + ParseEvent, + [ParseEvent], + undefined, + bigint, + bigint >, ) => void | undefined; } & ContractEventInput; @@ -50,53 +34,38 @@ export function watch< ? AbiEvent | string : ExtractAbiEventNames, >(options: WatchOptions) { - const rpcRequest = getRpcClient(options.contract, { + const rpcRequest = getRpcClient(options.contract.client, { chainId: options.contract.chainId, }); let lastBlock = 0n; - const parsedEvent = + const parsedEvent: ParseEvent = typeof options.event === "string" - ? parseAbiItem(options.event as any) - : options.event; + ? (parseAbiItem(options.event as string) as ParseEvent) + : (options.event as ParseEvent); if (parsedEvent.type !== "event") { throw new Error("Expected event"); } - rpcRequest({ - method: "eth_blockNumber", - params: [], - }).then((x) => { - lastBlock = hexToBigInt(x); + eth_blockNumber(rpcRequest).then((x) => { + lastBlock = x; }); const interval = setInterval(async function () { - const blockHex = await rpcRequest({ - method: "eth_blockNumber", - params: [], - }); - const newBlock = hexToBigInt(blockHex); + const newBlock = await eth_blockNumber(rpcRequest); if (lastBlock === 0n) { lastBlock = newBlock; } else if (newBlock > lastBlock) { - const logs = await rpcRequest({ - method: "eth_getLogs", - params: [ - { - address: options.contract.address, - topics: [ - encodeEventTopic({ - event: parsedEvent, - params: (options.params || []) as unknown[], - }), - ], - fromBlock: toHex(lastBlock + 1n), - toBlock: toHex(newBlock), - }, - ], + 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) { - // TODO parsing etc + // @ts-expect-error - this works fine options.onLogs(logs); } @@ -109,59 +78,3 @@ export function watch< clearInterval(interval); }; } - -// TODO clean all of this up - -function encodeEventTopic({ - event, - params, -}: { - event: AbiEvent; - params: unknown[]; -}) { - const definition = formatAbiItem(event); - const signature = toEventSelector(definition); - - let topics: Hex[] = []; - if (params && "inputs" in event) { - const indexedInputs = event.inputs?.filter( - (param) => "indexed" in param && param.indexed, - ); - const args_ = Array.isArray(params) - ? params - : // TODO: bring this back - // : Object.values(args).length > 0 - // ? indexedInputs?.map((x: any) => (args as any)[x.name]) ?? [] - []; - - if (args_.length > 0) { - topics = - indexedInputs?.map((param, i) => - Array.isArray(args_[i]) - ? (args_[i] as any).map((_: any, j: number) => - encodeArg({ param, value: (args_[i] as any)[j] }), - ) - : args_[i] - ? encodeArg({ param, value: args_[i] }) - : null, - ) ?? []; - } - } - return [signature, ...topics]; -} - -function encodeArg({ - param, - value, -}: { - param: AbiParameter; - value: AbiParameterToPrimitiveType; -}) { - if (param.type === "string" || param.type === "bytes") { - return keccak256(toBytes(value as string)); - } - if (param.type === "tuple" || param.type.match(/^(.*)\[(\d+)?\]$/)) { - throw new FilterTypeNotSupportedError(param.type); - } - return encodeAbiParameters([param], [value]); -} diff --git a/packages/thirdweb/src/event/event.ts b/packages/thirdweb/src/event/event.ts index 73af94a0682..4c1ff02dfaf 100644 --- a/packages/thirdweb/src/event/event.ts +++ b/packages/thirdweb/src/event/event.ts @@ -7,7 +7,7 @@ import type { import type { ThirdwebContract } from "../contract/index.js"; import type { ParseEvent } from "../abi/types.js"; -type Params = AbiParametersToPrimitiveTypes< +export type EventParams = AbiParametersToPrimitiveTypes< event["inputs"] >; @@ -17,7 +17,7 @@ export type ContractEventInput< > = { contract: ThirdwebContract; event: event; - params?: Params>; + params?: EventParams>; }; // the only difference here is that we don't alow string events diff --git a/packages/thirdweb/src/extensions/erc721/read/getNFT.ts b/packages/thirdweb/src/extensions/erc721/read/getNFT.ts index 5033eda81ba..5e83db33b1b 100644 --- a/packages/thirdweb/src/extensions/erc721/read/getNFT.ts +++ b/packages/thirdweb/src/extensions/erc721/read/getNFT.ts @@ -30,7 +30,7 @@ export const getNFT = /*@__PURE__*/ createReadExtension("erc721.getNFT")( ]); return parseNFT( await fetchTokenMetadata({ - client: options.contract, + client: options.contract.client, tokenId: options.tokenId, tokenUri: uri, }), diff --git a/packages/thirdweb/src/extensions/erc721/write/mintTo.ts b/packages/thirdweb/src/extensions/erc721/write/mintTo.ts index 29bfe5894bd..b25bcf7f1af 100644 --- a/packages/thirdweb/src/extensions/erc721/write/mintTo.ts +++ b/packages/thirdweb/src/extensions/erc721/write/mintTo.ts @@ -42,7 +42,7 @@ export function mintTo(options: TxOpts) { // load the upload code if we need it const { upload } = await import("../../../storage/upload.js"); tokenUri = ( - await upload(options.contract, { + await upload(options.contract.client, { files: [options.nft], }) )[0] as string; diff --git a/packages/thirdweb/src/gas/fee-data.ts b/packages/thirdweb/src/gas/fee-data.ts index d67ca42ac65..f1563b63da8 100644 --- a/packages/thirdweb/src/gas/fee-data.ts +++ b/packages/thirdweb/src/gas/fee-data.ts @@ -1,10 +1,11 @@ import type { ThirdwebClient } from "../client/client.js"; import { parseUnits } from "viem"; import { - blockByNumber, - gasPrice, - maxPriorityFeePerGas, -} from "../rpc/methods.js"; + eth_gasPrice, + eth_getBlockByNumber, + eth_maxPriorityFeePerGas, + getRpcClient, +} from "../rpc/index.js"; type FeeData = { maxFeePerGas: null | bigint; @@ -44,12 +45,11 @@ export async function getDynamicFeeData( let maxFeePerGas: null | bigint = null; let maxPriorityFeePerGas_: null | bigint = null; - const { getRpcClient } = await import("../rpc/index.js"); - const rpcClient = getRpcClient(client, { chainId }); + const rpcRequest = getRpcClient(client, { chainId }); - const [block, eth_maxPriorityFeePerGas] = await Promise.all([ - blockByNumber(rpcClient, "latest", false), - maxPriorityFeePerGas(rpcClient).catch(() => null), + const [block, maxPriorityFeePerGas] = await Promise.all([ + eth_getBlockByNumber(rpcRequest, { blockTag: "latest" }), + eth_maxPriorityFeePerGas(rpcRequest).catch(() => null), ]); const baseBlockFee = @@ -59,9 +59,9 @@ export async function getDynamicFeeData( if (chainId === 80001 || chainId === 137) { // for polygon, get fee data from gas station maxPriorityFeePerGas_ = await getPolygonGasPriorityFee(chainId); - } else if (eth_maxPriorityFeePerGas) { + } else if (maxPriorityFeePerGas) { // prioritize fee from eth_maxPriorityFeePerGas - maxPriorityFeePerGas_ = eth_maxPriorityFeePerGas; + maxPriorityFeePerGas_ = maxPriorityFeePerGas; } // TODO bring back(?) // else { @@ -103,9 +103,8 @@ export async function getGasPrice( client: ThirdwebClient, chainId: number, ): Promise { - const { getRpcClient } = await import("../rpc/index.js"); const rpcClient = getRpcClient(client, { chainId }); - const gasPrice_ = await gasPrice(rpcClient); + const gasPrice_ = await eth_gasPrice(rpcClient); const maxGasPrice = 300n; // 300 gwei const extraTip = (gasPrice_ / BigInt(100)) * BigInt(10); const txGasPrice = gasPrice_ + extraTip; diff --git a/packages/thirdweb/src/rpc/actions/eth_blockNumber.ts b/packages/thirdweb/src/rpc/actions/eth_blockNumber.ts new file mode 100644 index 00000000000..cb280f74e2a --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_blockNumber.ts @@ -0,0 +1,10 @@ +import { type EIP1193RequestFn, type EIP1474Methods, hexToBigInt } from "viem"; + +export async function eth_blockNumber( + request: EIP1193RequestFn, +): Promise { + const blockNumberHex = await request({ + method: "eth_blockNumber", + }); + return hexToBigInt(blockNumberHex); +} diff --git a/packages/thirdweb/src/rpc/actions/eth_call.ts b/packages/thirdweb/src/rpc/actions/eth_call.ts new file mode 100644 index 00000000000..be84fa2a49f --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_call.ts @@ -0,0 +1,28 @@ +import { + type BlockTag, + numberToHex, + type EIP1193RequestFn, + type EIP1474Methods, + type RpcTransactionRequest, + type Hex, +} from "viem"; + +export async function eth_call( + request: EIP1193RequestFn, + params: Partial & { + blockNumber?: bigint | number; + blockTag?: BlockTag; + }, +): Promise { + const { blockNumber, blockTag, ...txRequest } = params; + const blockNumberHex = blockNumber ? numberToHex(blockNumber) : undefined; + // TODO: per RPC spec omitting the block is allowed, however for some reason our RPCs don't like it, so we default to "latest" here + const block = blockNumberHex || blockTag || "latest"; + + return await request({ + method: "eth_call", + params: block + ? [txRequest as Partial, block] + : [txRequest as Partial], + }); +} diff --git a/packages/thirdweb/src/rpc/actions/eth_estimateGas.ts b/packages/thirdweb/src/rpc/actions/eth_estimateGas.ts new file mode 100644 index 00000000000..ec52a6a6a5e --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_estimateGas.ts @@ -0,0 +1,17 @@ +import { + hexToBigInt, + type EIP1193RequestFn, + type EIP1474Methods, + type RpcTransactionRequest, +} from "viem"; + +export async function eth_estimateGas( + request: EIP1193RequestFn, + transactionRequest: RpcTransactionRequest, +): Promise { + const estimateResult = await request({ + method: "eth_estimateGas", + params: [transactionRequest], + }); + return hexToBigInt(estimateResult); +} diff --git a/packages/thirdweb/src/rpc/actions/eth_gasPrice.ts b/packages/thirdweb/src/rpc/actions/eth_gasPrice.ts new file mode 100644 index 00000000000..d4370797845 --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_gasPrice.ts @@ -0,0 +1,10 @@ +import { hexToBigInt, type EIP1193RequestFn, type EIP1474Methods } from "viem"; + +export async function eth_gasPrice( + request: EIP1193RequestFn, +): Promise { + const result = await request({ + method: "eth_gasPrice", + }); + return hexToBigInt(result); +} diff --git a/packages/thirdweb/src/rpc/actions/eth_getBlockByHash.ts b/packages/thirdweb/src/rpc/actions/eth_getBlockByHash.ts new file mode 100644 index 00000000000..4474af0b89f --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_getBlockByHash.ts @@ -0,0 +1,39 @@ +import { + formatBlock, + type EIP1193RequestFn, + type EIP1474Methods, + type GetBlockReturnType, + type Hash, +} from "viem"; + +type GetBlockByHashParams = { + /** Whether or not to include transaction data in the response. */ + includeTransactions?: TIncludeTransactions; +} & { + /** Hash of the block. */ + blockHash: Hash; +}; + +export async function eth_getBlockByHash< + TIncludeTransactions extends boolean = false, +>( + request: EIP1193RequestFn, + { + blockHash, + includeTransactions: includeTransactions_, + }: GetBlockByHashParams, +): Promise> { + const includeTransactions = includeTransactions_ ?? false; + + const block = await request({ + method: "eth_getBlockByHash", + params: [blockHash, includeTransactions], + }); + if (!block) { + throw new Error("Block not found"); + } + return formatBlock(block) as GetBlockReturnType< + undefined, + TIncludeTransactions + >; +} diff --git a/packages/thirdweb/src/rpc/actions/eth_getBlockByNumber.ts b/packages/thirdweb/src/rpc/actions/eth_getBlockByNumber.ts new file mode 100644 index 00000000000..321c0dc054f --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_getBlockByNumber.ts @@ -0,0 +1,60 @@ +import { + formatBlock, + type BlockTag, + type EIP1193RequestFn, + type EIP1474Methods, + numberToHex, + type GetBlockReturnType, +} from "viem"; + +type GetBlockParameters< + TIncludeTransactions extends boolean = false, + TBlockTag extends BlockTag = "latest", +> = { + /** Whether or not to include transaction data in the response. */ + includeTransactions?: TIncludeTransactions; +} & ( + | { + /** The block number. */ + blockNumber?: bigint; + blockTag?: never; + } + | { + blockNumber?: never; + /** + * The block tag. + * default: 'latest' + */ + blockTag?: TBlockTag | BlockTag; + } +); + +export async function eth_getBlockByNumber< + TIncludeTransactions extends boolean = false, + TBlockTag extends BlockTag = "latest", +>( + request: EIP1193RequestFn, + { + blockNumber, + blockTag: blockTag_, + includeTransactions: includeTransactions_, + }: GetBlockParameters, +): Promise> { + const blockTag = blockTag_ ?? "latest"; + const includeTransactions = includeTransactions_ ?? false; + const blockNumberHex = + blockNumber !== undefined ? numberToHex(blockNumber) : undefined; + + const block = await request({ + method: "eth_getBlockByNumber", + params: [blockNumberHex || blockTag, includeTransactions], + }); + if (!block) { + throw new Error("Block not found"); + } + return formatBlock(block) as GetBlockReturnType< + undefined, + TIncludeTransactions, + TBlockTag + >; +} diff --git a/packages/thirdweb/src/rpc/actions/eth_getLogs.ts b/packages/thirdweb/src/rpc/actions/eth_getLogs.ts new file mode 100644 index 00000000000..01ce4f170e7 --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_getLogs.ts @@ -0,0 +1,139 @@ +import type { AbiEvent } from "abitype"; +import { + encodeEventTopics, + type BlockNumber, + type BlockTag, + type EIP1193RequestFn, + type EIP1474Methods, + type GetLogsParameters, + type GetLogsReturnType, + type LogTopic, + type EncodeEventTopicsParameters, + type RpcLog, + formatLog, + parseEventLogs, + numberToHex, +} from "viem"; + +export async function eth_getLogs< + const TAbiEvent extends AbiEvent | undefined = undefined, + const TAbiEvents extends + | readonly AbiEvent[] + | readonly unknown[] + | undefined = TAbiEvent extends AbiEvent ? [TAbiEvent] : undefined, + TStrict extends boolean | undefined = undefined, + TFromBlock extends BlockNumber | BlockTag | undefined = undefined, + TToBlock extends BlockNumber | BlockTag | undefined = undefined, +>( + request: EIP1193RequestFn, + { + address, + blockHash, + fromBlock, + toBlock, + event, + events: events_, + args, + strict: strict_, + }: GetLogsParameters< + TAbiEvent, + TAbiEvents, + TStrict, + TFromBlock, + TToBlock + > = {}, +): Promise< + GetLogsReturnType +> { + const strict = strict_ ?? false; + const events = events_ ?? (event ? [event] : undefined); + + let topics: LogTopic[] = []; + if (events) { + topics = [ + (events as AbiEvent[]).flatMap((event_) => + encodeEventTopics({ + abi: [event_], + eventName: (event_ as AbiEvent).name, + args, + } as EncodeEventTopicsParameters), + ), + ]; + if (event) { + topics = topics[0] as LogTopic[]; + } + } + + let logs: RpcLog[]; + if (blockHash) { + const param: { + address?: string | string[]; + topics: LogTopic[]; + blockHash: `0x${string}`; + } = { + topics, + blockHash, + }; + if (address) { + param.address = address; + } + logs = await request({ + method: "eth_getLogs", + params: [param], + }); + } else { + const param: { + address?: string | string[]; + topics?: LogTopic[]; + } & ( + | { + fromBlock?: BlockTag | `0x${string}`; + toBlock?: BlockTag | `0x${string}`; + blockHash?: never; + } + | { + fromBlock?: never; + toBlock?: never; + blockHash?: `0x${string}`; + } + ) = { topics }; + if (address) { + param.address = address; + } + + if (fromBlock) { + param.fromBlock = + typeof fromBlock === "bigint" ? numberToHex(fromBlock) : fromBlock; + } + if (toBlock) { + param.toBlock = + typeof toBlock === "bigint" ? numberToHex(toBlock) : toBlock; + } + logs = await request({ + method: "eth_getLogs", + params: [param], + }); + } + + const formattedLogs = logs.map((log) => formatLog(log)); + if (!events) { + return formattedLogs as GetLogsReturnType< + TAbiEvent, + TAbiEvents, + TStrict, + TFromBlock, + TToBlock + >; + } + return parseEventLogs({ + abi: events, + logs: formattedLogs, + strict, + }) as unknown as GetLogsReturnType< + TAbiEvent, + TAbiEvents, + TStrict, + TFromBlock, + TToBlock + >; +} diff --git a/packages/thirdweb/src/rpc/actions/eth_getTransactionCount.ts b/packages/thirdweb/src/rpc/actions/eth_getTransactionCount.ts new file mode 100644 index 00000000000..6d64456e500 --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_getTransactionCount.ts @@ -0,0 +1,18 @@ +import { + type EIP1193RequestFn, + type EIP1474Methods, + numberToHex, + hexToNumber, + type GetTransactionCountParameters, +} from "viem"; + +export async function eth_getTransactionCount( + request: EIP1193RequestFn, + { address, blockTag = "latest", blockNumber }: GetTransactionCountParameters, +): Promise { + const count = await request({ + method: "eth_getTransactionCount", + params: [address, blockNumber ? numberToHex(blockNumber) : blockTag], + }); + return hexToNumber(count); +} diff --git a/packages/thirdweb/src/rpc/actions/eth_getTransactionReceipt.ts b/packages/thirdweb/src/rpc/actions/eth_getTransactionReceipt.ts new file mode 100644 index 00000000000..d9d582b7f05 --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_getTransactionReceipt.ts @@ -0,0 +1,23 @@ +import { + formatTransactionReceipt, + type EIP1193RequestFn, + type GetTransactionReceiptParameters, + type EIP1474Methods, + type TransactionReceipt, +} from "viem"; + +export async function eth_getTransactionReceipt( + request: EIP1193RequestFn, + { hash }: GetTransactionReceiptParameters, +): Promise { + const receipt = await request({ + method: "eth_getTransactionReceipt", + params: [hash], + }); + + if (!receipt) { + throw new Error("Transaction receipt not found."); + } + + return formatTransactionReceipt(receipt); +} diff --git a/packages/thirdweb/src/rpc/actions/eth_maxPriorityFeePerGas.ts b/packages/thirdweb/src/rpc/actions/eth_maxPriorityFeePerGas.ts new file mode 100644 index 00000000000..2c9efb83db4 --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_maxPriorityFeePerGas.ts @@ -0,0 +1,10 @@ +import { hexToBigInt, type EIP1193RequestFn, type EIP1474Methods } from "viem"; + +export async function eth_maxPriorityFeePerGas( + request: EIP1193RequestFn, +): Promise { + const result = await request({ + method: "eth_maxPriorityFeePerGas", + }); + return hexToBigInt(result); +} diff --git a/packages/thirdweb/src/rpc/actions/eth_sendRawTransaction.ts b/packages/thirdweb/src/rpc/actions/eth_sendRawTransaction.ts new file mode 100644 index 00000000000..42158ab57e5 --- /dev/null +++ b/packages/thirdweb/src/rpc/actions/eth_sendRawTransaction.ts @@ -0,0 +1,11 @@ +import { type EIP1193RequestFn, type EIP1474Methods, type Hash } from "viem"; + +export async function eth_sendRawTransaction( + request: EIP1193RequestFn, + signedTransaction: Hash, +) { + return await request({ + method: "eth_sendRawTransaction", + params: [signedTransaction], + }); +} diff --git a/packages/thirdweb/src/rpc/blockNumber.ts b/packages/thirdweb/src/rpc/blockNumber.ts new file mode 100644 index 00000000000..04346737d33 --- /dev/null +++ b/packages/thirdweb/src/rpc/blockNumber.ts @@ -0,0 +1,114 @@ +import { getRpcClient } from "./rpc.js"; +import { eth_blockNumber } from "./actions/eth_blockNumber.js"; +import type { ThirdwebClient } from "../client/client.js"; + +const MAX_POLL_DELAY = 5000; // 5 seconds +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) ?? []; + // left-pad the blocktimes Array with the DEFAULT_POLL_DELAY + while (blockTimes.length < SLIDING_WINDOW_SIZE) { + blockTimes.unshift(DEFAULT_POLL_DELAY); + } + + const sum = blockTimes.reduce((acc, blockTime) => { + // never let the blockTime be less than our minimum + if (blockTime <= MIN_POLL_DELAY) { + return acc + MIN_POLL_DELAY; + } + // never let the blockTime be more than our maximum + if (blockTime >= MAX_POLL_DELAY) { + return acc + MAX_POLL_DELAY; + } + return acc + blockTime; + }, 0); + 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 + + let lastBlockNumber: bigint | undefined; + let lastBlockAt: number | undefined; + + const rpcRequest = getRpcClient(opts.client, { chainId: opts.chainId }); + + async function poll() { + // stop polling if there are no more subscriptions + if (!SUBSCRIPTIONS.get(opts.chainId)?.length) { + return; + } + const blockNumber = await eth_blockNumber(rpcRequest); + + if (!lastBlockNumber || blockNumber > lastBlockNumber) { + let newBlockNumbers = []; + if (lastBlockNumber) { + for (let i = lastBlockNumber + 1n; i <= blockNumber; i++) { + newBlockNumbers.push(BigInt(i)); + } + } else { + newBlockNumbers = [blockNumber]; + } + lastBlockNumber = blockNumber; + const currentTime = new Date().getTime(); + 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), + ); + } + lastBlockAt = currentTime; + newBlockNumbers.forEach((b) => { + opts.onNewBlockNumber(b); + }); + } + const currentApproximateBlockTime = getAverageBlockTime(opts.chainId); + + // sleep for the average block time for this chain (divided by the overPollRatio, which defaults to 2) + await sleep(currentApproximateBlockTime / (opts.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 () { + // remove the subscription + SUBSCRIPTIONS.set( + opts.chainId, + (SUBSCRIPTIONS.get(opts.chainId) ?? []).filter( + (fn) => fn !== opts.onNewBlockNumber, + ), + ); + }; +} + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/packages/thirdweb/src/rpc/index.ts b/packages/thirdweb/src/rpc/index.ts index fe09ba51571..c7ef895558f 100644 --- a/packages/thirdweb/src/rpc/index.ts +++ b/packages/thirdweb/src/rpc/index.ts @@ -1,187 +1,17 @@ -import type { ThirdwebClient } from "../client/client.js"; -import { stringify } from "../utils/json.js"; - -const RPC_CLIENT_CACHE = /* @__PURE__ */ new Map(); - -function rpcClientKey( - client: ThirdwebClient, - { chainId, ...rest }: RpcClientOptions, -): string { - return `${chainId}:${client.clientId}:${!!client.secretKey}:${JSON.stringify( - rest, - )}`; -} - -function rpcRequestKey(request: RPCRequest): string { - return `${request.method}:${JSON.stringify(request.params)}`; -} - -export type RpcClientOptions = { - chainId: number; -}; - -export type RPCRequest = { method: string; params: unknown[] }; - -type SuccessResult = { - result: T; - error?: never; -}; -type ErrorResult = { - result?: never; - error: T; -}; - -export type RPCResponse = - | SuccessResult - | ErrorResult; - -const DEFAULT_MAX_BATCH_SIZE = 100; -// default to no timeout (next tick) -const DEFAULT_BATCH_TIMEOUT_MS = 0; - -export type RPCClient = (request: RPCRequest) => Promise; - -export function getRpcClient( - client: ThirdwebClient, - options: RpcClientOptions, -): RPCClient { - const cacheKey = rpcClientKey(client, options); - if (RPC_CLIENT_CACHE.has(cacheKey)) { - return RPC_CLIENT_CACHE.get(cacheKey) as RPCClient; - } - const rpcClient: RPCClient = (() => { - // inflight requests - const inflightRequests = new Map>(); - let pendingBatch: Array<{ - request: RPCRequest & { id: number; jsonrpc: "2.0" }; - resolve: (value: RPCResponse | PromiseLike) => void; - reject: (reason?: any) => void; - requestKey: string; - }> = []; - let pendingBatchTimeout: ReturnType | null = null; - - function sendPendingBatch() { - // clear the timeout if any - if (pendingBatchTimeout) { - clearTimeout(pendingBatchTimeout); - pendingBatchTimeout = null; - } - - // assign ids to each request - const activeBatch = pendingBatch.slice().map((inflight, index) => { - // assign the id to the request - inflight.request.id = index; - // also assign the jsonrpc version - inflight.request.jsonrpc = "2.0"; - return inflight; - }); - // reset pendingBatch to empty - pendingBatch = []; - - fetchRpc(client, { - requests: activeBatch.map((inflight) => inflight.request), - chainId: options.chainId, - }) - .then((responses) => { - // for each response, resolve the inflight request - activeBatch.forEach((inflight, index) => { - const response = responses[index]; - // if we didn't get a response, reject the inflight request - if (!response) { - inflight.reject(new Error("no response")); - // if we got a response with an error, reject the inflight request - } else if (response.error) { - inflight.reject(response.error); - // otherwise, resolve the inflight request - } else { - // TODO: type this properly based on the method - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - inflight.resolve(response.result!); - } - // remove the inflight request from the inflightRequests map - inflightRequests.delete(inflight.requestKey); - }); - }) - .catch((err) => { - // http call failed, reject all inflight requests - activeBatch.forEach((inflight) => { - inflight.reject(err); - // remove the inflight request from the inflightRequests map - inflightRequests.delete(inflight.requestKey); - }); - }); - } - - return async (request) => { - const requestKey = rpcRequestKey(request); - // if the request for this key is already inflight, return the promise directly - if (inflightRequests.has(requestKey)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return inflightRequests.get(requestKey)!; - } - let resolve: (value: RPCResponse | PromiseLike) => void; - let reject: (reason?: any) => void; - const promise = new Promise((resolve_, reject_) => { - resolve = resolve_; - reject = reject_; - }); - inflightRequests.set(requestKey, promise); - // @ts-expect-error - they *are* definitely assgined within the promise constructor - pendingBatch.push({ request, resolve, reject, requestKey }); - // if there is no timeout, set one - if (!pendingBatchTimeout) { - pendingBatchTimeout = setTimeout( - sendPendingBatch, - DEFAULT_BATCH_TIMEOUT_MS, - ); - } - // if the batch is full, send it - if (pendingBatch.length >= DEFAULT_MAX_BATCH_SIZE) { - sendPendingBatch(); - } - return promise; - }; - })(); - RPC_CLIENT_CACHE.set(cacheKey, rpcClient); - return rpcClient; -} - -type FullRPCRequest = RPCRequest & { id: number; jsonrpc: "2.0" }; - -type FullRPCResponse = RPCResponse & { id: number; jsonrpc: "2.0" }; - -type FetchRpcOptions = { - requests: FullRPCRequest[]; - chainId: number; -}; - -async function fetchRpc( - client: ThirdwebClient, - { requests, chainId }: FetchRpcOptions, -): Promise { - const headers = new Headers({ - "Content-Type": "application/json", - }); - if (client.secretKey) { - headers.set("x-secret-key", client.secretKey); - } - const response = await fetch(`https://${chainId}.rpc.thirdweb.com`, { - headers, - body: stringify(requests), - method: "POST", - }); - - if (!response.ok) { - throw new Error(`RPC request failed with status ${response.status}`); - } - - let result; - - if (response.headers.get("Content-Type")?.startsWith("application/json")) { - result = await response.json(); - } else { - result = await response.text(); - } - - return result; -} +export { getRpcClient } from "./rpc.js"; + +// blockNumber watcher +export { watchBlockNumber } from "./blockNumber.js"; + +// all the actions +export { eth_gasPrice } from "./actions/eth_gasPrice.js"; +export { eth_getBlockByNumber } from "./actions/eth_getBlockByNumber.js"; +export { eth_getBlockByHash } from "./actions/eth_getBlockByHash.js"; +export { eth_getTransactionCount } from "./actions/eth_getTransactionCount.js"; +export { eth_getTransactionReceipt } from "./actions/eth_getTransactionReceipt.js"; +export { eth_maxPriorityFeePerGas } from "./actions/eth_maxPriorityFeePerGas.js"; +export { eth_blockNumber } from "./actions/eth_blockNumber.js"; +export { eth_estimateGas } from "./actions/eth_estimateGas.js"; +export { eth_call } from "./actions/eth_call.js"; +export { eth_getLogs } from "./actions/eth_getLogs.js"; +export { eth_sendRawTransaction } from "./actions/eth_sendRawTransaction.js"; diff --git a/packages/thirdweb/src/rpc/methods.ts b/packages/thirdweb/src/rpc/methods.ts deleted file mode 100644 index f76fa5fcc9d..00000000000 --- a/packages/thirdweb/src/rpc/methods.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { Address } from "abitype"; -import type { RPCClient } from "./index.js"; -import { - type BlockNumber, - type BlockTag, - type RpcBlock, - type RpcTransactionReceipt, - hexToBigInt, - hexToNumber, - formatBlock, - formatTransactionReceipt, -} from "viem"; - -export async function blockByNumber( - client: RPCClient, - blockNumber: BlockNumber | BlockTag, - includeTransactions = false, -) { - const result = await client({ - method: "eth_getBlockByNumber", - params: [blockNumber, includeTransactions], - }); - - return formatBlock(result as RpcBlock); -} - -export async function maxPriorityFeePerGas(client: RPCClient) { - const result = await client({ - method: "eth_maxPriorityFeePerGas", - params: [], - }); - return hexToBigInt(result); -} - -export async function gasPrice(client: RPCClient) { - const result = await client({ - method: "eth_gasPrice", - params: [], - }); - return hexToBigInt(result); -} - -export async function transactionCount( - client: RPCClient, - address: Address, - blockNumber: BlockNumber | BlockTag = "latest", -) { - const result = await client({ - method: "eth_getTransactionCount", - params: [address, blockNumber], - }); - return hexToNumber(result); -} - -export async function getTransactionReceipt(client: RPCClient, hash: string) { - const result = await client({ - method: "eth_getTransactionReceipt", - params: [hash], - }); - - // null means the tx is not mined yet - if (result === null) { - return null; - } - - return formatTransactionReceipt(result as RpcTransactionReceipt); -} diff --git a/packages/thirdweb/src/rpc/rpc.ts b/packages/thirdweb/src/rpc/rpc.ts new file mode 100644 index 00000000000..57644d9dbfa --- /dev/null +++ b/packages/thirdweb/src/rpc/rpc.ts @@ -0,0 +1,228 @@ +import type { ThirdwebClient } from "../client/client.js"; +import { stringify } from "../utils/json.js"; +import type { EIP1193RequestFn, EIP1474Methods, RpcSchema } from "viem"; + +type SuccessResult = { + method?: never; + result: T; + error?: never; +}; +type ErrorResult = { + method?: never; + result?: never; + error: T; +}; +type Subscription = { + method: "eth_subscription"; + error?: never; + result?: never; + params: { + subscription: string; + } & ( + | { + result: TResult; + error?: never; + } + | { + result?: never; + error: TError; + } + ); +}; + +export type RpcRequest = { + jsonrpc?: "2.0"; + method: string; + params?: any; + id?: number; +}; + +export type RpcResponse = { + jsonrpc: `${number}`; + id: number; +} & ( + | SuccessResult + | ErrorResult + | Subscription +); + +const RPC_CLIENT_MAP = new WeakMap(); + +type RPCOptions = { + readonly chainId: number; +}; + +function getRpcClientMap(client: ThirdwebClient) { + if (RPC_CLIENT_MAP.has(client)) { + return RPC_CLIENT_MAP.get(client); + } + const rpcClientMap = new Map(); + RPC_CLIENT_MAP.set(client, rpcClientMap); + return rpcClientMap; +} + +function rpcRequestKey(request: RpcRequest): string { + return `${request.method}:${JSON.stringify(request.params)}`; +} + +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, + options: RPCOptions, +): EIP1193RequestFn { + const rpcClientMap = getRpcClientMap(client); + if (rpcClientMap.has(options.chainId)) { + return rpcClientMap.get(options.chainId) as EIP1193RequestFn< + rpcSchema extends undefined ? EIP1474Methods : rpcSchema + >; + } + + const rpcClient: EIP1193RequestFn< + rpcSchema extends undefined ? EIP1474Methods : rpcSchema + > = (function () { + // inflight requests + const inflightRequests = new Map>(); + let pendingBatch: Array<{ + request: { + method: string; + params: unknown[]; + id: number; + jsonrpc: "2.0"; + }; + resolve: (value: any) => void; + reject: (reason?: any) => void; + requestKey: string; + }> = []; + let pendingBatchTimeout: ReturnType | null = null; + + function sendPendingBatch() { + // clear the timeout if any + if (pendingBatchTimeout) { + clearTimeout(pendingBatchTimeout); + pendingBatchTimeout = null; + } + + // assign ids to each request + const activeBatch = pendingBatch.slice().map((inflight, index) => { + // assign the id to the request + inflight.request.id = index; + // also assign the jsonrpc version + inflight.request.jsonrpc = "2.0"; + return inflight; + }); + // reset pendingBatch to empty + pendingBatch = []; + + fetchRpc(client, { + requests: activeBatch.map((inflight) => inflight.request), + chainId: options.chainId, + }) + .then((responses) => { + // for each response, resolve the inflight request + activeBatch.forEach((inflight, index) => { + const response = responses[index]; + // if we didn't get a response at all, reject the inflight request + if (!response) { + inflight.reject(new Error("No response")); + return; + } + if ("error" in response) { + inflight.reject(response.error); + // otherwise, resolve the inflight request + } else if (response.method === "eth_subscription") { + // TODO: handle subscription responses + throw new Error("Subscriptions not supported yet"); + } else { + inflight.resolve(response.result); + } + // remove the inflight request from the inflightRequests map + inflightRequests.delete(inflight.requestKey); + }); + }) + .catch((err) => { + // http call failed, reject all inflight requests + activeBatch.forEach((inflight) => { + inflight.reject(err); + // remove the inflight request from the inflightRequests map + inflightRequests.delete(inflight.requestKey); + }); + }); + } + + return async function (request) { + const requestKey = rpcRequestKey(request); + // if the request for this key is already inflight, return the promise directly + if (inflightRequests.has(requestKey)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return inflightRequests.get(requestKey)!; + } + let resolve: (value: any) => void; + let reject: (reason?: any) => void; + const promise = new Promise((resolve_, reject_) => { + resolve = resolve_; + reject = reject_; + }); + inflightRequests.set(requestKey, promise); + // @ts-expect-error - they *are* definitely assgined within the promise constructor + pendingBatch.push({ request, resolve, reject, requestKey }); + // if there is no timeout, set one + if (!pendingBatchTimeout) { + pendingBatchTimeout = setTimeout( + sendPendingBatch, + DEFAULT_BATCH_TIMEOUT_MS, + ); + } + // if the batch is full, send it + if (pendingBatch.length >= DEFAULT_MAX_BATCH_SIZE) { + sendPendingBatch(); + } + return promise; + }; + })(); + + rpcClientMap.set(options.chainId, rpcClient); + return rpcClient as EIP1193RequestFn< + rpcSchema extends undefined ? EIP1474Methods : rpcSchema + >; +} + +type FetchRpcOptions = { + requests: RpcRequest[]; + chainId: number; +}; + +async function fetchRpc( + client: ThirdwebClient, + { requests, chainId }: FetchRpcOptions, +): Promise { + const headers = new Headers({ + "Content-Type": "application/json", + }); + if (client.secretKey) { + headers.set("x-secret-key", client.secretKey); + } + const response = await fetch(`https://${chainId}.rpc.thirdweb.com`, { + headers, + body: stringify(requests), + method: "POST", + }); + + if (!response.ok) { + throw new Error(`RPC request failed with status ${response.status}`); + } + + let result; + + if (response.headers.get("Content-Type")?.startsWith("application/json")) { + result = await response.json(); + } else { + result = await response.text(); + } + + return result; +} diff --git a/packages/thirdweb/src/transaction/actions/estimate-gas.ts b/packages/thirdweb/src/transaction/actions/estimate-gas.ts index ded4adfe072..802553b405b 100644 --- a/packages/thirdweb/src/transaction/actions/estimate-gas.ts +++ b/packages/thirdweb/src/transaction/actions/estimate-gas.ts @@ -2,20 +2,20 @@ import type { AbiFunction } from "abitype"; import type { Transaction } from "../transaction.js"; import { getDefaultGasOverrides } from "../../gas/fee-data.js"; import { encode } from "./encode.js"; -import { formatTransactionRequest, hexToBigInt } from "viem/utils"; +import { formatTransactionRequest } from "viem/utils"; import type { Address } from "viem"; -import { getRpcClient } from "../../rpc/index.js"; +import { getRpcClient, eth_estimateGas } from "../../rpc/index.js"; export async function estimateGas( tx: Transaction, options?: { from?: string }, ): Promise { - const rpcRequest = getRpcClient(tx.contract, { + const rpcRequest = getRpcClient(tx.contract.client, { chainId: tx.contract.chainId, }); const [gasOverrides, encodedData] = await Promise.all([ - getDefaultGasOverrides(tx.contract, tx.contract.chainId), + getDefaultGasOverrides(tx.contract.client, tx.contract.chainId), encode(tx), ]); @@ -27,12 +27,5 @@ export async function estimateGas( from: (options?.from ?? undefined) as Address, }); - // make the call - // TODO: move into rpc/methods - const result = await rpcRequest({ - method: "eth_estimateGas", - params: [data], - }); - - return hexToBigInt(result); + return eth_estimateGas(rpcRequest, data); } diff --git a/packages/thirdweb/src/transaction/actions/read.ts b/packages/thirdweb/src/transaction/actions/read.ts index b8b6b864953..3ccf8059776 100644 --- a/packages/thirdweb/src/transaction/actions/read.ts +++ b/packages/thirdweb/src/transaction/actions/read.ts @@ -10,7 +10,7 @@ import { type Transaction, type TransactionInput, } from "../transaction.js"; -import { getRpcClient } from "../../rpc/index.js"; +import { eth_call, getRpcClient } from "../../rpc/index.js"; import { encode } from "./encode.js"; import { resolveAbi } from "./resolve-abi.js"; @@ -44,18 +44,12 @@ export async function readTx( throw new Error("Unable to resolve ABI"); } - const rpcRequest = getRpcClient(tx.contract, { + const rpcRequest = getRpcClient(tx.contract.client, { chainId: tx.contract.chainId, }); - const result = await rpcRequest({ - method: "eth_call", - params: [ - { - to: tx.contract.address, - data: encodedData, - }, - "latest", - ], + const result = await eth_call(rpcRequest, { + data: encodedData, + to: tx.contract.address, }); const decoded = decodeFunctionResult(resolvedAbi, result); diff --git a/packages/thirdweb/src/transaction/actions/send-transaction.ts b/packages/thirdweb/src/transaction/actions/send-transaction.ts index f217dd70101..ef915f4580d 100644 --- a/packages/thirdweb/src/transaction/actions/send-transaction.ts +++ b/packages/thirdweb/src/transaction/actions/send-transaction.ts @@ -11,27 +11,33 @@ export async function sendTransaction< throw new Error("not connected"); } const { getRpcClient } = await import("../../rpc/index.js"); - const rpcRequest = getRpcClient(tx.contract, { + const rpcRequest = getRpcClient(tx.contract.client, { chainId: tx.contract.chainId, }); - const [getDefaultGasOverrides, encode, transactionCount, estimateGas] = + const [getDefaultGasOverrides, encode, eth_getTransactionCount, estimateGas] = await Promise.all([ import("../../gas/fee-data.js").then((m) => m.getDefaultGasOverrides), import("./encode.js").then((m) => m.encode), - import("../../rpc/methods.js").then((m) => m.transactionCount), + import("../../rpc/actions/eth_getTransactionCount.js").then( + (m) => m.eth_getTransactionCount, + ), import("./estimate-gas.js").then((m) => m.estimateGas), ]); const [gasOverrides, encodedData, nextNonce, estimatedGas] = await Promise.all([ - getDefaultGasOverrides(tx.contract, tx.contract.chainId), + getDefaultGasOverrides(tx.contract.client, tx.contract.chainId), encode(tx), - transactionCount(rpcRequest, wallet.address), + eth_getTransactionCount(rpcRequest, { + address: wallet.address, + blockTag: "pending", + }), estimateGas(tx, { from: wallet.address }), ]); return wallet.sendTransaction({ + to: tx.contract.address, chainId: tx.contract.chainId, data: encodedData, gas: estimatedGas, 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 4c0ebb2f6e6..6fa97a3f3ac 100644 --- a/packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts +++ b/packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts @@ -1,11 +1,13 @@ -import type { TransactionReceipt } from "viem"; +import type { Hex, TransactionReceipt } from "viem"; import type { ThirdwebContract } from "../../contract/index.js"; -import { getRpcClient } from "../../rpc/index.js"; -import { getTransactionReceipt } from "../../rpc/methods.js"; +import { + getRpcClient, + eth_getTransactionReceipt, + watchBlockNumber, +} from "../../rpc/index.js"; import type { Abi } from "abitype"; -const POLL_LIMIT_MS = 1000 * 60 * 5; // 5 minutes -const POLL_WAIT_MS = 1000 * 5; // 5 seconds +const MAX_BLOCKS_WAIT_TIME = 10; const map = new Map>(); @@ -22,30 +24,49 @@ export function waitForReceipt({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return map.get(key)!; } - const promise = (async function () { + const promise = new Promise((resolve, reject) => { if (!transactionHash) { - throw new Error( - "Transaction has no txHash to wait for, did you execute it?", + reject( + new Error("Transaction has no txHash to wait for, did you execute it?"), ); } - const start = Date.now(); - const rpcClient = getRpcClient(contract, { chainId: contract.chainId }); - while (Date.now() - start < POLL_LIMIT_MS) { - // if we don't yet have a tx hash then we can't check for a receipt, so just try again - const receipt = await getTransactionReceipt(rpcClient, transactionHash); - if (receipt) { - return receipt; - } + const request = getRpcClient(contract.client, { + chainId: contract.chainId, + }); - await new Promise((resolve) => setTimeout(resolve, POLL_WAIT_MS)); - } - throw new Error("Timeout waiting for transaction receipt"); - })(); - // remove the promise from the map when it's done (one way or the other) - promise.finally(() => { + // start at -1 because the first block doesn't count + let blocksWaited = -1; + + const unwatch = watchBlockNumber({ + client: contract.client, + chainId: contract.chainId, + onNewBlockNumber: () => { + blocksWaited++; + console.log("blocksWaited", blocksWaited); + if (blocksWaited >= MAX_BLOCKS_WAIT_TIME) { + unwatch(); + reject(new Error("Transaction not found after 10 blocks")); + } + eth_getTransactionReceipt(request, { + hash: transactionHash as Hex, + }) + .then((receipt) => { + if (receipt) { + unwatch(); + return resolve(receipt); + } + }) + .catch(() => { + // noop, we'll try again on the next blocks + }); + }, + }); + // remove the promise from the map when it's done (one way or the other) + }).finally(() => { map.delete(key); }); + map.set(key, promise); return promise; } diff --git a/packages/thirdweb/src/wallets/private-key.ts b/packages/thirdweb/src/wallets/private-key.ts index aa7f355668b..05429b14c4c 100644 --- a/packages/thirdweb/src/wallets/private-key.ts +++ b/packages/thirdweb/src/wallets/private-key.ts @@ -1,5 +1,4 @@ import type { - Hash, Hex, PrivateKeyAccount, SignableMessage, @@ -8,8 +7,8 @@ import type { } from "viem"; import type { ThirdwebClient } from "../client/client.js"; import type { TypedData } from "abitype"; - import type { IWallet } from "./interfaces/wallet.js"; +import { eth_sendRawTransaction, getRpcClient } from "../rpc/index.js"; export function privateKeyWallet({ client }: { client: ThirdwebClient }) { return new PrivateKeyWallet(client); @@ -74,21 +73,13 @@ class PrivateKeyWallet implements IWallet { throw new Error("not connected"); } - const { getRpcClient } = await import("../rpc/index.js"); const rpcRequest = getRpcClient(this.client, { chainId: tx.chainId, }); const signedTx = await this.signTransaction(tx); - // send the tx - // TODO: move into rpc/methods - const result = await rpcRequest({ - method: "eth_sendRawTransaction", - params: [signedTx], - }); - - return result as Hash; + return await eth_sendRawTransaction(rpcRequest, signedTx); } public async disconnect() {