@@ -2,9 +2,10 @@ import { StatusCodes } from "http-status-codes";
22import { randomUUID } from "node:crypto" ;
33import { TransactionDB } from "../../db/transactions/db" ;
44import {
5- ParsedWalletDetails ,
65 getWalletDetails ,
76 isSmartBackendWallet ,
7+ type ParsedWalletDetails ,
8+ type SmartBackendWalletDetails ,
89} from "../../db/wallets/getWalletDetails" ;
910import { doesChainSupportService } from "../../lib/chain/chain-capabilities" ;
1011import { createCustomError } from "../../server/middleware/error" ;
@@ -15,121 +16,225 @@ import { reportUsage } from "../usage";
1516import { doSimulateTransaction } from "./simulateQueuedTransaction" ;
1617import type { InsertedTransaction , QueuedTransaction } from "./types" ;
1718
19+ /**
20+ * Transaction Processing Cases & SDK Compatibility Layer
21+ *
22+ * This code handles transaction processing across two SDK versions (v4 and v5) and two wallet types
23+ * (smart backend wallets and regular wallets). Each case needs different handling:
24+ *
25+ * Case 1: V5 SDK with Smart Backend Wallet
26+ * - 'from' address is the smart backend wallet address
27+ * - accountAddress must NOT be set (SDK shouldn't allow interacting with other accounts)
28+ * - Requires transformation:
29+ * * from -> becomes signer address (from wallet.accountSignerAddress)
30+ * * original from -> becomes account address
31+ * * to -> becomes target
32+ * * set isUserOp true
33+ * * add accountFactoryAddress and entrypoint from wallet details
34+ *
35+ * Case 2: V4 SDK with Smart Backend Wallet
36+ * - accountAddress is set to the smart backend wallet address
37+ * - 'from' address not in wallet DB
38+ * - Requires transformation:
39+ * * add entrypoint and accountFactory addresses from wallet details
40+ *
41+ * Case 3: V5 SDK with Regular Wallet
42+ * - 'from' address is a regular wallet
43+ * - No transformation needed, just add wallet type
44+ * - May optionally have accountAddress for sending via a smart account
45+ *
46+ * Case 4: V4 SDK with Regular Wallet
47+ * - Similar to Case 3
48+ * - Only difference is how we detect wallet (via accountAddress)
49+ */
50+
1851interface InsertTransactionData {
1952 insertedTransaction : InsertedTransaction ;
2053 idempotencyKey ?: string ;
2154 shouldSimulate ?: boolean ;
2255}
2356
57+ interface TransactionContext {
58+ processedTransaction : QueuedTransaction ;
59+ }
60+
61+ type SdkVersion = "v4" | "v5" ;
62+
63+ interface ResolvedWalletDetails {
64+ sdkVersion : SdkVersion ;
65+ walletDetails : ParsedWalletDetails ;
66+ }
67+
68+ const validateSmartBackendWalletChainSupport = async ( chainId : number ) => {
69+ if ( ! ( await doesChainSupportService ( chainId , "account-abstraction" ) ) ) {
70+ throw createCustomError (
71+ "Chain does not support smart backend wallets" ,
72+ StatusCodes . BAD_REQUEST ,
73+ "SBW_CHAIN_NOT_SUPPORTED" ,
74+ ) ;
75+ }
76+ } ;
77+
2478/**
25- * Enqueue a transaction to be submitted onchain.
26- *
27- * @param args
28- * @returns queueId
79+ * Transform transaction for Case 1 (V5 Smart Backend Wallet)
80+ * Type guard ensures walletDetails has required smart wallet properties
2981 */
30- export const insertTransaction = async (
31- args : InsertTransactionData ,
32- ) : Promise < string > => {
33- const { insertedTransaction, idempotencyKey, shouldSimulate = false } = args ;
82+ const transformV5SmartBackendWallet = async (
83+ transaction : QueuedTransaction ,
84+ walletDetails : SmartBackendWalletDetails , // Note: narrowed type
85+ ) : Promise < QueuedTransaction > => {
86+ await validateSmartBackendWalletChainSupport ( transaction . chainId ) ;
3487
35- // The queueId uniquely represents an enqueued transaction.
36- // It's also used as the idempotency key (default = no idempotence).
37- let queueId : string = randomUUID ( ) ;
38- if ( idempotencyKey ) {
39- queueId = idempotencyKey ;
40- if ( await TransactionDB . exists ( queueId ) ) {
41- // No-op. Return the existing queueId.
42- return queueId ;
43- }
88+ if ( transaction . accountAddress ) {
89+ throw createCustomError (
90+ "Smart backend wallets do not support interacting with other smart accounts" ,
91+ StatusCodes . BAD_REQUEST ,
92+ "INVALID_SMART_BACKEND_WALLET_INTERACTION" ,
93+ ) ;
4494 }
4595
46- // Get wallet details. For EOA and SBW (v5 endpoints), `from` should return a valid backend wallet.
47- // For SBW (v4 endpoints), `accountAddress` should return a valid backend wallet.
48- // Else the provided details are incorrect (user error).
49- let walletDetails : ParsedWalletDetails | undefined ;
50- let isSmartBackendWalletV4 = false ;
96+ return {
97+ ...transaction ,
98+ isUserOp : true ,
99+ signerAddress : walletDetails . accountSignerAddress ,
100+ from : walletDetails . accountSignerAddress ,
101+ accountAddress : transaction . from , // Original 'from' becomes the account
102+ target : transaction . to ,
103+ accountFactoryAddress : walletDetails . accountFactoryAddress ?? undefined ,
104+ entrypointAddress : walletDetails . entrypointAddress ?? undefined ,
105+ walletType : walletDetails . type ,
106+ } ;
107+ } ;
108+
109+ /**
110+ * Transform transaction for Case 2 (V4 Smart Backend Wallet)
111+ * Type guard ensures walletDetails has required smart wallet properties
112+ */
113+ const transformV4SmartBackendWallet = async (
114+ transaction : QueuedTransaction ,
115+ walletDetails : SmartBackendWalletDetails , // Note: narrowed type
116+ ) : Promise < QueuedTransaction > => {
117+ await validateSmartBackendWalletChainSupport ( transaction . chainId ) ;
118+
119+ return {
120+ ...transaction ,
121+ entrypointAddress : walletDetails . entrypointAddress ?? undefined ,
122+ accountFactoryAddress : walletDetails . accountFactoryAddress ?? undefined ,
123+ walletType : walletDetails . type ,
124+ } ;
125+ } ;
126+
127+ /**
128+ * Try to resolve wallet details, determining if we're in V4 or V5 case
129+ * For V5: wallet details should be found from 'from' address (Cases 1 & 3)
130+ * For V4: wallet details are found from accountAddress (Cases 2 & 4)
131+ */
132+ const resolveWalletDetails = async (
133+ transaction : QueuedTransaction ,
134+ ) : Promise < ResolvedWalletDetails > => {
135+ // Try V5 path first (Cases 1 & 3)
51136 try {
52- walletDetails = await getWalletDetails ( {
53- walletAddress : insertedTransaction . from ,
137+ const walletDetails = await getWalletDetails ( {
138+ walletAddress : transaction . from ,
54139 } ) ;
55- } catch { }
56- if ( ! walletDetails && insertedTransaction . accountAddress ) {
57- try {
58- walletDetails = await getWalletDetails ( {
59- walletAddress : insertedTransaction . accountAddress ,
60- } ) ;
61- isSmartBackendWalletV4 = true ;
62- } catch { }
140+ return { sdkVersion : "v5" , walletDetails } ;
141+ } catch { } // Silently handle V5 failure
142+
143+ // If primary address fails and no accountAddress, we can't proceed
144+ if ( ! transaction . accountAddress ) {
145+ throw createCustomError (
146+ "Account not found" ,
147+ StatusCodes . BAD_REQUEST ,
148+ "ACCOUNT_NOT_FOUND" ,
149+ ) ;
63150 }
64- if ( ! walletDetails ) {
151+
152+ // Try V4 path (Cases 2 & 4)
153+ try {
154+ const walletDetails = await getWalletDetails ( {
155+ walletAddress : transaction . accountAddress ,
156+ } ) ;
157+ return { sdkVersion : "v4" , walletDetails } ;
158+ } catch {
65159 throw createCustomError (
66160 "Account not found" ,
67161 StatusCodes . BAD_REQUEST ,
68162 "ACCOUNT_NOT_FOUND" ,
69163 ) ;
70164 }
165+ } ;
71166
72- let queuedTransaction : QueuedTransaction = {
73- ...insertedTransaction ,
74- status : "queued" ,
75- queueId,
76- queuedAt : new Date ( ) ,
77- resendCount : 0 ,
167+ /**
168+ * Handle both transformation cases and add wallet type for non-transformed cases
169+ * Uses type guard to ensure smart wallet properties are available when needed
170+ */
171+ const detectAndTransformTransaction = async (
172+ transaction : QueuedTransaction ,
173+ ) : Promise < TransactionContext > => {
174+ const { sdkVersion, walletDetails } = await resolveWalletDetails ( transaction ) ;
78175
79- walletType : walletDetails . type ,
80- from : getChecksumAddress ( insertedTransaction . from ) ,
81- to : getChecksumAddress ( insertedTransaction . to ) ,
82- signerAddress : getChecksumAddress ( insertedTransaction . signerAddress ) ,
83- accountAddress : getChecksumAddress ( insertedTransaction . accountAddress ) ,
84- accountSalt : insertedTransaction . accountSalt ,
85- target : getChecksumAddress ( insertedTransaction . target ) ,
86- sender : getChecksumAddress ( insertedTransaction . sender ) ,
87- value : insertedTransaction . value ?? 0n ,
88- } ;
176+ // isSmartBackendWallet is a type guard that narrows walletDetails
177+ if ( ! isSmartBackendWallet ( walletDetails ) ) {
178+ // Cases 3 & 4: Regular wallet cases just need wallet type
179+ return {
180+ processedTransaction : {
181+ ... transaction ,
182+ walletType : walletDetails . type ,
183+ } ,
184+ } ;
185+ }
89186
90- // Handle smart backend wallets details.
91- if ( isSmartBackendWallet ( walletDetails ) ) {
92- if (
93- ! ( await doesChainSupportService (
94- queuedTransaction . chainId ,
95- "account-abstraction" ,
96- ) )
97- ) {
98- throw createCustomError (
99- `Smart backend wallets do not support chain ${ queuedTransaction . chainId } .` ,
100- StatusCodes . BAD_REQUEST ,
101- "INVALID_SMART_BACKEND_WALLET_TRANSACTION" ,
102- ) ;
103- }
187+ // walletDetails is now narrowed to SmartBackendWalletDetails
188+ const processedTransaction = await ( sdkVersion === "v5"
189+ ? transformV5SmartBackendWallet ( transaction , walletDetails )
190+ : transformV4SmartBackendWallet ( transaction , walletDetails ) ) ;
104191
105- queuedTransaction = {
106- ...queuedTransaction ,
107- accountFactoryAddress : walletDetails . accountFactoryAddress ?? undefined ,
108- entrypointAddress : walletDetails . entrypointAddress ?? undefined ,
109- } ;
192+ return { processedTransaction } ;
193+ } ;
110194
111- if ( ! isSmartBackendWalletV4 ) {
112- if ( queuedTransaction . accountAddress ) {
113- // Disallow smart backend wallets from sending userOps.
114- throw createCustomError (
115- "Smart backend wallets do not support sending transactions with other smart accounts" ,
116- StatusCodes . BAD_REQUEST ,
117- "INVALID_SMART_BACKEND_WALLET_TRANSACTION" ,
118- ) ;
119- }
120-
121- queuedTransaction = {
122- ...queuedTransaction ,
123- isUserOp : true ,
124- signerAddress : walletDetails . accountSignerAddress ,
125- from : walletDetails . accountSignerAddress ,
126- accountAddress : queuedTransaction . from ,
127- target : queuedTransaction . to ,
128- } ;
129- }
195+ const normalizeAddresses = (
196+ transaction : InsertedTransaction ,
197+ ) : QueuedTransaction => ( {
198+ ...transaction ,
199+ status : "queued" ,
200+ queueId : "" , // Will be set later
201+ queuedAt : new Date ( ) ,
202+ resendCount : 0 ,
203+ from : getChecksumAddress ( transaction . from ) ,
204+ to : getChecksumAddress ( transaction . to ) ,
205+ signerAddress : getChecksumAddress ( transaction . signerAddress ) ,
206+ accountAddress : getChecksumAddress ( transaction . accountAddress ) ,
207+ accountSalt : transaction . accountSalt ,
208+ target : getChecksumAddress ( transaction . target ) ,
209+ sender : getChecksumAddress ( transaction . sender ) ,
210+ value : transaction . value ?? 0n ,
211+ walletType : "local" , // Will be set later
212+ } ) ;
213+
214+ /**
215+ * Enqueue a transaction to be submitted onchain.
216+ */
217+ export const insertTransaction = async (
218+ args : InsertTransactionData ,
219+ ) : Promise < string > => {
220+ const { insertedTransaction, idempotencyKey, shouldSimulate = false } = args ;
221+
222+ // Handle idempotency
223+ const queueId : string = idempotencyKey ?? randomUUID ( ) ;
224+ if ( idempotencyKey && ( await TransactionDB . exists ( queueId ) ) ) {
225+ return queueId ;
130226 }
131227
132- // Simulate the transaction.
228+ // Normalize addresses and create initial transaction
229+ let queuedTransaction = normalizeAddresses ( insertedTransaction ) ;
230+ queuedTransaction . queueId = queueId ;
231+
232+ // Detect case and transform transaction accordingly
233+ const { processedTransaction } =
234+ await detectAndTransformTransaction ( queuedTransaction ) ;
235+ queuedTransaction = processedTransaction ;
236+
237+ // Simulate if requested
133238 if ( shouldSimulate ) {
134239 const error = await doSimulateTransaction ( queuedTransaction ) ;
135240 if ( error ) {
@@ -141,13 +246,15 @@ export const insertTransaction = async (
141246 }
142247 }
143248
249+ // Queue transaction
144250 await TransactionDB . set ( queuedTransaction ) ;
145251 await SendTransactionQueue . add ( {
146252 queueId : queuedTransaction . queueId ,
147253 resendCount : 0 ,
148254 } ) ;
149- reportUsage ( [ { action : "queue_tx" , input : queuedTransaction } ] ) ;
150255
256+ // Report metrics
257+ reportUsage ( [ { action : "queue_tx" , input : queuedTransaction } ] ) ;
151258 recordMetrics ( {
152259 event : "transaction_queued" ,
153260 params : {
0 commit comments