UNPKG

@coinbase/agentkit

Version:

Coinbase AgentKit core primitives

334 lines (330 loc) 18 kB
"use strict"; 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;