From 2c8e5028dd55ca96f552fd303ef34ef573c3169d Mon Sep 17 00:00:00 2001 From: Archethect Date: Thu, 26 Dec 2024 10:56:28 +0100 Subject: [PATCH 1/8] feat(plugin-squid-router): add squid router support for cross chain swaps --- packages/plugin-squid-bridge/README.md | 93 +++++++ .../plugin-squid-bridge/eslint.config.mjs | 3 + packages/plugin-squid-bridge/package.json | 22 ++ .../src/actions/xChainSwap.ts | 243 ++++++++++++++++ .../plugin-squid-bridge/src/helpers/utils.ts | 259 ++++++++++++++++++ packages/plugin-squid-bridge/src/index.ts | 19 ++ .../src/providers/squidRouter.ts | 118 ++++++++ .../src/templates/index.ts | 25 ++ .../src/tests/transfer.test.ts | 55 ++++ .../src/tests/wallet.test.ts | 214 +++++++++++++++ .../plugin-squid-bridge/src/types/index.ts | 20 ++ packages/plugin-squid-bridge/tsconfig.json | 15 + packages/plugin-squid-bridge/tsup.config.ts | 21 ++ 13 files changed, 1107 insertions(+) create mode 100644 packages/plugin-squid-bridge/README.md create mode 100644 packages/plugin-squid-bridge/eslint.config.mjs create mode 100644 packages/plugin-squid-bridge/package.json create mode 100644 packages/plugin-squid-bridge/src/actions/xChainSwap.ts create mode 100644 packages/plugin-squid-bridge/src/helpers/utils.ts create mode 100644 packages/plugin-squid-bridge/src/index.ts create mode 100644 packages/plugin-squid-bridge/src/providers/squidRouter.ts create mode 100644 packages/plugin-squid-bridge/src/templates/index.ts create mode 100644 packages/plugin-squid-bridge/src/tests/transfer.test.ts create mode 100644 packages/plugin-squid-bridge/src/tests/wallet.test.ts create mode 100644 packages/plugin-squid-bridge/src/types/index.ts create mode 100644 packages/plugin-squid-bridge/tsconfig.json create mode 100644 packages/plugin-squid-bridge/tsup.config.ts diff --git a/packages/plugin-squid-bridge/README.md b/packages/plugin-squid-bridge/README.md new file mode 100644 index 0000000000..e2a3004325 --- /dev/null +++ b/packages/plugin-squid-bridge/README.md @@ -0,0 +1,93 @@ +# `@elizaos/plugin-squid-router` + +This plugin provides actions and providers for cross chain bridging leveraging [Squid Router](https://www.squidrouter.com/). + +--- + +## Configuration + +### Default Setup + +By default, **Ethereum mainnet** is enabled. To use it, simply add your private key to the `.env` file: + +```env +EVM_PRIVATE_KEY=your-private-key-here +``` + +### Adding Support for Other Chains + +To enable support for additional chains, add them to the character config like this: + +```json +"settings": { + "chains": { + "evm": [ + "base", "arbitrum", "iotex" + ] + } +} +``` + +Note: The chain names must match those in the viem/chains. + +### Custom RPC URLs + +By default, the RPC URL is inferred from the `viem/chains` config. To use a custom RPC URL for a specific chain, add the following to your `.env` file: + +```env +ETHEREUM_PROVIDER_=https://your-custom-rpc-url +``` + +**Example usage:** + +```env +ETHEREUM_PROVIDER_IOTEX=https://iotex-network.rpc.thirdweb.com +``` + +#### Custom RPC for Ethereum Mainnet + +To set a custom RPC URL for Ethereum mainnet, use: + +```env +EVM_PROVIDER_URL=https://your-custom-mainnet-rpc-url +``` + +## Provider + +The **Wallet Provider** initializes with the **first chain in the list** as the default (or Ethereum mainnet if none are added). It: + +- Provides the **context** of the currently connected address and its balance. +- Creates **Public** and **Wallet clients** to interact with the supported chains. +- Allows adding chains dynamically at runtime. + +--- + +## Actions + +### Transfer + +Transfer tokens from one address to another on any EVM-compatible chain. Just specify the: + +- **Amount** +- **Chain** +- **Recipient Address** + +**Example usage:** + +```bash +Transfer 1 ETH to 0xRecipient on arbitrum. +``` + +--- + +## Contribution + +The plugin contains tests. Whether you're using **TDD** or not, please make sure to run the tests before submitting a PR. + +### Running Tests + +Navigate to the `plugin-evm` directory and run: + +```bash +pnpm test +``` diff --git a/packages/plugin-squid-bridge/eslint.config.mjs b/packages/plugin-squid-bridge/eslint.config.mjs new file mode 100644 index 0000000000..92fe5bbebe --- /dev/null +++ b/packages/plugin-squid-bridge/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-squid-bridge/package.json b/packages/plugin-squid-bridge/package.json new file mode 100644 index 0000000000..b8b9625bbd --- /dev/null +++ b/packages/plugin-squid-bridge/package.json @@ -0,0 +1,22 @@ +{ + "name": "@elizaos/plugin-squid-bridge", + "version": "0.1.7-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "tsup": "8.3.5", + "@0xsquid/sdk": "2.8.29", + "@0xsquid/squid-types": "0.1.130" + }, + "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-bridge/src/actions/xChainSwap.ts b/packages/plugin-squid-bridge/src/actions/xChainSwap.ts new file mode 100644 index 0000000000..6a8c77b5d4 --- /dev/null +++ b/packages/plugin-squid-bridge/src/actions/xChainSwap.ts @@ -0,0 +1,243 @@ +import { + composeContext, + elizaLogger, + generateObject, generateObjectDeprecated, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State +} from "@elizaos/core"; +import {xChainSwapTemplate} from "../templates"; +import {convertToWei, isXChainSwapContent, validateSquidRouterConfig} from "../helpers/utils.ts"; +import {Squid} from "@0xsquid/sdk"; +import {ChainData, ChainType, Token} from "@0xsquid/squid-types"; +import {ethers} from "ethers"; +import {nativeTokenConstant, SquidToken} from "../types"; +import {initSquidRouterProvider} from "../providers/squidRouter.ts"; + +export { xChainSwapTemplate }; + +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, + }); + + // 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, + enableBoost: true, + }; + + console.log("Parameters:", params); // Printing the parameters for QA + + // 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" + ); + } + + // Execute the swap transaction + const tx = (await squidRouter.executeRoute({ + signer, + route, + })) as unknown as ethers.providers.TransactionResponse; + const txReceipt = await tx.wait(); + + // Show the transaction receipt with Axelarscan link + const axelarScanLink = "https://axelarscan.io/gmp/" + txReceipt.transactionHash; + console.log(`Finished! Check Axelarscan for details: ${axelarScanLink}`); + + + + } 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-bridge/src/helpers/utils.ts b/packages/plugin-squid-bridge/src/helpers/utils.ts new file mode 100644 index 0000000000..d40b8638d4 --- /dev/null +++ b/packages/plugin-squid-bridge/src/helpers/utils.ts @@ -0,0 +1,259 @@ +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.utils.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 +): content is XChainSwapContent { + // 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 false; + } +} + +// Helper Validation Functions + +const isValidEvmAddress = (address: string): boolean => { + return ethers.utils.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"), + + EVM_ADDRESS: z.string().optional(), + EVM_PRIVATE_KEY: z.string().optional(), + + SOLANA_ADDRESS: z.string().optional(), + SOLANA_PRIVATE_KEY: z.string().optional(), + + COSMOS_ADDRESS: z.string().optional(), + COSMOS_PRIVATE_KEY: z.string().optional(), + }) + .refine((data) => { + // Check if EVM pair is valid + const evmValid = + (data.EVM_ADDRESS && data.EVM_PRIVATE_KEY) && + isValidEvmAddress(data.EVM_ADDRESS) && + isValidEvmPrivateKey(data.EVM_PRIVATE_KEY); + + // Check if Solana pair is valid + const solanaValid = + (data.SOLANA_ADDRESS && data.SOLANA_PRIVATE_KEY) && + isValidSolanaAddress(data.SOLANA_ADDRESS) && + isValidSolanaPrivateKey(data.SOLANA_PRIVATE_KEY); + + // Check if Cosmos pair is valid + const cosmosValid = + (data.COSMOS_ADDRESS && data.COSMOS_PRIVATE_KEY) && + isValidCosmosAddress(data.COSMOS_ADDRESS) && + isValidCosmosPrivateKey(data.COSMOS_PRIVATE_KEY); + + return evmValid || solanaValid || cosmosValid; + }, { + 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.EVM_ADDRESS || data.EVM_PRIVATE_KEY) { + if (data.EVM_ADDRESS && !isValidEvmAddress(data.EVM_ADDRESS)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "EVM_ADDRESS is invalid or not checksummed correctly.", + path: ["EVM_ADDRESS"], + }); + } + + if (data.EVM_PRIVATE_KEY && !isValidEvmPrivateKey(data.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.EVM_ADDRESS && !data.EVM_PRIVATE_KEY) || (!data.EVM_ADDRESS && data.EVM_PRIVATE_KEY)) { + if (!data.EVM_ADDRESS) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "EVM_ADDRESS is required when EVM_PRIVATE_KEY is provided.", + path: ["EVM_ADDRESS"], + }); + } + if (!data.EVM_PRIVATE_KEY) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "EVM_PRIVATE_KEY is required when EVM_ADDRESS is provided.", + path: ["EVM_PRIVATE_KEY"], + }); + } + } + } + + // Solana Validation + if (data.SOLANA_ADDRESS || data.SOLANA_PRIVATE_KEY) { + if (data.SOLANA_ADDRESS && !isValidSolanaAddress(data.SOLANA_ADDRESS)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "SOLANA_ADDRESS is invalid.", + path: ["SOLANA_ADDRESS"], + }); + } + + if (data.SOLANA_PRIVATE_KEY && !isValidSolanaPrivateKey(data.SOLANA_PRIVATE_KEY)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "SOLANA_PRIVATE_KEY must be a 64-character hexadecimal string.", + path: ["SOLANA_PRIVATE_KEY"], + }); + } + + if ((data.SOLANA_ADDRESS && !data.SOLANA_PRIVATE_KEY) || (!data.SOLANA_ADDRESS && data.SOLANA_PRIVATE_KEY)) { + if (!data.SOLANA_ADDRESS) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "SOLANA_ADDRESS is required when SOLANA_PRIVATE_KEY is provided.", + path: ["SOLANA_ADDRESS"], + }); + } + if (!data.SOLANA_PRIVATE_KEY) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "SOLANA_PRIVATE_KEY is required when SOLANA_ADDRESS is provided.", + path: ["SOLANA_PRIVATE_KEY"], + }); + } + } + } + + // Cosmos Validation + if (data.COSMOS_ADDRESS || data.COSMOS_PRIVATE_KEY) { + if (data.COSMOS_ADDRESS && !isValidCosmosAddress(data.COSMOS_ADDRESS)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "COSMOS_ADDRESS is invalid.", + path: ["COSMOS_ADDRESS"], + }); + } + + if (data.COSMOS_PRIVATE_KEY && !isValidCosmosPrivateKey(data.COSMOS_PRIVATE_KEY)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "COSMOS_PRIVATE_KEY must be a 64-character hexadecimal string.", + path: ["COSMOS_PRIVATE_KEY"], + }); + } + + if ((data.COSMOS_ADDRESS && !data.COSMOS_PRIVATE_KEY) || (!data.COSMOS_ADDRESS && data.COSMOS_PRIVATE_KEY)) { + if (!data.COSMOS_ADDRESS) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "COSMOS_ADDRESS is required when COSMOS_PRIVATE_KEY is provided.", + path: ["COSMOS_ADDRESS"], + }); + } + if (!data.COSMOS_PRIVATE_KEY) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "COSMOS_PRIVATE_KEY is required when COSMOS_ADDRESS is provided.", + path: ["COSMOS_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"), + EVM_ADDRESS: runtime.getSetting("EVM_ADDRESS"), + EVM_PRIVATE_KEY: runtime.getSetting("EVM_PRIVATE_KEY"), + SOLANA_ADDRESS: runtime.getSetting("SOLANA_ADDRESS"), + SOLANA_PRIVATE_KEY: runtime.getSetting("SOLANA_PRIVATE_KEY"), + COSMOS_ADDRESS: runtime.getSetting("COSMOS_ADDRESS"), + COSMOS_PRIVATE_KEY: runtime.getSetting("COSMOS_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-bridge/src/index.ts b/packages/plugin-squid-bridge/src/index.ts new file mode 100644 index 0000000000..e4ce7bb7e0 --- /dev/null +++ b/packages/plugin-squid-bridge/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 squidBridgePlugin: Plugin = { + name: "squid-bridge", + description: "Squid router bridge plugin", + providers: [squidRouterProvider], + evaluators: [], + services: [], + actions: [xChainSwapAction], +}; + +export default squidBridgePlugin; diff --git a/packages/plugin-squid-bridge/src/providers/squidRouter.ts b/packages/plugin-squid-bridge/src/providers/squidRouter.ts new file mode 100644 index 0000000000..736c663382 --- /dev/null +++ b/packages/plugin-squid-bridge/src/providers/squidRouter.ts @@ -0,0 +1,118 @@ +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 squidIntergatorID: string + ) { + this.squid = getSDK(squidSDKUrl,squidIntergatorID); + } + + async initialize(): Promise { + if(!this.squid.initialized) { + await this.squid.init(); + } + } + + 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.providers.JsonRpcProvider(chain.rpc); + return new ethers.Wallet(runtime.getSetting("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-bridge/src/templates/index.ts b/packages/plugin-squid-bridge/src/templates/index.ts new file mode 100644 index 0000000000..4c6ac39b4f --- /dev/null +++ b/packages/plugin-squid-bridge/src/templates/index.ts @@ -0,0 +1,25 @@ +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) +- Source chain +- Destination chain +- Amount to swap, denominated in the token to be sent +- Destination address (if specified) + +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-bridge/src/tests/transfer.test.ts b/packages/plugin-squid-bridge/src/tests/transfer.test.ts new file mode 100644 index 0000000000..a6159db76d --- /dev/null +++ b/packages/plugin-squid-bridge/src/tests/transfer.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { Account, Chain } from "viem"; + +import { TransferAction } from "../actions/transfer"; +import { WalletProvider } from "../providers/wallet"; + +describe("Transfer Action", () => { + let wp: WalletProvider; + + beforeEach(async () => { + const pk = generatePrivateKey(); + const customChains = prepareChains(); + wp = new WalletProvider(pk, customChains); + }); + describe("Constructor", () => { + it("should initialize with wallet provider", () => { + const ta = new TransferAction(wp); + + expect(ta).toBeDefined(); + }); + }); + describe("Transfer", () => { + let ta: TransferAction; + let receiver: Account; + + beforeEach(() => { + ta = new TransferAction(wp); + receiver = privateKeyToAccount(generatePrivateKey()); + }); + + it("throws if not enough gas", async () => { + await expect( + ta.transfer({ + fromChain: "iotexTestnet", + toAddress: receiver.address, + amount: "1", + }) + ).rejects.toThrow( + "Transfer failed: The total cost (gas * gas fee + value) of executing this transaction exceeds the balance of the account." + ); + }); + }); +}); + +const prepareChains = () => { + const customChains: Record = {}; + const chainNames = ["iotexTestnet"]; + chainNames.forEach( + (chain) => + (customChains[chain] = WalletProvider.genChainFromName(chain)) + ); + + return customChains; +}; diff --git a/packages/plugin-squid-bridge/src/tests/wallet.test.ts b/packages/plugin-squid-bridge/src/tests/wallet.test.ts new file mode 100644 index 0000000000..a6b227a470 --- /dev/null +++ b/packages/plugin-squid-bridge/src/tests/wallet.test.ts @@ -0,0 +1,214 @@ +import { describe, it, expect, beforeAll, beforeEach } from "vitest"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { mainnet, iotex, arbitrum, Chain } from "viem/chains"; + +import { WalletProvider } from "../providers/wallet"; + +const customRpcUrls = { + mainnet: "custom-rpc.mainnet.io", + arbitrum: "custom-rpc.base.io", + iotex: "custom-rpc.iotex.io", +}; + +describe("Wallet provider", () => { + let walletProvider: WalletProvider; + let pk: `0x${string}`; + const customChains: Record = {}; + + beforeAll(() => { + pk = generatePrivateKey(); + + const chainNames = ["iotex", "arbitrum"]; + chainNames.forEach( + (chain) => + (customChains[chain] = WalletProvider.genChainFromName(chain)) + ); + }); + + describe("Constructor", () => { + it("sets address", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + + walletProvider = new WalletProvider(pk); + + expect(walletProvider.getAddress()).toEqual(expectedAddress); + }); + it("sets default chain to ethereum mainnet", () => { + walletProvider = new WalletProvider(pk); + + expect(walletProvider.chains.mainnet.id).toEqual(mainnet.id); + expect(walletProvider.getCurrentChain().id).toEqual(mainnet.id); + }); + it("sets custom chains", () => { + walletProvider = new WalletProvider(pk, customChains); + + expect(walletProvider.chains.iotex.id).toEqual(iotex.id); + expect(walletProvider.chains.arbitrum.id).toEqual(arbitrum.id); + }); + it("sets the first provided custom chain as current chain", () => { + walletProvider = new WalletProvider(pk, customChains); + + expect(walletProvider.getCurrentChain().id).toEqual(iotex.id); + }); + }); + describe("Clients", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk); + }); + it("generates public client", () => { + const client = walletProvider.getPublicClient("mainnet"); + expect(client.chain.id).toEqual(mainnet.id); + expect(client.transport.url).toEqual( + mainnet.rpcUrls.default.http[0] + ); + }); + it("generates public client with custom rpcurl", () => { + const chain = WalletProvider.genChainFromName( + "mainnet", + customRpcUrls.mainnet + ); + const wp = new WalletProvider(pk, { ["mainnet"]: chain }); + + const client = wp.getPublicClient("mainnet"); + expect(client.chain.id).toEqual(mainnet.id); + expect(client.chain.rpcUrls.default.http[0]).toEqual( + mainnet.rpcUrls.default.http[0] + ); + expect(client.chain.rpcUrls.custom.http[0]).toEqual( + customRpcUrls.mainnet + ); + expect(client.transport.url).toEqual(customRpcUrls.mainnet); + }); + it("generates wallet client", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + + const client = walletProvider.getWalletClient("mainnet"); + + expect(client.account.address).toEqual(expectedAddress); + expect(client.transport.url).toEqual( + mainnet.rpcUrls.default.http[0] + ); + }); + it("generates wallet client with custom rpcurl", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + const chain = WalletProvider.genChainFromName( + "mainnet", + customRpcUrls.mainnet + ); + const wp = new WalletProvider(pk, { ["mainnet"]: chain }); + + const client = wp.getWalletClient("mainnet"); + + expect(client.account.address).toEqual(expectedAddress); + expect(client.chain.id).toEqual(mainnet.id); + expect(client.chain.rpcUrls.default.http[0]).toEqual( + mainnet.rpcUrls.default.http[0] + ); + expect(client.chain.rpcUrls.custom.http[0]).toEqual( + customRpcUrls.mainnet + ); + expect(client.transport.url).toEqual(customRpcUrls.mainnet); + }); + }); + describe("Balance", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk, customChains); + }); + it("should fetch balance", async () => { + const bal = await walletProvider.getWalletBalance(); + + expect(bal).toEqual("0"); + }); + it("should fetch balance for a specific added chain", async () => { + const bal = await walletProvider.getWalletBalanceForChain("iotex"); + + expect(bal).toEqual("0"); + }); + it("should return null if chain is not added", async () => { + const bal = await walletProvider.getWalletBalanceForChain("base"); + expect(bal).toBeNull(); + }); + }); + describe("Chain", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk, customChains); + }); + it("generates chains from chain name", () => { + const chainName = "iotex"; + const chain: Chain = WalletProvider.genChainFromName(chainName); + + expect(chain.rpcUrls.default.http[0]).toEqual( + iotex.rpcUrls.default.http[0] + ); + }); + it("generates chains from chain name with custom rpc url", () => { + const chainName = "iotex"; + const customRpcUrl = "custom.url.io"; + const chain: Chain = WalletProvider.genChainFromName( + chainName, + customRpcUrl + ); + + expect(chain.rpcUrls.default.http[0]).toEqual( + iotex.rpcUrls.default.http[0] + ); + expect(chain.rpcUrls.custom.http[0]).toEqual(customRpcUrl); + }); + it("switches chain", () => { + const initialChain = walletProvider.getCurrentChain().id; + expect(initialChain).toEqual(iotex.id); + + walletProvider.switchChain("mainnet"); + + const newChain = walletProvider.getCurrentChain().id; + expect(newChain).toEqual(mainnet.id); + }); + it("switches chain (by adding new chain)", () => { + const initialChain = walletProvider.getCurrentChain().id; + expect(initialChain).toEqual(iotex.id); + + walletProvider.switchChain("arbitrum"); + + const newChain = walletProvider.getCurrentChain().id; + expect(newChain).toEqual(arbitrum.id); + }); + it("adds chain", () => { + const initialChains = walletProvider.chains; + expect(initialChains.base).toBeUndefined(); + + const base = WalletProvider.genChainFromName("base"); + walletProvider.addChain({ base }); + const newChains = walletProvider.chains; + expect(newChains.arbitrum.id).toEqual(arbitrum.id); + }); + it("gets chain configs", () => { + const chain = walletProvider.getChainConfigs("iotex"); + + expect(chain.id).toEqual(iotex.id); + }); + it("throws if tries to switch to an invalid chain", () => { + const initialChain = walletProvider.getCurrentChain().id; + expect(initialChain).toEqual(iotex.id); + + // intentionally set incorrect chain, ts will complain + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => walletProvider.switchChain("eth")).toThrow(); + }); + it("throws if unsupported chain name", () => { + // intentionally set incorrect chain, ts will complain + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => WalletProvider.genChainFromName("ethereum")).toThrow(); + }); + it("throws if invalid chain name", () => { + // intentionally set incorrect chain, ts will complain + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => WalletProvider.genChainFromName("eth")).toThrow(); + }); + }); +}); diff --git a/packages/plugin-squid-bridge/src/types/index.ts b/packages/plugin-squid-bridge/src/types/index.ts new file mode 100644 index 0000000000..518eefce05 --- /dev/null +++ b/packages/plugin-squid-bridge/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-bridge/tsconfig.json b/packages/plugin-squid-bridge/tsconfig.json new file mode 100644 index 0000000000..2d8d3fe818 --- /dev/null +++ b/packages/plugin-squid-bridge/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-bridge/tsup.config.ts b/packages/plugin-squid-bridge/tsup.config.ts new file mode 100644 index 0000000000..5b79223bfc --- /dev/null +++ b/packages/plugin-squid-bridge/tsup.config.ts @@ -0,0 +1,21 @@ +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", + "viem", + "@0xsquid/sdk", + ], +}); From 2746396b7aca879757f5f5fa5711183dd452856f Mon Sep 17 00:00:00 2001 From: Archethect Date: Thu, 26 Dec 2024 13:16:07 +0100 Subject: [PATCH 2/8] add router tests + env.example changes + rename to squid-router --- .env.example | 7 + packages/plugin-squid-bridge/README.md | 93 -------- .../src/tests/transfer.test.ts | 55 ----- .../src/tests/wallet.test.ts | 214 ------------------ .../eslint.config.mjs | 0 .../package.json | 10 +- .../src/actions/xChainSwap.ts | 0 .../src/helpers/utils.ts | 0 .../src/index.ts | 0 .../src/providers/squidRouter.ts | 4 +- .../src/templates/index.ts | 0 .../src/tests/router.test.ts | 129 +++++++++++ .../src/types/index.ts | 0 .../tsconfig.json | 0 .../tsup.config.ts | 2 +- 15 files changed, 145 insertions(+), 369 deletions(-) delete mode 100644 packages/plugin-squid-bridge/README.md delete mode 100644 packages/plugin-squid-bridge/src/tests/transfer.test.ts delete mode 100644 packages/plugin-squid-bridge/src/tests/wallet.test.ts rename packages/{plugin-squid-bridge => plugin-squid-router}/eslint.config.mjs (100%) rename packages/{plugin-squid-bridge => plugin-squid-router}/package.json (74%) rename packages/{plugin-squid-bridge => plugin-squid-router}/src/actions/xChainSwap.ts (100%) rename packages/{plugin-squid-bridge => plugin-squid-router}/src/helpers/utils.ts (100%) rename packages/{plugin-squid-bridge => plugin-squid-router}/src/index.ts (100%) rename packages/{plugin-squid-bridge => plugin-squid-router}/src/providers/squidRouter.ts (97%) rename packages/{plugin-squid-bridge => plugin-squid-router}/src/templates/index.ts (100%) create mode 100644 packages/plugin-squid-router/src/tests/router.test.ts rename packages/{plugin-squid-bridge => plugin-squid-router}/src/types/index.ts (100%) rename packages/{plugin-squid-bridge => plugin-squid-router}/tsconfig.json (100%) rename packages/{plugin-squid-bridge => plugin-squid-router}/tsup.config.ts (94%) diff --git a/.env.example b/.env.example index 414f963858..6531943c5a 100644 --- a/.env.example +++ b/.env.example @@ -340,3 +340,10 @@ STORY_PRIVATE_KEY= # Story private key 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= # get URL through https://docs.squidrouter.com/ +SQUID_INTEGRATOR_ID= # get integrator id through https://docs.squidrouter.com/ +EVM_ADDRESS= +EVM_PRIVATE_KEY= + diff --git a/packages/plugin-squid-bridge/README.md b/packages/plugin-squid-bridge/README.md deleted file mode 100644 index e2a3004325..0000000000 --- a/packages/plugin-squid-bridge/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# `@elizaos/plugin-squid-router` - -This plugin provides actions and providers for cross chain bridging leveraging [Squid Router](https://www.squidrouter.com/). - ---- - -## Configuration - -### Default Setup - -By default, **Ethereum mainnet** is enabled. To use it, simply add your private key to the `.env` file: - -```env -EVM_PRIVATE_KEY=your-private-key-here -``` - -### Adding Support for Other Chains - -To enable support for additional chains, add them to the character config like this: - -```json -"settings": { - "chains": { - "evm": [ - "base", "arbitrum", "iotex" - ] - } -} -``` - -Note: The chain names must match those in the viem/chains. - -### Custom RPC URLs - -By default, the RPC URL is inferred from the `viem/chains` config. To use a custom RPC URL for a specific chain, add the following to your `.env` file: - -```env -ETHEREUM_PROVIDER_=https://your-custom-rpc-url -``` - -**Example usage:** - -```env -ETHEREUM_PROVIDER_IOTEX=https://iotex-network.rpc.thirdweb.com -``` - -#### Custom RPC for Ethereum Mainnet - -To set a custom RPC URL for Ethereum mainnet, use: - -```env -EVM_PROVIDER_URL=https://your-custom-mainnet-rpc-url -``` - -## Provider - -The **Wallet Provider** initializes with the **first chain in the list** as the default (or Ethereum mainnet if none are added). It: - -- Provides the **context** of the currently connected address and its balance. -- Creates **Public** and **Wallet clients** to interact with the supported chains. -- Allows adding chains dynamically at runtime. - ---- - -## Actions - -### Transfer - -Transfer tokens from one address to another on any EVM-compatible chain. Just specify the: - -- **Amount** -- **Chain** -- **Recipient Address** - -**Example usage:** - -```bash -Transfer 1 ETH to 0xRecipient on arbitrum. -``` - ---- - -## Contribution - -The plugin contains tests. Whether you're using **TDD** or not, please make sure to run the tests before submitting a PR. - -### Running Tests - -Navigate to the `plugin-evm` directory and run: - -```bash -pnpm test -``` diff --git a/packages/plugin-squid-bridge/src/tests/transfer.test.ts b/packages/plugin-squid-bridge/src/tests/transfer.test.ts deleted file mode 100644 index a6159db76d..0000000000 --- a/packages/plugin-squid-bridge/src/tests/transfer.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; -import { Account, Chain } from "viem"; - -import { TransferAction } from "../actions/transfer"; -import { WalletProvider } from "../providers/wallet"; - -describe("Transfer Action", () => { - let wp: WalletProvider; - - beforeEach(async () => { - const pk = generatePrivateKey(); - const customChains = prepareChains(); - wp = new WalletProvider(pk, customChains); - }); - describe("Constructor", () => { - it("should initialize with wallet provider", () => { - const ta = new TransferAction(wp); - - expect(ta).toBeDefined(); - }); - }); - describe("Transfer", () => { - let ta: TransferAction; - let receiver: Account; - - beforeEach(() => { - ta = new TransferAction(wp); - receiver = privateKeyToAccount(generatePrivateKey()); - }); - - it("throws if not enough gas", async () => { - await expect( - ta.transfer({ - fromChain: "iotexTestnet", - toAddress: receiver.address, - amount: "1", - }) - ).rejects.toThrow( - "Transfer failed: The total cost (gas * gas fee + value) of executing this transaction exceeds the balance of the account." - ); - }); - }); -}); - -const prepareChains = () => { - const customChains: Record = {}; - const chainNames = ["iotexTestnet"]; - chainNames.forEach( - (chain) => - (customChains[chain] = WalletProvider.genChainFromName(chain)) - ); - - return customChains; -}; diff --git a/packages/plugin-squid-bridge/src/tests/wallet.test.ts b/packages/plugin-squid-bridge/src/tests/wallet.test.ts deleted file mode 100644 index a6b227a470..0000000000 --- a/packages/plugin-squid-bridge/src/tests/wallet.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { describe, it, expect, beforeAll, beforeEach } from "vitest"; -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; -import { mainnet, iotex, arbitrum, Chain } from "viem/chains"; - -import { WalletProvider } from "../providers/wallet"; - -const customRpcUrls = { - mainnet: "custom-rpc.mainnet.io", - arbitrum: "custom-rpc.base.io", - iotex: "custom-rpc.iotex.io", -}; - -describe("Wallet provider", () => { - let walletProvider: WalletProvider; - let pk: `0x${string}`; - const customChains: Record = {}; - - beforeAll(() => { - pk = generatePrivateKey(); - - const chainNames = ["iotex", "arbitrum"]; - chainNames.forEach( - (chain) => - (customChains[chain] = WalletProvider.genChainFromName(chain)) - ); - }); - - describe("Constructor", () => { - it("sets address", () => { - const account = privateKeyToAccount(pk); - const expectedAddress = account.address; - - walletProvider = new WalletProvider(pk); - - expect(walletProvider.getAddress()).toEqual(expectedAddress); - }); - it("sets default chain to ethereum mainnet", () => { - walletProvider = new WalletProvider(pk); - - expect(walletProvider.chains.mainnet.id).toEqual(mainnet.id); - expect(walletProvider.getCurrentChain().id).toEqual(mainnet.id); - }); - it("sets custom chains", () => { - walletProvider = new WalletProvider(pk, customChains); - - expect(walletProvider.chains.iotex.id).toEqual(iotex.id); - expect(walletProvider.chains.arbitrum.id).toEqual(arbitrum.id); - }); - it("sets the first provided custom chain as current chain", () => { - walletProvider = new WalletProvider(pk, customChains); - - expect(walletProvider.getCurrentChain().id).toEqual(iotex.id); - }); - }); - describe("Clients", () => { - beforeEach(() => { - walletProvider = new WalletProvider(pk); - }); - it("generates public client", () => { - const client = walletProvider.getPublicClient("mainnet"); - expect(client.chain.id).toEqual(mainnet.id); - expect(client.transport.url).toEqual( - mainnet.rpcUrls.default.http[0] - ); - }); - it("generates public client with custom rpcurl", () => { - const chain = WalletProvider.genChainFromName( - "mainnet", - customRpcUrls.mainnet - ); - const wp = new WalletProvider(pk, { ["mainnet"]: chain }); - - const client = wp.getPublicClient("mainnet"); - expect(client.chain.id).toEqual(mainnet.id); - expect(client.chain.rpcUrls.default.http[0]).toEqual( - mainnet.rpcUrls.default.http[0] - ); - expect(client.chain.rpcUrls.custom.http[0]).toEqual( - customRpcUrls.mainnet - ); - expect(client.transport.url).toEqual(customRpcUrls.mainnet); - }); - it("generates wallet client", () => { - const account = privateKeyToAccount(pk); - const expectedAddress = account.address; - - const client = walletProvider.getWalletClient("mainnet"); - - expect(client.account.address).toEqual(expectedAddress); - expect(client.transport.url).toEqual( - mainnet.rpcUrls.default.http[0] - ); - }); - it("generates wallet client with custom rpcurl", () => { - const account = privateKeyToAccount(pk); - const expectedAddress = account.address; - const chain = WalletProvider.genChainFromName( - "mainnet", - customRpcUrls.mainnet - ); - const wp = new WalletProvider(pk, { ["mainnet"]: chain }); - - const client = wp.getWalletClient("mainnet"); - - expect(client.account.address).toEqual(expectedAddress); - expect(client.chain.id).toEqual(mainnet.id); - expect(client.chain.rpcUrls.default.http[0]).toEqual( - mainnet.rpcUrls.default.http[0] - ); - expect(client.chain.rpcUrls.custom.http[0]).toEqual( - customRpcUrls.mainnet - ); - expect(client.transport.url).toEqual(customRpcUrls.mainnet); - }); - }); - describe("Balance", () => { - beforeEach(() => { - walletProvider = new WalletProvider(pk, customChains); - }); - it("should fetch balance", async () => { - const bal = await walletProvider.getWalletBalance(); - - expect(bal).toEqual("0"); - }); - it("should fetch balance for a specific added chain", async () => { - const bal = await walletProvider.getWalletBalanceForChain("iotex"); - - expect(bal).toEqual("0"); - }); - it("should return null if chain is not added", async () => { - const bal = await walletProvider.getWalletBalanceForChain("base"); - expect(bal).toBeNull(); - }); - }); - describe("Chain", () => { - beforeEach(() => { - walletProvider = new WalletProvider(pk, customChains); - }); - it("generates chains from chain name", () => { - const chainName = "iotex"; - const chain: Chain = WalletProvider.genChainFromName(chainName); - - expect(chain.rpcUrls.default.http[0]).toEqual( - iotex.rpcUrls.default.http[0] - ); - }); - it("generates chains from chain name with custom rpc url", () => { - const chainName = "iotex"; - const customRpcUrl = "custom.url.io"; - const chain: Chain = WalletProvider.genChainFromName( - chainName, - customRpcUrl - ); - - expect(chain.rpcUrls.default.http[0]).toEqual( - iotex.rpcUrls.default.http[0] - ); - expect(chain.rpcUrls.custom.http[0]).toEqual(customRpcUrl); - }); - it("switches chain", () => { - const initialChain = walletProvider.getCurrentChain().id; - expect(initialChain).toEqual(iotex.id); - - walletProvider.switchChain("mainnet"); - - const newChain = walletProvider.getCurrentChain().id; - expect(newChain).toEqual(mainnet.id); - }); - it("switches chain (by adding new chain)", () => { - const initialChain = walletProvider.getCurrentChain().id; - expect(initialChain).toEqual(iotex.id); - - walletProvider.switchChain("arbitrum"); - - const newChain = walletProvider.getCurrentChain().id; - expect(newChain).toEqual(arbitrum.id); - }); - it("adds chain", () => { - const initialChains = walletProvider.chains; - expect(initialChains.base).toBeUndefined(); - - const base = WalletProvider.genChainFromName("base"); - walletProvider.addChain({ base }); - const newChains = walletProvider.chains; - expect(newChains.arbitrum.id).toEqual(arbitrum.id); - }); - it("gets chain configs", () => { - const chain = walletProvider.getChainConfigs("iotex"); - - expect(chain.id).toEqual(iotex.id); - }); - it("throws if tries to switch to an invalid chain", () => { - const initialChain = walletProvider.getCurrentChain().id; - expect(initialChain).toEqual(iotex.id); - - // intentionally set incorrect chain, ts will complain - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(() => walletProvider.switchChain("eth")).toThrow(); - }); - it("throws if unsupported chain name", () => { - // intentionally set incorrect chain, ts will complain - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(() => WalletProvider.genChainFromName("ethereum")).toThrow(); - }); - it("throws if invalid chain name", () => { - // intentionally set incorrect chain, ts will complain - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(() => WalletProvider.genChainFromName("eth")).toThrow(); - }); - }); -}); diff --git a/packages/plugin-squid-bridge/eslint.config.mjs b/packages/plugin-squid-router/eslint.config.mjs similarity index 100% rename from packages/plugin-squid-bridge/eslint.config.mjs rename to packages/plugin-squid-router/eslint.config.mjs diff --git a/packages/plugin-squid-bridge/package.json b/packages/plugin-squid-router/package.json similarity index 74% rename from packages/plugin-squid-bridge/package.json rename to packages/plugin-squid-router/package.json index b8b9625bbd..b6f7289d3f 100644 --- a/packages/plugin-squid-bridge/package.json +++ b/packages/plugin-squid-router/package.json @@ -1,14 +1,16 @@ { - "name": "@elizaos/plugin-squid-bridge", + "name": "@elizaos/plugin-squid-router", "version": "0.1.7-alpha.1", "main": "dist/index.js", "type": "module", "types": "dist/index.d.ts", "dependencies": { - "@elizaos/core": "workspace:*", - "tsup": "8.3.5", "@0xsquid/sdk": "2.8.29", - "@0xsquid/squid-types": "0.1.130" + "@0xsquid/squid-types": "0.1.130", + "@elizaos/core": "workspace:*", + "optional": "0.1.4", + "sharp": "0.33.5", + "tsup": "8.3.5" }, "scripts": { "build": "tsup --format esm --dts", diff --git a/packages/plugin-squid-bridge/src/actions/xChainSwap.ts b/packages/plugin-squid-router/src/actions/xChainSwap.ts similarity index 100% rename from packages/plugin-squid-bridge/src/actions/xChainSwap.ts rename to packages/plugin-squid-router/src/actions/xChainSwap.ts diff --git a/packages/plugin-squid-bridge/src/helpers/utils.ts b/packages/plugin-squid-router/src/helpers/utils.ts similarity index 100% rename from packages/plugin-squid-bridge/src/helpers/utils.ts rename to packages/plugin-squid-router/src/helpers/utils.ts diff --git a/packages/plugin-squid-bridge/src/index.ts b/packages/plugin-squid-router/src/index.ts similarity index 100% rename from packages/plugin-squid-bridge/src/index.ts rename to packages/plugin-squid-router/src/index.ts diff --git a/packages/plugin-squid-bridge/src/providers/squidRouter.ts b/packages/plugin-squid-router/src/providers/squidRouter.ts similarity index 97% rename from packages/plugin-squid-bridge/src/providers/squidRouter.ts rename to packages/plugin-squid-router/src/providers/squidRouter.ts index 736c663382..d1738dd930 100644 --- a/packages/plugin-squid-bridge/src/providers/squidRouter.ts +++ b/packages/plugin-squid-router/src/providers/squidRouter.ts @@ -19,9 +19,9 @@ export class SquidRouterProvider { constructor( private squidSDKUrl: string, - private squidIntergatorID: string + private squidIntegratorID: string ) { - this.squid = getSDK(squidSDKUrl,squidIntergatorID); + this.squid = getSDK(squidSDKUrl,squidIntegratorID); } async initialize(): Promise { diff --git a/packages/plugin-squid-bridge/src/templates/index.ts b/packages/plugin-squid-router/src/templates/index.ts similarity index 100% rename from packages/plugin-squid-bridge/src/templates/index.ts rename to packages/plugin-squid-router/src/templates/index.ts 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 0000000000..a96e6104df --- /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 === "EVM_PRIVATE_KEY") return "9a2bb49ab3fc4084e61a73c061b8a64041ce22ad57d8b99d938be2ac3143f2fa"; + if (key === "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-bridge/src/types/index.ts b/packages/plugin-squid-router/src/types/index.ts similarity index 100% rename from packages/plugin-squid-bridge/src/types/index.ts rename to packages/plugin-squid-router/src/types/index.ts diff --git a/packages/plugin-squid-bridge/tsconfig.json b/packages/plugin-squid-router/tsconfig.json similarity index 100% rename from packages/plugin-squid-bridge/tsconfig.json rename to packages/plugin-squid-router/tsconfig.json diff --git a/packages/plugin-squid-bridge/tsup.config.ts b/packages/plugin-squid-router/tsup.config.ts similarity index 94% rename from packages/plugin-squid-bridge/tsup.config.ts rename to packages/plugin-squid-router/tsup.config.ts index 5b79223bfc..96d8be9a7e 100644 --- a/packages/plugin-squid-bridge/tsup.config.ts +++ b/packages/plugin-squid-router/tsup.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ "https", "http", "agentkeepalive", - "viem", + "@0xsquid/squid-types", "@0xsquid/sdk", ], }); From a8524c5d5a6e40676fe08987c1480fb5b360fcd6 Mon Sep 17 00:00:00 2001 From: Archethect Date: Thu, 26 Dec 2024 13:26:47 +0100 Subject: [PATCH 3/8] prefix env vars with SQUID_ --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 6531943c5a..d1efc92019 100644 --- a/.env.example +++ b/.env.example @@ -344,6 +344,6 @@ PINATA_JWT= # Pinata JWT for uploading files to IPFS # Squid Router SQUID_SDK_URL= # get URL through https://docs.squidrouter.com/ SQUID_INTEGRATOR_ID= # get integrator id through https://docs.squidrouter.com/ -EVM_ADDRESS= -EVM_PRIVATE_KEY= +SQUID_EVM_ADDRESS= +SQUID_EVM_PRIVATE_KEY= From 4c2fe5c67dc382a4915d46a519fd37a960f4eb68 Mon Sep 17 00:00:00 2001 From: Archethect Date: Thu, 26 Dec 2024 14:33:26 +0100 Subject: [PATCH 4/8] Add readme + add plugin support to agent --- .env.example | 2 +- agent/package.json | 1 + agent/src/index.ts | 9 +- packages/plugin-squid-router/README.md | 26 ++++ .../src/actions/xChainSwap.ts | 2 +- .../plugin-squid-router/src/helpers/utils.ts | 122 ++---------------- packages/plugin-squid-router/src/index.ts | 8 +- .../src/providers/squidRouter.ts | 2 +- .../src/tests/router.test.ts | 4 +- 9 files changed, 58 insertions(+), 118 deletions(-) create mode 100644 packages/plugin-squid-router/README.md diff --git a/.env.example b/.env.example index d1efc92019..0f545b772e 100644 --- a/.env.example +++ b/.env.example @@ -342,7 +342,7 @@ STORY_API_KEY= # Story API key PINATA_JWT= # Pinata JWT for uploading files to IPFS # Squid Router -SQUID_SDK_URL= # get URL through https://docs.squidrouter.com/ +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= diff --git a/agent/package.json b/agent/package.json index bb71707018..fd151790cf 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 21a6832cc9..968865e48f 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -57,6 +57,7 @@ import { TEEMode, teePlugin } from "@elizaos/plugin-tee"; import { tonPlugin } from "@elizaos/plugin-ton"; import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; 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"; @@ -536,7 +537,7 @@ export async function createAgent( getSecret(character, "HEURIST_API_KEY") ? imageGenerationPlugin : null, - getSecret(character, "FAL_API_KEY") + getSecret(character, "FAL_API_KEY") ? ThreeDGenerationPlugin : null, ...(getSecret(character, "COINBASE_API_KEY") && @@ -570,6 +571,12 @@ 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") + ? 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 0000000000..20038ab02e --- /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/src/actions/xChainSwap.ts b/packages/plugin-squid-router/src/actions/xChainSwap.ts index 6a8c77b5d4..cf493e8d6f 100644 --- a/packages/plugin-squid-router/src/actions/xChainSwap.ts +++ b/packages/plugin-squid-router/src/actions/xChainSwap.ts @@ -154,7 +154,7 @@ export const xChainSwapAction = { // Show the transaction receipt with Axelarscan link const axelarScanLink = "https://axelarscan.io/gmp/" + txReceipt.transactionHash; - console.log(`Finished! Check Axelarscan for details: ${axelarScanLink}`); + elizaLogger.log(`Finished! Check Axelarscan for details: ${axelarScanLink}`); diff --git a/packages/plugin-squid-router/src/helpers/utils.ts b/packages/plugin-squid-router/src/helpers/utils.ts index d40b8638d4..8563f0e326 100644 --- a/packages/plugin-squid-router/src/helpers/utils.ts +++ b/packages/plugin-squid-router/src/helpers/utils.ts @@ -84,43 +84,25 @@ export const squidRouterEnvSchema = z SQUID_INTEGRATOR_ID: z.string().min(1, "Squid Integrator ID is required"), SQUID_SDK_URL: z.string().min(1, "Squid SDK URL is required"), - EVM_ADDRESS: z.string().optional(), - EVM_PRIVATE_KEY: z.string().optional(), - - SOLANA_ADDRESS: z.string().optional(), - SOLANA_PRIVATE_KEY: z.string().optional(), - - COSMOS_ADDRESS: z.string().optional(), - COSMOS_PRIVATE_KEY: z.string().optional(), + 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.EVM_ADDRESS && data.EVM_PRIVATE_KEY) && - isValidEvmAddress(data.EVM_ADDRESS) && - isValidEvmPrivateKey(data.EVM_PRIVATE_KEY); - - // Check if Solana pair is valid - const solanaValid = - (data.SOLANA_ADDRESS && data.SOLANA_PRIVATE_KEY) && - isValidSolanaAddress(data.SOLANA_ADDRESS) && - isValidSolanaPrivateKey(data.SOLANA_PRIVATE_KEY); + (data.SQUID_EVM_ADDRESS && data.SQUID_EVM_PRIVATE_KEY) && + isValidEvmAddress(data.SQUID_EVM_ADDRESS) && + isValidEvmPrivateKey(data.SQUID_EVM_PRIVATE_KEY); - // Check if Cosmos pair is valid - const cosmosValid = - (data.COSMOS_ADDRESS && data.COSMOS_PRIVATE_KEY) && - isValidCosmosAddress(data.COSMOS_ADDRESS) && - isValidCosmosPrivateKey(data.COSMOS_PRIVATE_KEY); - - return evmValid || solanaValid || cosmosValid; + 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.EVM_ADDRESS || data.EVM_PRIVATE_KEY) { - if (data.EVM_ADDRESS && !isValidEvmAddress(data.EVM_ADDRESS)) { + 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.", @@ -128,7 +110,7 @@ export const squidRouterEnvSchema = z }); } - if (data.EVM_PRIVATE_KEY && !isValidEvmPrivateKey(data.EVM_PRIVATE_KEY)) { + 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.", @@ -136,15 +118,15 @@ export const squidRouterEnvSchema = z }); } - if ((data.EVM_ADDRESS && !data.EVM_PRIVATE_KEY) || (!data.EVM_ADDRESS && data.EVM_PRIVATE_KEY)) { - if (!data.EVM_ADDRESS) { + 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.EVM_PRIVATE_KEY) { + if (!data.SQUID_EVM_PRIVATE_KEY) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "EVM_PRIVATE_KEY is required when EVM_ADDRESS is provided.", @@ -153,78 +135,6 @@ export const squidRouterEnvSchema = z } } } - - // Solana Validation - if (data.SOLANA_ADDRESS || data.SOLANA_PRIVATE_KEY) { - if (data.SOLANA_ADDRESS && !isValidSolanaAddress(data.SOLANA_ADDRESS)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "SOLANA_ADDRESS is invalid.", - path: ["SOLANA_ADDRESS"], - }); - } - - if (data.SOLANA_PRIVATE_KEY && !isValidSolanaPrivateKey(data.SOLANA_PRIVATE_KEY)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "SOLANA_PRIVATE_KEY must be a 64-character hexadecimal string.", - path: ["SOLANA_PRIVATE_KEY"], - }); - } - - if ((data.SOLANA_ADDRESS && !data.SOLANA_PRIVATE_KEY) || (!data.SOLANA_ADDRESS && data.SOLANA_PRIVATE_KEY)) { - if (!data.SOLANA_ADDRESS) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "SOLANA_ADDRESS is required when SOLANA_PRIVATE_KEY is provided.", - path: ["SOLANA_ADDRESS"], - }); - } - if (!data.SOLANA_PRIVATE_KEY) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "SOLANA_PRIVATE_KEY is required when SOLANA_ADDRESS is provided.", - path: ["SOLANA_PRIVATE_KEY"], - }); - } - } - } - - // Cosmos Validation - if (data.COSMOS_ADDRESS || data.COSMOS_PRIVATE_KEY) { - if (data.COSMOS_ADDRESS && !isValidCosmosAddress(data.COSMOS_ADDRESS)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "COSMOS_ADDRESS is invalid.", - path: ["COSMOS_ADDRESS"], - }); - } - - if (data.COSMOS_PRIVATE_KEY && !isValidCosmosPrivateKey(data.COSMOS_PRIVATE_KEY)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "COSMOS_PRIVATE_KEY must be a 64-character hexadecimal string.", - path: ["COSMOS_PRIVATE_KEY"], - }); - } - - if ((data.COSMOS_ADDRESS && !data.COSMOS_PRIVATE_KEY) || (!data.COSMOS_ADDRESS && data.COSMOS_PRIVATE_KEY)) { - if (!data.COSMOS_ADDRESS) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "COSMOS_ADDRESS is required when COSMOS_PRIVATE_KEY is provided.", - path: ["COSMOS_ADDRESS"], - }); - } - if (!data.COSMOS_PRIVATE_KEY) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "COSMOS_PRIVATE_KEY is required when COSMOS_ADDRESS is provided.", - path: ["COSMOS_PRIVATE_KEY"], - }); - } - } - } }); export type SquidRouterConfig = z.infer; @@ -236,12 +146,8 @@ export async function validateSquidRouterConfig( const config = { SQUID_INTEGRATOR_ID: runtime.getSetting("SQUID_INTEGRATOR_ID"), SQUID_SDK_URL: runtime.getSetting("SQUID_SDK_URL"), - EVM_ADDRESS: runtime.getSetting("EVM_ADDRESS"), - EVM_PRIVATE_KEY: runtime.getSetting("EVM_PRIVATE_KEY"), - SOLANA_ADDRESS: runtime.getSetting("SOLANA_ADDRESS"), - SOLANA_PRIVATE_KEY: runtime.getSetting("SOLANA_PRIVATE_KEY"), - COSMOS_ADDRESS: runtime.getSetting("COSMOS_ADDRESS"), - COSMOS_PRIVATE_KEY: runtime.getSetting("COSMOS_PRIVATE_KEY"), + SQUID_EVM_ADDRESS: runtime.getSetting("SQUID_EVM_ADDRESS"), + SQUID_EVM_PRIVATE_KEY: runtime.getSetting("SQUID_EVM_PRIVATE_KEY"), }; return squidRouterEnvSchema.parse(config); diff --git a/packages/plugin-squid-router/src/index.ts b/packages/plugin-squid-router/src/index.ts index e4ce7bb7e0..fe3cfeb1fe 100644 --- a/packages/plugin-squid-router/src/index.ts +++ b/packages/plugin-squid-router/src/index.ts @@ -7,13 +7,13 @@ export * from "./types"; import type { Plugin } from "@elizaos/core"; import { xChainSwapAction } from "./actions/xChainSwap.ts"; -export const squidBridgePlugin: Plugin = { - name: "squid-bridge", - description: "Squid router bridge plugin", +export const squidRouterPlugin: Plugin = { + name: "squid-router", + description: "Squid router plugin", providers: [squidRouterProvider], evaluators: [], services: [], actions: [xChainSwapAction], }; -export default squidBridgePlugin; +export default squidRouterPlugin; diff --git a/packages/plugin-squid-router/src/providers/squidRouter.ts b/packages/plugin-squid-router/src/providers/squidRouter.ts index d1738dd930..89b7dcc90a 100644 --- a/packages/plugin-squid-router/src/providers/squidRouter.ts +++ b/packages/plugin-squid-router/src/providers/squidRouter.ts @@ -79,7 +79,7 @@ export class SquidRouterProvider { try { if(chain.chainType === ChainType.EVM) { const provider = new ethers.providers.JsonRpcProvider(chain.rpc); - return new ethers.Wallet(runtime.getSetting("EVM_PRIVATE_KEY"), provider); + return new ethers.Wallet(runtime.getSetting("SQUID_EVM_PRIVATE_KEY"), provider); } else { throw Error("Cannot instantiate EVM signer for non-EVM chain"); } diff --git a/packages/plugin-squid-router/src/tests/router.test.ts b/packages/plugin-squid-router/src/tests/router.test.ts index a96e6104df..a1eeaf99cd 100644 --- a/packages/plugin-squid-router/src/tests/router.test.ts +++ b/packages/plugin-squid-router/src/tests/router.test.ts @@ -32,8 +32,8 @@ describe("SquidRouterProvider", () => { 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 === "EVM_PRIVATE_KEY") return "9a2bb49ab3fc4084e61a73c061b8a64041ce22ad57d8b99d938be2ac3143f2fa"; - if (key === "EVM_ADDRESS") return "0xbb5F4ddaBbbb0AcD2086527A887b208b06A3BFdb"; + if (key === "SQUID_EVM_PRIVATE_KEY") return "9a2bb49ab3fc4084e61a73c061b8a64041ce22ad57d8b99d938be2ac3143f2fa"; + if (key === "SQUID_EVM_ADDRESS") return "0xbb5F4ddaBbbb0AcD2086527A887b208b06A3BFdb"; return undefined; }), }; From 692b8512f5156e81deb2a4325a9a84d28bf6fc75 Mon Sep 17 00:00:00 2001 From: Archethect Date: Thu, 26 Dec 2024 17:36:03 +0100 Subject: [PATCH 5/8] Add a default destination address --- .../plugin-squid-router/src/actions/xChainSwap.ts | 4 ++++ packages/plugin-squid-router/src/helpers/utils.ts | 14 +++++++++++--- .../plugin-squid-router/src/templates/index.ts | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/plugin-squid-router/src/actions/xChainSwap.ts b/packages/plugin-squid-router/src/actions/xChainSwap.ts index cf493e8d6f..014498e882 100644 --- a/packages/plugin-squid-router/src/actions/xChainSwap.ts +++ b/packages/plugin-squid-router/src/actions/xChainSwap.ts @@ -67,6 +67,10 @@ export const xChainSwapAction = { modelClass: ModelClass.SMALL, }); + if(content.toAddress === null) { + content.toAddress = runtime.getSetting("SQUID_EVM_ADDRESS"); + } + // Validate transfer content if (!isXChainSwapContent(content)) { console.error("Invalid content for X_CHAIN_SWAP action."); diff --git a/packages/plugin-squid-router/src/helpers/utils.ts b/packages/plugin-squid-router/src/helpers/utils.ts index 8563f0e326..7c1c994a04 100644 --- a/packages/plugin-squid-router/src/helpers/utils.ts +++ b/packages/plugin-squid-router/src/helpers/utils.ts @@ -27,7 +27,14 @@ export function convertToWei(amount: string | number, token: SquidToken): string export function isXChainSwapContent( content: XChainSwapContent -): content is XChainSwapContent { +): boolean { + + console.log("content.fromChain: ",typeof content.fromChain); + console.log("content.toChain: ",typeof content.toChain); + console.log("content.fromToken: ",typeof content.fromToken); + console.log("content.toToken: ",typeof content.toToken); + console.log("content.toAddress: ",typeof content.toAddress); + console.log("content.amount: ",typeof content.amount); // Validate types const validTypes = typeof content.fromChain === "string" && @@ -37,9 +44,10 @@ export function isXChainSwapContent( typeof content.toAddress === "string" && (typeof content.amount === "string" || typeof content.amount === "number"); - if (!validTypes) { - return false; + if (validTypes) { + return true; } + return false } // Helper Validation Functions diff --git a/packages/plugin-squid-router/src/templates/index.ts b/packages/plugin-squid-router/src/templates/index.ts index 4c6ac39b4f..a3e5935603 100644 --- a/packages/plugin-squid-router/src/templates/index.ts +++ b/packages/plugin-squid-router/src/templates/index.ts @@ -10,6 +10,8 @@ Extract the following information about the requested cross chain swap: - 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. + Respond with a JSON markdown block containing only the extracted values: \`\`\`json From ac52671b27d032e932059339e1553f2abe300643 Mon Sep 17 00:00:00 2001 From: Archethect Date: Thu, 26 Dec 2024 18:40:27 +0100 Subject: [PATCH 6/8] some fixes related to ethers import --- packages/core/src/generation.ts | 2 +- packages/plugin-squid-router/package.json | 1 + packages/plugin-squid-router/src/actions/xChainSwap.ts | 9 +++------ packages/plugin-squid-router/src/helpers/utils.ts | 4 ++-- .../plugin-squid-router/src/providers/squidRouter.ts | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/core/src/generation.ts b/packages/core/src/generation.ts index f93b5abe1b..033720b5a9 100644 --- a/packages/core/src/generation.ts +++ b/packages/core/src/generation.ts @@ -160,7 +160,7 @@ export async function generateText({ modelConfiguration?.maxInputTokens || models[provider].settings.maxInputTokens; const max_response_length = - modelConfiguration.max_response_length || + modelConfiguration?.max_response_length || models[provider].settings.maxOutputTokens; const apiKey = runtime.token; diff --git a/packages/plugin-squid-router/package.json b/packages/plugin-squid-router/package.json index b6f7289d3f..d0adce91df 100644 --- a/packages/plugin-squid-router/package.json +++ b/packages/plugin-squid-router/package.json @@ -8,6 +8,7 @@ "@0xsquid/sdk": "2.8.29", "@0xsquid/squid-types": "0.1.130", "@elizaos/core": "workspace:*", + "ethers": "6.8.1", "optional": "0.1.4", "sharp": "0.33.5", "tsup": "8.3.5" diff --git a/packages/plugin-squid-router/src/actions/xChainSwap.ts b/packages/plugin-squid-router/src/actions/xChainSwap.ts index 014498e882..86e758e1f8 100644 --- a/packages/plugin-squid-router/src/actions/xChainSwap.ts +++ b/packages/plugin-squid-router/src/actions/xChainSwap.ts @@ -1,7 +1,7 @@ import { composeContext, elizaLogger, - generateObject, generateObjectDeprecated, + generateObjectDeprecated, HandlerCallback, IAgentRuntime, Memory, @@ -10,10 +10,7 @@ import { } from "@elizaos/core"; import {xChainSwapTemplate} from "../templates"; import {convertToWei, isXChainSwapContent, validateSquidRouterConfig} from "../helpers/utils.ts"; -import {Squid} from "@0xsquid/sdk"; -import {ChainData, ChainType, Token} from "@0xsquid/squid-types"; import {ethers} from "ethers"; -import {nativeTokenConstant, SquidToken} from "../types"; import {initSquidRouterProvider} from "../providers/squidRouter.ts"; export { xChainSwapTemplate }; @@ -153,11 +150,11 @@ export const xChainSwapAction = { const tx = (await squidRouter.executeRoute({ signer, route, - })) as unknown as ethers.providers.TransactionResponse; + })) as unknown as ethers.TransactionResponse; const txReceipt = await tx.wait(); // Show the transaction receipt with Axelarscan link - const axelarScanLink = "https://axelarscan.io/gmp/" + txReceipt.transactionHash; + const axelarScanLink = "https://axelarscan.io/gmp/" + txReceipt.hash; elizaLogger.log(`Finished! Check Axelarscan for details: ${axelarScanLink}`); diff --git a/packages/plugin-squid-router/src/helpers/utils.ts b/packages/plugin-squid-router/src/helpers/utils.ts index 7c1c994a04..e8cab21891 100644 --- a/packages/plugin-squid-router/src/helpers/utils.ts +++ b/packages/plugin-squid-router/src/helpers/utils.ts @@ -15,7 +15,7 @@ export function convertToWei(amount: string | number, token: SquidToken): string const amountString = typeof amount === 'number' ? amount.toString() : amount; // Use ethers.js to parse the amount into the smallest unit - const parsedAmount = ethers.utils.parseUnits(amountString, token.decimals); + const parsedAmount = ethers.parseUnits(amountString, token.decimals); // Return the parsed amount as a string return parsedAmount.toString(); @@ -53,7 +53,7 @@ export function isXChainSwapContent( // Helper Validation Functions const isValidEvmAddress = (address: string): boolean => { - return ethers.utils.isAddress(address); + return ethers.isAddress(address); }; const isValidEvmPrivateKey = (key: string): boolean => { diff --git a/packages/plugin-squid-router/src/providers/squidRouter.ts b/packages/plugin-squid-router/src/providers/squidRouter.ts index 89b7dcc90a..cffbaa7635 100644 --- a/packages/plugin-squid-router/src/providers/squidRouter.ts +++ b/packages/plugin-squid-router/src/providers/squidRouter.ts @@ -78,7 +78,7 @@ export class SquidRouterProvider { async getEVMSignerForChain(chain: ChainData, runtime): Promise { try { if(chain.chainType === ChainType.EVM) { - const provider = new ethers.providers.JsonRpcProvider(chain.rpc); + 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"); From d1c68b8b5a0aa9bd5f27c56f5cc38f8b7514e883 Mon Sep 17 00:00:00 2001 From: Archethect Date: Thu, 26 Dec 2024 23:12:34 +0100 Subject: [PATCH 7/8] rate limiting avoidance + package versions --- packages/plugin-squid-router/package.json | 2 +- .../src/actions/xChainSwap.ts | 18 +++++++++++++++++- .../plugin-squid-router/src/helpers/utils.ts | 6 ------ .../src/providers/squidRouter.ts | 6 +++++- packages/plugin-squid-router/tsup.config.ts | 4 +--- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/plugin-squid-router/package.json b/packages/plugin-squid-router/package.json index d0adce91df..1bc7706188 100644 --- a/packages/plugin-squid-router/package.json +++ b/packages/plugin-squid-router/package.json @@ -6,7 +6,7 @@ "types": "dist/index.d.ts", "dependencies": { "@0xsquid/sdk": "2.8.29", - "@0xsquid/squid-types": "0.1.130", + "@0xsquid/squid-types": "0.1.122", "@elizaos/core": "workspace:*", "ethers": "6.8.1", "optional": "0.1.4", diff --git a/packages/plugin-squid-router/src/actions/xChainSwap.ts b/packages/plugin-squid-router/src/actions/xChainSwap.ts index 86e758e1f8..0b330829af 100644 --- a/packages/plugin-squid-router/src/actions/xChainSwap.ts +++ b/packages/plugin-squid-router/src/actions/xChainSwap.ts @@ -15,6 +15,8 @@ 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)" @@ -124,11 +126,14 @@ export const xChainSwapAction = { toChain: toChainObject.chainId, toToken: toTokenObject.address, toAddress: content.toAddress, - enableBoost: true, + quoteOnly: false }; console.log("Parameters:", params); // Printing the parameters for QA + //Wait 500ms to avoid rate limiting + await delay(1000); + // Get the swap route using Squid SDK const {route} = await squidRouter.getRoute(params); console.log("Calculated route:", route.estimate.toAmount); @@ -146,6 +151,9 @@ export const xChainSwapAction = { ); } + //Wait 500ms to avoid rate limiting + await delay(1000); + // Execute the swap transaction const tx = (await squidRouter.executeRoute({ signer, @@ -157,6 +165,14 @@ export const xChainSwapAction = { 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) { diff --git a/packages/plugin-squid-router/src/helpers/utils.ts b/packages/plugin-squid-router/src/helpers/utils.ts index e8cab21891..6909ca8382 100644 --- a/packages/plugin-squid-router/src/helpers/utils.ts +++ b/packages/plugin-squid-router/src/helpers/utils.ts @@ -29,12 +29,6 @@ export function isXChainSwapContent( content: XChainSwapContent ): boolean { - console.log("content.fromChain: ",typeof content.fromChain); - console.log("content.toChain: ",typeof content.toChain); - console.log("content.fromToken: ",typeof content.fromToken); - console.log("content.toToken: ",typeof content.toToken); - console.log("content.toAddress: ",typeof content.toAddress); - console.log("content.amount: ",typeof content.amount); // Validate types const validTypes = typeof content.fromChain === "string" && diff --git a/packages/plugin-squid-router/src/providers/squidRouter.ts b/packages/plugin-squid-router/src/providers/squidRouter.ts index cffbaa7635..5034b23f82 100644 --- a/packages/plugin-squid-router/src/providers/squidRouter.ts +++ b/packages/plugin-squid-router/src/providers/squidRouter.ts @@ -9,7 +9,7 @@ import {nativeTokenConstant, SquidToken} from "../types"; const getSDK = (baseUrl: string, integratorId: string): Squid => { const squid = new Squid({ baseUrl: baseUrl, - integratorId: integratorId, + integratorId: integratorId }); return squid; }; @@ -30,6 +30,10 @@ export class SquidRouterProvider { } } + getSquidObject(): Squid { + return this.squid; + } + getChains(): ChainData[] { return this.squid.chains; } diff --git a/packages/plugin-squid-router/tsup.config.ts b/packages/plugin-squid-router/tsup.config.ts index 96d8be9a7e..299db52c34 100644 --- a/packages/plugin-squid-router/tsup.config.ts +++ b/packages/plugin-squid-router/tsup.config.ts @@ -14,8 +14,6 @@ export default defineConfig({ "@node-llama-cpp", "https", "http", - "agentkeepalive", - "@0xsquid/squid-types", - "@0xsquid/sdk", + "agentkeepalive" ], }); From dedd2f570be68060aca25fe21fe3fbccf5bc11b2 Mon Sep 17 00:00:00 2001 From: Archethect Date: Fri, 27 Dec 2024 12:13:20 +0100 Subject: [PATCH 8/8] Add throttle interval support + improved template for cross chain swaps --- .env.example | 1 + agent/src/index.ts | 3 ++- packages/plugin-squid-router/src/actions/xChainSwap.ts | 10 ++++++---- packages/plugin-squid-router/src/templates/index.ts | 6 ++++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 828967c464..7ad504c4e0 100644 --- a/.env.example +++ b/.env.example @@ -346,6 +346,7 @@ SQUID_SDK_URL=https://apiplus.squidrouter.com # Default: https://apiplus.squidro 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= diff --git a/agent/src/index.ts b/agent/src/index.ts index 1e4c5032ec..31ac4ea7d0 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -576,7 +576,8 @@ export async function createAgent( getSecret(character, "SQUID_SDK_URL") && getSecret(character, "SQUID_INTEGRATOR_ID") && getSecret(character, "SQUID_EVM_ADDRESS") && - getSecret(character, "SQUID_EVM_PRIVATE_KEY") + getSecret(character, "SQUID_EVM_PRIVATE_KEY") && + getSecret(character, "SQUID_API_THROTTLE_INTERVAL") ? squidRouterPlugin : null, diff --git a/packages/plugin-squid-router/src/actions/xChainSwap.ts b/packages/plugin-squid-router/src/actions/xChainSwap.ts index 0b330829af..778ad68a2f 100644 --- a/packages/plugin-squid-router/src/actions/xChainSwap.ts +++ b/packages/plugin-squid-router/src/actions/xChainSwap.ts @@ -70,6 +70,8 @@ export const xChainSwapAction = { 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."); @@ -131,8 +133,9 @@ export const xChainSwapAction = { console.log("Parameters:", params); // Printing the parameters for QA - //Wait 500ms to avoid rate limiting - await delay(1000); + 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); @@ -151,8 +154,7 @@ export const xChainSwapAction = { ); } - //Wait 500ms to avoid rate limiting - await delay(1000); + await delay(throttleInterval); // Execute the swap transaction const tx = (await squidRouter.executeRoute({ diff --git a/packages/plugin-squid-router/src/templates/index.ts b/packages/plugin-squid-router/src/templates/index.ts index a3e5935603..433b8fa072 100644 --- a/packages/plugin-squid-router/src/templates/index.ts +++ b/packages/plugin-squid-router/src/templates/index.ts @@ -4,13 +4,15 @@ export const xChainSwapTemplate = `Given the recent messages and wallet informat Extract the following information about the requested cross chain swap: - Token symbol to swap from -- Token symbol to swap into (if defined) +- Token symbol to swap into (if defined, otherwise the same as the token symbol to swap from) - Source chain -- Destination 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: