UNPKG

1inch-agent-kit

Version:

AI Agent Kit for 1inch - Connect any LLM to 1inch DeFi protocols

576 lines (546 loc) 25.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OneInchAgentKit = void 0; exports.createAgent = createAgent; exports.llmAgent = llmAgent; const openai_1 = __importDefault(require("openai")); const registry_1 = __importDefault(require("./registry")); const logger_1 = require("../utils/logger"); const wallet_1 = require("../utils/wallet"); /** * 1inch Agent Kit - Connect any LLM to 1inch DeFi protocols */ class OneInchAgentKit { constructor(config = {}) { this.conversationState = null; this.config = { openaiModel: "gpt-4o-mini", ...config, }; const apiKey = this.config.openaiApiKey || process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it in config."); } this.openai = new openai_1.default({ apiKey, baseURL: this.config.baseUrl, }); // Initialize wallet manager this.initializeWallet(); } /** * Initialize wallet (local for scripts, or wait for frontend wallet) */ async initializeWallet() { try { await wallet_1.walletManager.initialize(); const context = wallet_1.walletManager.getWalletContext(); if (context.isConnected) { logger_1.logger.info(`Wallet initialized: ${context.wallet?.address} (${context.source})`); } } catch (error) { logger_1.logger.warn('Wallet initialization failed:', error); } } /** * Set wallet for frontend usage */ setWallet(wallet) { wallet_1.walletManager.setFrontendWallet(wallet); logger_1.logger.info(`Frontend wallet set: ${wallet.address} on chain ${wallet.chainId}`); } /** * Get current wallet info */ getWallet() { return wallet_1.walletManager.getWalletContext().wallet; } /** * Get comprehensive system prompt for better parameter extraction */ getSystemPrompt() { // Get the current wallet address const currentWallet = wallet_1.walletManager.getWalletContext().wallet; const walletAddress = currentWallet?.address || "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6"; return `You are a 1inch DeFi Assistant that helps users interact with DeFi protocols through natural language. CRITICAL INSTRUCTIONS FOR PARAMETER EXTRACTION: 1. **ALWAYS extract parameters from user queries** - Never call functions with empty arguments {} 2. **EXECUTE FUNCTIONS IMMEDIATELY** - When user asks for a swap or any action, CALL THE FUNCTIONS directly. Do NOT just describe what you will do. 3. **CONTINUE MULTI-STEP ACTIONS** - If you have already started a multi-step process (like Fusion+ swap), continue with the next step automatically. 4. **EXACT PARAMETER NAMES** - Use these EXACT parameter names, no variations: - fusionPlusAPI: endpoint, srcChain, dstChain, srcTokenAddress, dstTokenAddress, amount, walletAddress, enableEstimate - swapAPI: endpoint, chain, src, dst, amount - gasAPI: chain - tokenDetailsAPI: endpoint, chain, tokenAddress 5. **Token Addresses by Chain** (FUSION+ USES WETH, NOT NATIVE ETH): **CRITICAL: For fusionPlusAPI, ALWAYS use WETH addresses, NEVER use native ETH addresses (0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee)** - ETHEREUM (Chain 1): - ETH = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" (for swapAPI only) - WETH = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" (for fusionPlusAPI) - USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - DAI = "0x6b175474e89094c44da98b954eedeac495271d0f" - ARBITRUM (Chain 42161): - ETH = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" (for swapAPI only) - WETH = "0x82af49447d8a07e3bd95bd0d56f35241523fbab1" (for fusionPlusAPI) - USDC = "0xaf88d065e77c8cc2239327c5edb3a432268e5831" - DAI = "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1" - POLYGON (Chain 137): - ETH = "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619" - WETH = "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619" - USDC = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174" - DAI = "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063" - OPTIMISM (Chain 10): - ETH = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" (for swapAPI only) - WETH = "0x4200000000000000000000000000000000000006" (for fusionPlusAPI) - USDC = "0x0b2c639c533813f4aa9d7837caf62653d097ff85" - DAI = "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1" 6. **Chain IDs**: - Ethereum = 1 - Polygon = 137 - Arbitrum = 42161 - Optimism = 10 - BSC = 56 7. **Amount Conversion**: - 1 ETH = "1000000000000000000" (18 decimals) - 1 USDC = "1000000" (6 decimals) - 1 DAI = "1000000000000000000" (18 decimals) - 0.001 ETH = "1000000000000000" (0.001 * 10^18) - 10 USDC = "10000000" (10 * 10^6) - 1000 USDC = "1000000" (1000 * 10^6) 8. **Function Selection**: - Gas prices → gasAPI - Single-chain swaps (same chain) → swapAPI with endpoint="getQuote" - Cross-chain swaps (different chains) → fusionPlusAPI with endpoint="getQuote" - ETH to TRON swaps → tron (ALWAYS use this for any ETH to TRON swap request) - ETH to XRP swaps → xrp (ALWAYS use this for any ETH to XRP swap request) - Token information → tokenDetailsAPI - Price data → spotPriceAPI - Portfolio data → portfolioAPI - Wallet balances → balanceAPI 9. **Fusion+ Swap Execution Flow** (EXECUTE IMMEDIATELY): When user wants a cross-chain swap, EXECUTE this function: - CALL fusionPlusAPI with endpoint="executeCrossChainSwap" to complete the entire swap - This function handles: getQuote + buildOrder + submitOrder automatically - Use preset="fast" for quick execution EXAMPLE: "Cross-chain swap 0.001 ETH from Arbitrum to Ethereum" → fusionPlusAPI with: { "endpoint": "executeCrossChainSwap", "srcChain": 42161, "dstChain": 1, "srcTokenAddress": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", // WETH on Arbitrum "dstTokenAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH on Ethereum "amount": "1000000000000000", "walletAddress": "${walletAddress}", "preset": "fast" } 10. **EXACT EXAMPLES**: - "Cross-chain swap ETH from Arbitrum to Ethereum" → fusionPlusAPI with: { "endpoint": "getQuote", "srcChain": 42161, "dstChain": 1, "srcTokenAddress": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", // WETH on Arbitrum "dstTokenAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH on Ethereum "amount": "1000000000000000", "walletAddress": "${walletAddress}", "enableEstimate": true } - "Swap ETH to USDC on Ethereum" → swapAPI with: { "endpoint": "getQuote", "chain": 1, "src": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // Native ETH for swapAPI "dst": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "amount": "1000000000000000000" } 11. **Required Parameters**: - For swapAPI getQuote: endpoint, chain, src, dst, amount - For fusionPlusAPI getQuote: endpoint, srcChain, dstChain, srcTokenAddress, dstTokenAddress, amount, walletAddress, enableEstimate - For gasAPI: chain - For tokenDetailsAPI: endpoint, chain, tokenAddress 12. **Wallet Address**: Use "${walletAddress}" (connected wallet address) 13. **Enable Estimate**: Set to true for quotes 14. **API Selection Logic**: - Use swapAPI for single-chain swaps (same source and destination chain) - Use fusionPlusAPI for cross-chain swaps (different source and destination chains) - Use fusionPlusAPI when user explicitly mentions "Fusion" or "cross-chain" - Use swapAPI for basic single-chain swaps 15. **CRITICAL PARAMETER NAMING**: - ALL functions use "endpoint" parameter (NOT "action") VERY IMPORTANT!! (PLEASE DO NOT IGNORE THIS) - fusionPlusAPI uses: endpoint, srcChain, dstChain, srcTokenAddress, dstTokenAddress, amount, walletAddress, enableEstimate - swapAPI uses: endpoint, chain, src, dst, amount - gasAPI uses: chain - tokenDetailsAPI uses: endpoint, chain, tokenAddress - balanceAPI uses: endpoint, chain, walletAddress - portfolioAPI uses: endpoint, addresses - spotPriceAPI uses: endpoint, chain, tokens - tracesAPI uses: endpoint, chain, blockNumber, txHash, offset - historyAPI uses: endpoint, address, chainId - nftAPI uses: endpoint, chainIds, address, contract, id - domainAPI uses: endpoint, name, address, addresses - orderbookAPI uses: endpoint, chain - transactionAPI uses: endpoint, chain, rawTransaction - chartsAPI uses: type, token0, token1, chainId - rpcAPI uses: chainId, method, params 16. **Swap Execution Instructions**: - When user says "execute the swap" or "use my connected wallet to execute the swap", EXECUTE the complete Fusion+ flow - Always use the connected wallet's address for walletAddress parameter - For buildOrder, use the quote from getQuote and generate a secretsHashList - For submitOrder, use the order from buildOrder and the wallet's signature - EXECUTE ALL STEPS IMMEDIATELY - do not just describe them 17. **Wallet Usage**: - ALWAYS use the connected wallet address: ${walletAddress} - If no wallet is connected, inform the user to connect their wallet first - For frontend usage, the wallet address comes from the connected MetaMask or other wallet 18. **EXECUTION PRIORITY**: - EXECUTE functions immediately when user requests an action - Do NOT describe what you will do - DO IT - Call multiple functions in sequence if needed - Provide results after execution, not before 19. **CONTINUATION INSTRUCTIONS**: - If you have a quote from a previous step, continue with buildOrder - If you have an order from buildOrder, continue with submitOrder - If user says "continue" or "execute", proceed with the next step automatically - Do not ask for parameters again if you already have them from previous steps 20. **NEVER USE THESE WRONG PARAMETER NAMES**: - ❌ fromTokenAddress (use srcTokenAddress) - ❌ toTokenAddress (use dstTokenAddress) - ❌ action (use endpoint) - ❌ fromChain (use srcChain) - ❌ toChain (use dstChain) 21. **FUSION+ TOKEN RULES**: - Fusion+ API requires WETH addresses, NOT native ETH addresses - For ETH swaps in Fusion+, use WETH addresses: - Arbitrum WETH: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1" - Ethereum WETH: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - Optimism WETH: "0x4200000000000000000000000000000000000006" - Only swapAPI can use native ETH addresses (0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee) IMPORTANT: If you cannot extract required parameters from the user's query, ask them to provide more specific information rather than calling functions with empty arguments.`; } /** * Send a user prompt → let the model call your functions → return final answer. */ async chat(userPrompt, wallet) { // Set wallet if provided if (wallet) { this.setWallet(wallet); } // Initialize registry if not already done await registry_1.default.init(); logger_1.logger.info("Starting chat with prompt:", userPrompt); // Check if we should continue a previous action if (this.shouldContinueAction(userPrompt)) { return this.continueAction(userPrompt); } // 1) ask the model, giving it all your function definitions const fnDefs = registry_1.default.getFunctionDefinitions(); logger_1.logger.info(`Available functions: ${fnDefs.map(f => f.name).join(', ')}`); // Debug: Log the tracesAPI function definition const tracesAPIDef = fnDefs.find(f => f.name === 'tracesAPI'); if (tracesAPIDef) { logger_1.logger.info('tracesAPI function definition:', JSON.stringify(tracesAPIDef, null, 2)); } else { logger_1.logger.error('tracesAPI function not found in registry!'); } // Get current system prompt with latest wallet address const systemPrompt = this.getSystemPrompt(); logger_1.logger.info(`Using wallet address: ${wallet_1.walletManager.getWalletContext().wallet?.address || 'none'}`); const first = await this.openai.chat.completions.create({ model: this.config.openaiModel, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt } ], tools: fnDefs.map(def => ({ type: "function", function: def })), tool_choice: "auto", }); const msg = first.choices[0].message; logger_1.logger.info("First response received:", msg); // 2) If it didn't call a function, just return the text if (!msg.tool_calls || msg.tool_calls.length === 0) { logger_1.logger.info("No function calls made, returning direct response"); return { content: msg.content ?? "", }; } // 3) Otherwise, parse arguments & invoke your handler const functionCalls = []; for (const toolCall of msg.tool_calls) { const { name, arguments: argStr } = toolCall.function; logger_1.logger.info(`Raw function call - name: ${name}, arguments: ${argStr}`); const args = JSON.parse(argStr || "{}"); logger_1.logger.info(`Calling function: ${name} with args:`, args); try { const result = await registry_1.default.callFunction(name, args); functionCalls.push({ name, arguments: args, result, }); logger_1.logger.info(`Function ${name} completed successfully`); // Update conversation state based on function results this.updateConversationState(name, args, result); } catch (error) { logger_1.logger.error(`Function ${name} failed:`, error); functionCalls.push({ name, arguments: args, result: { error: error instanceof Error ? error.message : String(error) }, }); } } // 4) Send the function's results back into the chat for a final response const messages = [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt }, msg, ]; // Add function results - match by index to avoid duplicates for (let i = 0; i < functionCalls.length; i++) { const call = functionCalls[i]; const toolCall = msg.tool_calls[i]; // Use index instead of find() if (toolCall && toolCall.function.name === call.name) { messages.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(call.result), }); } } const second = await this.openai.chat.completions.create({ model: this.config.openaiModel, messages, }); const finalMessage = second.choices[0].message; logger_1.logger.debug("Final response received:", finalMessage); return { content: finalMessage?.content ?? "", functionCalls, }; } /** * Check if we should continue a previous action */ shouldContinueAction(userPrompt) { if (!this.conversationState) return false; const continueKeywords = ['continue', 'execute', 'proceed', 'next', 'build', 'submit', 'complete']; const lowerPrompt = userPrompt.toLowerCase(); return continueKeywords.some(keyword => lowerPrompt.includes(keyword)); } /** * Continue with the next step of a multi-step action */ async continueAction(userPrompt) { if (!this.conversationState) { return { content: "No previous action to continue." }; } logger_1.logger.info(`Continuing action: ${this.conversationState.currentAction} at step ${this.conversationState.step}`); // For Fusion+ swap, continue with the next step if (this.conversationState.currentAction === 'fusionPlusSwap') { if (this.conversationState.step === 1 && this.conversationState.quote) { // Step 2: Build Order return this.executeBuildOrder(); } else if (this.conversationState.step === 2 && this.conversationState.order) { // Step 3: Submit Order return this.executeSubmitOrder(); } } return { content: "Action completed or no next step available." }; } /** * Execute buildOrder step */ async executeBuildOrder() { const quote = this.conversationState.quote; const walletAddress = wallet_1.walletManager.getWalletContext().wallet?.address; if (!walletAddress) { return { content: "No wallet connected. Please connect your wallet first." }; } // Use the correct WETH addresses directly for Fusion+ API const buildOrderArgs = { endpoint: "buildOrder", srcChain: 42161, // Arbitrum dstChain: 1, // Ethereum srcTokenAddress: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", // Arbitrum WETH dstTokenAddress: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // Ethereum WETH amount: quote.srcTokenAmount || "1000000000000000", walletAddress: walletAddress, quote: quote, secretsHashList: ["0x315b47a8c3780434b153667588db4ca628526e20000000000000000000000000"] }; try { const result = await registry_1.default.callFunction('fusionPlusAPI', buildOrderArgs); this.conversationState.step = 2; this.conversationState.order = result; return { content: `Order built successfully! Now proceeding to submit the order.`, functionCalls: [{ name: 'fusionPlusAPI', arguments: buildOrderArgs, result: result }] }; } catch (error) { return { content: `Failed to build order: ${error}`, functionCalls: [{ name: 'fusionPlusAPI', arguments: buildOrderArgs, result: { error: error instanceof Error ? error.message : String(error) } }] }; } } /** * Execute submitOrder step */ async executeSubmitOrder() { const order = this.conversationState.order; const quote = this.conversationState.quote; // Get the original quote const walletAddress = wallet_1.walletManager.getWalletContext().wallet?.address; if (!walletAddress) { return { content: "No wallet connected. Please connect your wallet first." }; } // Debug: Log the order structure to understand the data logger_1.logger.info('Order structure:', JSON.stringify(order, null, 2)); // Extract the order data from the SDK response const orderInput = { salt: order.typedData.salt, makerAsset: order.typedData.makerAsset, takerAsset: order.typedData.takerAsset, maker: order.typedData.maker, receiver: order.typedData.receiver, makingAmount: order.typedData.makingAmount, takingAmount: order.typedData.takingAmount, makerTraits: order.typedData.makerTraits }; logger_1.logger.info('Extracted orderInput:', JSON.stringify(orderInput, null, 2)); const submitOrderArgs = { endpoint: "submitOrder", order: orderInput, srcChainId: 42161, // Use the source chain from the original quote signature: "0x", // SDK handles signing internally extension: order.extension || "0x", // Use the extension from the built order quoteId: order.quoteId || "", // Use the quoteId from the built order secretHashes: order.secretHashes || [] // Use the secretHashes from the built order }; try { const result = await registry_1.default.callFunction('fusionPlusAPI', submitOrderArgs); // Check if frontend signing is required if (result.requiresFrontendSigning) { return { content: `🔐 **Signature Required**\n\nYour wallet needs to sign this transaction to complete the Fusion+ swap. Please check your MetaMask for a signature request.`, functionCalls: [{ name: 'fusionPlusAPI', arguments: submitOrderArgs, result: { error: 'FRONTEND_SIGNING_REQUIRED: Order needs to be signed by frontend wallet', order: orderInput, srcChainId: 42161, quoteId: order.quoteId || "", secretHashes: order.secretHashes || [], extension: order.extension || "0x" } }] }; } this.conversationState = null; // Reset after completion return { content: `Order submitted successfully! Your cross-chain swap is now being processed.`, functionCalls: [{ name: 'fusionPlusAPI', arguments: submitOrderArgs, result: result }] }; } catch (error) { return { content: `Failed to submit order: ${error}`, functionCalls: [{ name: 'fusionPlusAPI', arguments: submitOrderArgs, result: { error: error instanceof Error ? error.message : String(error) } }] }; } } /** * Update conversation state based on function results */ updateConversationState(functionName, args, result) { if (functionName === 'fusionPlusAPI' && args.endpoint === 'getQuote' && !result.error) { // Started a Fusion+ swap this.conversationState = { currentAction: 'fusionPlusSwap', step: 1, data: args, quote: result }; logger_1.logger.info('Started Fusion+ swap flow'); } else if (functionName === 'fusionPlusAPI' && args.endpoint === 'buildOrder' && !result.error) { // Built order successfully if (this.conversationState?.currentAction === 'fusionPlusSwap') { this.conversationState.step = 2; this.conversationState.order = result; logger_1.logger.info('Built order successfully'); } } else if (functionName === 'fusionPlusAPI' && args.endpoint === 'submitOrder' && !result.error) { // Submitted order successfully this.conversationState = null; // Reset after completion logger_1.logger.info('Submitted order successfully'); } } /** * Get available functions */ async getAvailableFunctions() { await registry_1.default.init(); return registry_1.default.getAvailableFunctions(); } /** * Check if a specific function is available */ async hasFunction(name) { await registry_1.default.init(); return registry_1.default.hasFunction(name); } /** * Get function definitions for external use */ async getFunctionDefinitions() { await registry_1.default.init(); return registry_1.default.getFunctionDefinitions(); } } exports.OneInchAgentKit = OneInchAgentKit; /** * Convenience function for quick usage */ async function createAgent(config) { return new OneInchAgentKit(config); } /** * Legacy function for backward compatibility */ async function llmAgent(userPrompt, config) { const agent = new OneInchAgentKit(config); const response = await agent.chat(userPrompt); return response.content; } //# sourceMappingURL=llmAgent.js.map