@@ -2,7 +2,10 @@ import assert from "node:assert";
22import { 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,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.
650763export const initSendTransactionWorker = ( ) => {
651764 const _worker = new Worker ( SendTransactionQueue . q . name , handler , {
0 commit comments