291 lines
8.3 KiB
TypeScript
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],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|