Skip to content

Commit

Permalink
batch transactions for smart wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
joaquim-verges committed Feb 15, 2024
1 parent e6d2f85 commit 59753c5
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -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<WaitForReceiptOptions> {
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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type { PreparedTransaction } from "../prepare-transaction.js";
type SendTransactionOptions = {
transaction: PreparedTransaction;
account: Account;
gasless?: boolean;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/thirdweb/src/transaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions packages/thirdweb/src/wallets/interfaces/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,7 @@ export type Account = {
// OPTIONAL
signTransaction?: (tx: TransactionSerializable) => Promise<Hex>;
estimateGas?: (tx: PreparedTransaction) => Promise<bigint>;
sendBatchTransaction?: (
txs: SendTransactionOption[],
) => Promise<TransactionOrUserOpHash>;
};
62 changes: 50 additions & 12 deletions packages/thirdweb/src/wallets/smart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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,
};
}
41 changes: 34 additions & 7 deletions packages/thirdweb/src/wallets/smart/lib/calls.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,6 +7,7 @@ import {
type PreparedTransaction,
} from "../../../index.js";
import type { Account } from "../../index.js";
import type { SendTransactionOption } from "../../interfaces/wallet.js";

/**
* @internal
Expand Down Expand Up @@ -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"),
],
});
}
17 changes: 13 additions & 4 deletions packages/thirdweb/src/wallets/smart/lib/receipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
27 changes: 12 additions & 15 deletions packages/thirdweb/src/wallets/smart/lib/userop.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,26 +27,19 @@ 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<UserOperation> {
const { factoryContract, accountContract, transaction, options } = args;
const { factoryContract, accountContract, executeTx, options } = args;
const isDeployed = await isContractDeployed(accountContract);
const initCode = isDeployed
? "0x"
: await getAccountInitCode({
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,
Expand All @@ -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,
Expand Down
10 changes: 6 additions & 4 deletions packages/thirdweb/src/wallets/smart/types.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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;
Expand Down

0 comments on commit 59753c5

Please sign in to comment.