@elizaos/plugin-squid-router
Version:
ElizaOS plugin for cross-chain token swaps using Squid Router
415 lines (360 loc) • 12.2 kB
text/typescript
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",
]);
});
});
});