1
1
import {
2
2
baseMainnet ,
3
3
baseMainnetClient ,
4
- config ,
5
4
sendAccountFactoryAbi ,
6
5
sendAccountFactoryAddress ,
7
6
sendTokenAbi ,
@@ -10,22 +9,25 @@ import {
10
9
} from '@my/wagmi'
11
10
import { base16 } from '@scure/base'
12
11
import { TRPCError } from '@trpc/server'
13
- import { waitForTransactionReceipt } from '@wagmi/core'
14
12
import { base16Regex } from 'app/utils/base16Regex'
13
+ import { hexToBytea } from 'app/utils/hexToBytea'
15
14
import { COSEECDHAtoXY } from 'app/utils/passkeys'
16
- import { USEROP_SALT , getSendAccountCreateArgs , entrypoint } from 'app/utils/userop'
15
+ import { supabaseAdmin } from 'app/utils/supabase/admin'
16
+ import { throwIf } from 'app/utils/throwIf'
17
+ import { USEROP_SALT , entrypoint , getSendAccountCreateArgs } from 'app/utils/userop'
17
18
import debug from 'debug'
19
+ import PQueue from 'p-queue'
18
20
import { getSenderAddress } from 'permissionless'
19
21
import {
20
22
concat ,
21
23
createWalletClient ,
22
24
encodeFunctionData ,
25
+ getAbiItem ,
23
26
http ,
24
27
maxUint256 ,
25
- zeroAddress ,
26
28
publicActions ,
27
- getAbiItem ,
28
29
withRetry ,
30
+ zeroAddress ,
29
31
} from 'viem'
30
32
import { privateKeyToAccount } from 'viem/accounts'
31
33
import { z } from 'zod'
@@ -43,6 +45,9 @@ const sendAccountFactoryClient = createWalletClient({
43
45
transport : http ( baseMainnetClient . transport . url ) ,
44
46
} ) . extend ( publicActions )
45
47
48
+ // nonce storage to avoid nonce conflicts
49
+ const nonceQueue = new PQueue ( { concurrency : 1 } )
50
+
46
51
export const sendAccountRouter = createTRPCRouter ( {
47
52
create : protectedProcedure
48
53
. input (
@@ -141,68 +146,85 @@ export const sendAccountRouter = createTRPCRouter({
141
146
} )
142
147
}
143
148
144
- // @todo ensure address is not already deployed
145
- // throw new TRPCError({
146
- // code: 'INTERNAL_SERVER_ERROR',
147
- // message: 'Address already deployed',
148
- // })
149
+ const contractDeployed = await sendAccountFactoryClient . getBytecode ( {
150
+ address : senderAddress ,
151
+ } )
152
+ log ( 'createAccount' , 'contractDeployed' , contractDeployed )
153
+ if ( contractDeployed ) {
154
+ throw new TRPCError ( {
155
+ code : 'INTERNAL_SERVER_ERROR' ,
156
+ message : 'Address already deployed' ,
157
+ } )
158
+ }
149
159
150
- await withRetry (
160
+ const initCalls = [
161
+ // approve USDC to paymaster
162
+ {
163
+ dest : usdcAddress [ baseMainnetClient . chain . id ] ,
164
+ value : 0n ,
165
+ data : encodeFunctionData ( {
166
+ abi : sendTokenAbi ,
167
+ functionName : 'approve' ,
168
+ args : [ tokenPaymasterAddress [ baseMainnetClient . chain . id ] , maxUint256 ] ,
169
+ } ) ,
170
+ } ,
171
+ ]
172
+
173
+ const { request } = await sendAccountFactoryClient
174
+ . simulateContract ( {
175
+ address : sendAccountFactoryAddress [ sendAccountFactoryClient . chain . id ] ,
176
+ abi : sendAccountFactoryAbi ,
177
+ functionName : 'createAccount' ,
178
+ args : [
179
+ keySlot , // key slot
180
+ xyPubKey , // public key
181
+ initCalls , // init calls
182
+ USEROP_SALT , // salt
183
+ ] ,
184
+ value : 0n ,
185
+ } )
186
+ . catch ( ( e ) => {
187
+ log ( 'createAccount' , 'simulateContract' , e )
188
+ throw e
189
+ } )
190
+
191
+ const hash = await withRetry (
151
192
async function createAccount ( ) {
152
- log ( 'createAccount' , 'start' )
153
- const initCalls = [
154
- // approve USDC to paymaster
155
- {
156
- dest : usdcAddress [ baseMainnetClient . chain . id ] ,
157
- value : 0n ,
158
- data : encodeFunctionData ( {
159
- abi : sendTokenAbi ,
160
- functionName : 'approve' ,
161
- args : [ tokenPaymasterAddress [ baseMainnetClient . chain . id ] , maxUint256 ] ,
162
- } ) ,
163
- } ,
164
- ]
165
-
166
- const { request } = await sendAccountFactoryClient
167
- . simulateContract ( {
168
- address : sendAccountFactoryAddress [ sendAccountFactoryClient . chain . id ] ,
169
- abi : sendAccountFactoryAbi ,
170
- functionName : 'createAccount' ,
171
- args : [
172
- keySlot , // key slot
173
- xyPubKey , // public key
174
- initCalls , // init calls
175
- USEROP_SALT , // salt
176
- ] ,
177
- value : 0n ,
178
- } )
179
- . catch ( ( e ) => {
180
- log ( 'createAccount' , 'simulateContract' , e )
181
- throw e
182
- } )
183
-
184
- log ( 'createAccount' , 'tx request' )
185
-
186
- const hash = await sendAccountFactoryClient . writeContract ( request ) . catch ( ( e ) => {
187
- log ( 'createAccount' , 'writeContract' , e )
188
- throw e
193
+ const hash = await nonceQueue . add ( async ( ) => {
194
+ log ( 'createAccount queue' , 'start' )
195
+ const nonce = await sendAccountFactoryClient
196
+ . getTransactionCount ( {
197
+ address : account . address ,
198
+ blockTag : 'pending' ,
199
+ } )
200
+ . catch ( ( e ) => {
201
+ log ( 'createAccount queue' , 'getTransactionCount' , e )
202
+ throw e
203
+ } )
204
+ log ( 'createAccount queue' , 'tx request' , `nonce=${ nonce } ` )
205
+ const hash = await sendAccountFactoryClient
206
+ . writeContract ( {
207
+ ...request ,
208
+ nonce : nonce ,
209
+ } )
210
+ . catch ( ( e ) => {
211
+ log ( 'createAccount queue' , 'writeContract' , e )
212
+ throw e
213
+ } )
214
+
215
+ log ( 'createAccount queue' , `hash=${ hash } ` )
216
+ return hash
189
217
} )
190
218
191
219
log ( 'createAccount' , `hash=${ hash } ` )
192
220
193
- await waitForTransactionReceipt ( config , {
194
- chainId : baseMainnetClient . chain . id ,
195
- hash,
196
- } ) . catch ( ( e ) => {
197
- log ( 'createAccount' , 'waitForTransactionReceipt' , e )
198
- throw e
199
- } )
221
+ return hash
200
222
} ,
201
223
{
202
224
retryCount : 20 ,
203
225
delay : ( { count, error } ) => {
204
226
const backoff = 500 + Math . random ( ) * 100 // add some randomness to the backoff
205
- log ( `delay count=${ count } backoff=${ backoff } error=${ error } ` )
227
+ log ( `createAccount delay count=${ count } backoff=${ backoff } error=${ error } ` )
206
228
return backoff
207
229
} ,
208
230
shouldRetry ( { count, error } ) {
@@ -215,10 +237,56 @@ export const sendAccountRouter = createTRPCRouter({
215
237
} ,
216
238
}
217
239
) . catch ( ( e ) => {
218
- log ( 'waitForTransactionReceipt' , e )
240
+ log ( 'createAccount failed' , e )
241
+ console . error ( 'createAccount failed' , e )
219
242
throw new TRPCError ( { code : 'INTERNAL_SERVER_ERROR' , message : e . message } )
220
243
} )
221
244
245
+ if ( ! hash ) {
246
+ log ( 'createAccount' , 'hash is null' )
247
+ throw new TRPCError ( {
248
+ code : 'INTERNAL_SERVER_ERROR' ,
249
+ message : 'Failed to create send account' ,
250
+ } )
251
+ }
252
+
253
+ await withRetry (
254
+ async function waitForTransactionReceipt ( ) {
255
+ const { count, error } = await supabaseAdmin
256
+ . from ( 'send_account_created' )
257
+ . select ( '*' , { count : 'exact' , head : true } )
258
+ . eq ( 'tx_hash' , hexToBytea ( hash as `0x${string } `) )
259
+ . eq ( 'account' , hexToBytea ( senderAddress ) )
260
+ . single ( )
261
+ throwIf ( error )
262
+ log ( 'waitForTransactionReceipt' , `count=${ count } ` )
263
+ return count
264
+ } ,
265
+ {
266
+ retryCount : 20 ,
267
+ delay : ( { count, error } ) => {
268
+ const backoff = 500 + Math . random ( ) * 100 // add some randomness to the backoff
269
+ log ( `waitForTransactionReceipt delay count=${ count } backoff=${ backoff } ` , error )
270
+ return backoff
271
+ } ,
272
+ shouldRetry ( { count, error } ) {
273
+ // @todo handle other errors like balance not enough, invalid nonce, etc
274
+ console . error ( 'waitForTransactionReceipt failed' , count , error )
275
+ if ( error . message . includes ( 'Failed to create send account' ) ) {
276
+ return false
277
+ }
278
+ return true
279
+ } ,
280
+ }
281
+ ) . catch ( ( e ) => {
282
+ log ( 'waitForTransactionReceipt failed' , e )
283
+ console . error ( 'waitForTransactionReceipt failed' , e )
284
+ throw new TRPCError ( {
285
+ code : 'INTERNAL_SERVER_ERROR' ,
286
+ message : e . message ?? 'Failed waiting for transaction receipt' ,
287
+ } )
288
+ } )
289
+
222
290
const { error } = await supabase . rpc ( 'create_send_account' , {
223
291
send_account : {
224
292
address : senderAddress ,
0 commit comments