Files
237-combo/examples/ts/uniswap-v3-swap.ts
2026-02-09 21:51:30 -08:00

163 lines
5.1 KiB
TypeScript

/**
* Uniswap v3: Exact input swap via SwapRouter02
*
* This example demonstrates how to execute a swap on Uniswap v3
* using the SwapRouter02 contract.
*/
import { createWalletRpcClient } from '../../src/utils/chain-config.js';
import { getUniswapSwapRouter02 } from '../../src/utils/addresses.js';
import { getTokenMetadata, parseTokenAmount } from '../../src/utils/tokens.js';
import { waitForTransaction } from '../../src/utils/rpc.js';
import type { Address } from 'viem';
const CHAIN_ID = 1; // Mainnet (change to 8453 for Base, etc.)
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;
// Uniswap v3 SwapRouter02 ABI
const SWAP_ROUTER_ABI = [
{
name: 'exactInputSingle',
type: 'function',
stateMutability: 'payable',
inputs: [
{
components: [
{ name: 'tokenIn', type: 'address' },
{ name: 'tokenOut', type: 'address' },
{ name: 'fee', type: 'uint24' },
{ name: 'recipient', type: 'address' },
{ name: 'deadline', type: 'uint256' },
{ name: 'amountIn', type: 'uint256' },
{ name: 'amountOutMinimum', type: 'uint256' },
{ name: 'sqrtPriceLimitX96', type: 'uint160' },
],
name: 'params',
type: 'tuple',
},
],
outputs: [{ name: 'amountOut', type: 'uint256' }],
},
{
name: 'exactInput',
type: 'function',
stateMutability: 'payable',
inputs: [
{
components: [
{
components: [
{ name: 'tokenIn', type: 'address' },
{ name: 'tokenOut', type: 'address' },
{ name: 'fee', type: 'uint24' },
],
name: 'path',
type: 'tuple[]',
},
{ name: 'recipient', type: 'address' },
{ name: 'deadline', type: 'uint256' },
{ name: 'amountIn', type: 'uint256' },
{ name: 'amountOutMinimum', type: 'uint256' },
],
name: 'params',
type: 'tuple',
},
],
outputs: [{ name: 'amountOut', type: 'uint256' }],
},
] as const;
// ERC20 ABI for approvals
const ERC20_ABI = [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ name: '', type: 'bool' }],
},
] as const;
// Uniswap v3 fee tiers (0.01%, 0.05%, 0.3%, 1%)
const FEE_TIER_LOW = 100; // 0.01%
const FEE_TIER_MEDIUM = 500; // 0.05%
const FEE_TIER_STANDARD = 3000; // 0.3%
const FEE_TIER_HIGH = 10000; // 1%
async function swapExactInputSingle() {
const walletClient = createWalletRpcClient(CHAIN_ID, PRIVATE_KEY);
const publicClient = walletClient as any;
const account = walletClient.account?.address;
if (!account) {
throw new Error('No account available');
}
const routerAddress = getUniswapSwapRouter02(CHAIN_ID);
// Token configuration
const tokenIn = getTokenMetadata(CHAIN_ID, 'USDC');
const tokenOut = getTokenMetadata(CHAIN_ID, 'WETH');
// Swap parameters
const amountIn = parseTokenAmount('1000', tokenIn.decimals); // 1000 USDC
const slippageTolerance = 50; // 0.5% in basis points (adjust based on market conditions)
const fee = FEE_TIER_STANDARD; // 0.3% fee tier (most liquid for major pairs)
const deadline = BigInt(Math.floor(Date.now() / 1000) + 600); // 10 minutes
console.log(`Swapping ${amountIn} ${tokenIn.symbol} for ${tokenOut.symbol}`);
console.log(`Router: ${routerAddress}`);
console.log(`Fee tier: ${fee} (0.3%)`);
console.log(`Slippage tolerance: ${slippageTolerance / 100}%`);
// Step 1: Get quote (in production, use QuoterV2 contract)
// For now, we'll set amountOutMinimum to 0 (not recommended in production!)
// In production, always query the pool first to get expected output
const amountOutMinimum = 0n; // TODO: Query QuoterV2 for expected output and apply slippage
// Step 2: Approve token spending
console.log('\n1. Approving token spending...');
const approveTx = await walletClient.writeContract({
address: tokenIn.address,
abi: ERC20_ABI,
functionName: 'approve',
args: [routerAddress, amountIn],
});
await waitForTransaction(publicClient, approveTx);
console.log(`Approved: ${approveTx}`);
// Step 3: Execute swap
console.log('\n2. Executing swap...');
const swapTx = await walletClient.writeContract({
address: routerAddress,
abi: SWAP_ROUTER_ABI,
functionName: 'exactInputSingle',
args: [
{
tokenIn: tokenIn.address,
tokenOut: tokenOut.address,
fee,
recipient: account,
deadline,
amountIn,
amountOutMinimum,
sqrtPriceLimitX96: 0n, // No price limit
},
],
});
await waitForTransaction(publicClient, swapTx);
console.log(`Swap executed: ${swapTx}`);
console.log('\n✅ Swap completed successfully!');
}
// Run if executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
swapExactInputSingle().catch(console.error);
}
export { swapExactInputSingle };