Skip to content

Commit 2207433

Browse files
committed
Upgrade thirdweb to v5.100.1 and update UserOp handling for gas
overrides
1 parent 3a5dce9 commit 2207433

File tree

3 files changed

+479
-105
lines changed

3 files changed

+479
-105
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"prisma": "^5.14.0",
7070
"prom-client": "^15.1.3",
7171
"superjson": "^2.2.1",
72-
"thirdweb": "5.96.5",
72+
"thirdweb": "^5.100.1",
7373
"undici": "^6.20.1",
7474
"uuid": "^9.0.1",
7575
"viem": "2.22.17",

src/worker/tasks/send-transaction-worker.ts

Lines changed: 167 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import assert from "node:assert";
22
import { type Job, type Processor, Worker } from "bullmq";
33
import superjson from "superjson";
44
import {
5+
type Address,
6+
type Chain,
57
type Hex,
8+
type ThirdwebClient,
69
getAddress,
710
getContract,
811
readContract,
@@ -13,9 +16,9 @@ import { getChainMetadata } from "thirdweb/chains";
1316
import { isZkSyncChain, stringify } from "thirdweb/utils";
1417
import type { Account } from "thirdweb/wallets";
1518
import {
16-
type UserOperation,
1719
bundleUserOp,
18-
createAndSignUserOp,
20+
prepareUserOp,
21+
signUserOp,
1922
smartWallet,
2023
} from "thirdweb/wallets/smart";
2124
import { getContractAddress } from "viem";
@@ -60,6 +63,8 @@ import {
6063
SendTransactionQueue,
6164
} from "../queues/send-transaction-queue";
6265

66+
type VersionedUserOp = Awaited<ReturnType<typeof prepareUserOp>>;
67+
6368
/**
6469
* Submit a transaction to RPC (EOA transactions) or bundler (userOps).
6570
*
@@ -180,61 +185,97 @@ const _sendUserOp = async (
180185
};
181186
}
182187

183-
let signedUserOp: UserOperation;
184-
try {
185-
// Resolve the user factory from the provided address, or from the `factory()` method if found.
186-
let accountFactoryAddress = userProvidedAccountFactoryAddress;
187-
if (!accountFactoryAddress) {
188-
// TODO: this is not a good solution since the assumption that the account has a factory function is not guaranteed
189-
// instead, we should use default account factory address or throw here.
190-
try {
191-
const smartAccountContract = getContract({
192-
client: thirdwebClient,
193-
chain,
194-
address: accountAddress,
195-
});
196-
const onchainAccountFactoryAddress = await readContract({
197-
contract: smartAccountContract,
198-
method: "function factory() view returns (address)",
199-
params: [],
200-
});
201-
accountFactoryAddress = getAddress(onchainAccountFactoryAddress);
202-
} catch {
203-
throw new Error(
204-
`Failed to find factory address for account '${accountAddress}' on chain '${chainId}'`,
205-
);
206-
}
188+
// Part 1: Prepare the userop
189+
// Step 1: Get factory address
190+
let accountFactoryAddress: Address | undefined;
191+
192+
if (userProvidedAccountFactoryAddress) {
193+
accountFactoryAddress = userProvidedAccountFactoryAddress;
194+
} else {
195+
const smartAccountContract = getContract({
196+
client: thirdwebClient,
197+
chain,
198+
address: accountAddress,
199+
});
200+
201+
try {
202+
const onchainAccountFactoryAddress = await readContract({
203+
contract: smartAccountContract,
204+
method: "function factory() view returns (address)",
205+
params: [],
206+
});
207+
accountFactoryAddress = getAddress(onchainAccountFactoryAddress);
208+
} catch (error) {
209+
const errorMessage = `${wrapError(error, "RPC").message} Failed to find factory address for account`;
210+
const erroredTransaction: ErroredTransaction = {
211+
...queuedTransaction,
212+
status: "errored",
213+
errorMessage,
214+
};
215+
job.log(`Failed to get account factory address: ${errorMessage}`);
216+
return erroredTransaction;
207217
}
218+
}
208219

209-
const transactions = queuedTransaction.batchOperations
210-
? queuedTransaction.batchOperations.map((op) => ({
211-
...op,
212-
chain,
220+
// Step 2: Get entrypoint address
221+
let entrypointAddress: string | undefined;
222+
if (userProvidedEntrypointAddress) {
223+
entrypointAddress = queuedTransaction.entrypointAddress;
224+
} else {
225+
try {
226+
entrypointAddress = await getEntrypointFromFactory(
227+
adminAccount.address,
228+
thirdwebClient,
229+
chain,
230+
);
231+
} catch (error) {
232+
const errorMessage = `${wrapError(error, "RPC").message} Failed to find entrypoint address for account factory`;
233+
const erroredTransaction: ErroredTransaction = {
234+
...queuedTransaction,
235+
status: "errored",
236+
errorMessage,
237+
};
238+
job.log(
239+
`Failed to find entrypoint address for account factory: ${errorMessage}`,
240+
);
241+
return erroredTransaction;
242+
}
243+
}
244+
245+
// Step 3: Transform transactions for userop
246+
const transactions = queuedTransaction.batchOperations
247+
? queuedTransaction.batchOperations.map((op) => ({
248+
...op,
249+
chain,
250+
client: thirdwebClient,
251+
}))
252+
: [
253+
{
213254
client: thirdwebClient,
214-
}))
215-
: [
216-
{
217-
client: thirdwebClient,
218-
chain,
219-
data: queuedTransaction.data,
220-
value: queuedTransaction.value,
221-
...overrides, // gas-overrides
222-
to: getChecksumAddress(toAddress),
223-
},
224-
];
225-
226-
signedUserOp = (await createAndSignUserOp({
227-
client: thirdwebClient,
255+
chain,
256+
data: queuedTransaction.data,
257+
value: queuedTransaction.value,
258+
...overrides, // gas-overrides
259+
to: getChecksumAddress(toAddress),
260+
},
261+
];
262+
263+
// Step 4: Prepare userop
264+
let unsignedUserOp: VersionedUserOp | undefined;
265+
266+
try {
267+
unsignedUserOp = await prepareUserOp({
228268
transactions,
229269
adminAccount,
270+
client: thirdwebClient,
230271
smartWalletOptions: {
231272
chain,
232273
sponsorGas: true,
233-
factoryAddress: accountFactoryAddress,
274+
factoryAddress: accountFactoryAddress, // from step 1
234275
overrides: {
235276
accountAddress,
236277
accountSalt,
237-
entrypointAddress: userProvidedEntrypointAddress,
278+
entrypointAddress, // from step 2
238279
// TODO: let user pass entrypoint address for 0.7 support
239280
},
240281
},
@@ -243,7 +284,7 @@ const _sendUserOp = async (
243284
// until the previous userop for the same account is mined
244285
// we don't want this behavior in the engine context
245286
waitForDeployment: false,
246-
})) as UserOperation; // TODO support entrypoint v0.7 accounts
287+
});
247288
} catch (error) {
248289
const errorMessage = wrapError(error, "Bundler").message;
249290
const erroredTransaction: ErroredTransaction = {
@@ -255,16 +296,66 @@ const _sendUserOp = async (
255296
return erroredTransaction;
256297
}
257298

258-
job.log(`Populated userOp: ${stringify(signedUserOp)}`);
299+
// Handle if `maxFeePerGas` is overridden.
300+
// Set it if the transaction will be sent, otherwise delay the job.
301+
if (overrides?.maxFeePerGas && unsignedUserOp.maxFeePerGas) {
302+
if (overrides.maxFeePerGas > unsignedUserOp.maxFeePerGas) {
303+
unsignedUserOp.maxFeePerGas = overrides.maxFeePerGas;
304+
} else {
305+
const retryAt = _minutesFromNow(5);
306+
job.log(
307+
`Override gas fee (${overrides.maxFeePerGas}) is lower than onchain fee (${unsignedUserOp.maxFeePerGas}). Delaying job until ${retryAt}.`,
308+
);
309+
await job.moveToDelayed(retryAt.getTime());
310+
return null;
311+
}
312+
}
259313

260-
const userOpHash = await bundleUserOp({
261-
userOp: signedUserOp,
262-
options: {
314+
// Part 2: Sign the userop
315+
let signedUserOp: VersionedUserOp | undefined;
316+
try {
317+
signedUserOp = await signUserOp({
263318
client: thirdwebClient,
264319
chain,
265-
entrypointAddress: userProvidedEntrypointAddress,
266-
},
267-
});
320+
adminAccount,
321+
entrypointAddress,
322+
userOp: unsignedUserOp,
323+
});
324+
} catch (error) {
325+
const errorMessage = `${wrapError(error, "Bundler").message} Failed to sign prepared userop`;
326+
const erroredTransaction: ErroredTransaction = {
327+
...queuedTransaction,
328+
status: "errored",
329+
errorMessage,
330+
};
331+
job.log(`Failed to sign userop: ${errorMessage}`);
332+
return erroredTransaction;
333+
}
334+
335+
job.log(`Populated and signed userOp: ${stringify(signedUserOp)}`);
336+
337+
// Finally: bundle the userop
338+
let userOpHash: Hex;
339+
340+
try {
341+
userOpHash = await bundleUserOp({
342+
userOp: signedUserOp,
343+
options: {
344+
client: thirdwebClient,
345+
chain,
346+
entrypointAddress: userProvidedEntrypointAddress,
347+
},
348+
});
349+
} catch (error) {
350+
const errorMessage = `${wrapError(error, "Bundler").message} Failed to bundle userop`;
351+
const erroredTransaction: ErroredTransaction = {
352+
...queuedTransaction,
353+
status: "errored",
354+
errorMessage,
355+
};
356+
job.log(`Failed to bundle userop: ${errorMessage}`);
357+
return erroredTransaction;
358+
}
268359

269360
return {
270361
...queuedTransaction,
@@ -646,6 +737,28 @@ export function _updateGasFees(
646737
return updated;
647738
}
648739

740+
async function getEntrypointFromFactory(
741+
factoryAddress: string,
742+
client: ThirdwebClient,
743+
chain: Chain,
744+
) {
745+
const factoryContract = getContract({
746+
address: factoryAddress,
747+
client,
748+
chain,
749+
});
750+
try {
751+
const entrypointAddress = await readContract({
752+
contract: factoryContract,
753+
method: "function entrypoint() public view returns (address)",
754+
params: [],
755+
});
756+
return entrypointAddress;
757+
} catch {
758+
return undefined;
759+
}
760+
}
761+
649762
// Must be explicitly called for the worker to run on this host.
650763
export const initSendTransactionWorker = () => {
651764
const _worker = new Worker(SendTransactionQueue.q.name, handler, {

0 commit comments

Comments
 (0)