@elizaos/plugin-squid-router
Version:
ElizaOS plugin for cross-chain token swaps using Squid Router
543 lines (531 loc) • 18.1 kB
JavaScript
// src/providers/squidRouter.ts
import { Squid } from "@0xsquid/sdk";
import { ethers as ethers2 } from "ethers";
import {
ChainType
} from "@0xsquid/squid-types";
// src/helpers/utils.ts
import { z } from "zod";
import { ethers } from "ethers";
function convertToWei(amount, token) {
if (typeof token.decimals !== "number" || token.decimals < 0 || token.decimals > 255) {
throw new Error("Invalid decimals value in token object.");
}
try {
const amountString = typeof amount === "number" ? amount.toString() : amount;
const parsedAmount = ethers.parseUnits(amountString, token.decimals);
return parsedAmount.toString();
} catch (error) {
throw new Error(`Failed to convert amount: ${error.message}`);
}
}
function isXChainSwapContent(content) {
const validTypes = typeof content.fromChain === "string" && typeof content.toChain === "string" && typeof content.fromToken === "string" && typeof content.toToken === "string" && typeof content.toAddress === "string" && (typeof content.amount === "string" || typeof content.amount === "number");
if (validTypes) {
return true;
}
return false;
}
var isValidEvmAddress = (address) => {
return ethers.isAddress(address);
};
var isValidEvmPrivateKey = (key) => {
const cleanedKey = key.startsWith("0x") ? key.slice(2) : key;
return /^[0-9a-fA-F]{64}$/.test(cleanedKey);
};
var squidRouterEnvSchema = z.object({
SQUID_INTEGRATOR_ID: z.string().min(1, "Squid Integrator ID is required"),
SQUID_SDK_URL: z.string().min(1, "Squid SDK URL is required"),
SQUID_EVM_ADDRESS: z.string().min(1, "Squid Integrator ID is required"),
SQUID_EVM_PRIVATE_KEY: z.string().min(1, "Squid Integrator ID is required")
}).refine(
(data) => {
const evmValid = data.SQUID_EVM_ADDRESS && data.SQUID_EVM_PRIVATE_KEY && isValidEvmAddress(data.SQUID_EVM_ADDRESS) && isValidEvmPrivateKey(data.SQUID_EVM_PRIVATE_KEY);
return evmValid;
},
{
message: "At least one valid address and private key pair is required: EVM, Solana, or Cosmos.",
path: []
// Global error
}
).superRefine((data, ctx) => {
if (data.SQUID_EVM_ADDRESS || data.SQUID_EVM_PRIVATE_KEY) {
if (data.SQUID_EVM_ADDRESS && !isValidEvmAddress(data.SQUID_EVM_ADDRESS)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "EVM_ADDRESS is invalid or not checksummed correctly.",
path: ["EVM_ADDRESS"]
});
}
if (data.SQUID_EVM_PRIVATE_KEY && !isValidEvmPrivateKey(data.SQUID_EVM_PRIVATE_KEY)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "EVM_PRIVATE_KEY must be a 64-character hexadecimal string.",
path: ["EVM_PRIVATE_KEY"]
});
}
if (data.SQUID_EVM_ADDRESS && !data.SQUID_EVM_PRIVATE_KEY || !data.SQUID_EVM_ADDRESS && data.SQUID_EVM_PRIVATE_KEY) {
if (!data.SQUID_EVM_ADDRESS) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "EVM_ADDRESS is required when EVM_PRIVATE_KEY is provided.",
path: ["EVM_ADDRESS"]
});
}
if (!data.SQUID_EVM_PRIVATE_KEY) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "EVM_PRIVATE_KEY is required when EVM_ADDRESS is provided.",
path: ["EVM_PRIVATE_KEY"]
});
}
}
}
});
async function validateSquidRouterConfig(runtime) {
try {
const config = {
SQUID_INTEGRATOR_ID: runtime.getSetting("SQUID_INTEGRATOR_ID"),
SQUID_SDK_URL: runtime.getSetting("SQUID_SDK_URL"),
SQUID_EVM_ADDRESS: runtime.getSetting("SQUID_EVM_ADDRESS"),
SQUID_EVM_PRIVATE_KEY: runtime.getSetting("SQUID_EVM_PRIVATE_KEY")
};
return squidRouterEnvSchema.parse(config);
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors.map((err) => `${err.path.join(".")}: ${err.message}`).join("\n");
throw new Error(
`Squid Router configuration validation failed:
${errorMessages}`
);
}
throw error;
}
}
// src/types/index.ts
var nativeTokenConstant = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
// src/providers/squidRouter.ts
var getSDK = (baseUrl, integratorId) => {
const squid = new Squid({
baseUrl,
integratorId
});
return squid;
};
var SquidRouterProvider = class {
constructor(squidSDKUrl, squidIntegratorID) {
this.squidSDKUrl = squidSDKUrl;
this.squidIntegratorID = squidIntegratorID;
this.squid = getSDK(squidSDKUrl, squidIntegratorID);
}
squid;
async initialize() {
if (!this.squid.initialized) {
await this.squid.init();
}
}
getSquidObject() {
return this.squid;
}
getChains() {
return this.squid.chains;
}
getTokens() {
return this.squid.tokens;
}
getChain(targetChainName) {
const normalizedTarget = targetChainName.toLowerCase();
const targetChain = this.getChains().find(
(chain) => chain.networkName.toLowerCase() === normalizedTarget
);
if (targetChain && targetChain.chainType === ChainType.EVM) {
return targetChain;
}
return void 0;
}
getToken(targetChain, targetTokenSymbol) {
const normalizedTargetToken = targetTokenSymbol.toLowerCase();
if (normalizedTargetToken === targetChain.nativeCurrency.symbol) {
return {
address: nativeTokenConstant,
isNative: true,
symbol: targetTokenSymbol,
decimals: targetChain.nativeCurrency.decimals,
enabled: true
};
}
const targetToken = this.getTokens().find(
(token) => token.symbol.toLowerCase() === normalizedTargetToken && token.chainId === targetChain.chainId
);
if (!targetToken) {
return void 0;
}
return {
address: targetToken.address,
isNative: false,
symbol: targetTokenSymbol,
decimals: targetToken.decimals,
enabled: targetToken.disabled ?? true
};
}
async getRoute(route) {
return await this.squid.getRoute(route);
}
async executeRoute(route) {
return await this.squid.executeRoute(route);
}
async getEVMSignerForChain(chain, runtime) {
try {
if (chain.chainType === ChainType.EVM) {
const provider = new ethers2.JsonRpcProvider(chain.rpc);
return new ethers2.Wallet(
runtime.getSetting("SQUID_EVM_PRIVATE_KEY"),
provider
);
}
throw new Error("Cannot instantiate EVM signer for non-EVM chain");
} catch (error) {
throw new Error(`Cannot instantiate EVM signer: ${error}`);
}
}
// async getEVMSignerForChain(chain: ChainData, runtime): Promise<ethers.Signer> {
// try {
// if(chain.chainType === ChainType.EVM) {
// const provider = new ethers.JsonRpcProvider(chain.rpc);
// return new ethers.Wallet(runtime.getSetting("SQUID_EVM_PRIVATE_KEY"), provider);
// } else {
// throw Error("Cannot instantiate EVM signer for non-EVM chain");
// }
// } catch (error) {
// throw Error("Cannot instantiate EVM signer: "+error);
// }
// }
};
var initSquidRouterProvider = async (runtime) => {
await validateSquidRouterConfig(runtime);
const sdkUrl = runtime.getSetting("SQUID_SDK_URL");
const integratorId = runtime.getSetting("SQUID_INTEGRATOR_ID");
return new SquidRouterProvider(sdkUrl, integratorId);
};
var squidRouterProvider = {
name: "squidRouter",
get: async (runtime, _message, state) => {
try {
const provider = await initSquidRouterProvider(runtime);
await provider.initialize();
const chains = provider.getChains();
const tokens = provider.getTokens();
return {
text: "Squid Router provider setup successful.",
values: {
sdkUrl: runtime.getSetting("SQUID_SDK_URL"),
integratorId: runtime.getSetting("SQUID_INTEGRATOR_ID"),
address: runtime.getSetting("SQUID_EVM_ADDRESS"),
chainsCount: chains.length,
tokensCount: tokens.length
},
data: {
chains: chains.map((chain) => ({
chainId: chain.chainId,
chainName: chain.networkName,
chainType: chain.chainType
})),
initialized: true
}
};
} catch (error) {
console.error("Error in Squid Router provider:", error);
return {
text: "Error initializing Squid Router provider",
values: {
error: error instanceof Error ? error.message : String(error)
}
};
}
}
};
// src/actions/xChainSwap.ts
import {
composePromptFromState,
elizaLogger,
parseKeyValueXml,
ModelType
} from "@elizaos/core";
// src/templates/index.ts
var xChainSwapTemplate = `Given the recent messages and wallet information below:
{{recentMessages}}
Extract the following information about the requested cross chain swap:
- Token symbol to swap from
- Token symbol to swap into (if defined, otherwise the same as the token symbol to swap from)
- Source chain
- Destination chain (if defined, otherwise the same as the source chain)
- Amount to swap, denominated in the token to be sent
- Destination address (if specified)
If the destination address is not specified, the EVM address of the runtime should be used.
If the token to swap into is not specified, the token to swap from should be used.
If the destination chain is not specified, the source chain should be used.
Respond with an XML block containing only the extracted values:
<response>
<fromToken>string or null</fromToken>
<toToken>string or null</toToken>
<fromChain>string or null</fromChain>
<toChain>string or null</toChain>
<amount>string or null</amount>
<toAddress>string or null</toAddress>
</response>
`;
// src/actions/xChainSwap.ts
import { ethers as ethers3 } from "ethers";
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
var approveSpending = async (transactionRequestTarget, fromToken, fromAmount, signer) => {
const erc20Abi = [
"function approve(address spender, uint256 amount) public returns (bool)"
];
const tokenContract = new ethers3.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;
}
};
var xChainSwapAction = {
name: "X_CHAIN_SWAP",
description: "Swaps tokens across chains from the agent's wallet to a recipient wallet. \nBy 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.\nThe system supports bridging, cross chain swaps and normal swaps.",
handler: async (runtime, message, state, _options = {}, callback) => {
elizaLogger.log("Starting X_CHAIN_SWAP handler...");
let currentState = state;
if (!currentState) {
currentState = await runtime.composeState(message);
} else {
currentState = await runtime.composeState(message, ["RECENT_MESSAGES"]);
}
const xChainSwapPrompt = composePromptFromState({
state: currentState,
// Use the new variable
template: xChainSwapTemplate
});
const result = await runtime.useModel(ModelType.TEXT_SMALL, {
prompt: xChainSwapPrompt,
stopSequences: []
});
const content = parseKeyValueXml(result);
if (!content) {
elizaLogger.error("Failed to parse XML response");
if (callback) {
callback({
text: "Unable to process cross-chain swap request. Invalid response format.",
content: { error: "Failed to parse response" }
});
}
return false;
}
if (content.toAddress === null) {
content.toAddress = runtime.getSetting("SQUID_EVM_ADDRESS");
}
elizaLogger.log("swap content: ", JSON.stringify(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 = await 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 (!toTokenObject?.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);
const throttleInterval = runtime.getSetting("SQUID_API_THROTTLE_INTERVAL") ? Number(runtime.getSetting("SQUID_API_THROTTLE_INTERVAL")) : 0;
await delay(throttleInterval);
const { route } = await squidRouter.getRoute(params);
console.log("Calculated route:", route.estimate.toAmount);
const transactionRequest = route.transactionRequest;
if (transactionRequest && "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);
const tx = await squidRouter.executeRoute({
signer,
route
});
const txReceipt = await tx.wait();
const axelarScanLink = `https://axelarscan.io/gmp/${txReceipt?.hash || "unknown"}`;
elizaLogger.log(
`Finished! Check Axelarscan for details: ${axelarScanLink}`
);
if (callback) {
callback({
text: `Swap completed successfully! Check Axelarscan for details:
${axelarScanLink}`,
// Fix: Use template literal
content: {}
});
}
} catch (error) {
elizaLogger.error("Error during cross-chain swap:", error);
if (callback) {
const errorMessage = error instanceof Error ? error.message : String(error);
callback({
text: `Error during cross-chain swap: ${errorMessage}`,
content: { error: errorMessage }
});
}
return false;
}
return true;
},
template: xChainSwapTemplate,
validate: async (runtime) => {
await validateSquidRouterConfig(runtime);
return true;
},
examples: [
[
{
name: "{{user1}}",
content: {
text: "Bridge 1 ETH from Ethereum to Base"
}
},
{
name: "{{agent}}",
content: {
text: "Sure, I'll send 1 ETH from Ethereum to Base",
action: "X_CHAIN_SWAP"
}
},
{
name: "{{agent}}",
content: {
text: "Successfully sent 1 ETH to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62 on Base\nTransaction: 0x4fed598033f0added272c3ddefd4d83a521634a738474400b27378db462a76ec"
}
}
],
[
{
name: "{{user1}}",
content: {
text: "Please swap 1 SOL into USDC from Solana to Base on address 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8"
}
},
{
name: "{{agent}}",
content: {
text: "Sure, I'll swap 1 SOL into USDC from Solana to Base on address 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8",
action: "X_CHAIN_SWAP"
}
},
{
name: "{{agent}}",
content: {
text: "Successfully Swapped 1 SOL into USDC and sent to 0xF43042865f4D3B32A19ECBD1C7d4d924613c41E8 on Base\nTransaction: 2sj3ifA5iPdRDfnkyK5LZ4KoyN57AH2QoHFSzuefom11F1rgdiUriYf2CodBbq9LBi77Q5bLHz4CShveisTu954B"
}
}
],
[
{
name: "{{user1}}",
content: {
text: "Send 100 UNI from Arbitrum to Ethereum"
}
},
{
name: "{{agent}}",
content: {
text: "Sure, I'll send 100 UNI to Ethereum right away.",
action: "X_CHAIN_SWAP"
}
},
{
name: "{{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"
]
};
// src/index.ts
var squidRouterPlugin = {
name: "squid-router",
description: "Squid router plugin",
providers: [squidRouterProvider],
evaluators: [],
services: [],
actions: [xChainSwapAction]
};
var index_default = squidRouterPlugin;
export {
SquidRouterProvider,
index_default as default,
initSquidRouterProvider,
nativeTokenConstant,
squidRouterPlugin,
squidRouterProvider,
xChainSwapAction,
xChainSwapTemplate
};
//# sourceMappingURL=index.js.map