import { ethers } from 'ethers'; import { formatUnits } from 'ethers'; import { IDexService } from './DexService.js'; import { SwapQuote } from '../types/index.js'; import { ERC20_ABI } from '../aave/contracts.js'; // Uniswap V3 Router ABI (simplified) const UNISWAP_V3_ROUTER_ABI = [ 'function exactInputSingle(tuple(address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)', 'function multicall(uint256 deadline, bytes[] calldata data) external payable returns (bytes[] memory results)', ] as const; // Uniswap V3 Quoter ABI const UNISWAP_V3_QUOTER_ABI = [ 'function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) external returns (uint256 amountOut)', ] as const; // Common fee tiers for stablecoin pairs const STABLECOIN_FEE_TIERS = [100, 500, 3000]; // 0.01%, 0.05%, 0.3% export class UniswapV3Service implements IDexService { private router: ethers.Contract; private quoter?: ethers.Contract; private signer: ethers.Signer; constructor( signer: ethers.Signer, routerAddress: string, quoterAddress?: string ) { this.signer = signer; this.router = new ethers.Contract(routerAddress, UNISWAP_V3_ROUTER_ABI, signer); if (quoterAddress) { this.quoter = new ethers.Contract(quoterAddress, UNISWAP_V3_QUOTER_ABI, signer); } } getName(): string { return 'Uniswap V3'; } /** * Get quote for a swap */ async getQuote( tokenIn: string, tokenOut: string, amountIn: bigint ): Promise { if (!this.quoter) { // If no quoter, estimate with a simple calculation (not accurate, but better than nothing) return { amountOut: amountIn, // Assume 1:1 for stablecoins priceImpact: 0.001, // 0.1% estimated }; } let bestQuote: bigint = 0n; // Try different fee tiers to find the best quote for (const fee of STABLECOIN_FEE_TIERS) { try { const quote = await this.quoter.quoteExactInputSingle.staticCall( tokenIn, tokenOut, fee, amountIn, 0 ); if (quote > bestQuote) { bestQuote = quote; } } catch (error) { // Pool might not exist for this fee tier, continue continue; } } if (bestQuote === 0n) { throw new Error('No valid pool found for token pair'); } // Calculate price impact (simplified - assumes 1:1 for stablecoins) const amountInNum = Number(formatUnits(amountIn, 18)); const amountOutNum = Number(formatUnits(bestQuote, 18)); const priceImpact = Math.abs((amountInNum - amountOutNum) / amountInNum); return { amountOut: bestQuote, priceImpact, }; } /** * Execute swap on Uniswap V3 */ async swap( tokenIn: string, tokenOut: string, amountIn: bigint, amountOutMin: bigint, recipient: string, deadline?: number ): Promise { // Ensure approval const tokenContract = new ethers.Contract(tokenIn, ERC20_ABI, this.signer); const currentAllowance = await tokenContract.allowance( await this.signer.getAddress(), this.router.target as string ); if (currentAllowance < amountIn) { const approveTx = await tokenContract.approve(this.router.target as string, ethers.MaxUint256); await approveTx.wait(); } // Get best fee tier let bestFee = 500; // Default to 0.05% if (this.quoter) { let bestQuote: bigint = 0n; for (const fee of STABLECOIN_FEE_TIERS) { try { const quote = await this.quoter.quoteExactInputSingle.staticCall( tokenIn, tokenOut, fee, amountIn, 0 ); if (quote > bestQuote) { bestQuote = quote; bestFee = fee; } } catch { continue; } } } const finalDeadline = deadline || Math.floor(Date.now() / 1000) + 1800; // 30 minutes default const tx = await this.router.exactInputSingle({ tokenIn, tokenOut, fee: bestFee, recipient, deadline: finalDeadline, amountIn, amountOutMinimum: amountOutMin, sqrtPriceLimitX96: 0, }); return tx; } }