Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cross chain swaps through Squid Router #1482

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,13 @@ STORY_API_BASE_URL= # Story API base URL
STORY_API_KEY= # Story API key
PINATA_JWT= # Pinata JWT for uploading files to IPFS

# Squid Router
SQUID_SDK_URL=https://apiplus.squidrouter.com # Default: https://apiplus.squidrouter.com
SQUID_INTEGRATOR_ID= # get integrator id through https://docs.squidrouter.com/
SQUID_EVM_ADDRESS=
SQUID_EVM_PRIVATE_KEY=
SQUID_API_THROTTLE_INTERVAL= # Default: 0; Used to throttle API calls to avoid rate limiting (in ms)

# Cronos zkEVM
CRONOSZKEVM_ADDRESS=
CRONOSZKEVM_PRIVATE_KEY=
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
9 changes: 9 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { tonPlugin } from "@elizaos/plugin-ton";
import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era";
import { cronosZkEVMPlugin } from "@elizaos/plugin-cronoszkevm";
import { abstractPlugin } from "@elizaos/plugin-abstract";
import { squidRouterPlugin } from "@elizaos/plugin-squid-router";
import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
Expand Down Expand Up @@ -572,6 +573,14 @@ export async function createAgent(
getSecret(character, "TON_PRIVATE_KEY") ? tonPlugin : null,
getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null,
getSecret(character, "STORY_PRIVATE_KEY") ? storyPlugin : null,
getSecret(character, "SQUID_SDK_URL") &&
getSecret(character, "SQUID_INTEGRATOR_ID") &&
getSecret(character, "SQUID_EVM_ADDRESS") &&
getSecret(character, "SQUID_EVM_PRIVATE_KEY") &&
getSecret(character, "SQUID_API_THROTTLE_INTERVAL")
? squidRouterPlugin
: null,

].filter(Boolean),
providers: [],
actions: [],
Expand Down
26 changes: 26 additions & 0 deletions packages/plugin-squid-router/README.md
Original file line number Diff line number Diff line change
@@ -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`
3 changes: 3 additions & 0 deletions packages/plugin-squid-router/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import eslintGlobalConfig from "../../eslint.config.mjs";

export default [...eslintGlobalConfig];
25 changes: 25 additions & 0 deletions packages/plugin-squid-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@elizaos/plugin-squid-router",
"version": "0.1.7-alpha.1",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"dependencies": {
"@0xsquid/sdk": "2.8.29",
"@0xsquid/squid-types": "0.1.122",
"@elizaos/core": "workspace:*",
"ethers": "6.8.1",
"optional": "0.1.4",
"sharp": "0.33.5",
"tsup": "8.3.5"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch",
"test": "vitest run",
"lint": "eslint --fix --cache ."
},
"peerDependencies": {
"whatwg-url": "7.1.0"
}
}
262 changes: 262 additions & 0 deletions packages/plugin-squid-router/src/actions/xChainSwap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import {
composeContext,
elizaLogger,
generateObjectDeprecated,
HandlerCallback,
IAgentRuntime,
Memory,
ModelClass,
State
} from "@elizaos/core";
import {xChainSwapTemplate} from "../templates";
import {convertToWei, isXChainSwapContent, validateSquidRouterConfig} from "../helpers/utils.ts";
import {ethers} from "ethers";
import {initSquidRouterProvider} from "../providers/squidRouter.ts";

export { xChainSwapTemplate };

const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

const approveSpending = async (transactionRequestTarget: string, fromToken: string, fromAmount: string, signer: ethers.Signer) => {
const erc20Abi = [
"function approve(address spender, uint256 amount) public returns (bool)"
];
const tokenContract = new ethers.Contract(fromToken, erc20Abi, signer);
try {
const tx = await tokenContract.approve(transactionRequestTarget, fromAmount);
await tx.wait();
console.log(`Approved ${fromAmount} tokens for ${transactionRequestTarget}`);
} catch (error) {
console.error('Approval failed:', error);
throw error;
}
};

export const xChainSwapAction = {
name: "X_CHAIN_SWAP",
description: "Swaps tokens across chains from the agent's wallet to a recipient wallet. \n"+
"By default the senders configured wallets will be used to send the assets to on the destination chains, unless clearly defined otherwise by providing a recipient address.\n" +
"The system supports bridging, cross chain swaps and normal swaps.",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
_options: { [key: string]: unknown },
callback?: HandlerCallback
): Promise<boolean> => {
elizaLogger.log("Starting X_CHAIN_SWAP handler...");

// Initialize or update state
if (!state) {
state = (await runtime.composeState(message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}

// Compose X chain swap context
const xChainSwapContext = composeContext({
state,
template: xChainSwapTemplate,
});

// Generate X chain swap content
const content = await generateObjectDeprecated({
runtime,
context: xChainSwapContext,
modelClass: ModelClass.SMALL,
});

if(content.toAddress === null) {
content.toAddress = runtime.getSetting("SQUID_EVM_ADDRESS");
}

elizaLogger.log("swap content: ",JSON.stringify(content));

// Validate transfer content
if (!isXChainSwapContent(content)) {
console.error("Invalid content for X_CHAIN_SWAP action.");
if (callback) {
callback({
text: "Unable to process cross-chain swap request. Invalid content provided.",
content: { error: "Invalid cross-chain swap content" },
});
}
return false;
}

try {

const squidRouter = initSquidRouterProvider(runtime);
await squidRouter.initialize();
console.log("Initialized Squid SDK");

const fromChainObject = squidRouter.getChain(content.fromChain);
if(!fromChainObject) {
throw new Error(
"Chain to swap from is not supported."
);
}

const toChainObject = squidRouter.getChain(content.toChain);
if(!toChainObject) {
throw new Error(
"Chain to swap to is not supported."
);
}

const fromTokenObject = squidRouter.getToken(fromChainObject, content.fromToken);
if(!fromTokenObject?.enabled) {
throw new Error(
"Token to swap from is not supported."
);
}

const toTokenObject = squidRouter.getToken(toChainObject, content.toToken);
if(!fromTokenObject?.enabled) {
throw new Error(
"Token to swap into is not supported."
);
}

const signer = await squidRouter.getEVMSignerForChain(fromChainObject, runtime);

const params = {
fromAddress: await signer.getAddress(),
fromChain: fromChainObject.chainId,
fromToken: fromTokenObject.address,
fromAmount: convertToWei(content.amount, fromTokenObject),
toChain: toChainObject.chainId,
toToken: toTokenObject.address,
toAddress: content.toAddress,
quoteOnly: false
};

console.log("Parameters:", params); // Printing the parameters for QA

const throttleInterval = runtime.getSetting("SQUID_API_THROTTLE_INTERVAL") ? Number(runtime.getSetting("SQUID_API_THROTTLE_INTERVAL")) : 0

await delay(throttleInterval);

// Get the swap route using Squid SDK
const {route} = await squidRouter.getRoute(params);
console.log("Calculated route:", route.estimate.toAmount);

const transactionRequest = route.transactionRequest;

// Approve the transactionRequest.target to spend fromAmount of fromToken
if ("target" in transactionRequest) {
if(!fromTokenObject.isNative) {
await approveSpending(transactionRequest.target, params.fromToken, params.fromAmount, signer);
}
} else {
throw new Error(
"Non-expected transaction request"
);
}

await delay(throttleInterval);

// Execute the swap transaction
const tx = (await squidRouter.executeRoute({
signer,
route,
})) as unknown as ethers.TransactionResponse;
const txReceipt = await tx.wait();

// Show the transaction receipt with Axelarscan link
const axelarScanLink = "https://axelarscan.io/gmp/" + txReceipt.hash;
elizaLogger.log(`Finished! Check Axelarscan for details: ${axelarScanLink}`);

if (callback) {
callback({
text:
"Swap completed successfully! Check Axelarscan for details:\n " + axelarScanLink,
content: {},
});
}



} catch (error) {
elizaLogger.error("Error during cross-chain swap:", error);
if (callback) {
callback({
text: `Error during cross-chain swap: ${error.message}`,
content: { error: error.message },
});
}
return false;
}
},
template: xChainSwapTemplate,
validate: async (runtime: IAgentRuntime) => {
await validateSquidRouterConfig(runtime);
return true;
},
examples: [
[
{
user: "{{user1}}",
content: {
text: "Bridge 1 ETH from Ethereum to Base",
},
},
{
user: "{{agent}}",
content: {
text: "Sure, I'll send 1 ETH from Ethereum to Base",
action: "X_CHAIN_SWAP",
},
},
{
user: "{{agent}}",
content: {
text: "Successfully sent 1 ETH to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62 on Base\nTransaction: 0x4fed598033f0added272c3ddefd4d83a521634a738474400b27378db462a76ec",
},
},
],
[
{
user: "{{user1}}",
content: {
text: "Please swap 1 SOL into USDC from Solana to Base on address 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8",
},
},
{
user: "{{agent}}",
content: {
text: "Sure, I'll swap 1 SOL into USDC from Solana to Base on address 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8",
action: "X_CHAIN_SWAP",
},
},
{
user: "{{agent}}",
content: {
text: "Successfully Swapped 1 SOL into USDC and sent to 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8 on Base\nTransaction: 2sj3ifA5iPdRDfnkyK5LZ4KoyN57AH2QoHFSzuefom11F1rgdiUriYf2CodBbq9LBi77Q5bLHz4CShveisTu954B",
},
},
],
[
{
user: "{{user1}}",
content: {
text: "Send 100 UNI from Arbitrum to Ethereum",
},
},
{
user: "{{agent}}",
content: {
text: "Sure, I'll send 100 UNI to Ethereum right away.",
action: "X_CHAIN_SWAP",
},
},
{
user: "{{agent}}",
content: {
text: "Successfully sent 100 UNI to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62 on Ethereum\nTransaction: 0x4fed598033f0added272c3ddefd4d83a521634a738474400b27378db462a76ec",
},
},
]
],
similes: ["CROSS_CHAIN_SWAP", "CROSS_CHAIN_BRIDGE", "MOVE_CROSS_CHAIN", "SWAP","BRIDGE"],
}; // TODO: add more examples / similies
Loading
Loading