@@ -2,7 +2,10 @@ import assert from "node:assert";
2
2
import { type Job , type Processor , Worker } from "bullmq" ;
3
3
import superjson from "superjson" ;
4
4
import {
5
+ type Address ,
6
+ type Chain ,
5
7
type Hex ,
8
+ type ThirdwebClient ,
6
9
getAddress ,
7
10
getContract ,
8
11
readContract ,
@@ -13,9 +16,9 @@ import { getChainMetadata } from "thirdweb/chains";
13
16
import { isZkSyncChain , stringify } from "thirdweb/utils" ;
14
17
import type { Account } from "thirdweb/wallets" ;
15
18
import {
16
- type UserOperation ,
17
19
bundleUserOp ,
18
- createAndSignUserOp ,
20
+ prepareUserOp ,
21
+ signUserOp ,
19
22
smartWallet ,
20
23
} from "thirdweb/wallets/smart" ;
21
24
import { getContractAddress } from "viem" ;
@@ -60,6 +63,8 @@ import {
60
63
SendTransactionQueue ,
61
64
} from "../queues/send-transaction-queue" ;
62
65
66
+ type VersionedUserOp = Awaited < ReturnType < typeof prepareUserOp > > ;
67
+
63
68
/**
64
69
* Submit a transaction to RPC (EOA transactions) or bundler (userOps).
65
70
*
@@ -180,61 +185,97 @@ const _sendUserOp = async (
180
185
} ;
181
186
}
182
187
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 ;
207
217
}
218
+ }
208
219
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
+ {
213
254
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 ( {
228
268
transactions,
229
269
adminAccount,
270
+ client : thirdwebClient ,
230
271
smartWalletOptions : {
231
272
chain,
232
273
sponsorGas : true ,
233
- factoryAddress : accountFactoryAddress ,
274
+ factoryAddress : accountFactoryAddress , // from step 1
234
275
overrides : {
235
276
accountAddress,
236
277
accountSalt,
237
- entrypointAddress : userProvidedEntrypointAddress ,
278
+ entrypointAddress, // from step 2
238
279
// TODO: let user pass entrypoint address for 0.7 support
239
280
} ,
240
281
} ,
@@ -243,7 +284,7 @@ const _sendUserOp = async (
243
284
// until the previous userop for the same account is mined
244
285
// we don't want this behavior in the engine context
245
286
waitForDeployment : false ,
246
- } ) ) as UserOperation ; // TODO support entrypoint v0.7 accounts
287
+ } ) ;
247
288
} catch ( error ) {
248
289
const errorMessage = wrapError ( error , "Bundler" ) . message ;
249
290
const erroredTransaction : ErroredTransaction = {
@@ -255,16 +296,66 @@ const _sendUserOp = async (
255
296
return erroredTransaction ;
256
297
}
257
298
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
+ }
259
313
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 ( {
263
318
client : thirdwebClient ,
264
319
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
+ }
268
359
269
360
return {
270
361
...queuedTransaction ,
@@ -646,6 +737,28 @@ export function _updateGasFees(
646
737
return updated ;
647
738
}
648
739
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
+
649
762
// Must be explicitly called for the worker to run on this host.
650
763
export const initSendTransactionWorker = ( ) => {
651
764
const _worker = new Worker ( SendTransactionQueue . q . name , handler , {
0 commit comments