Skip to content

Commit 392d655

Browse files
committed
refactor insertTransaction
1 parent 5b31cfc commit 392d655

File tree

1 file changed

+197
-90
lines changed

1 file changed

+197
-90
lines changed

src/utils/transaction/insertTransaction.ts

Lines changed: 197 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { StatusCodes } from "http-status-codes";
22
import { randomUUID } from "node:crypto";
33
import { TransactionDB } from "../../db/transactions/db";
44
import {
5-
ParsedWalletDetails,
65
getWalletDetails,
76
isSmartBackendWallet,
7+
type ParsedWalletDetails,
8+
type SmartBackendWalletDetails,
89
} from "../../db/wallets/getWalletDetails";
910
import { doesChainSupportService } from "../../lib/chain/chain-capabilities";
1011
import { createCustomError } from "../../server/middleware/error";
@@ -15,121 +16,225 @@ import { reportUsage } from "../usage";
1516
import { doSimulateTransaction } from "./simulateQueuedTransaction";
1617
import 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+
1851
interface 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

Comments
 (0)