feat: allow sending smart wallet transactions in parallel (#4544)
## Problem solved
Short description of the bug fixed or feature added
<!-- start pr-codex -->
---
## PR-Codex overview
This PR focuses on enabling smart wallet transactions to be sent in parallel.
### Detailed summary
- Added the ability to execute 2 transactions in parallel in smart wallets
- Implemented functions to handle account deployment in parallel transactions
> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
<!-- end pr-codex -->
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"thirdweb": patch
|
||||
---
|
||||
|
||||
Allow smart wallet transactions to be sent in parallel
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
} from "./lib/calls.js";
|
||||
import { getDefaultAccountFactory } from "./lib/constants.js";
|
||||
import {
|
||||
clearAccountDeploying,
|
||||
createUnsignedUserOp,
|
||||
signUserOp,
|
||||
waitForUserOpReceipt,
|
||||
@@ -597,6 +598,9 @@ async function _sendUserOp(args: {
|
||||
userOpHash,
|
||||
});
|
||||
|
||||
// reset the isDeploying flag after every transaction
|
||||
clearAccountDeploying(options.accountContract);
|
||||
|
||||
return {
|
||||
client: options.client,
|
||||
chain: options.chain,
|
||||
|
||||
@@ -49,6 +49,24 @@ import { getPackedUserOperation } from "./packUserOp.js";
|
||||
import { getPaymasterAndData } from "./paymaster.js";
|
||||
import { generateRandomUint192 } from "./utils.js";
|
||||
|
||||
const isDeployingSet: Set<string> = new Set();
|
||||
|
||||
const getKey = (accountContract: ThirdwebContract) => {
|
||||
return `${accountContract.chain.id}:${accountContract.address}`;
|
||||
};
|
||||
|
||||
export const markAccountDeploying = (accountContract: ThirdwebContract) => {
|
||||
isDeployingSet.add(getKey(accountContract));
|
||||
};
|
||||
|
||||
export const clearAccountDeploying = (accountContract: ThirdwebContract) => {
|
||||
isDeployingSet.delete(getKey(accountContract));
|
||||
};
|
||||
|
||||
export const isAccountDeploying = (accountContract: ThirdwebContract) => {
|
||||
return isDeployingSet.has(getKey(accountContract));
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for the user operation to be mined.
|
||||
* @param args - The options and user operation hash
|
||||
@@ -266,17 +284,25 @@ async function populateUserOp_v0_7(args: {
|
||||
maxPriorityFeePerGas,
|
||||
} = args;
|
||||
const { chain, client } = bundlerOptions;
|
||||
const factory = isDeployed ? undefined : factoryContract.address;
|
||||
const factoryData = isDeployed
|
||||
? "0x"
|
||||
: await encode(
|
||||
prepareCreateAccount({
|
||||
factoryContract: factoryContract,
|
||||
adminAddress,
|
||||
accountSalt: overrides?.accountSalt,
|
||||
createAccountOverride: overrides?.createAccount,
|
||||
}),
|
||||
);
|
||||
|
||||
let factory: string | undefined;
|
||||
let factoryData: Hex;
|
||||
// lock until account is deployed if needed to avoid 'sender already created' errors when sending multiple transactions in parallel
|
||||
if (isDeployed || isAccountDeploying(accountContract)) {
|
||||
factoryData = "0x";
|
||||
await waitForAccountDeployed(accountContract);
|
||||
} else {
|
||||
factory = factoryContract.address;
|
||||
factoryData = await encode(
|
||||
prepareCreateAccount({
|
||||
factoryContract: factoryContract,
|
||||
adminAddress,
|
||||
accountSalt: overrides?.accountSalt,
|
||||
createAccountOverride: overrides?.createAccount,
|
||||
}),
|
||||
);
|
||||
markAccountDeploying(accountContract);
|
||||
}
|
||||
|
||||
const partialOp: UserOperationV07 = {
|
||||
sender: accountContract.address,
|
||||
@@ -395,14 +421,21 @@ async function populateUserOp_v0_6(args: {
|
||||
maxPriorityFeePerGas,
|
||||
} = args;
|
||||
const { chain, client } = bundlerOptions;
|
||||
const initCode = isDeployed
|
||||
? "0x"
|
||||
: await getAccountInitCode({
|
||||
factoryContract: factoryContract,
|
||||
adminAddress,
|
||||
accountSalt: overrides?.accountSalt,
|
||||
createAccountOverride: overrides?.createAccount,
|
||||
});
|
||||
let initCode: Hex;
|
||||
|
||||
// lock until account is deployed if needed to avoid 'sender already created' errors when sending multiple transactions in parallel
|
||||
if (isDeployed || isAccountDeploying(accountContract)) {
|
||||
initCode = "0x";
|
||||
await waitForAccountDeployed(accountContract);
|
||||
} else {
|
||||
initCode = await getAccountInitCode({
|
||||
factoryContract: factoryContract,
|
||||
adminAddress,
|
||||
accountSalt: overrides?.accountSalt,
|
||||
createAccountOverride: overrides?.createAccount,
|
||||
});
|
||||
markAccountDeploying(accountContract);
|
||||
}
|
||||
|
||||
const partialOp: UserOperationV06 = {
|
||||
sender: accountContract.address,
|
||||
@@ -688,3 +721,15 @@ export async function createAndSignUserOp(options: {
|
||||
});
|
||||
return signedUserOp;
|
||||
}
|
||||
|
||||
async function waitForAccountDeployed(accountContract: ThirdwebContract) {
|
||||
const startTime = Date.now();
|
||||
while (isAccountDeploying(accountContract)) {
|
||||
if (Date.now() - startTime > 60000) {
|
||||
throw new Error(
|
||||
"Account deployment is taking too long (over 1 minute). Please try again.",
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,5 +202,60 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential(
|
||||
expect(logs.some((l) => l.args.signer === newAdmin.address)).toBe(true);
|
||||
expect(logs.some((l) => l.args.isAdmin)).toBe(true);
|
||||
});
|
||||
|
||||
it("can execute a 2 tx in parallel", async () => {
|
||||
const newSmartWallet = smartWallet({
|
||||
chain,
|
||||
gasless: true,
|
||||
overrides: {
|
||||
accountSalt: "test",
|
||||
},
|
||||
});
|
||||
const newSmartAccount = await newSmartWallet.connect({
|
||||
client: TEST_CLIENT,
|
||||
personalAccount,
|
||||
});
|
||||
console.log("newSmartAccount", newSmartAccount.address);
|
||||
const newSmartAccountContract = getContract({
|
||||
address: newSmartAccount.address,
|
||||
chain,
|
||||
client,
|
||||
});
|
||||
let isDeployed = await isContractDeployed(newSmartAccountContract);
|
||||
expect(isDeployed).toEqual(false);
|
||||
|
||||
// sending transactions in parallel should deploy the account and not cause errors
|
||||
const txs = await Promise.all([
|
||||
sendAndConfirmTransaction({
|
||||
transaction: claimTo({
|
||||
contract,
|
||||
quantity: 1n,
|
||||
to: newSmartAccount.address,
|
||||
tokenId: 0n,
|
||||
}),
|
||||
account: newSmartAccount,
|
||||
}),
|
||||
sendAndConfirmTransaction({
|
||||
transaction: claimTo({
|
||||
contract,
|
||||
quantity: 1n,
|
||||
to: newSmartAccount.address,
|
||||
tokenId: 0n,
|
||||
}),
|
||||
account: newSmartAccount,
|
||||
}),
|
||||
]);
|
||||
expect(txs.length).toEqual(2);
|
||||
expect(txs.every((t) => t.transactionHash.length === 66)).toBe(true);
|
||||
|
||||
isDeployed = await isContractDeployed(newSmartAccountContract);
|
||||
expect(isDeployed).toEqual(true);
|
||||
const balance = await balanceOf({
|
||||
contract,
|
||||
owner: newSmartAccountContract.address,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(balance).toEqual(2n);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -301,5 +301,60 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential(
|
||||
});
|
||||
expect(balance).toEqual(1n);
|
||||
});
|
||||
|
||||
it("can execute a 2 tx in parallel", async () => {
|
||||
const newSmartWallet = smartWallet({
|
||||
chain,
|
||||
gasless: true,
|
||||
overrides: {
|
||||
accountSalt: "test",
|
||||
},
|
||||
});
|
||||
const newSmartAccount = await newSmartWallet.connect({
|
||||
client: TEST_CLIENT,
|
||||
personalAccount,
|
||||
});
|
||||
console.log("newSmartAccount", newSmartAccount.address);
|
||||
const newSmartAccountContract = getContract({
|
||||
address: newSmartAccount.address,
|
||||
chain,
|
||||
client,
|
||||
});
|
||||
let isDeployed = await isContractDeployed(newSmartAccountContract);
|
||||
expect(isDeployed).toEqual(false);
|
||||
|
||||
// sending transactions in parallel should deploy the account and not cause errors
|
||||
const txs = await Promise.all([
|
||||
sendAndConfirmTransaction({
|
||||
transaction: claimTo({
|
||||
contract,
|
||||
quantity: 1n,
|
||||
to: newSmartAccount.address,
|
||||
tokenId: 0n,
|
||||
}),
|
||||
account: newSmartAccount,
|
||||
}),
|
||||
sendAndConfirmTransaction({
|
||||
transaction: claimTo({
|
||||
contract,
|
||||
quantity: 1n,
|
||||
to: newSmartAccount.address,
|
||||
tokenId: 0n,
|
||||
}),
|
||||
account: newSmartAccount,
|
||||
}),
|
||||
]);
|
||||
expect(txs.length).toEqual(2);
|
||||
expect(txs.every((t) => t.transactionHash.length === 66)).toBe(true);
|
||||
|
||||
isDeployed = await isContractDeployed(newSmartAccountContract);
|
||||
expect(isDeployed).toEqual(true);
|
||||
const balance = await balanceOf({
|
||||
contract,
|
||||
owner: newSmartAccountContract.address,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(balance).toEqual(2n);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -74,7 +74,7 @@ describe.runIf(process.env.TW_SECRET_KEY).skip(
|
||||
personalAccount,
|
||||
});
|
||||
const zkCandySmartWalletAddress = zkCandySmartAccount.address;
|
||||
const preparedTx = await prepareTransaction({
|
||||
const preparedTx = prepareTransaction({
|
||||
chain: defineChain(302),
|
||||
client: client,
|
||||
to: zkCandySmartWalletAddress,
|
||||
|
||||
Reference in New Issue
Block a user