@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
334 lines (330 loc) • 18 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _AcrossActionProvider_privateKey;
Object.defineProperty(exports, "__esModule", { value: true });
exports.acrossActionProvider = exports.AcrossActionProvider = void 0;
const zod_1 = require("zod");
const viem_1 = require("viem");
const actionProvider_1 = require("../actionProvider");
const network_1 = require("../../network");
const actionDecorator_1 = require("../actionDecorator");
const schemas_1 = require("./schemas");
const wallet_providers_1 = require("../../wallet-providers");
const utils_1 = require("./utils");
const accounts_1 = require("viem/accounts");
const constants_1 = require("../erc20/constants");
/**
* AcrossActionProvider provides actions for cross-chain bridging via Across Protocol.
*/
class AcrossActionProvider extends actionProvider_1.ActionProvider {
/**
* Constructor for the AcrossActionProvider.
*
* @param config - The configuration options for the AcrossActionProvider.
*/
constructor(config) {
super("across", []);
_AcrossActionProvider_privateKey.set(this, void 0);
/**
* Checks if the Across action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Across action provider supports the network, false otherwise.
*/
this.supportsNetwork = (network) => {
// Across only supports EVM-compatible chains
return network.protocolFamily === "evm";
};
__classPrivateFieldSet(this, _AcrossActionProvider_privateKey, config.privateKey, "f");
const account = (0, accounts_1.privateKeyToAccount)(__classPrivateFieldGet(this, _AcrossActionProvider_privateKey, "f"));
if (!account)
throw new Error("Invalid private key");
}
/**
* Bridges a token from one chain to another using Across Protocol.
*
* @param walletProvider - The wallet provider to use for the transaction.
* @param args - The input arguments for the action.
* @returns A message containing the bridge details.
*/
async bridgeToken(walletProvider, args) {
try {
// Use dynamic import to get the Across SDK
const acrossModule = await import("@across-protocol/app-sdk");
const createAcrossClient = acrossModule.createAcrossClient;
// Get recipient address if provided, otherwise use sender
const address = walletProvider.getAddress();
const recipient = (args.recipient || address);
// Get origin chain
const originChain = (0, network_1.getChain)(walletProvider.getNetwork().chainId);
if (!originChain) {
throw new Error(`Unsupported origin chain: ${walletProvider.getNetwork()}`);
}
// Get destination chain
const destinationNetworkId = network_1.CHAIN_ID_TO_NETWORK_ID[Number(args.destinationChainId)];
const destinationChain = network_1.NETWORK_ID_TO_VIEM_CHAIN[destinationNetworkId];
if (!destinationChain) {
throw new Error(`Unsupported destination chain: ${args.destinationChainId}`);
}
// Sanity checks
if (originChain.id === destinationChain.id) {
throw new Error("Origin and destination chains cannot be the same");
}
const useTestnet = (0, utils_1.isAcrossSupportedTestnet)(originChain.id);
if (useTestnet !== (0, utils_1.isAcrossSupportedTestnet)(destinationChain.id)) {
throw new Error(`Cross-chain transfers between ${originChain.name} and ${destinationChain.name} are not supported.
Origin and destination chains must either be both testnets or both mainnets.`);
}
// Create wallet client
const account = (0, accounts_1.privateKeyToAccount)(__classPrivateFieldGet(this, _AcrossActionProvider_privateKey, "f"));
if (account.address !== walletProvider.getAddress()) {
throw new Error("Private key does not match wallet provider address");
}
const walletClient = (0, viem_1.createWalletClient)({
account,
chain: originChain,
transport: (0, viem_1.http)(),
});
// Create Across client
const acrossClient = createAcrossClient({
chains: [originChain, destinationChain],
useTestnet,
});
// Get chain details to find token information
const chainDetails = await acrossClient.getSupportedChains({});
const originChainDetails = chainDetails.find(chain => chain.chainId === originChain.id);
if (!originChainDetails) {
throw new Error(`Origin chain ${originChain.id} not supported by Across Protocol`);
}
// Find token by symbol on the origin chain
const inputTokens = originChainDetails.inputTokens;
if (!inputTokens || inputTokens.length === 0) {
throw new Error(`No input tokens available on chain ${originChain.id}`);
}
const tokenInfo = inputTokens.find(token => token.symbol.toUpperCase() === args.inputTokenSymbol.toUpperCase());
if (!tokenInfo) {
throw new Error(`Token ${args.inputTokenSymbol} not found on chain ${originChain.id}. Available tokens: ${inputTokens.map(t => t.symbol).join(", ")}`);
}
// Get token address and decimals to parse the amount
const inputToken = tokenInfo.address;
const decimals = tokenInfo.decimals;
const inputAmount = (0, viem_1.parseUnits)(args.amount, decimals);
// Check balance
const isNative = args.inputTokenSymbol.toUpperCase() === "ETH";
if (isNative) {
// Check native ETH balance
const ethBalance = await walletProvider.getBalance();
if (ethBalance < inputAmount) {
throw new Error(`Insufficient balance. Requested to bridge ${(0, viem_1.formatUnits)(inputAmount, decimals)} ${args.inputTokenSymbol} but balance is only ${(0, viem_1.formatUnits)(ethBalance, decimals)} ${args.inputTokenSymbol}`);
}
}
else {
// Check ERC20 token balance
const tokenBalance = (await walletProvider.readContract({
address: inputToken,
abi: constants_1.abi,
functionName: "balanceOf",
args: [address],
}));
if (tokenBalance < inputAmount) {
throw new Error(`Insufficient balance. Requested to bridge ${(0, viem_1.formatUnits)(inputAmount, decimals)} ${args.inputTokenSymbol} but balance is only ${(0, viem_1.formatUnits)(tokenBalance, decimals)} ${args.inputTokenSymbol}`);
}
}
// Get available routes
const routeInfo = await acrossClient.getAvailableRoutes({
originChainId: originChain.id,
destinationChainId: destinationChain.id,
originToken: inputToken,
});
// Select the appropriate route for native ETH or ERC20 token
const route = routeInfo.find(route => route.isNative === isNative);
if (!route) {
throw new Error(`No routes available from chain ${originChain.name} to chain ${destinationChain.name} for token ${args.inputTokenSymbol}`);
}
// Get quote
const quote = await acrossClient.getQuote({
route,
inputAmount,
recipient,
});
// Convert units to readable format
const formattedInfo = {
minDeposit: (0, viem_1.formatUnits)(quote.limits.minDeposit, decimals),
maxDeposit: (0, viem_1.formatUnits)(quote.limits.maxDeposit, decimals),
inputAmount: (0, viem_1.formatUnits)(quote.deposit.inputAmount, decimals),
outputAmount: (0, viem_1.formatUnits)(quote.deposit.outputAmount, decimals),
};
// Check if input amount is within valid deposit range
if (quote.deposit.inputAmount < quote.limits.minDeposit) {
throw new Error(`Input amount ${formattedInfo.inputAmount} ${args.inputTokenSymbol} is below the minimum deposit of ${formattedInfo.minDeposit} ${args.inputTokenSymbol}`);
}
if (quote.deposit.inputAmount > quote.limits.maxDeposit) {
throw new Error(`Input amount ${formattedInfo.inputAmount} ${args.inputTokenSymbol} exceeds the maximum deposit of ${formattedInfo.maxDeposit} ${args.inputTokenSymbol}`);
}
// Check if output amount is within acceptable slippage limits
const actualSlippagePercentage = ((Number(formattedInfo.inputAmount) - Number(formattedInfo.outputAmount)) /
Number(formattedInfo.inputAmount)) *
100;
if (actualSlippagePercentage > args.maxSplippage) {
throw new Error(`Output amount has high slippage of ${actualSlippagePercentage.toFixed(2)}%, which exceeds the maximum allowed slippage of ${args.maxSplippage}%. ` +
`Input: ${formattedInfo.inputAmount} ${args.inputTokenSymbol}, Output: ${formattedInfo.outputAmount} ${args.inputTokenSymbol}`);
}
//Approve ERC20 token if needed
let approvalTxHash;
if (!isNative) {
approvalTxHash = await walletProvider.sendTransaction({
to: inputToken,
data: (0, viem_1.encodeFunctionData)({
abi: constants_1.abi,
functionName: "approve",
args: [quote.deposit.spokePoolAddress, quote.deposit.inputAmount],
}),
});
await walletProvider.waitForTransactionReceipt(approvalTxHash);
}
// Simulate the deposit transaction
const { request } = await acrossClient.simulateDepositTx({
walletClient: walletClient,
deposit: quote.deposit,
});
// Execute the deposit transaction
const transactionHash = await walletClient.writeContract(request);
// Wait for tx to be mined
const { depositId } = await acrossClient.waitForDepositTx({
transactionHash,
originChainId: originChain.id,
});
return `
Successfully deposited tokens:
- From: Chain ${originChain.id} (${originChain.name})
- To: Chain ${destinationChain.id} (${destinationChain.name})
- Token: ${args.inputTokenSymbol} (${inputToken})
- Input Amount: ${formattedInfo.inputAmount} ${args.inputTokenSymbol}
- Output Amount: ${formattedInfo.outputAmount} ${args.inputTokenSymbol}
- Recipient: ${recipient}
${!isNative ? `- Transaction Hash for approval: ${approvalTxHash}\n` : ""}
- Transaction Hash for deposit: ${transactionHash}
- Deposit ID: ${depositId}
`;
}
catch (error) {
return `Error with Across SDK: ${error}`;
}
}
/**
* Checks the status of a bridge deposit via Across Protocol.
*
* @param walletProvider - The wallet provider to use for the transaction.
* @param args - The input arguments for the action.
* @returns A message containing the deposit status details.
*/
async checkDepositStatus(walletProvider, args) {
const originChainId = Number(args.originChainId) || Number(walletProvider.getNetwork().chainId);
if ((0, utils_1.isAcrossSupportedTestnet)(originChainId)) {
throw new Error("Checking deposit status on testnets is currently not supported by the Across API");
}
try {
const response = await fetch(`https://app.across.to/api/deposit/status?originChainId=${originChainId}&depositId=${args.depositId}`, {
method: "GET",
});
if (!response.ok) {
throw new Error(`Across API request failed with status ${response.status}`);
}
const apiData = await response.json();
// Get chain names
const originChainName = (0, network_1.getChain)(String(apiData.originChainId))?.name || "Unknown Chain";
const destinationChainName = (0, network_1.getChain)(String(apiData.destinationChainId))?.name || "Unknown Chain";
// Create structured response
const structuredResponse = {
status: apiData.status || "unknown",
depositTxInfo: apiData.depositTxHash
? {
txHash: apiData.depositTxHash,
chainId: apiData.originChainId,
chainName: originChainName,
}
: null,
fillTxInfo: apiData.fillTx
? {
txHash: apiData.fillTx,
chainId: apiData.destinationChainId,
chainName: destinationChainName,
}
: null,
depositRefundTxInfo: apiData.depositRefundTxHash
? {
txHash: apiData.depositRefundTxHash,
chainId: apiData.originChainId,
chainName: originChainName,
}
: null,
};
return JSON.stringify(structuredResponse, null, 2);
}
catch (error) {
return `Error checking deposit status: ${error}`;
}
}
}
exports.AcrossActionProvider = AcrossActionProvider;
_AcrossActionProvider_privateKey = new WeakMap();
__decorate([
(0, actionDecorator_1.CreateAction)({
name: "bridge_token",
description: `
This tool will bridge tokens from the current chain to another chain using the Across Protocol.
It takes the following inputs:
- destinationChainId: The chain ID of the destination chain (e.g. 8453 for base-mainnet)
- inputTokenSymbol: The symbol of the token to bridge (e.g. 'ETH', 'USDC')
- amount: The amount of tokens to bridge in whole units (e.g. 1.5 WETH, 10 USDC)
- recipient: (Optional) The recipient address on the destination chain (defaults to sender)
- maxSplippage: (Optional) The maximum slippage percentage (defaults to 1.5%)
Important notes:
- Origin chain is the currently connected chain of the wallet provider
- Supports cross-chain transfers between EVM-compatible chains for both mainnets and test networks
- Testnet deposits are not refunded if not filled on destination chain
- Ensure sufficient balance of the input token before bridging
- Returns deposit ID that can be used to check the status of the deposit
`,
schema: schemas_1.BridgeTokenSchema,
}),
__metadata("design:type", Function),
__metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
__metadata("design:returntype", Promise)
], AcrossActionProvider.prototype, "bridgeToken", null);
__decorate([
(0, actionDecorator_1.CreateAction)({
name: "check_deposit_status",
description: `
This tool will check the status of a cross-chain bridge deposit on the Across Protocol.
It takes the following inputs:
- originChainId: The chain ID of the origin chain (defaults to the current chain)
- depositId: The ID of the deposit to check (returned by the bridge deposit transaction)
`,
schema: schemas_1.CheckDepositStatusSchema,
}),
__metadata("design:type", Function),
__metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
__metadata("design:returntype", Promise)
], AcrossActionProvider.prototype, "checkDepositStatus", null);
const acrossActionProvider = (config) => new AcrossActionProvider(config);
exports.acrossActionProvider = acrossActionProvider;