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:
joaquim-verges
2024-09-12 08:06:33 +00:00
parent 6077fedd75
commit 40f01e5e89
6 changed files with 184 additions and 20 deletions
+5
View File
@@ -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,