@@ -33,9 +33,53 @@ import { SettlementCache } from "../../settlement-cache";
3333import type { FacilitatorSvmSigner } from "../../signer" ;
3434import type { ExactSvmPayloadV2 } from "../../types" ;
3535import { decodeTransactionFromPayload , getTokenPayerFromTransaction } from "../../utils" ;
36+ import { verifySmartWalletTransaction } from "./smartWalletVerification" ;
37+
38+ /**
39+ * Configuration options for ExactSvmScheme.
40+ */
41+ export type ExactSvmSchemeOptions = {
42+ /**
43+ * Enable simulation-based smart wallet verification.
44+ * When enabled, transactions rejected by the static validation path
45+ * (unknown programs, wrong instruction count) are re-verified using
46+ * simulation inner instruction analysis. Works for any smart wallet
47+ * program (Squads, Swig, SPL Governance, etc.) without per-wallet parsers.
48+ *
49+ * Default: false (only standard wallet transactions are accepted)
50+ */
51+ enableSmartWalletVerification ?: boolean ;
52+
53+ /**
54+ * Maximum compute units allowed for smart wallet transactions.
55+ * Smart wallet programs need more CU for CPI overhead.
56+ * Only applies when enableSmartWalletVerification is true.
57+ *
58+ * Default: 400,000
59+ */
60+ smartWalletMaxComputeUnits ?: number ;
61+
62+ /**
63+ * Maximum priority fee in microlamports for smart wallet transactions.
64+ * Only applies when enableSmartWalletVerification is true.
65+ *
66+ * Default: 50,000
67+ */
68+ smartWalletMaxPriorityFeeMicroLamports ?: number ;
69+ } ;
3670
3771/**
3872 * SVM facilitator implementation for the Exact payment scheme.
73+ *
74+ * Dual-path verification:
75+ *
76+ * Path 1 (Static): Strict positional instruction validation for standard wallets.
77+ * Fast, preserves existing behavior.
78+ *
79+ * Path 2 (Simulation): Outcome-based verification for smart wallets.
80+ * When Path 1 rejects a transaction and smart wallet verification is enabled,
81+ * falls back to simulation-based validation that inspects CPI inner instructions.
82+ * Works for any wallet program that executes TransferChecked via CPI.
3983 */
4084export class ExactSvmScheme implements SchemeNetworkFacilitator {
4185 readonly scheme = "exact" ;
@@ -44,15 +88,16 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
4488 private readonly settlementCache : SettlementCache ;
4589
4690 /**
47- * Creates a new ExactSvmFacilitator instance.
91+ * Creates a new ExactSvmScheme instance.
4892 *
49- * @param signer - The SVM RPC client for facilitator operations
93+ * @param signer - The SVM signer for facilitator operations
5094 * @param settlementCache - Optional shared settlement cache (one is created if omitted)
51- * @returns ExactSvmFacilitator instance
95+ * @param options - Optional configuration for smart wallet verification
5296 */
5397 constructor (
5498 private readonly signer : FacilitatorSvmSigner ,
5599 settlementCache ?: SettlementCache ,
100+ private readonly options ?: ExactSvmSchemeOptions ,
56101 ) {
57102 this . settlementCache = settlementCache ?? new SettlementCache ( ) ;
58103 }
@@ -70,9 +115,11 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
70115 const addresses = this . signer . getAddresses ( ) ;
71116 const randomIndex = Math . floor ( Math . random ( ) * addresses . length ) ;
72117
73- return {
74- feePayer : addresses [ randomIndex ] ,
75- } ;
118+ const extra : Record < string , unknown > = { feePayer : addresses [ randomIndex ] } ;
119+ if ( this . options ?. enableSmartWalletVerification ) {
120+ extra . features = { smartWalletSupported : true } ;
121+ }
122+ return extra ;
76123 }
77124
78125 /**
@@ -134,7 +181,7 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
134181 } ;
135182 }
136183
137- // Step 2: Parse and Validate Transaction Structure
184+ // Step 2: Parse Transaction
138185 let transaction ;
139186 try {
140187 transaction = decodeTransactionFromPayload ( exactSvmPayload ) ;
@@ -146,6 +193,135 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
146193 } ;
147194 }
148195
196+ // ─── Path 1: Static validation (standard wallets) ───────────────────
197+ const staticResult = await this . verifyStaticPath (
198+ transaction ,
199+ exactSvmPayload ,
200+ requirements ,
201+ signerAddresses ,
202+ ) ;
203+
204+ if ( staticResult . isValid ) {
205+ return staticResult ;
206+ }
207+
208+ // ─── Path 2: Simulation-based verification (smart wallets) ──────────
209+ // If static validation failed and smart wallet verification is enabled,
210+ // try simulation-based outcome verification. This handles any wallet
211+ // program (Squads, Swig, SPL Governance, etc.) that executes
212+ // TransferChecked via CPI.
213+ if ( this . options ?. enableSmartWalletVerification ) {
214+ const feePayer = requirements . extra . feePayer ;
215+ return verifySmartWalletTransaction (
216+ exactSvmPayload . transaction ,
217+ requirements ,
218+ this . signer ,
219+ feePayer ,
220+ signerAddresses ,
221+ {
222+ enabled : true ,
223+ maxComputeUnits : this . options . smartWalletMaxComputeUnits ,
224+ maxPriorityFeeMicroLamports : this . options . smartWalletMaxPriorityFeeMicroLamports ,
225+ } ,
226+ ) ;
227+ }
228+
229+ return staticResult ;
230+ }
231+
232+ /**
233+ * Settles a payment by submitting the transaction.
234+ * Ensures the correct signer is used based on the feePayer specified in requirements.
235+ *
236+ * @param payload - The payment payload to settle
237+ * @param requirements - The payment requirements
238+ * @returns Promise resolving to settlement response
239+ */
240+ async settle (
241+ payload : PaymentPayload ,
242+ requirements : PaymentRequirements ,
243+ ) : Promise < SettleResponse > {
244+ const exactSvmPayload = payload . payload as ExactSvmPayloadV2 ;
245+
246+ const valid = await this . verify ( payload , requirements ) ;
247+ if ( ! valid . isValid ) {
248+ return {
249+ success : false ,
250+ network : payload . accepted . network ,
251+ transaction : "" ,
252+ errorReason : valid . invalidReason ?? "verification_failed" ,
253+ payer : valid . payer || "" ,
254+ } ;
255+ }
256+
257+ // Duplicate settlement check: reject if this transaction is already being settled.
258+ // Must occur before any async work so concurrent calls for the same tx are caught.
259+ const txKey = exactSvmPayload . transaction ;
260+ if ( this . settlementCache . isDuplicate ( txKey ) ) {
261+ return {
262+ success : false ,
263+ network : payload . accepted . network ,
264+ transaction : "" ,
265+ errorReason : "duplicate_settlement" ,
266+ payer : valid . payer || "" ,
267+ } ;
268+ }
269+
270+ try {
271+ // Extract feePayer from requirements (already validated in verify)
272+ const feePayer = requirements . extra . feePayer as Address ;
273+
274+ // Sign transaction with the feePayer's signer
275+ const fullySignedTransaction = await this . signer . signTransaction (
276+ exactSvmPayload . transaction ,
277+ feePayer ,
278+ requirements . network ,
279+ ) ;
280+
281+ // Send transaction to network
282+ const signature = await this . signer . sendTransaction (
283+ fullySignedTransaction ,
284+ requirements . network ,
285+ ) ;
286+
287+ // Wait for confirmation
288+ await this . signer . confirmTransaction ( signature , requirements . network ) ;
289+
290+ return {
291+ success : true ,
292+ transaction : signature ,
293+ network : payload . accepted . network ,
294+ payer : valid . payer ,
295+ } ;
296+ } catch ( error ) {
297+ console . error ( "Failed to settle transaction:" , error ) ;
298+ return {
299+ success : false ,
300+ errorReason : "transaction_failed" ,
301+ transaction : "" ,
302+ network : payload . accepted . network ,
303+ payer : valid . payer || "" ,
304+ } ;
305+ }
306+ }
307+
308+ /**
309+ * Path 1: Static instruction-layout verification for standard wallets.
310+ * Validates positional instruction structure, program allowlist, and
311+ * transfer details. Unchanged from the original implementation.
312+ *
313+ * @param transaction - Decoded transaction to verify
314+ * @param exactSvmPayload - The raw SVM payload containing the base64 transaction
315+ * @param requirements - Payment requirements to verify against
316+ * @param signerAddresses - Facilitator signer addresses (for self-spend protection)
317+ * @returns Verification result
318+ */
319+ private async verifyStaticPath (
320+ transaction : ReturnType < typeof decodeTransactionFromPayload > ,
321+ exactSvmPayload : ExactSvmPayloadV2 ,
322+ requirements : PaymentRequirements ,
323+ signerAddresses : string [ ] ,
324+ ) : Promise < VerifyResponse > {
149325 const compiled = getCompiledTransactionMessageDecoder ( ) . decode ( transaction . messageBytes ) ;
150326 const decompiled = decompileTransactionMessage ( compiled ) ;
151327 const instructions = decompiled . instructions ?? [ ] ;
@@ -164,7 +340,7 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
164340 } ;
165341 }
166342
167- // Step 3: Verify Compute Budget Instructions
343+ // Verify Compute Budget Instructions
168344 try {
169345 this . verifyComputeLimitInstruction ( instructions [ 0 ] as never ) ;
170346 this . verifyComputePriceInstruction ( instructions [ 1 ] as never ) ;
@@ -186,7 +362,7 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
186362 } ;
187363 }
188364
189- // Step 4: Verify Transfer Instruction
365+ // Verify Transfer Instruction
190366 const transferIx = instructions [ 2 ] ;
191367 const programAddress = transferIx . programAddress . toString ( ) ;
192368
@@ -201,7 +377,6 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
201377 } ;
202378 }
203379
204- // Parse the transfer instruction using the appropriate library helper
205380 let parsedTransfer ;
206381 try {
207382 if ( programAddress === TOKEN_PROGRAM_ADDRESS . toString ( ) ) {
@@ -228,7 +403,6 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
228403 } ;
229404 }
230405
231- // Verify mint address matches requirements
232406 const mintAddress = parsedTransfer . accounts . mint . address . toString ( ) ;
233407 if ( mintAddress !== requirements . asset ) {
234408 return {
@@ -238,7 +412,6 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
238412 } ;
239413 }
240414
241- // Verify destination ATA matches expected ATA for payTo address
242415 const destATA = parsedTransfer . accounts . destination . address . toString ( ) ;
243416 try {
244417 const [ expectedDestATA ] = await findAssociatedTokenPda ( {
@@ -265,7 +438,6 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
265438 } ;
266439 }
267440
268- // Verify transfer amount meets requirements
269441 const amount = parsedTransfer . data . amount ;
270442 if ( amount !== BigInt ( requirements . amount ) ) {
271443 return {
@@ -275,7 +447,7 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
275447 } ;
276448 }
277449
278- // Step 5: Verify optional instructions (if present)
450+ // Verify optional instructions (if present)
279451 // Allowed optional programs: Lighthouse (wallet protection) and Memo (uniqueness)
280452 const optionalInstructions = instructions . slice ( 3 ) ;
281453 const invalidReasonByIndex = [
@@ -301,19 +473,17 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
301473 } ;
302474 }
303475
304- // Step 6: Sign and Simulate Transaction
476+ // Sign and Simulate Transaction
305477 // CRITICAL: Simulation proves transaction will succeed (catches insufficient balance, invalid accounts, etc)
306478 try {
307- const feePayer = requirements . extra . feePayer as Address ;
479+ const feePayer = requirements . extra ! . feePayer as Address ;
308480
309- // Sign transaction with the feePayer's signer
310481 const fullySignedTransaction = await this . signer . signTransaction (
311482 exactSvmPayload . transaction ,
312483 feePayer ,
313484 requirements . network ,
314485 ) ;
315486
316- // Simulate to verify transaction would succeed
317487 await this . signer . simulateTransaction ( fullySignedTransaction , requirements . network ) ;
318488 } catch ( error ) {
319489 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
@@ -332,82 +502,6 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
332502 } ;
333503 }
334504
335- /**
336- * Settles a payment by submitting the transaction.
337- * Ensures the correct signer is used based on the feePayer specified in requirements.
338- *
339- * @param payload - The payment payload to settle
340- * @param requirements - The payment requirements
341- * @returns Promise resolving to settlement response
342- */
343- async settle (
344- payload : PaymentPayload ,
345- requirements : PaymentRequirements ,
346- ) : Promise < SettleResponse > {
347- const exactSvmPayload = payload . payload as ExactSvmPayloadV2 ;
348-
349- const valid = await this . verify ( payload , requirements ) ;
350- if ( ! valid . isValid ) {
351- return {
352- success : false ,
353- network : payload . accepted . network ,
354- transaction : "" ,
355- errorReason : valid . invalidReason ?? "verification_failed" ,
356- payer : valid . payer || "" ,
357- } ;
358- }
359-
360- // Duplicate settlement check: reject if this transaction is already being settled.
361- // Must occur before any async work so concurrent calls for the same tx are caught.
362- const txKey = exactSvmPayload . transaction ;
363- if ( this . settlementCache . isDuplicate ( txKey ) ) {
364- return {
365- success : false ,
366- network : payload . accepted . network ,
367- transaction : "" ,
368- errorReason : "duplicate_settlement" ,
369- payer : valid . payer || "" ,
370- } ;
371- }
372-
373- try {
374- // Extract feePayer from requirements (already validated in verify)
375- const feePayer = requirements . extra . feePayer as Address ;
376-
377- // Sign transaction with the feePayer's signer
378- const fullySignedTransaction = await this . signer . signTransaction (
379- exactSvmPayload . transaction ,
380- feePayer ,
381- requirements . network ,
382- ) ;
383-
384- // Send transaction to network
385- const signature = await this . signer . sendTransaction (
386- fullySignedTransaction ,
387- requirements . network ,
388- ) ;
389-
390- // Wait for confirmation
391- await this . signer . confirmTransaction ( signature , requirements . network ) ;
392-
393- return {
394- success : true ,
395- transaction : signature ,
396- network : payload . accepted . network ,
397- payer : valid . payer ,
398- } ;
399- } catch ( error ) {
400- console . error ( "Failed to settle transaction:" , error ) ;
401- return {
402- success : false ,
403- errorReason : "transaction_failed" ,
404- transaction : "" ,
405- network : payload . accepted . network ,
406- payer : valid . payer || "" ,
407- } ;
408- }
409- }
410-
411505 /**
412506 * Verify that the compute limit instruction is valid.
413507 *
0 commit comments