Skip to content

Commit

Permalink
Merge pull request #1482 from Archethect/feature/squid-bridge
Browse files Browse the repository at this point in the history
feat: Add cross chain swaps through Squid Router
  • Loading branch information
odilitime authored Jan 14, 2025
2 parents c7e9a0b + cf7305d commit 48ef4c5
Show file tree
Hide file tree
Showing 15 changed files with 852 additions and 3 deletions.
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

0 comments on commit 48ef4c5

Please sign in to comment.