diff --git a/packages/thirdweb/src/transaction/actions/send-batch-transaction.ts b/packages/thirdweb/src/transaction/actions/send-batch-transaction.ts new file mode 100644 index 00000000000..cbeac85a409 --- /dev/null +++ b/packages/thirdweb/src/transaction/actions/send-batch-transaction.ts @@ -0,0 +1,72 @@ +import type { WaitForReceiptOptions } from "./wait-for-tx-receipt.js"; +import type { + Account, + SendTransactionOption, +} from "../../wallets/interfaces/wallet.js"; +import type { PreparedTransaction } from "../prepare-transaction.js"; +import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js"; +import { encode } from "./encode.js"; +import { getChainIdFromChain } from "../../chain/index.js"; + +type SendBatchTransactionOptions = { + transactions: PreparedTransaction[]; + account: Account; +}; + +/** + * Sends a transaction using the provided wallet. + * @param options - The options for sending the transaction. + * @returns A promise that resolves to the transaction hash. + * @throws An error if the wallet is not connected. + * @transaction + * @example + * ```ts + * import { sendTransaction } from "thirdweb"; + * const transactionHash = await sendTransaction({ + * account, + * transaction + * }); + * ``` + */ +export async function sendBatchTransaction( + options: SendBatchTransactionOptions, +): Promise { + if (!options.account.address) { + throw new Error("not connected"); + } + if (options.transactions.length === 0) { + throw new Error("No transactions to send"); + } + const firstTx = options.transactions[0]; + if (!firstTx) { + throw new Error("No transactions to send"); + } + if (options.account.sendBatchTransaction) { + const serializedTxs: SendTransactionOption[] = await Promise.all( + options.transactions.map(async (tx) => { + // no need to estimate gas for these, gas will be estimated on the entire batch + const [data, to, accessList, value] = await Promise.all([ + encode(tx), + resolvePromisedValue(tx.to), + resolvePromisedValue(tx.accessList), + resolvePromisedValue(tx.value), + ]); + const serializedTx: SendTransactionOption = { + data, + chainId: Number(getChainIdFromChain(tx.chain)), + to, + value, + accessList, + }; + return serializedTx; + }), + ); + const result = await options.account.sendBatchTransaction(serializedTxs); + return { + ...result, + transaction: firstTx, + }; + } else { + throw new Error("Account doesn't implement sendBatchTransaction"); + } +} diff --git a/packages/thirdweb/src/transaction/actions/send-transaction.ts b/packages/thirdweb/src/transaction/actions/send-transaction.ts index a49b034fe88..8830bbd2828 100644 --- a/packages/thirdweb/src/transaction/actions/send-transaction.ts +++ b/packages/thirdweb/src/transaction/actions/send-transaction.ts @@ -8,7 +8,6 @@ import type { PreparedTransaction } from "../prepare-transaction.js"; type SendTransactionOptions = { transaction: PreparedTransaction; account: Account; - gasless?: boolean; }; /** 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 9281c45e409..fad131486b6 100644 --- a/packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts +++ b/packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts @@ -98,13 +98,10 @@ export function waitForReceipt( lastBlockNumber = blockNumber; if (event) { - console.log("event", event); const receipt = await eth_getTransactionReceipt(request, { hash: event.transactionHash, }); - // TODO check if the event has success = false and decode the revert reason - // stop the polling unwatch(); // resolve the top level promise with the receipt diff --git a/packages/thirdweb/src/transaction/index.ts b/packages/thirdweb/src/transaction/index.ts index 944ad3cc3be..fb003597acb 100644 --- a/packages/thirdweb/src/transaction/index.ts +++ b/packages/thirdweb/src/transaction/index.ts @@ -24,6 +24,7 @@ export { encode } from "./actions/encode.js"; export { estimateGas, type EstimateGasResult } from "./actions/estimate-gas.js"; export { waitForReceipt } from "./actions/wait-for-tx-receipt.js"; export { sendTransaction } from "./actions/send-transaction.js"; +export { sendBatchTransaction } from "./actions/send-batch-transaction.js"; export { simulateTransaction } from "./actions/simulate.js"; //types & utils diff --git a/packages/thirdweb/src/wallets/interfaces/wallet.ts b/packages/thirdweb/src/wallets/interfaces/wallet.ts index 161425e8446..6d1c02d49f5 100644 --- a/packages/thirdweb/src/wallets/interfaces/wallet.ts +++ b/packages/thirdweb/src/wallets/interfaces/wallet.ts @@ -57,4 +57,7 @@ export type Account = { // OPTIONAL signTransaction?: (tx: TransactionSerializable) => Promise; estimateGas?: (tx: PreparedTransaction) => Promise; + sendBatchTransaction?: ( + txs: SendTransactionOption[], + ) => Promise; }; diff --git a/packages/thirdweb/src/wallets/smart/index.ts b/packages/thirdweb/src/wallets/smart/index.ts index 95d404c539f..6bf341514a3 100644 --- a/packages/thirdweb/src/wallets/smart/index.ts +++ b/packages/thirdweb/src/wallets/smart/index.ts @@ -10,13 +10,18 @@ import type { } from "./types.js"; import { createUnsignedUserOp, signUserOp } from "./lib/userop.js"; import { bundleUserOp } from "./lib/bundler.js"; -import { getContract } from "../../contract/contract.js"; -import { predictAddress } from "./lib/calls.js"; +import { getContract, type ThirdwebContract } from "../../contract/contract.js"; +import { + predictAddress, + prepareBatchExecute, + prepareExecute, +} from "./lib/calls.js"; import { saveConnectParamsToStorage, type WithPersonalWalletConnectionOptions, } from "../manager/storage.js"; import { getChainIdFromChain } from "../../chain/index.js"; +import type { PreparedTransaction } from "../../index.js"; /** * Creates a smart wallet. @@ -172,24 +177,31 @@ async function smartAccount( return { address: accountAddress, - async sendTransaction(tx: SendTransactionOption) { - const unsignedUserOp = await createUnsignedUserOp({ + async sendTransaction(transaction: SendTransactionOption) { + const executeTx = prepareExecute({ + accountContract, + options, + transaction, + }); + return _sendUserOp({ factoryContract, accountContract, - transaction: tx, + executeTx, options, }); - const signedUserOp = await signUserOp({ + }, + async sendBatchTransaction(transactions: SendTransactionOption[]) { + const executeTx = prepareBatchExecute({ + accountContract, options, - userOp: unsignedUserOp, + transactions, }); - const userOpHash = await bundleUserOp({ + return _sendUserOp({ + factoryContract, + accountContract, + executeTx, options, - userOp: signedUserOp, }); - return { - userOpHash, - }; }, async estimateGas() { // estimation is done in createUnsignedUserOp @@ -205,3 +217,29 @@ async function smartAccount( }, }; } + +async function _sendUserOp(args: { + factoryContract: ThirdwebContract; + accountContract: ThirdwebContract; + executeTx: PreparedTransaction; + options: SmartWalletOptions & { personalAccount: Account }; +}) { + const { factoryContract, accountContract, executeTx, options } = args; + const unsignedUserOp = await createUnsignedUserOp({ + factoryContract, + accountContract, + executeTx, + options, + }); + const signedUserOp = await signUserOp({ + options, + userOp: unsignedUserOp, + }); + const userOpHash = await bundleUserOp({ + options, + userOp: signedUserOp, + }); + return { + userOpHash, + }; +} diff --git a/packages/thirdweb/src/wallets/smart/lib/calls.ts b/packages/thirdweb/src/wallets/smart/lib/calls.ts index b9e8ab6a834..57bf243e13f 100644 --- a/packages/thirdweb/src/wallets/smart/lib/calls.ts +++ b/packages/thirdweb/src/wallets/smart/lib/calls.ts @@ -1,4 +1,4 @@ -import { toHex, type Hex } from "viem"; +import { toHex } from "viem"; import type { ThirdwebContract } from "../../../contract/contract.js"; import type { SmartWalletOptions } from "../types.js"; import { readContract } from "../../../transaction/read-contract.js"; @@ -7,6 +7,7 @@ import { type PreparedTransaction, } from "../../../index.js"; import type { Account } from "../../index.js"; +import type { SendTransactionOption } from "../../interfaces/wallet.js"; /** * @internal @@ -55,17 +56,43 @@ export function prepareCreateAccount(args: { export function prepareExecute(args: { accountContract: ThirdwebContract; options: SmartWalletOptions; - target: string; - value: bigint; - data: Hex; + transaction: SendTransactionOption; }): PreparedTransaction { - const { accountContract, options, target, value, data } = args; + const { accountContract, options, transaction } = args; if (options.overrides?.execute) { - return options.overrides.execute(accountContract, target, value, data); + return options.overrides.execute(accountContract, transaction); } return prepareContractCall({ contract: accountContract, method: "function execute(address, uint256, bytes)", - params: [target, value, data], + params: [ + transaction.to || "", + transaction.value || 0n, + transaction.data || "0x", + ], + }); +} + +/** + * @internal + */ +export function prepareBatchExecute(args: { + accountContract: ThirdwebContract; + options: SmartWalletOptions; + transactions: SendTransactionOption[]; +}): PreparedTransaction { + const { accountContract, options, transactions } = args; + if (options.overrides?.executeBatch) { + return options.overrides.executeBatch(accountContract, transactions); + } + return prepareContractCall({ + contract: accountContract, + method: + "function executeBatch(address[] calldata _target,uint256[] calldata _value,bytes[] calldata _calldata)", + params: [ + transactions.map((tx) => tx.to || ""), + transactions.map((tx) => tx.value || 0n), + transactions.map((tx) => tx.data || "0x"), + ], }); } diff --git a/packages/thirdweb/src/wallets/smart/lib/receipts.ts b/packages/thirdweb/src/wallets/smart/lib/receipts.ts index 5c9ab5a3841..3741052ee6f 100644 --- a/packages/thirdweb/src/wallets/smart/lib/receipts.ts +++ b/packages/thirdweb/src/wallets/smart/lib/receipts.ts @@ -16,7 +16,6 @@ export async function getUserOpEventFromEntrypoint(args: { userOpHash: string; }) { const { blockNumber, blockRange, chain, userOpHash, client } = args; - console.log("getUserOpEventFromEntrypoint", blockNumber, blockRange); const fromBlock = blockNumber > blockRange ? blockNumber - blockRange : blockNumber; const entryPointContract = getContract({ @@ -44,7 +43,10 @@ export async function getUserOpEventFromEntrypoint(args: { if (event && event.args.success === false) { const revertOpEvent = prepareEvent({ signature: - "event UserOperationRevertReasonEvent(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason)", + "event UserOperationRevertReason(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason)", + filters: { + userOpHash: userOpHash as Hex, + }, }); const revertEvent = await getContractEvents({ contract: entryPointContract, @@ -56,9 +58,16 @@ export async function getUserOpEventFromEntrypoint(args: { const message = decodeErrorResult({ data: firstRevertEvent.args.revertReason, }); - throw new Error(`UserOp failed with reason: ${message.args.join(",")}`); + throw new Error( + `UserOp failed with reason: '${message.args.join(",")}' at txHash: ${ + event.transactionHash + }`, + ); } else { - throw new Error("UserOp failed with unknown reason"); + throw new Error( + "UserOp failed with unknown reason with txHash: " + + event.transactionHash, + ); } } return event; diff --git a/packages/thirdweb/src/wallets/smart/lib/userop.ts b/packages/thirdweb/src/wallets/smart/lib/userop.ts index 7da06be343e..c97f9cfc682 100644 --- a/packages/thirdweb/src/wallets/smart/lib/userop.ts +++ b/packages/thirdweb/src/wallets/smart/lib/userop.ts @@ -1,17 +1,20 @@ import { keccak256, concat, type Hex, encodeAbiParameters } from "viem"; import type { SmartWalletOptions, UserOperation } from "../types.js"; -import type { SendTransactionOption } from "../../interfaces/wallet.js"; import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js"; import type { ThirdwebContract } from "../../../contract/contract.js"; import { encode } from "../../../transaction/actions/encode.js"; import { getDefaultGasOverrides } from "../../../gas/fee-data.js"; -import { getChainIdFromChain } from "../../../index.js"; +import { + getChainIdFromChain, + type PreparedTransaction, +} from "../../../index.js"; import { DUMMY_SIGNATURE, ENTRYPOINT_ADDRESS } from "./constants.js"; import { getPaymasterAndData } from "./paymaster.js"; import { estimateUserOpGas } from "./bundler.js"; import { randomNonce } from "./utils.js"; -import { prepareCreateAccount, prepareExecute } from "./calls.js"; +import { prepareCreateAccount } from "./calls.js"; import type { Account } from "../../interfaces/wallet.js"; +import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-value.js"; /** * Create an unsigned user operation @@ -24,10 +27,10 @@ import type { Account } from "../../interfaces/wallet.js"; export async function createUnsignedUserOp(args: { factoryContract: ThirdwebContract; accountContract: ThirdwebContract; - transaction: SendTransactionOption; + executeTx: PreparedTransaction; options: SmartWalletOptions & { personalAccount: Account }; }): Promise { - const { factoryContract, accountContract, transaction, options } = args; + const { factoryContract, accountContract, executeTx, options } = args; const isDeployed = await isContractDeployed(accountContract); const initCode = isDeployed ? "0x" @@ -35,15 +38,8 @@ export async function createUnsignedUserOp(args: { factoryContract, options, }); - const executeTx = prepareExecute({ - accountContract, - options, - target: transaction.to || "", - value: transaction.value || 0n, - data: transaction.data || "0x", - }); const callData = await encode(executeTx); - let { maxFeePerGas, maxPriorityFeePerGas } = transaction; + let { maxFeePerGas, maxPriorityFeePerGas } = executeTx; if (!maxFeePerGas || !maxPriorityFeePerGas) { const feeData = await getDefaultGasOverrides( factoryContract.client, @@ -65,8 +61,9 @@ export async function createUnsignedUserOp(args: { nonce, initCode, callData, - maxFeePerGas: maxFeePerGas ?? 0n, - maxPriorityFeePerGas: maxPriorityFeePerGas ?? 0n, + maxFeePerGas: (await resolvePromisedValue(maxFeePerGas)) ?? 0n, + maxPriorityFeePerGas: + (await resolvePromisedValue(maxPriorityFeePerGas)) ?? 0n, callGasLimit: 0n, verificationGasLimit: 0n, preVerificationGas: 0n, diff --git a/packages/thirdweb/src/wallets/smart/types.ts b/packages/thirdweb/src/wallets/smart/types.ts index 7d298edbb5f..3b921c5947e 100644 --- a/packages/thirdweb/src/wallets/smart/types.ts +++ b/packages/thirdweb/src/wallets/smart/types.ts @@ -1,7 +1,7 @@ import type { Chain } from "../../chain/index.js"; import type { ThirdwebClient } from "../../client/client.js"; import type { PreparedTransaction, ThirdwebContract } from "../../index.js"; -import type { Wallet } from "../interfaces/wallet.js"; +import type { SendTransactionOption, Wallet } from "../interfaces/wallet.js"; import type { Address, Hex } from "viem"; import type { WalletMetadata } from "../types.js"; @@ -20,9 +20,11 @@ export type SmartWalletOptions = { createAccount?: (factoryContract: ThirdwebContract) => PreparedTransaction; execute?: ( accountContract: ThirdwebContract, - target: string, - value: bigint, - data: string, + transaction: SendTransactionOption, + ) => PreparedTransaction; + executeBatch?: ( + accountContract: ThirdwebContract, + transactions: SendTransactionOption[], ) => PreparedTransaction; }; metadata?: WalletMetadata;