11import assert from "node:assert" ;
2- import { type Job , type Processor , Worker } from "bullmq" ;
2+ import { DelayedError , type Job , type Processor , Worker } from "bullmq" ;
33import superjson from "superjson" ;
44import {
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";
1316import { isZkSyncChain , stringify } from "thirdweb/utils" ;
1417import type { Account } from "thirdweb/wallets" ;
1518import {
16- type UserOperation ,
1719 bundleUserOp ,
18- createAndSignUserOp ,
20+ prepareUserOp ,
21+ signUserOp ,
1922 smartWallet ,
2023} from "thirdweb/wallets/smart" ;
2124import { getContractAddress } from "viem" ;
@@ -60,12 +63,17 @@ 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 *
6671 * This worker also handles retried EOA transactions.
6772 */
68- const handler : Processor < string , void , string > = async ( job : Job < string > ) => {
73+ const handler : Processor < string , void , string > = async (
74+ job : Job < string > ,
75+ token ?: string ,
76+ ) => {
6977 const { queueId, resendCount } = superjson . parse < SendTransactionData > (
7078 job . data ,
7179 ) ;
@@ -84,9 +92,9 @@ const handler: Processor<string, void, string> = async (job: Job<string>) => {
8492
8593 if ( transaction . status === "queued" ) {
8694 if ( transaction . isUserOp ) {
87- resultTransaction = await _sendUserOp ( job , transaction ) ;
95+ resultTransaction = await _sendUserOp ( job , transaction , token ) ;
8896 } else {
89- resultTransaction = await _sendTransaction ( job , transaction ) ;
97+ resultTransaction = await _sendTransaction ( job , transaction , token ) ;
9098 }
9199 } else if ( transaction . status === "sent" ) {
92100 resultTransaction = await _resendTransaction ( job , transaction , resendCount ) ;
@@ -116,6 +124,7 @@ const handler: Processor<string, void, string> = async (job: Job<string>) => {
116124const _sendUserOp = async (
117125 job : Job ,
118126 queuedTransaction : QueuedTransaction ,
127+ token ?: string ,
119128) : Promise < SentTransaction | ErroredTransaction | null > => {
120129 assert ( queuedTransaction . isUserOp ) ;
121130
@@ -180,61 +189,97 @@ const _sendUserOp = async (
180189 } ;
181190 }
182191
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- }
192+ // Part 1: Prepare the userop
193+ // Step 1: Get factory address
194+ let accountFactoryAddress : Address | undefined ;
195+
196+ if ( userProvidedAccountFactoryAddress ) {
197+ accountFactoryAddress = userProvidedAccountFactoryAddress ;
198+ } else {
199+ const smartAccountContract = getContract ( {
200+ client : thirdwebClient ,
201+ chain,
202+ address : accountAddress ,
203+ } ) ;
204+
205+ try {
206+ const onchainAccountFactoryAddress = await readContract ( {
207+ contract : smartAccountContract ,
208+ method : "function factory() view returns (address)" ,
209+ params : [ ] ,
210+ } ) ;
211+ accountFactoryAddress = getAddress ( onchainAccountFactoryAddress ) ;
212+ } catch ( error ) {
213+ const errorMessage = `${ wrapError ( error , "RPC" ) . message } Failed to find factory address for account` ;
214+ const erroredTransaction : ErroredTransaction = {
215+ ...queuedTransaction ,
216+ status : "errored" ,
217+ errorMessage,
218+ } ;
219+ job . log ( `Failed to get account factory address: ${ errorMessage } ` ) ;
220+ return erroredTransaction ;
207221 }
222+ }
208223
209- const transactions = queuedTransaction . batchOperations
210- ? queuedTransaction . batchOperations . map ( ( op ) => ( {
211- ...op ,
212- chain,
224+ // Step 2: Get entrypoint address
225+ let entrypointAddress : Address | undefined ;
226+ if ( userProvidedEntrypointAddress ) {
227+ entrypointAddress = queuedTransaction . entrypointAddress ;
228+ } else {
229+ try {
230+ entrypointAddress = await getEntrypointFromFactory (
231+ adminAccount . address ,
232+ thirdwebClient ,
233+ chain ,
234+ ) ;
235+ } catch ( error ) {
236+ const errorMessage = `${ wrapError ( error , "RPC" ) . message } Failed to find entrypoint address for account factory` ;
237+ const erroredTransaction : ErroredTransaction = {
238+ ...queuedTransaction ,
239+ status : "errored" ,
240+ errorMessage,
241+ } ;
242+ job . log (
243+ `Failed to find entrypoint address for account factory: ${ errorMessage } ` ,
244+ ) ;
245+ return erroredTransaction ;
246+ }
247+ }
248+
249+ // Step 3: Transform transactions for userop
250+ const transactions = queuedTransaction . batchOperations
251+ ? queuedTransaction . batchOperations . map ( ( op ) => ( {
252+ ...op ,
253+ chain,
254+ client : thirdwebClient ,
255+ } ) )
256+ : [
257+ {
213258 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 ,
259+ chain,
260+ data : queuedTransaction . data ,
261+ value : queuedTransaction . value ,
262+ ...overrides , // gas-overrides
263+ to : getChecksumAddress ( toAddress ) ,
264+ } ,
265+ ] ;
266+
267+ // Step 4: Prepare userop
268+ let unsignedUserOp : VersionedUserOp | undefined ;
269+
270+ try {
271+ unsignedUserOp = await prepareUserOp ( {
228272 transactions,
229273 adminAccount,
274+ client : thirdwebClient ,
230275 smartWalletOptions : {
231276 chain,
232277 sponsorGas : true ,
233- factoryAddress : accountFactoryAddress ,
278+ factoryAddress : accountFactoryAddress , // from step 1
234279 overrides : {
235280 accountAddress,
236281 accountSalt,
237- entrypointAddress : userProvidedEntrypointAddress ,
282+ entrypointAddress, // from step 2
238283 // TODO: let user pass entrypoint address for 0.7 support
239284 } ,
240285 } ,
@@ -243,7 +288,7 @@ const _sendUserOp = async (
243288 // until the previous userop for the same account is mined
244289 // we don't want this behavior in the engine context
245290 waitForDeployment : false ,
246- } ) ) as UserOperation ; // TODO support entrypoint v0.7 accounts
291+ } ) ;
247292 } catch ( error ) {
248293 const errorMessage = wrapError ( error , "Bundler" ) . message ;
249294 const erroredTransaction : ErroredTransaction = {
@@ -255,16 +300,71 @@ const _sendUserOp = async (
255300 return erroredTransaction ;
256301 }
257302
258- job . log ( `Populated userOp: ${ stringify ( signedUserOp ) } ` ) ;
303+ // Handle if `maxFeePerGas` is overridden.
304+ // Set it if the transaction will be sent, otherwise delay the job.
305+ if (
306+ typeof overrides ?. maxFeePerGas !== "undefined" &&
307+ unsignedUserOp . maxFeePerGas
308+ ) {
309+ if ( overrides . maxFeePerGas > unsignedUserOp . maxFeePerGas ) {
310+ unsignedUserOp . maxFeePerGas = overrides . maxFeePerGas ;
311+ } else {
312+ const retryAt = _minutesFromNow ( 5 ) ;
313+ job . log (
314+ `Override gas fee (${ overrides . maxFeePerGas } ) is lower than onchain fee (${ unsignedUserOp . maxFeePerGas } ). Delaying job until ${ retryAt } .` ,
315+ ) ;
316+ // token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying
317+ await job . moveToDelayed ( retryAt . getTime ( ) , token ) ;
318+ // throwing delayed error is required to notify bullmq worker not to complete or fail the job
319+ throw new DelayedError ( "Delaying job due to gas fee override" ) ;
320+ }
321+ }
259322
260- const userOpHash = await bundleUserOp ( {
261- userOp : signedUserOp ,
262- options : {
323+ // Part 2: Sign the userop
324+ let signedUserOp : VersionedUserOp | undefined ;
325+ try {
326+ signedUserOp = await signUserOp ( {
263327 client : thirdwebClient ,
264328 chain,
265- entrypointAddress : userProvidedEntrypointAddress ,
266- } ,
267- } ) ;
329+ adminAccount,
330+ entrypointAddress,
331+ userOp : unsignedUserOp ,
332+ } ) ;
333+ } catch ( error ) {
334+ const errorMessage = `${ wrapError ( error , "Bundler" ) . message } Failed to sign prepared userop` ;
335+ const erroredTransaction : ErroredTransaction = {
336+ ...queuedTransaction ,
337+ status : "errored" ,
338+ errorMessage,
339+ } ;
340+ job . log ( `Failed to sign userop: ${ errorMessage } ` ) ;
341+ return erroredTransaction ;
342+ }
343+
344+ job . log ( `Populated and signed userOp: ${ stringify ( signedUserOp ) } ` ) ;
345+
346+ // Finally: bundle the userop
347+ let userOpHash : Hex ;
348+
349+ try {
350+ userOpHash = await bundleUserOp ( {
351+ userOp : signedUserOp ,
352+ options : {
353+ client : thirdwebClient ,
354+ chain,
355+ entrypointAddress : userProvidedEntrypointAddress ,
356+ } ,
357+ } ) ;
358+ } catch ( error ) {
359+ const errorMessage = `${ wrapError ( error , "Bundler" ) . message } Failed to bundle userop` ;
360+ const erroredTransaction : ErroredTransaction = {
361+ ...queuedTransaction ,
362+ status : "errored" ,
363+ errorMessage,
364+ } ;
365+ job . log ( `Failed to bundle userop: ${ errorMessage } ` ) ;
366+ return erroredTransaction ;
367+ }
268368
269369 return {
270370 ...queuedTransaction ,
@@ -283,6 +383,7 @@ const _sendUserOp = async (
283383const _sendTransaction = async (
284384 job : Job ,
285385 queuedTransaction : QueuedTransaction ,
386+ token ?: string ,
286387) : Promise < SentTransaction | ErroredTransaction | null > => {
287388 assert ( ! queuedTransaction . isUserOp ) ;
288389
@@ -372,8 +473,8 @@ const _sendTransaction = async (
372473 job . log (
373474 `Override gas fee (${ overrides . maxFeePerGas } ) is lower than onchain fee (${ populatedTransaction . maxFeePerGas } ). Delaying job until ${ retryAt } .` ,
374475 ) ;
375- await job . moveToDelayed ( retryAt . getTime ( ) ) ;
376- return null ;
476+ await job . moveToDelayed ( retryAt . getTime ( ) , token ) ;
477+ throw new DelayedError ( "Delaying job due to gas fee override" ) ;
377478 }
378479 }
379480
@@ -646,6 +747,28 @@ export function _updateGasFees(
646747 return updated ;
647748}
648749
750+ async function getEntrypointFromFactory (
751+ factoryAddress : string ,
752+ client : ThirdwebClient ,
753+ chain : Chain ,
754+ ) {
755+ const factoryContract = getContract ( {
756+ address : factoryAddress ,
757+ client,
758+ chain,
759+ } ) ;
760+ try {
761+ const entrypointAddress = await readContract ( {
762+ contract : factoryContract ,
763+ method : "function entrypoint() public view returns (address)" ,
764+ params : [ ] ,
765+ } ) ;
766+ return entrypointAddress ;
767+ } catch {
768+ return undefined ;
769+ }
770+ }
771+
649772// Must be explicitly called for the worker to run on this host.
650773export const initSendTransactionWorker = ( ) => {
651774 const _worker = new Worker ( SendTransactionQueue . q . name , handler , {
0 commit comments