Files
237-combo/src/strat/adapters/uniswap-v3-adapter.ts
2026-02-09 21:51:30 -08:00

291 lines
8.3 KiB
TypeScript

import type {
ProtocolAdapter,
StepContext,
StepResult,
ViewContext,
RuntimeAddresses,
Network,
} from '../types.js';
import { getChainConfig } from '../../utils/addresses.js';
import { getTokenMetadata, parseTokenAmount } from '../../utils/tokens.js';
import type { Address } from 'viem';
import { parseUnits } from 'viem';
/**
* Uniswap v3 Protocol Adapter
* Implements Uniswap v3 swap operations
*/
// Uniswap 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: 'exactOutputSingle',
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: 'amountOut', type: 'uint256' },
{ name: 'amountInMaximum', type: 'uint256' },
{ name: 'sqrtPriceLimitX96', type: 'uint160' },
],
name: 'params',
type: 'tuple',
},
],
outputs: [{ name: 'amountIn', type: 'uint256' }],
},
] as const;
// ERC20 ABI
const ERC20_ABI = [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ name: '', type: 'bool' }],
},
{
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
},
{
name: 'allowance',
type: 'function',
stateMutability: 'view',
inputs: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
],
outputs: [{ name: '', type: 'uint256' }],
},
] as const;
export class UniswapV3Adapter implements ProtocolAdapter {
name = 'uniswap-v3';
async discover(network: Network): Promise<RuntimeAddresses> {
const config = getChainConfig(network.chainId);
return {
swapRouter: config.uniswap.swapRouter02,
};
}
actions = {
exactInputSingle: async (ctx: StepContext, args: any): Promise<StepResult> => {
const { tokenIn, tokenOut, fee, amountIn, amountOutMinimum, recipient, deadline } = args;
const tokenInAddress = this.resolveToken(tokenIn, ctx.network.chainId);
const tokenOutAddress = this.resolveToken(tokenOut, ctx.network.chainId);
const amountInValue = this.parseAmount(amountIn, tokenInAddress, ctx.network.chainId);
const feeValue = fee || 3000; // Default to 0.3%
const recipientAddress = this.resolveAddress(
recipient || '$accounts.trader',
ctx
);
const deadlineValue = deadline || BigInt(Math.floor(Date.now() / 1000) + 600);
const amountOutMinimumValue = amountOutMinimum
? this.parseAmount(amountOutMinimum, tokenOutAddress, ctx.network.chainId)
: 0n;
const routerAddress = ctx.addresses['uniswap-v3']?.swapRouter;
if (!routerAddress) {
throw new Error('Uniswap swap router address not found');
}
// Approve if needed
await this.ensureApproval(
ctx,
tokenInAddress,
routerAddress,
amountInValue
);
const hash = await ctx.walletClient.writeContract({
address: routerAddress,
abi: SWAP_ROUTER_ABI,
functionName: 'exactInputSingle',
args: [
{
tokenIn: tokenInAddress,
tokenOut: tokenOutAddress,
fee: feeValue,
recipient: recipientAddress,
deadline: deadlineValue,
amountIn: amountInValue,
amountOutMinimum: amountOutMinimumValue,
sqrtPriceLimitX96: 0n,
},
],
});
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash });
return {
success: true,
gasUsed: receipt.gasUsed,
events: receipt.logs,
txHash: hash,
};
},
exactOutputSingle: async (ctx: StepContext, args: any): Promise<StepResult> => {
const { tokenIn, tokenOut, fee, amountOut, amountInMaximum, recipient, deadline } = args;
const tokenInAddress = this.resolveToken(tokenIn, ctx.network.chainId);
const tokenOutAddress = this.resolveToken(tokenOut, ctx.network.chainId);
const amountOutValue = this.parseAmount(amountOut, tokenOutAddress, ctx.network.chainId);
const feeValue = fee || 3000;
const recipientAddress = this.resolveAddress(
recipient || '$accounts.trader',
ctx
);
const deadlineValue = deadline || BigInt(Math.floor(Date.now() / 1000) + 600);
const amountInMaximumValue = amountInMaximum
? this.parseAmount(amountInMaximum, tokenInAddress, ctx.network.chainId)
: parseUnits('1000000', 18); // Very high default
const routerAddress = ctx.addresses['uniswap-v3']?.swapRouter;
if (!routerAddress) {
throw new Error('Uniswap swap router address not found');
}
// Approve if needed (will need to approve the maximum)
await this.ensureApproval(
ctx,
tokenInAddress,
routerAddress,
amountInMaximumValue
);
const hash = await ctx.walletClient.writeContract({
address: routerAddress,
abi: SWAP_ROUTER_ABI,
functionName: 'exactOutputSingle',
args: [
{
tokenIn: tokenInAddress,
tokenOut: tokenOutAddress,
fee: feeValue,
recipient: recipientAddress,
deadline: deadlineValue,
amountOut: amountOutValue,
amountInMaximum: amountInMaximumValue,
sqrtPriceLimitX96: 0n,
},
],
});
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash });
return {
success: true,
gasUsed: receipt.gasUsed,
events: receipt.logs,
txHash: hash,
};
},
};
views = {};
private resolveToken(symbol: string, chainId: number): Address {
try {
const token = getTokenMetadata(chainId, symbol as any);
return token.address;
} catch {
// Try as address directly
return symbol as Address;
}
}
private parseAmount(amount: string, tokenAddress: Address, chainId: number): bigint {
try {
const token = getTokenMetadata(chainId, tokenAddress as any);
return parseTokenAmount(amount, token.decimals);
} catch {
// Assume 18 decimals if token not found
return parseUnits(amount, 18);
}
}
private resolveAddress(address: string, ctx: StepContext): Address {
if (address.startsWith('$')) {
const path = address.slice(1).split('.');
let value: any = ctx;
for (const key of path) {
value = value[key];
}
if (typeof value === 'string') {
return value as Address;
}
if (value && typeof value === 'object' && 'address' in value) {
return value.address;
}
return value;
}
return address as Address;
}
private async ensureApproval(
ctx: StepContext,
tokenAddress: Address,
spender: Address,
amount: bigint
): Promise<void> {
const userAddress = ctx.accounts.trader?.address || ctx.accounts.trader;
const { maxUint256 } = await import('viem');
const currentAllowance = await ctx.publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'allowance',
args: [userAddress, spender],
});
if (currentAllowance < amount) {
await ctx.walletClient.writeContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'approve',
args: [spender, maxUint256],
});
}
}
}