@@ -2,8 +2,8 @@ import { createWalletClient, createPublicClient, defineChain, http, type Hex, ty
22import { base } from 'viem/chains' ;
33import { privateKeyToAccount } from 'viem/accounts' ;
44import { CHAINS } from '../config/chains' ;
5- import { buildUsdcToNativeSwapCommands } from './uniswap-swap' ;
6- import { ERC20_ABI } from '../config/uniswap-abis' ;
5+ import { buildUsdcToNativeSwapCommands , buildUsdcToAnagoSwapCommands , buildMultiHopPath } from './uniswap-swap' ;
6+ import { ERC20_ABI , QUOTER_V2_ABI , FEE_TIERS } from '../config/uniswap-abis' ;
77
88// Define Monad chain for viem
99const monad = defineChain ( {
@@ -63,6 +63,7 @@ interface SwapParams {
6363 userDestination : Hex ;
6464 estimatedUsdcAmount : bigint ;
6565 chain : 'base' | 'monad' ;
66+ targetToken ?: string ; // e.g., 'MON', 'MON_MON', 'ANAGO', 'ANAGO_MON', 'ETH', 'ETH_BASE'
6667}
6768
6869interface GasFees {
@@ -257,6 +258,91 @@ export class TransactionSigner {
257258 }
258259 }
259260
261+ // Cache for valid fee tiers (token path -> fee tiers)
262+ // Key format: "token0-token1-token2"
263+ private feeTierCache : Map < string , { feeTiers : number [ ] ; timestamp : number } > = new Map ( ) ;
264+ private readonly FEE_TIER_CACHE_TTL_MS = 30 * 60 * 1000 ; // 30 minutes cache
265+
266+ /**
267+ * Find valid fee tiers for a multi-hop swap by trying different combinations with the quoter
268+ * Returns the first valid fee tier combination that returns a quote
269+ * Results are cached for 30 minutes
270+ */
271+ async findValidFeeTiers (
272+ tokens : `0x${string } `[ ] ,
273+ amountIn : bigint ,
274+ chain : 'monad'
275+ ) : Promise < { feeTiers : number [ ] ; amountOut : bigint } | null > {
276+ const { publicClient } = this . getClients ( chain ) ;
277+ const quoterAddress = CHAINS . monad . quoterV2Address as Hex ;
278+
279+ // Check cache first
280+ const cacheKey = tokens . map ( t => t . toLowerCase ( ) ) . join ( '-' ) ;
281+ const now = Date . now ( ) ;
282+ const cached = this . feeTierCache . get ( cacheKey ) ;
283+
284+ if ( cached && ( now - cached . timestamp ) < this . FEE_TIER_CACHE_TTL_MS ) {
285+ console . info ( `⚡ [TX SIGNER] Using cached fee tiers: ${ cached . feeTiers . join ( '/' ) } ` ) ;
286+
287+ // Still need to get a fresh quote for the amount
288+ try {
289+ const path = buildMultiHopPath ( tokens , cached . feeTiers ) ;
290+ const result = await publicClient . simulateContract ( {
291+ address : quoterAddress ,
292+ abi : QUOTER_V2_ABI ,
293+ functionName : 'quoteExactInput' ,
294+ args : [ path , amountIn ] ,
295+ } ) ;
296+ const amountOut = result . result [ 0 ] as bigint ;
297+ return { feeTiers : cached . feeTiers , amountOut } ;
298+ } catch {
299+ // Cache might be stale, clear it and re-discover
300+ console . info ( `⚠️ [TX SIGNER] Cached fee tiers no longer valid, re-discovering...` ) ;
301+ this . feeTierCache . delete ( cacheKey ) ;
302+ }
303+ }
304+
305+ console . info ( `🔍 [TX SIGNER] Finding valid fee tiers for ${ tokens . length } -token path...` ) ;
306+
307+ // For a 3-token path (e.g., USDC -> WMON -> ANAGO), we need 2 fee tiers
308+ // Try all combinations of fee tiers
309+ for ( const firstFee of FEE_TIERS ) {
310+ for ( const secondFee of FEE_TIERS ) {
311+ try {
312+ const path = buildMultiHopPath ( tokens , [ firstFee , secondFee ] ) ;
313+
314+ // Use simulate instead of call for nonpayable function
315+ const result = await publicClient . simulateContract ( {
316+ address : quoterAddress ,
317+ abi : QUOTER_V2_ABI ,
318+ functionName : 'quoteExactInput' ,
319+ args : [ path , amountIn ] ,
320+ } ) ;
321+
322+ const amountOut = result . result [ 0 ] as bigint ;
323+
324+ if ( amountOut > 0n ) {
325+ console . info ( `✅ [TX SIGNER] Found valid path with fee tiers: ${ firstFee } /${ secondFee } , amountOut: ${ amountOut } ` ) ;
326+
327+ // Cache the result
328+ this . feeTierCache . set ( cacheKey , {
329+ feeTiers : [ firstFee , secondFee ] ,
330+ timestamp : now ,
331+ } ) ;
332+
333+ return { feeTiers : [ firstFee , secondFee ] , amountOut } ;
334+ }
335+ } catch {
336+ // This fee tier combination doesn't work, try next
337+ continue ;
338+ }
339+ }
340+ }
341+
342+ console . error ( '❌ [TX SIGNER] No valid fee tier combination found' ) ;
343+ return null ;
344+ }
345+
260346 async signAndSubmitMint ( params : SignAndSubmitParams ) : Promise < { txHash : Hex ; confirmed ?: boolean ; error ?: string } > {
261347 const chainLower = params . chain . toLowerCase ( ) ;
262348
@@ -486,12 +572,41 @@ export class TransactionSigner {
486572 } ) as [ bigint , bigint , bigint ] ;
487573 console . info ( `🔍 [TX SIGNER] Pre-swap Permit2→Router allowance: ${ permit2Allowance [ 0 ] . toString ( ) } , expires: ${ permit2Allowance [ 1 ] . toString ( ) } ` ) ;
488574
489- // Build Uniswap swap commands
490- const { commands, inputs, deadline } = buildUsdcToNativeSwapCommands ( {
491- amountIn : usdcAmount ,
492- recipient : params . userDestination ,
493- chain : chainName ,
494- } ) ;
575+ // Determine if we're swapping to ANAGO or native token
576+ const isAnagoSwap = params . targetToken === 'ANAGO' || params . targetToken === 'ANAGO_MON' ;
577+
578+ // For ANAGO swaps, find valid fee tiers first (ANAGO only exists on Monad)
579+ let feeTiers : { first : number ; second : number } | undefined ;
580+ if ( isAnagoSwap ) {
581+ const monadConfig = CHAINS . monad ;
582+ const tokens : `0x${string } `[ ] = [
583+ monadConfig . usdcAddress as `0x${string } `,
584+ monadConfig . wmonAddress as `0x${string } `,
585+ monadConfig . anagoAddress as `0x${string } `,
586+ ] ;
587+
588+ const validPath = await this . findValidFeeTiers ( tokens , usdcAmount , 'monad' ) ;
589+ if ( validPath ) {
590+ feeTiers = { first : validPath . feeTiers [ 0 ] , second : validPath . feeTiers [ 1 ] } ;
591+ console . info ( `💰 [TX SIGNER] Expected output: ${ validPath . amountOut } ANAGO (raw)` ) ;
592+ } else {
593+ throw new Error ( 'No valid liquidity pool found for USDC → WMON → ANAGO swap path' ) ;
594+ }
595+ }
596+
597+ // Build Uniswap swap commands based on target token
598+ const { commands, inputs, deadline } = isAnagoSwap
599+ ? buildUsdcToAnagoSwapCommands ( {
600+ amountIn : usdcAmount ,
601+ recipient : params . userDestination ,
602+ targetToken : 'ANAGO' ,
603+ feeTiers,
604+ } )
605+ : buildUsdcToNativeSwapCommands ( {
606+ amountIn : usdcAmount ,
607+ recipient : params . userDestination ,
608+ chain : chainName ,
609+ } ) ;
495610
496611 const universalRouterAddress = chainConfig . universalRouterAddress as Hex ;
497612 const universalRouterAbi : Abi = [
@@ -548,7 +663,8 @@ export class TransactionSigner {
548663 throw new Error ( 'Uniswap swap transaction failed' ) ;
549664 }
550665
551- console . info ( `✅ [TX SIGNER] Swap completed - ${ nativeSymbol } sent to user` ) ;
666+ const swapTargetSymbol = isAnagoSwap ? 'ANAGO' : nativeSymbol ;
667+ console . info ( `✅ [TX SIGNER] Swap completed - ${ swapTargetSymbol } sent to user` ) ;
552668
553669 return { txHash : swapTxHash , confirmed : true } ;
554670 } catch ( error ) {
0 commit comments