UNPKG

@elizaos/plugin-squid-router

Version:

ElizaOS plugin for cross-chain token swaps using Squid Router

304 lines (273 loc) 9.19 kB
import { composePromptFromState, elizaLogger, parseKeyValueXml, type HandlerCallback, type IAgentRuntime, type Memory, ModelType, type State, } from "@elizaos/core"; import { xChainSwapTemplate } from "../templates"; import { convertToWei, isXChainSwapContent, validateSquidRouterConfig, } from "../helpers/utils.ts"; import { ethers } from "ethers"; import { initSquidRouterProvider } from "../providers/squidRouter.ts"; import type { XChainSwapContent } from "../types"; export { xChainSwapTemplate }; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const approveSpending = async ( transactionRequestTarget: string, fromToken: string, fromAmount: string, signer: ethers.Signer ) => { const erc20Abi = [ "function approve(address spender, uint256 amount) public returns (bool)", ]; const tokenContract = new ethers.Contract(fromToken, erc20Abi, signer); try { const tx = await tokenContract.approve( transactionRequestTarget, fromAmount ); await tx.wait(); console.log( `Approved ${fromAmount} tokens for ${transactionRequestTarget}` ); } catch (error) { console.error("Approval failed:", error); throw error; } }; export const xChainSwapAction = { name: "X_CHAIN_SWAP", description: "Swaps tokens across chains from the agent's wallet to a recipient wallet. \n" + "By default the senders configured wallets will be used to send the assets to on the destination chains, unless clearly defined otherwise by providing a recipient address.\n" + "The system supports bridging, cross chain swaps and normal swaps.", handler: async ( runtime: IAgentRuntime, message: Memory, state?: State, _options: { [key: string]: unknown } = {}, callback?: HandlerCallback ): Promise<boolean> => { elizaLogger.log("Starting X_CHAIN_SWAP handler..."); let currentState = state; // Create new variable if (!currentState) { currentState = await runtime.composeState(message); } else { currentState = await runtime.composeState(message, ["RECENT_MESSAGES"]); } // Compose X chain swap prompt const xChainSwapPrompt = composePromptFromState({ state: currentState, // Use the new variable template: xChainSwapTemplate, }); // Generate X chain swap content using new model API const result = await runtime.useModel(ModelType.TEXT_SMALL, { prompt: xChainSwapPrompt, stopSequences: [], }); const content = parseKeyValueXml(result); if (!content) { elizaLogger.error("Failed to parse XML response"); if (callback) { callback({ text: "Unable to process cross-chain swap request. Invalid response format.", content: { error: "Failed to parse response" }, }); } return false; } if (content.toAddress === null) { content.toAddress = runtime.getSetting("SQUID_EVM_ADDRESS"); } elizaLogger.log("swap content: ", JSON.stringify(content)); // Validate transfer content if (!isXChainSwapContent(content as XChainSwapContent)) { console.error("Invalid content for X_CHAIN_SWAP action."); if (callback) { callback({ text: "Unable to process cross-chain swap request. Invalid content provided.", content: { error: "Invalid cross-chain swap content" }, }); } return false; } try { const squidRouter = await initSquidRouterProvider(runtime); await squidRouter.initialize(); console.log("Initialized Squid SDK"); const fromChainObject = squidRouter.getChain(content.fromChain); if (!fromChainObject) { throw new Error("Chain to swap from is not supported."); } const toChainObject = squidRouter.getChain(content.toChain); if (!toChainObject) { throw new Error("Chain to swap to is not supported."); } const fromTokenObject = squidRouter.getToken( fromChainObject, content.fromToken ); if (!fromTokenObject?.enabled) { throw new Error("Token to swap from is not supported."); } const toTokenObject = squidRouter.getToken( toChainObject, content.toToken ); if (!toTokenObject?.enabled) { throw new Error("Token to swap into is not supported."); } const signer = await squidRouter.getEVMSignerForChain( fromChainObject, runtime ); const params = { fromAddress: await signer.getAddress(), fromChain: fromChainObject.chainId, fromToken: fromTokenObject.address, fromAmount: convertToWei(content.amount, fromTokenObject!), toChain: toChainObject.chainId, toToken: toTokenObject.address, toAddress: content.toAddress, quoteOnly: false, }; console.log("Parameters:", params); // Printing the parameters for QA const throttleInterval = runtime.getSetting("SQUID_API_THROTTLE_INTERVAL") ? Number(runtime.getSetting("SQUID_API_THROTTLE_INTERVAL")) : 0; await delay(throttleInterval); // Get the swap route using Squid SDK const { route } = await squidRouter.getRoute(params); console.log("Calculated route:", route.estimate.toAmount); const transactionRequest = route.transactionRequest; // Approve the transactionRequest.target to spend fromAmount of fromToken if (transactionRequest && "target" in transactionRequest) { if (!fromTokenObject.isNative) { await approveSpending( transactionRequest.target, params.fromToken, params.fromAmount, signer ); } } else { throw new Error("Non-expected transaction request"); } await delay(throttleInterval); // Execute the swap transaction const tx = (await squidRouter.executeRoute({ signer, route, })) as unknown as ethers.TransactionResponse; const txReceipt = await tx.wait(); // Show the transaction receipt with Axelarscan link const axelarScanLink = `https://axelarscan.io/gmp/${txReceipt?.hash || "unknown"}`; // Fix: Use template literal elizaLogger.log( `Finished! Check Axelarscan for details: ${axelarScanLink}` ); if (callback) { callback({ text: `Swap completed successfully! Check Axelarscan for details:\n${axelarScanLink}`, // Fix: Use template literal content: {}, }); } } catch (error) { elizaLogger.error("Error during cross-chain swap:", error); if (callback) { const errorMessage = error instanceof Error ? error.message : String(error); callback({ text: `Error during cross-chain swap: ${errorMessage}`, content: { error: errorMessage }, }); } return false; } return true; }, template: xChainSwapTemplate, validate: async (runtime: IAgentRuntime) => { await validateSquidRouterConfig(runtime); return true; }, examples: [ [ { name: "{{user1}}", content: { text: "Bridge 1 ETH from Ethereum to Base", }, }, { name: "{{agent}}", content: { text: "Sure, I'll send 1 ETH from Ethereum to Base", action: "X_CHAIN_SWAP", }, }, { name: "{{agent}}", content: { text: "Successfully sent 1 ETH to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62 on Base\nTransaction: 0x4fed598033f0added272c3ddefd4d83a521634a738474400b27378db462a76ec", }, }, ], [ { name: "{{user1}}", content: { text: "Please swap 1 SOL into USDC from Solana to Base on address 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8", }, }, { name: "{{agent}}", content: { text: "Sure, I'll swap 1 SOL into USDC from Solana to Base on address 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8", action: "X_CHAIN_SWAP", }, }, { name: "{{agent}}", content: { text: "Successfully Swapped 1 SOL into USDC and sent to 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8 on Base\nTransaction: 2sj3ifA5iPdRDfnkyK5LZ4KoyN57AH2QoHFSzuefom11F1rgdiUriYf2CodBbq9LBi77Q5bLHz4CShveisTu954B", }, }, ], [ { name: "{{user1}}", content: { text: "Send 100 UNI from Arbitrum to Ethereum", }, }, { name: "{{agent}}", content: { text: "Sure, I'll send 100 UNI to Ethereum right away.", action: "X_CHAIN_SWAP", }, }, { name: "{{agent}}", content: { text: "Successfully sent 100 UNI to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62 on Ethereum\nTransaction: 0x4fed598033f0added272c3ddefd4d83a521634a738474400b27378db462a76ec", }, }, ], ], similes: [ "CROSS_CHAIN_SWAP", "CROSS_CHAIN_BRIDGE", "MOVE_CROSS_CHAIN", "SWAP", "BRIDGE", ], }; // TODO: add more examples / similies