158 lines
4.5 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
|