diff --git a/.env.example b/.env.example index d42850008..7ad504c4e 100644 --- a/.env.example +++ b/.env.example @@ -341,6 +341,13 @@ STORY_API_BASE_URL= # Story API base URL STORY_API_KEY= # Story API key PINATA_JWT= # Pinata JWT for uploading files to IPFS +# Squid Router +SQUID_SDK_URL=https://apiplus.squidrouter.com # Default: https://apiplus.squidrouter.com +SQUID_INTEGRATOR_ID= # get integrator id through https://docs.squidrouter.com/ +SQUID_EVM_ADDRESS= +SQUID_EVM_PRIVATE_KEY= +SQUID_API_THROTTLE_INTERVAL= # Default: 0; Used to throttle API calls to avoid rate limiting (in ms) + # Cronos zkEVM CRONOSZKEVM_ADDRESS= CRONOSZKEVM_PRIVATE_KEY= diff --git a/agent/package.json b/agent/package.json index 1f18fa155..589d165b1 100644 --- a/agent/package.json +++ b/agent/package.json @@ -45,6 +45,7 @@ "@elizaos/plugin-nft-generation": "workspace:*", "@elizaos/plugin-node": "workspace:*", "@elizaos/plugin-solana": "workspace:*", + "@elizaos/plugin-squid-router": "workspace:*", "@elizaos/plugin-starknet": "workspace:*", "@elizaos/plugin-ton": "workspace:*", "@elizaos/plugin-sui": "workspace:*", diff --git a/agent/src/index.ts b/agent/src/index.ts index 8ee5d362b..31ac4ea7d 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -58,6 +58,7 @@ import { tonPlugin } from "@elizaos/plugin-ton"; import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; import { cronosZkEVMPlugin } from "@elizaos/plugin-cronoszkevm"; import { abstractPlugin } from "@elizaos/plugin-abstract"; +import { squidRouterPlugin } from "@elizaos/plugin-squid-router"; import Database from "better-sqlite3"; import fs from "fs"; import path from "path"; @@ -572,6 +573,14 @@ export async function createAgent( getSecret(character, "TON_PRIVATE_KEY") ? tonPlugin : null, getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null, getSecret(character, "STORY_PRIVATE_KEY") ? storyPlugin : null, + getSecret(character, "SQUID_SDK_URL") && + getSecret(character, "SQUID_INTEGRATOR_ID") && + getSecret(character, "SQUID_EVM_ADDRESS") && + getSecret(character, "SQUID_EVM_PRIVATE_KEY") && + getSecret(character, "SQUID_API_THROTTLE_INTERVAL") + ? squidRouterPlugin + : null, + ].filter(Boolean), providers: [], actions: [], diff --git a/packages/plugin-squid-router/README.md b/packages/plugin-squid-router/README.md new file mode 100644 index 000000000..20038ab02 --- /dev/null +++ b/packages/plugin-squid-router/README.md @@ -0,0 +1,26 @@ +# @elizaos/squid-router + +This plugin adds Squid Router functionality to Eliza agents. It allows cross chain swaps between blockchains. +For now, only swaps beteen EVM chains are supported, but the plan is to add swaps from/to Solana and the Cosomos ecosystem. +For supported chains and tokens, please refer to the [Squid Router documentation](https://docs.squidrouter.com/). + +## Configuration + +The plugin requires the following configuration: +``` +# Squid Router +SQUID_SDK_URL=https://apiplus.squidrouter.com # Default: https://apiplus.squidrouter.com +SQUID_INTEGRATOR_ID= # get integrator id through https://docs.squidrouter.com/ +SQUID_EVM_ADDRESS= +SQUID_EVM_PRIVATE_KEY= +``` + +## Actions + +### Cross Chain Swap + +name: `X_CHAIN_SWAP` + +Perform cross chain swaps for both native and ERC20 tokens supported by Squid Router. + +Message sample: `Bridge 1 ETH from Ethereum to Base` diff --git a/packages/plugin-squid-router/eslint.config.mjs b/packages/plugin-squid-router/eslint.config.mjs new file mode 100644 index 000000000..92fe5bbeb --- /dev/null +++ b/packages/plugin-squid-router/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-squid-router/package.json b/packages/plugin-squid-router/package.json new file mode 100644 index 000000000..1bc770618 --- /dev/null +++ b/packages/plugin-squid-router/package.json @@ -0,0 +1,25 @@ +{ + "name": "@elizaos/plugin-squid-router", + "version": "0.1.7-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@0xsquid/sdk": "2.8.29", + "@0xsquid/squid-types": "0.1.122", + "@elizaos/core": "workspace:*", + "ethers": "6.8.1", + "optional": "0.1.4", + "sharp": "0.33.5", + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "test": "vitest run", + "lint": "eslint --fix --cache ." + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-squid-router/src/actions/xChainSwap.ts b/packages/plugin-squid-router/src/actions/xChainSwap.ts new file mode 100644 index 000000000..778ad68a2 --- /dev/null +++ b/packages/plugin-squid-router/src/actions/xChainSwap.ts @@ -0,0 +1,262 @@ +import { + composeContext, + elizaLogger, + generateObjectDeprecated, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + 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"; + +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 => { + elizaLogger.log("Starting X_CHAIN_SWAP handler..."); + + // Initialize or update state + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose X chain swap context + const xChainSwapContext = composeContext({ + state, + template: xChainSwapTemplate, + }); + + // Generate X chain swap content + const content = await generateObjectDeprecated({ + runtime, + context: xChainSwapContext, + modelClass: ModelClass.SMALL, + }); + + if(content.toAddress === null) { + content.toAddress = runtime.getSetting("SQUID_EVM_ADDRESS"); + } + + elizaLogger.log("swap content: ",JSON.stringify(content)); + + // Validate transfer content + if (!isXChainSwapContent(content)) { + 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 = 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(!fromTokenObject?.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 ("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; + elizaLogger.log(`Finished! Check Axelarscan for details: ${axelarScanLink}`); + + if (callback) { + callback({ + text: + "Swap completed successfully! Check Axelarscan for details:\n " + axelarScanLink, + content: {}, + }); + } + + + + } catch (error) { + elizaLogger.error("Error during cross-chain swap:", error); + if (callback) { + callback({ + text: `Error during cross-chain swap: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + template: xChainSwapTemplate, + validate: async (runtime: IAgentRuntime) => { + await validateSquidRouterConfig(runtime); + return true; + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Bridge 1 ETH from Ethereum to Base", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll send 1 ETH from Ethereum to Base", + action: "X_CHAIN_SWAP", + }, + }, + { + user: "{{agent}}", + content: { + text: "Successfully sent 1 ETH to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62 on Base\nTransaction: 0x4fed598033f0added272c3ddefd4d83a521634a738474400b27378db462a76ec", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Please swap 1 SOL into USDC from Solana to Base on address 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll swap 1 SOL into USDC from Solana to Base on address 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8", + action: "X_CHAIN_SWAP", + }, + }, + { + user: "{{agent}}", + content: { + text: "Successfully Swapped 1 SOL into USDC and sent to 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8 on Base\nTransaction: 2sj3ifA5iPdRDfnkyK5LZ4KoyN57AH2QoHFSzuefom11F1rgdiUriYf2CodBbq9LBi77Q5bLHz4CShveisTu954B", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Send 100 UNI from Arbitrum to Ethereum", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll send 100 UNI to Ethereum right away.", + action: "X_CHAIN_SWAP", + }, + }, + { + user: "{{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 diff --git a/packages/plugin-squid-router/src/helpers/utils.ts b/packages/plugin-squid-router/src/helpers/utils.ts new file mode 100644 index 000000000..6909ca838 --- /dev/null +++ b/packages/plugin-squid-router/src/helpers/utils.ts @@ -0,0 +1,167 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { z } from 'zod'; +import { ethers } from 'ethers'; +import { bech32 } from 'bech32'; +import bs58 from 'bs58'; +import {SquidToken, XChainSwapContent} from "../types"; + +export function convertToWei(amount: string | number, token: SquidToken): string { + if (typeof token.decimals !== 'number' || token.decimals < 0 || token.decimals > 255) { + throw new Error("Invalid decimals value in token object."); + } + + try { + // Ensure amount is a string for ethers.js + const amountString = typeof amount === 'number' ? amount.toString() : amount; + + // Use ethers.js to parse the amount into the smallest unit + const parsedAmount = ethers.parseUnits(amountString, token.decimals); + + // Return the parsed amount as a string + return parsedAmount.toString(); + } catch (error) { + throw new Error(`Failed to convert amount: ${(error as Error).message}`); + } +} + + +export function isXChainSwapContent( + content: XChainSwapContent +): boolean { + + // Validate types + const validTypes = + typeof content.fromChain === "string" && + typeof content.toChain === "string" && + typeof content.fromToken === "string" && + typeof content.toToken === "string" && + typeof content.toAddress === "string" && + (typeof content.amount === "string" || + typeof content.amount === "number"); + if (validTypes) { + return true; + } + return false +} + +// Helper Validation Functions + +const isValidEvmAddress = (address: string): boolean => { + return ethers.isAddress(address); +}; + +const isValidEvmPrivateKey = (key: string): boolean => { + const cleanedKey = key.startsWith('0x') ? key.slice(2) : key; + return /^[0-9a-fA-F]{64}$/.test(cleanedKey); +}; + +const isValidSolanaAddress = (address: string): boolean => { + try { + const decoded = bs58.decode(address); + return decoded.length === 32; // Corrected from 32 || 44 to only 32 + } catch { + return false; + } +}; + +const isValidSolanaPrivateKey = (key: string): boolean => { + return /^[0-9a-fA-F]{64}$/.test(key); +}; + +const isValidCosmosAddress = (address: string): boolean => { + try { + const decoded = bech32.decode(address); + return decoded.prefix.startsWith('cosmos') && decoded.words.length === 52; + } catch { + return false; + } +}; + +const isValidCosmosPrivateKey = (key: string): boolean => { + return /^[0-9a-fA-F]{64}$/.test(key); +}; + +export const squidRouterEnvSchema = z + .object({ + SQUID_INTEGRATOR_ID: z.string().min(1, "Squid Integrator ID is required"), + SQUID_SDK_URL: z.string().min(1, "Squid SDK URL is required"), + + SQUID_EVM_ADDRESS: z.string().min(1, "Squid Integrator ID is required"), + SQUID_EVM_PRIVATE_KEY: z.string().min(1, "Squid Integrator ID is required"), + }) + .refine((data) => { + // Check if EVM pair is valid + const evmValid = + (data.SQUID_EVM_ADDRESS && data.SQUID_EVM_PRIVATE_KEY) && + isValidEvmAddress(data.SQUID_EVM_ADDRESS) && + isValidEvmPrivateKey(data.SQUID_EVM_PRIVATE_KEY); + + return evmValid; + }, { + message: "At least one valid address and private key pair is required: EVM, Solana, or Cosmos.", + path: [], // Global error + }) + .superRefine((data, ctx) => { + // EVM Validation + if (data.SQUID_EVM_ADDRESS || data.SQUID_EVM_PRIVATE_KEY) { + if (data.SQUID_EVM_ADDRESS && !isValidEvmAddress(data.SQUID_EVM_ADDRESS)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "EVM_ADDRESS is invalid or not checksummed correctly.", + path: ["EVM_ADDRESS"], + }); + } + + if (data.SQUID_EVM_PRIVATE_KEY && !isValidEvmPrivateKey(data.SQUID_EVM_PRIVATE_KEY)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "EVM_PRIVATE_KEY must be a 64-character hexadecimal string.", + path: ["EVM_PRIVATE_KEY"], + }); + } + + if ((data.SQUID_EVM_ADDRESS && !data.SQUID_EVM_PRIVATE_KEY) || (!data.SQUID_EVM_ADDRESS && data.SQUID_EVM_PRIVATE_KEY)) { + if (!data.SQUID_EVM_ADDRESS) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "EVM_ADDRESS is required when EVM_PRIVATE_KEY is provided.", + path: ["EVM_ADDRESS"], + }); + } + if (!data.SQUID_EVM_PRIVATE_KEY) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "EVM_PRIVATE_KEY is required when EVM_ADDRESS is provided.", + path: ["EVM_PRIVATE_KEY"], + }); + } + } + } + }); + +export type SquidRouterConfig = z.infer; + +export async function validateSquidRouterConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + SQUID_INTEGRATOR_ID: runtime.getSetting("SQUID_INTEGRATOR_ID"), + SQUID_SDK_URL: runtime.getSetting("SQUID_SDK_URL"), + SQUID_EVM_ADDRESS: runtime.getSetting("SQUID_EVM_ADDRESS"), + SQUID_EVM_PRIVATE_KEY: runtime.getSetting("SQUID_EVM_PRIVATE_KEY"), + }; + + return squidRouterEnvSchema.parse(config); + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessages = error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n"); + throw new Error( + `Squid Router configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/plugin-squid-router/src/index.ts b/packages/plugin-squid-router/src/index.ts new file mode 100644 index 000000000..fe3cfeb1f --- /dev/null +++ b/packages/plugin-squid-router/src/index.ts @@ -0,0 +1,19 @@ +import {squidRouterProvider} from "./providers/squidRouter.ts"; + +export * from "./actions/xChainSwap.ts"; +export * from "./providers/squidRouter.ts"; +export * from "./types"; + +import type { Plugin } from "@elizaos/core"; +import { xChainSwapAction } from "./actions/xChainSwap.ts"; + +export const squidRouterPlugin: Plugin = { + name: "squid-router", + description: "Squid router plugin", + providers: [squidRouterProvider], + evaluators: [], + services: [], + actions: [xChainSwapAction], +}; + +export default squidRouterPlugin; diff --git a/packages/plugin-squid-router/src/providers/squidRouter.ts b/packages/plugin-squid-router/src/providers/squidRouter.ts new file mode 100644 index 000000000..5034b23f8 --- /dev/null +++ b/packages/plugin-squid-router/src/providers/squidRouter.ts @@ -0,0 +1,122 @@ +import {IAgentRuntime, Memory, Provider, State} from "@elizaos/core"; +import {Squid} from "@0xsquid/sdk"; +import {ethers} from "ethers"; +import {ChainData, ChainType, RouteRequest, RouteResponse, Token} from "@0xsquid/squid-types"; +import {validateSquidRouterConfig} from "../helpers/utils.ts"; +import {ExecuteRoute, TransactionResponses} from "@0xsquid/sdk/dist/types"; +import {nativeTokenConstant, SquidToken} from "../types"; + +const getSDK = (baseUrl: string, integratorId: string): Squid => { + const squid = new Squid({ + baseUrl: baseUrl, + integratorId: integratorId + }); + return squid; +}; + +export class SquidRouterProvider { + private squid: Squid; + + constructor( + private squidSDKUrl: string, + private squidIntegratorID: string + ) { + this.squid = getSDK(squidSDKUrl,squidIntegratorID); + } + + async initialize(): Promise { + if(!this.squid.initialized) { + await this.squid.init(); + } + } + + getSquidObject(): Squid { + return this.squid; + } + + getChains(): ChainData[] { + return this.squid.chains; + } + + getTokens(): Token[] { + return this.squid.tokens; + } + getChain(targetChainName: string): ChainData | undefined { + const normalizedTarget = targetChainName.toLowerCase(); + const targetChain = this.getChains().find(chain => chain.networkName.toLowerCase() === normalizedTarget); + //For now only support EVM. Will add Cosmos, Solana in later releases + if(targetChain.chainType === ChainType.EVM) { + return targetChain; + } + } + + getToken(targetChain: ChainData, targetTokenSymbol: string): SquidToken | undefined { + const normalizedTargetToken = targetTokenSymbol.toLowerCase(); + if(normalizedTargetToken === targetChain.nativeCurrency.symbol) { + return { + address: nativeTokenConstant, + isNative: true, + symbol: targetTokenSymbol, + decimals: targetChain.nativeCurrency.decimals, + enabled: true + } + } + const targetToken = this.getTokens().find(token => token.symbol.toLowerCase() === normalizedTargetToken && token.chainId === targetChain.chainId); + return { + address: targetToken.address, + isNative: false, + symbol: targetTokenSymbol, + decimals: targetToken.decimals, + enabled: targetToken.disabled ?? true + } + } + + async getRoute(route: RouteRequest): Promise{ + return await this.squid.getRoute(route); + } + + async executeRoute(route: ExecuteRoute): Promise{ + return await this.squid.executeRoute(route); + } + + async getEVMSignerForChain(chain: ChainData, runtime): Promise { + try { + if(chain.chainType === ChainType.EVM) { + const provider = new ethers.JsonRpcProvider(chain.rpc); + return new ethers.Wallet(runtime.getSetting("SQUID_EVM_PRIVATE_KEY"), provider); + } else { + throw Error("Cannot instantiate EVM signer for non-EVM chain"); + } + } catch (error) { + throw Error("Cannot instantiate EVM signer: "+error); + } + } +} + +export const initSquidRouterProvider = (runtime: IAgentRuntime) => { + validateSquidRouterConfig(runtime); + + const sdkUrl = runtime.getSetting("SQUID_SDK_URL"); + const integratorId = runtime.getSetting("SQUID_INTEGRATOR_ID"); + + return new SquidRouterProvider(sdkUrl, integratorId); +}; + +const squidRouterProvider: Provider = { + get: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State + ): Promise => { + try { + const provider = initSquidRouterProvider(runtime); + return "Squid Router provider setup successful." + } catch (error) { + console.error("Error in Squid Router provider:", error); + return null; + } + }, +}; + +// Module exports +export { squidRouterProvider }; diff --git a/packages/plugin-squid-router/src/templates/index.ts b/packages/plugin-squid-router/src/templates/index.ts new file mode 100644 index 000000000..433b8fa07 --- /dev/null +++ b/packages/plugin-squid-router/src/templates/index.ts @@ -0,0 +1,29 @@ +export const xChainSwapTemplate = `Given the recent messages and wallet information below: + +{{recentMessages}} + +Extract the following information about the requested cross chain swap: +- Token symbol to swap from +- Token symbol to swap into (if defined, otherwise the same as the token symbol to swap from) +- Source chain +- Destination chain (if defined, otherwise the same as the source chain) +- Amount to swap, denominated in the token to be sent +- Destination address (if specified) + +If the destination address is not specified, the EVM address of the runtime should be used. +If the token to swap into is not specified, the token to swap from should be used. +If the destination chain is not specified, the source chain should be used. + +Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ + "fromToken": string | null, + "toToken": string | null, + "fromChain": string | null, + "toChain": string | null, + "amount": string | null, + "toAddress": string | null +} +\`\`\` +`; diff --git a/packages/plugin-squid-router/src/tests/router.test.ts b/packages/plugin-squid-router/src/tests/router.test.ts new file mode 100644 index 000000000..a1eeaf99c --- /dev/null +++ b/packages/plugin-squid-router/src/tests/router.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; +import { defaultCharacter } from "@elizaos/core"; +import { initSquidRouterProvider, SquidRouterProvider } from "../providers/squidRouter.ts"; +import {ChainType} from "@0xsquid/squid-types"; + +// Mock Squid module +vi.mock('@0xsquid/sdk', () => { + return { + Squid: vi.fn().mockImplementation(() => { + return { + initialized: false, + init: vi.fn().mockResolvedValue(undefined), + getRoute: vi.fn().mockResolvedValue({}), + executeRoute: vi.fn().mockResolvedValue({}), + chains: [{networkName: "ethereum", chainType: ChainType.EVM, nativeCurrency: {symbol: "ETH", decimals: 18}, chainId: "1"}], + tokens: [{symbol: "ETH", chainId: "1", address: "0x0", decimals: 18}], + }; + }) + }; +}); + +describe("SquidRouterProvider", () => { + let routerProvider: SquidRouterProvider; + let mockedRuntime; + + beforeEach(async () => { + vi.clearAllMocks(); + + mockedRuntime = { + character: defaultCharacter, + getSetting: vi.fn().mockImplementation((key: string) => { + if (key === "SQUID_SDK_URL") return "test_sdk_url"; + if (key === "SQUID_INTEGRATOR_ID") return "test_integrator_id"; + //public/private key for testing + if (key === "SQUID_EVM_PRIVATE_KEY") return "9a2bb49ab3fc4084e61a73c061b8a64041ce22ad57d8b99d938be2ac3143f2fa"; + if (key === "SQUID_EVM_ADDRESS") return "0xbb5F4ddaBbbb0AcD2086527A887b208b06A3BFdb"; + return undefined; + }), + }; + + routerProvider = initSquidRouterProvider(mockedRuntime); + await routerProvider.initialize(); + }); + + afterEach(() => { + vi.clearAllTimers(); + }); + + describe("Initialization", () => { + it("should initialize the Squid SDK", async () => { + expect(routerProvider).toBeDefined(); + expect(routerProvider.getChains()).toBeDefined(); + expect(routerProvider.getTokens()).toBeDefined(); + }); + }); + + describe("getChains", () => { + it("should return a list of chains", () => { + const chains = routerProvider.getChains(); + expect(chains).toBeInstanceOf(Array); + }); + }); + + describe("getTokens", () => { + it("should return a list of tokens", () => { + const tokens = routerProvider.getTokens(); + expect(tokens).toBeInstanceOf(Array); + }); + }); + + describe("getChain", () => { + it("should return the correct chain data for a given chain name", () => { + const chain = routerProvider.getChain("ethereum"); + expect(chain).toBeDefined(); + expect(chain.networkName).toEqual("ethereum"); + }); + }); + + describe("getToken", () => { + it("should return the correct token data for a given chain and token symbol", () => { + const chain = routerProvider.getChain("ethereum"); + const token = routerProvider.getToken(chain, "ETH"); + expect(token).toBeDefined(); + expect(token.symbol).toEqual("ETH"); + }); + }); + + describe("getRoute", () => { + it("should return a route response for a given route request", async () => { + const routeRequest = { + fromChain: "ethereum", + toChain: "polygon", + fromToken: "ETH", + toToken: "MATIC", + amount: "1", + fromAddress: "0xYourAddress", + toAddress: "0xRecipientAddress", + }; + const routeResponse = await routerProvider.getRoute(routeRequest); + expect(routeResponse).toBeDefined(); + }); + }); + + describe("executeRoute", () => { + it("should execute a route and return transaction responses", async () => { + const executeRoute = { + route: { + fromChain: "ethereum", + toChain: "polygon", + fromToken: "ETH", + toToken: "MATIC", + amount: "1", + fromAddress: "0xYourAddress", + toAddress: "0xRecipientAddress", + }, + }; + const transactionResponses = await routerProvider.executeRoute(executeRoute); + expect(transactionResponses).toBeDefined(); + }); + }); + + describe("getEVMSignerForChain", () => { + it("should return an EVM signer for a given chain", async () => { + const chain = routerProvider.getChain("ethereum"); + const signer = await routerProvider.getEVMSignerForChain(chain, mockedRuntime); + expect(signer).toBeDefined(); + }); + }); +}); diff --git a/packages/plugin-squid-router/src/types/index.ts b/packages/plugin-squid-router/src/types/index.ts new file mode 100644 index 000000000..518eefce0 --- /dev/null +++ b/packages/plugin-squid-router/src/types/index.ts @@ -0,0 +1,20 @@ +import {Content} from "@elizaos/core"; + +export const nativeTokenConstant = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; + +export interface XChainSwapContent extends Content { + fromToken: string; + toToken: string; + fromChain: string; + toChain: string; + amount: string | number; + toAddress: string; +} + +export interface SquidToken { + address: string; + isNative: boolean; + symbol: string; + decimals: number; + enabled: boolean; +} diff --git a/packages/plugin-squid-router/tsconfig.json b/packages/plugin-squid-router/tsconfig.json new file mode 100644 index 000000000..2d8d3fe81 --- /dev/null +++ b/packages/plugin-squid-router/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src", + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ], + "declaration": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/plugin-squid-router/tsup.config.ts b/packages/plugin-squid-router/tsup.config.ts new file mode 100644 index 000000000..299db52c3 --- /dev/null +++ b/packages/plugin-squid-router/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive" + ], +});