Files
27-combi/src/dex/UniswapV3Service.ts
2026-02-09 21:51:30 -08:00

158 lines
4.5 KiB
TypeScript

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<SwapQuote> {
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<ethers.ContractTransactionResponse> {
// 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;
}
}