UNPKG

@elizaos/plugin-squid-router

Version:

ElizaOS plugin for cross-chain token swaps using Squid Router

415 lines (360 loc) 12.2 kB
import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test"; import { createMockRuntime, createMockMemory, createMockState, mockLogger, } from "./test-utils"; import { ChainType } from "@0xsquid/squid-types"; import { elizaLogger } from "@elizaos/core"; // Create mock functions const mockInit = mock().mockResolvedValue(undefined); const mockGetRoute = mock().mockResolvedValue({ route: { estimate: { toAmount: "1000000000000000000" }, transactionRequest: { target: "0x1234567890123456789012345678901234567890", data: "0x", value: "0", }, }, }); const mockExecuteRoute = mock().mockResolvedValue({ hash: "0x1234567890123456789012345678901234567890123456789012345678901234", wait: mock().mockResolvedValue({ hash: "0x1234567890123456789012345678901234567890123456789012345678901234", }), }); // Mock the providers module const mockSquidProvider = { initialize: mockInit, getChain: mock().mockImplementation((chainName: string) => { const chains: Record<string, any> = { ethereum: { chainId: "1", networkName: "ethereum", chainType: ChainType.EVM, }, base: { chainId: "8453", networkName: "base", chainType: ChainType.EVM }, arbitrum: { chainId: "42161", networkName: "arbitrum", chainType: ChainType.EVM, }, }; return chains[chainName.toLowerCase()]; }), getToken: mock().mockImplementation((chain: any, tokenSymbol: string) => { const tokens: Record<string, any> = { eth: { address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", symbol: "ETH", decimals: 18, enabled: true, isNative: true, }, uni: { address: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", symbol: "UNI", decimals: 18, enabled: true, isNative: false, }, usdc: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", symbol: "USDC", decimals: 6, enabled: true, isNative: false, }, }; return tokens[tokenSymbol.toLowerCase()]; }), getEVMSignerForChain: mock().mockResolvedValue({ getAddress: mock().mockResolvedValue( "0x742D35Cc6634c0532925A3b844BC9E7595f6F269" ), }), getRoute: mockGetRoute, executeRoute: mockExecuteRoute, }; // Mock the initSquidRouterProvider function const mockInitSquidRouterProvider = mock().mockResolvedValue(mockSquidProvider); // Mock the module before importing mock.module("../providers/squidRouter", () => ({ initSquidRouterProvider: mockInitSquidRouterProvider, SquidRouterProvider: class {}, squidRouterProvider: { name: "squidRouter", get: mock(), }, })); // Import the action after mocking const { xChainSwapAction } = await import("../actions/xChainSwap"); describe("xChainSwapAction", () => { let mockRuntime: any; let mockMessage: any; let mockCallback: any; beforeEach(() => { mockLogger(); mockRuntime = createMockRuntime({ useModel: mock().mockResolvedValue(`<response> <fromToken>ETH</fromToken> <toToken>ETH</toToken> <fromChain>ethereum</fromChain> <toChain>base</toChain> <amount>1</amount> <toAddress>0x742D35Cc6634c0532925A3b844BC9E7595f6F269</toAddress> </response>`), }); mockMessage = createMockMemory({ content: { text: "Bridge 1 ETH from Ethereum to Base", }, }); mockCallback = mock(); // Reset all mocks mockInit.mockClear(); mockSquidProvider.getChain.mockClear(); mockSquidProvider.getToken.mockClear(); mockGetRoute.mockClear(); mockExecuteRoute.mockClear(); mockInitSquidRouterProvider.mockClear(); }); describe("Basic Properties", () => { it("should have correct name", () => { expect(xChainSwapAction.name).toBe("X_CHAIN_SWAP"); }); it("should have description", () => { expect(xChainSwapAction.description).toContain( "Swaps tokens across chains" ); }); it("should have template", () => { expect(xChainSwapAction.template).toContain("{{recentMessages}}"); }); it("should have examples", () => { expect(xChainSwapAction.examples).toBeDefined(); expect(xChainSwapAction.examples.length).toBeGreaterThan(0); }); it("should have similes", () => { expect(xChainSwapAction.similes).toContain("CROSS_CHAIN_SWAP"); expect(xChainSwapAction.similes).toContain("BRIDGE"); }); }); describe("Validate Function", () => { it("should validate successfully with correct config", async () => { const result = await xChainSwapAction.validate(mockRuntime); expect(result).toBe(true); }); it("should throw error with missing config", async () => { mockRuntime.getSetting.mockImplementation((key: string) => { if (key === "SQUID_INTEGRATOR_ID") return undefined; return "test-value"; }); await expect(xChainSwapAction.validate(mockRuntime)).rejects.toThrow(); }); }); describe("Handler Function", () => { it("should successfully execute a cross-chain swap", async () => { const result = await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); expect(result).toBe(true); expect(mockInit).toHaveBeenCalled(); expect(mockSquidProvider.getChain).toHaveBeenCalledWith("ethereum"); expect(mockSquidProvider.getChain).toHaveBeenCalledWith("base"); expect(mockCallback).toHaveBeenCalledWith({ text: expect.stringContaining("Swap completed successfully"), content: {}, }); }); it("should handle invalid XML response", async () => { mockRuntime.useModel.mockResolvedValue("Invalid XML"); const result = await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); expect(result).toBe(false); expect(mockCallback).toHaveBeenCalledWith({ text: "Unable to process cross-chain swap request. Invalid response format.", content: { error: "Failed to parse response" }, }); }); it("should handle invalid swap content", async () => { mockRuntime.useModel.mockResolvedValue(`<response> <fromToken>ETH</fromToken> </response>`); const result = await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); expect(result).toBe(false); expect(mockCallback).toHaveBeenCalledWith({ text: "Unable to process cross-chain swap request. Invalid content provided.", content: { error: "Invalid cross-chain swap content" }, }); }); it("should handle unsupported chain", async () => { mockRuntime.useModel.mockResolvedValue(`<response> <fromToken>ETH</fromToken> <toToken>ETH</toToken> <fromChain>unsupported</fromChain> <toChain>base</toChain> <amount>1</amount> <toAddress>0x742D35Cc6634c0532925A3b844BC9E7595f6F269</toAddress> </response>`); mockSquidProvider.getChain.mockReturnValue(undefined); const result = await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); expect(result).toBe(false); expect(mockCallback).toHaveBeenCalledWith({ text: expect.stringContaining("Error during cross-chain swap"), content: { error: expect.stringContaining("not supported") }, }); }); it("should handle unsupported token", async () => { mockSquidProvider.getToken.mockReturnValue(undefined); const result = await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); expect(result).toBe(false); expect(mockCallback).toHaveBeenCalledWith({ text: expect.stringContaining("Error during cross-chain swap"), content: { error: expect.stringContaining("not supported") }, }); }); it("should use default toAddress when not provided", async () => { mockRuntime.useModel.mockResolvedValue(`<response> <fromToken>ETH</fromToken> <toToken>ETH</toToken> <fromChain>ethereum</fromChain> <toChain>base</toChain> <amount>1</amount> <toAddress>null</toAddress> </response>`); const result = await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); // Simply check that the handler was called expect(mockCallback).toHaveBeenCalled(); }); it("should handle transaction execution errors", async () => { // Use an invalid chain to trigger an error mockRuntime.useModel.mockResolvedValue(`<response> <fromToken>ETH</fromToken> <toToken>ETH</toToken> <fromChain>invalidchain</fromChain> <toChain>base</toChain> <amount>1</amount> <toAddress>0x742D35Cc6634c0532925A3b844BC9E7595f6F269</toAddress> </response>`); const result = await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); expect(result).toBe(false); expect(mockCallback).toHaveBeenCalled(); const callArg = mockCallback.mock.calls[0][0]; expect(callArg.text).toContain("Error during cross-chain swap"); expect(callArg.content.error).toBeDefined(); }); it("should skip approval for native tokens", async () => { const approveSpendingSpy = spyOn(console, "log"); await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); // Should not see approval log for native ETH expect(approveSpendingSpy).not.toHaveBeenCalledWith( expect.stringContaining("Approved") ); }); it("should approve spending for non-native tokens", async () => { mockRuntime.useModel.mockResolvedValue(`<response> <fromToken>UNI</fromToken> <toToken>USDC</toToken> <fromChain>ethereum</fromChain> <toChain>base</toChain> <amount>100</amount> <toAddress>0x742D35Cc6634c0532925A3b844BC9E7595f6F269</toAddress> </response>`); const result = await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); // Just verify the handler was called expect(mockCallback).toHaveBeenCalled(); }); it("should respect API throttle interval", async () => { mockRuntime.getSetting.mockImplementation((key: string) => { if (key === "SQUID_API_THROTTLE_INTERVAL") return "100"; return createMockRuntime().getSetting(key); }); const result = await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); // Just verify the handler was called expect(mockCallback).toHaveBeenCalled(); }); it("should compose state when state is not provided", async () => { await xChainSwapAction.handler( mockRuntime, mockMessage, undefined, {}, mockCallback ); expect(mockRuntime.composeState).toHaveBeenCalledWith(mockMessage); }); it("should update state when state is provided", async () => { const mockState = createMockState(); await xChainSwapAction.handler( mockRuntime, mockMessage, mockState as any, {}, mockCallback ); expect(mockRuntime.composeState).toHaveBeenCalledWith(mockMessage, [ "RECENT_MESSAGES", ]); }); }); });