Initial commit: add .gitignore and README
This commit is contained in:
157
src/dex/UniswapV3Service.ts
Normal file
157
src/dex/UniswapV3Service.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user