kiban-agent-kit
Version:
Open-source framework connecting AI agents to Katana ecosystem protocols
275 lines (274 loc) • 12.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SwapService = exports.UNISWAP_V3_QUOTER_ABI = exports.UNISWAP_V3_ROUTER_ABI = exports.COMMON_TOKENS = exports.UNISWAP_V3_QUOTER = exports.UNISWAP_V3_ROUTER = void 0;
const viem_1 = require("viem");
// Uniswap V3 Router address
exports.UNISWAP_V3_ROUTER = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
exports.UNISWAP_V3_QUOTER = "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6";
// Common token addresses
exports.COMMON_TOKENS = {
ETH: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // Special address to represent ETH
WETH: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
DAI: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
};
// Uniswap V3 Router ABI (minimal for swap functionality)
exports.UNISWAP_V3_ROUTER_ABI = [
{
inputs: [
{
components: [
{ internalType: "address", name: "tokenIn", type: "address" },
{ internalType: "address", name: "tokenOut", type: "address" },
{ internalType: "uint24", name: "fee", type: "uint24" },
{ internalType: "address", name: "recipient", type: "address" },
{ internalType: "uint256", name: "deadline", type: "uint256" },
{ internalType: "uint256", name: "amountIn", type: "uint256" },
{
internalType: "uint256",
name: "amountOutMinimum",
type: "uint256",
},
{
internalType: "uint160",
name: "sqrtPriceLimitX96",
type: "uint160",
},
],
internalType: "struct ISwapRouter.ExactInputSingleParams",
name: "params",
type: "tuple",
},
],
name: "exactInputSingle",
outputs: [{ internalType: "uint256", name: "amountOut", type: "uint256" }],
stateMutability: "payable",
type: "function",
},
];
// Uniswap V3 Quoter ABI (minimal for quote functionality)
exports.UNISWAP_V3_QUOTER_ABI = [
{
inputs: [
{ internalType: "address", name: "tokenIn", type: "address" },
{ internalType: "address", name: "tokenOut", type: "address" },
{ internalType: "uint24", name: "fee", type: "uint24" },
{ internalType: "uint256", name: "amountIn", type: "uint256" },
{ internalType: "uint160", name: "sqrtPriceLimitX96", type: "uint160" },
],
name: "quoteExactInputSingle",
outputs: [{ internalType: "uint256", name: "amountOut", type: "uint256" }],
stateMutability: "nonpayable",
type: "function",
},
];
/**
* Service for token swapping using Uniswap V3
*/
class SwapService {
constructor(agent) {
this.agent = agent;
}
/**
* Helper to normalize token addresses (handle ETH vs WETH)
*/
normalizeTokenAddress(token) {
// Handle common token symbols (case insensitive)
const upperToken = token.toUpperCase();
if (upperToken === "ETH") {
return exports.COMMON_TOKENS.ETH;
}
if (upperToken === "WETH") {
return exports.COMMON_TOKENS.WETH;
}
if (upperToken === "USDC") {
return exports.COMMON_TOKENS.USDC;
}
if (upperToken === "USDT") {
return exports.COMMON_TOKENS.USDT;
}
if (upperToken === "DAI") {
return exports.COMMON_TOKENS.DAI;
}
// Return the address as is
return token;
}
/**
* Get a quote for swapping tokens
*/
async getSwapQuote(params) {
const { tokenIn, tokenOut, amount, slippagePercentage = 0.5 } = params;
console.log(`Getting quote for swap: ${amount} ${tokenIn} -> ${tokenOut}`);
// Normalize token addresses
const normalizedTokenIn = this.normalizeTokenAddress(tokenIn);
const normalizedTokenOut = this.normalizeTokenAddress(tokenOut);
// Check if we're dealing with ETH
const isETHIn = normalizedTokenIn === exports.COMMON_TOKENS.ETH;
const isETHOut = normalizedTokenOut === exports.COMMON_TOKENS.ETH;
// Get token metadata - use WETH for ETH
const tokenInInfo = await this.agent.getTokenInfo(isETHIn ? exports.COMMON_TOKENS.WETH : normalizedTokenIn);
const tokenOutInfo = await this.agent.getTokenInfo(isETHOut ? exports.COMMON_TOKENS.WETH : normalizedTokenOut);
// Parse amount with proper decimals
const amountIn = (0, viem_1.parseUnits)(amount, tokenInInfo.decimals);
// Use 0.3% fee pool as default
const fee = 3000;
try {
// We need to use the internal method to access the protected clients property
// This is a workaround for the protected access
const publicClient = this.agent.clients.public;
const amountOutResult = await publicClient.readContract({
address: exports.UNISWAP_V3_QUOTER,
abi: exports.UNISWAP_V3_QUOTER_ABI,
functionName: "quoteExactInputSingle",
args: [
isETHIn ? exports.COMMON_TOKENS.WETH : normalizedTokenIn,
isETHOut ? exports.COMMON_TOKENS.WETH : normalizedTokenOut,
BigInt(fee),
amountIn,
0n, // sqrtPriceLimitX96 (0 = no limit)
],
});
// Cast the result to bigint
const amountOut = amountOutResult;
// Calculate minimum amount out with slippage
const minimumAmountOut = (amountOut * BigInt(10000 - Math.round(slippagePercentage * 100))) /
10000n;
// Format amounts for human readability
const formattedAmountIn = (0, viem_1.formatUnits)(amountIn, tokenInInfo.decimals);
const formattedAmountOut = (0, viem_1.formatUnits)(amountOut, tokenOutInfo.decimals);
const formattedMinimumAmountOut = (0, viem_1.formatUnits)(minimumAmountOut, tokenOutInfo.decimals);
// Calculate execution price
const executionPrice = Number(amountOut) / Number(amountIn);
const formattedExecutionPrice = (executionPrice *
10 ** (tokenInInfo.decimals - tokenOutInfo.decimals)).toFixed(6);
// Calculate price impact (simplified)
const priceImpact = "< 1%"; // Placeholder
return {
tokenIn: {
address: normalizedTokenIn,
symbol: tokenInInfo.symbol,
decimals: tokenInInfo.decimals,
amount: formattedAmountIn,
},
tokenOut: {
address: normalizedTokenOut,
symbol: tokenOutInfo.symbol,
decimals: tokenOutInfo.decimals,
amount: formattedAmountOut,
},
executionPrice: `1 ${tokenInInfo.symbol} = ${formattedExecutionPrice} ${tokenOutInfo.symbol}`,
minimumAmountOut: formattedMinimumAmountOut,
priceImpact,
};
}
catch (error) {
throw new Error(`Failed to get swap quote: ${error.message}`);
}
}
/**
* Execute a token swap
*/
async swapTokens(params) {
const { tokenIn, tokenOut, amount, slippagePercentage = 0.5, recipient, } = params;
// Get quote first to calculate expected output
const quote = await this.getSwapQuote(params);
// Normalize token addresses
const normalizedTokenIn = this.normalizeTokenAddress(tokenIn);
const normalizedTokenOut = this.normalizeTokenAddress(tokenOut);
// Check if we're dealing with ETH
const isETHIn = normalizedTokenIn === exports.COMMON_TOKENS.ETH;
const isETHOut = normalizedTokenOut === exports.COMMON_TOKENS.ETH;
// Get token metadata - use WETH for ETH
const tokenInInfo = await this.agent.getTokenInfo(isETHIn ? exports.COMMON_TOKENS.WETH : normalizedTokenIn);
// Parse amount with proper decimals
const amountIn = (0, viem_1.parseUnits)(amount, tokenInInfo.decimals);
// Calculate minimum amount out from quote
const minimumAmountOut = (0, viem_1.parseUnits)(quote.minimumAmountOut, quote.tokenOut.decimals);
// Set up swap parameters
const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20); // 20 minutes from now
const recipientAddress = recipient || this.agent.getAddress();
// Use 0.3% fee pool as default
const fee = 3000;
try {
// If tokenIn is not ETH, we need to approve the router first
if (!isETHIn) {
// Check if we have enough allowance
const allowance = await this.agent.getTokenAllowance({
token: normalizedTokenIn,
owner: this.agent.getAddress(),
spender: exports.UNISWAP_V3_ROUTER,
});
if (allowance < amountIn) {
// Approve the router to spend our tokens
await this.agent.approveTokenSpending({
token: normalizedTokenIn,
spender: exports.UNISWAP_V3_ROUTER,
amount: amount,
});
}
}
// Prepare the swap parameters
const swapParams = {
tokenIn: isETHIn ? exports.COMMON_TOKENS.WETH : normalizedTokenIn,
tokenOut: isETHOut ? exports.COMMON_TOKENS.WETH : normalizedTokenOut,
fee: BigInt(fee),
recipient: isETHOut ? this.agent.getAddress() : recipientAddress,
deadline,
amountIn,
amountOutMinimum: minimumAmountOut,
sqrtPriceLimitX96: 0n, // 0 = no limit
};
// We need to use the internal method to access the protected clients property
// This is a workaround for the protected access
const walletClient = this.agent.clients.wallet;
const chain = this.agent.chain;
const account = this.agent.account;
// Execute the swap
let txHash;
if (isETHIn) {
// For ETH -> Token swaps, we need to use the payable function and send ETH
console.log(`Swapping ${amount} ETH to tokens`);
txHash = await walletClient.writeContract({
address: exports.UNISWAP_V3_ROUTER,
abi: exports.UNISWAP_V3_ROUTER_ABI,
functionName: "exactInputSingle",
args: [swapParams],
value: amountIn, // This is the ETH amount we're swapping
chain,
account,
});
}
else {
// For Token -> Token or Token -> ETH swaps
txHash = await walletClient.writeContract({
address: exports.UNISWAP_V3_ROUTER,
abi: exports.UNISWAP_V3_ROUTER_ABI,
functionName: "exactInputSingle",
args: [swapParams],
chain,
account,
});
}
// Wait for transaction to be mined
await this.agent.waitForTransaction(txHash);
return {
hash: txHash,
tokenIn: quote.tokenIn.symbol,
tokenOut: quote.tokenOut.symbol,
amountIn: quote.tokenIn.amount,
expectedAmountOut: quote.tokenOut.amount,
};
}
catch (error) {
console.error("Swap error details:", error);
if (error.message.includes("insufficient funds")) {
// Get current balance for better error message
const balance = await this.agent.getNativeBalance();
throw new Error(`Insufficient funds for swap. You have ${balance} ETH but the transaction requires ${amount} ETH plus gas fees.`);
}
throw new Error(`Failed to execute swap: ${error.message}`);
}
}
}
exports.SwapService = SwapService;