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

feat: Add cross chain swaps through Squid Router #1482

Merged
merged 19 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2c8e502
feat(plugin-squid-router): add squid router support for cross chain s…
Archethect Dec 26, 2024
020b9cb
Merge branch 'main' into feature/squid-bridge
Archethect Dec 26, 2024
ff8506a
Merge branch 'develop' into feature/squid-bridge
Archethect Dec 26, 2024
2746396
add router tests + env.example changes + rename to squid-router
Archethect Dec 26, 2024
a8524c5
prefix env vars with SQUID_
Archethect Dec 26, 2024
4c2fe5c
Add readme + add plugin support to agent
Archethect Dec 26, 2024
5d046e8
Merge branch 'develop' into feature/squid-bridge
Archethect Dec 26, 2024
692b851
Add a default destination address
Archethect Dec 26, 2024
ac52671
some fixes related to ethers import
Archethect Dec 26, 2024
d1c68b8
rate limiting avoidance + package versions
Archethect Dec 26, 2024
a721b7b
Merge branch 'develop' into feature/squid-bridge
Archethect Dec 26, 2024
575b630
Merge branch 'develop' into feature/squid-bridge
Archethect Dec 27, 2024
dedd2f5
Add throttle interval support + improved template for cross chain swaps
Archethect Dec 27, 2024
ba4e8fd
Merge branch 'develop' into feature/squid-bridge
Archethect Jan 2, 2025
962077b
Merge branch 'develop' into feature/squid-bridge
Archethect Jan 2, 2025
d4f1b3f
Merge branch 'develop' into feature/squid-bridge
Archethect Jan 2, 2025
8a4e85a
Merge branch 'develop' into feature/squid-bridge
Archethect Jan 2, 2025
3ad0900
Merge branch 'develop' into feature/squid-bridge
odilitime Jan 3, 2025
cf7305d
Merge branch 'develop' into feature/squid-bridge
odilitime Jan 14, 2025
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
10 changes: 7 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,13 @@ ZEROG_EVM_RPC=
ZEROG_PRIVATE_KEY=
ZEROG_FLOW_ADDRESS=

# 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)

# TEE Configuration
# TEE_MODE options:
# - LOCAL: Uses simulator at localhost:8090 (for local development)
Expand Down Expand Up @@ -530,9 +537,6 @@ AWS_S3_UPLOAD_PATH=
# Deepgram
DEEPGRAM_API_KEY=

# Web search API Configuration
TAVILY_API_KEY=

# Verifiable Inference Configuration
VERIFIABLE_INFERENCE_ENABLED=false # Set to false to disable verifiable inference
VERIFIABLE_INFERENCE_PROVIDER=opacity # Options: opacity
Expand Down
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@elizaos/plugin-node": "workspace:*",
"@elizaos/plugin-solana": "workspace:*",
"@elizaos/plugin-solana-agentkit": "workspace:*",
"@elizaos/plugin-squid-router": "workspace:*",
"@elizaos/plugin-autonome": "workspace:*",
"@elizaos/plugin-starknet": "workspace:*",
"@elizaos/plugin-stargaze": "workspace:*",
Expand Down
8 changes: 8 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { TEEMode, teePlugin } from "@elizaos/plugin-tee";
import { teeLogPlugin } from "@elizaos/plugin-tee-log";
import { teeMarlinPlugin } from "@elizaos/plugin-tee-marlin";
import { tonPlugin } from "@elizaos/plugin-ton";
import { squidRouterPlugin } from "@elizaos/plugin-squid-router";
import { webSearchPlugin } from "@elizaos/plugin-web-search";

import { giphyPlugin } from "@elizaos/plugin-giphy";
Expand Down Expand Up @@ -847,6 +848,13 @@ export async function createAgent(
getSecret(character, "THIRDWEB_SECRET_KEY") ? thirdwebPlugin : 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,
getSecret(character, "FUEL_PRIVATE_KEY") ? fuelPlugin : null,
getSecret(character, "AVALANCHE_PRIVATE_KEY")
? avalanchePlugin
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