UNPKG

@coinbase/agentkit

Version:

Coinbase AgentKit core primitives

387 lines (384 loc) 16.9 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); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.sushiRouterActionProvider = exports.SushiRouterActionProvider = void 0; const zod_1 = require("zod"); const wallet_providers_1 = require("../../wallet-providers"); const actionDecorator_1 = require("../actionDecorator"); const actionProvider_1 = require("../actionProvider"); const sushiRouterSchemas_1 = require("./sushiRouterSchemas"); const evm_1 = require("sushi/evm"); const viem_1 = require("viem"); const constants_1 = require("./constants"); /** * SushiRouterActionProvider is an action provider for Sushi. * * This provider is used for any action that uses the Sushi Router API. */ class SushiRouterActionProvider extends actionProvider_1.ActionProvider { /** * Constructor for the SushiRouterActionProvider class. */ constructor() { super("sushi-router", []); } /** * Swaps a specified amount of a from token to a to token for the wallet. * * @param walletProvider - The wallet provider to swap the tokens from. * @param args - The input arguments for the action. * @returns A message containing the swap details. */ async swap(walletProvider, args) { try { const chainId = Number((await walletProvider.getNetwork()).chainId); // Compatible chainId is expected since it should be pre-checked in supportsNetwork if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) { return `Unsupported chainId: ${chainId}`; } const chain = (0, evm_1.getEvmChainById)(chainId); const decimalsIn = await fetchDecimals({ walletProvider, token: args.fromAssetAddress }); if (!decimalsIn.success) { return decimalsIn.message; } const amountIn = (0, viem_1.parseUnits)(args.amount, decimalsIn.decimals); // First fetch to see if the swap is even possible const firstSwap = await handleGetSwap({ amount: amountIn, chainId, tokenIn: args.fromAssetAddress, tokenOut: args.toAssetAddress, maxSlippage: args.maxSlippage, recipient: walletProvider.getAddress(), }); if (firstSwap.swap.status !== evm_1.RouteStatus.Success) { return firstSwap.message; } // Check if the wallet has enough balance to perform the swap const balance = await handleBalance({ walletProvider, token: firstSwap.swap.tokenFrom, minAmount: amountIn, }); if (!balance.success) { return balance.message; } const approval = await handleApproval({ walletProvider, token: args.fromAssetAddress, to: firstSwap.swap.tx.to, amount: amountIn, }); if (!approval.success) { return approval.message; } // Refetch in case the route changed during approval const secondSwap = await handleGetSwap({ amount: amountIn, chainId, tokenIn: args.fromAssetAddress, tokenOut: args.toAssetAddress, maxSlippage: args.maxSlippage, recipient: walletProvider.getAddress(), }); if (secondSwap.swap.status !== evm_1.RouteStatus.Success) { return secondSwap.message; } const swapHash = await walletProvider.sendTransaction({ from: secondSwap.swap.tx.from, to: secondSwap.swap.tx.to, data: secondSwap.swap.tx.data, value: BigInt(secondSwap.swap.tx.value || 0), }); const swapReceipt = await walletProvider.waitForTransactionReceipt(swapHash); if (swapReceipt.status === "reverted" || swapReceipt.status === "failed") { return `Swap failed: Transaction Reverted.\n - Transaction hash: ${swapHash}\n - Transaction link: ${chain.getTransactionUrl(swapHash)}`; } // Find the Route event log, which includes the actual amountOut const [routeLog] = swapReceipt.logs .filter(log => (0, viem_1.encodeEventTopics)({ abi: constants_1.routeProcessor9Abi_Route, eventName: "Route", })[0] === log.topics[0]) .map(log => (0, viem_1.decodeEventLog)({ abi: constants_1.routeProcessor9Abi_Route, data: log.data, topics: log.topics, })); return `Swapped ${(0, viem_1.formatUnits)(routeLog.args.amountIn, secondSwap.swap.tokenFrom.decimals)} of ${secondSwap.swap.tokenFrom.symbol} (${args.fromAssetAddress}) for ${(0, viem_1.formatUnits)(routeLog.args.amountOut, secondSwap.swap.tokenTo.decimals)} of ${secondSwap.swap.tokenTo.symbol} (${args.toAssetAddress}) on ${chain.shortName} - Transaction hash: ${swapHash} - Transaction link: ${chain.getTransactionUrl(swapHash)}`; } catch (error) { return `Error swapping tokens: ${error}`; } } /** * Gets a quote for a specified amount of a from token to a to token * * @param walletProvider - The wallet provider to swap the tokens from. * @param args - The input arguments for the action. * @returns A message containing the quote details. */ async quote(walletProvider, args) { try { const chainId = Number((await walletProvider.getNetwork()).chainId); // Compatible chainId is expected since it should be pre-checked in supportsNetwork if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) { return `Unsupported chainId: ${chainId}`; } const decimalsIn = await fetchDecimals({ walletProvider, token: args.fromAssetAddress }); if (!decimalsIn.success) { return decimalsIn.message; } const amountIn = (0, viem_1.parseUnits)(args.amount, decimalsIn.decimals); const swap = await handleGetSwap({ amount: amountIn, chainId, tokenIn: args.fromAssetAddress, tokenOut: args.toAssetAddress, maxSlippage: 0.0005, // 0.05% recipient: walletProvider.getAddress(), }); return swap.message; } catch (error) { return `Error quoting for tokens: ${error}`; } } /** * Custom action providers are supported on all networks * * @param network - The network to checkpointSaver * @returns True if the network is supported, false otherwise */ supportsNetwork(network) { if (network.protocolFamily !== "evm" || !network.chainId) { return false; } return (0, evm_1.isSwapApiSupportedChainId)(Number(network.chainId)); } } exports.SushiRouterActionProvider = SushiRouterActionProvider; __decorate([ (0, actionDecorator_1.CreateAction)({ name: "swap", description: `This tool will swap a specified amount of a 'from token' (erc20) to a 'to token' (erc20) for the wallet. It takes the following inputs: - The human-readable amount of the 'from token' to swap - The from token address to trade - The token address to receive from the swap - The maximum slippage allowed for the swap, where 0 is 0% and 1 is 100%, the default is 0.005 (0.05%) Important notes: - The native asset (ie 'eth' on 'ethereum-mainnet') is represented by ${evm_1.nativeAddress} (not the wrapped native asset!) - Fetch a quote first before the swap action. Stop, ask the user if they want to proceed. If the user answers affirmatively, then swap - If you are not absolutely sure about token addresses, either use an action to fetch the token address or ask the user `, schema: sushiRouterSchemas_1.SushiSwapSchema, }), __metadata("design:type", Function), __metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]), __metadata("design:returntype", Promise) ], SushiRouterActionProvider.prototype, "swap", null); __decorate([ (0, actionDecorator_1.CreateAction)({ name: "quote", description: `This tool will fetch a quote for a specified amount of a 'from token' (erc20 or native ETH) to a 'to token' (erc20 or native ETH). It takes the following inputs: - The human-readable amount of the 'from token' to fetch a quote for - The from token address to fetch a quote for - The token address to receive from the quoted swap Important notes: - The native asset (ie 'eth' on 'ethereum-mainnet') is represented by ${evm_1.nativeAddress} (not the wrapped native asset!) - This action does not require any on-chain transactions or gas - If you are not 100% certain about token addresses, use an action to fetch the token address first or ask the user - NEVER assume that tokens have the same address on across networks (ie the address of 'usdc' on 'ethereum-mainnet' is different from 'usdc' on 'base-mainnet') `, schema: sushiRouterSchemas_1.SushiQuoteSchema, }), __metadata("design:type", Function), __metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]), __metadata("design:returntype", Promise) ], SushiRouterActionProvider.prototype, "quote", null); /** * Fetches the number of decimals for the token * * @param root0 - The input arguments for the action * @param root0.walletProvider - The wallet provider to fetch the decimals from * @param root0.token - The token address to fetch the decimals for * * @returns The number of decimals for the token */ async function fetchDecimals({ walletProvider, token, }) { const chainId = Number((await walletProvider.getNetwork()).chainId); if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) { return { success: false, message: `Unsupported chainId: ${chainId}`, }; } if (token === evm_1.nativeAddress) { return { success: true, decimals: evm_1.EvmNative.fromChainId(chainId).decimals }; } const decimals = (await walletProvider.readContract({ address: token, abi: viem_1.erc20Abi, functionName: "decimals", })); return { success: true, decimals }; } /** * Checks if the wallet has enough balance to perform the swap * * @param root0 - The input arguments for the action * @param root0.walletProvider - The wallet provider to fetch the balance from * @param root0.token - The token address to fetch the balance for * @param root0.token.address - The token address to fetch the balance for * @param root0.token.symbol - The token symbol to fetch the balance for * @param root0.token.decimals - The token decimals to fetch the balance for * @param root0.minAmount - The minimum amount to check for * * @returns The balance of the wallet */ async function handleBalance({ walletProvider, token, minAmount, }) { let balance; if (token.address.toLowerCase() === evm_1.nativeAddress) { balance = await walletProvider.getBalance(); } else { balance = (await walletProvider.readContract({ address: token.address, abi: viem_1.erc20Abi, functionName: "balanceOf", args: [walletProvider.getAddress()], })); } if (balance < minAmount) { return { success: false, message: `Swap failed: Insufficient balance for ${token.symbol} (${token.address}) - Balance: ${(0, viem_1.formatUnits)(balance, token.decimals)} - Required: ${(0, viem_1.formatUnits)(minAmount, token.decimals)}`, }; } return { success: true, }; } /** * * Wraps the getSwap function, providing messages for possible states * * @param root0 - The input arguments for the action * @param root0.amount - The amount to swap * @param root0.chainId - The chainId to swap on * @param root0.tokenIn - The input token address * @param root0.tokenOut - The output token address * @param root0.maxSlippage - The maximum slippage allowed * @param root0.recipient - The recipient of the swap * * @returns The result of the swap and a message */ async function handleGetSwap({ amount, chainId, tokenIn, tokenOut, maxSlippage, recipient, }) { if (!(0, evm_1.isRedSnwapperChainId)(chainId)) { return { swap: { status: evm_1.RouteStatus.NoWay }, message: `Unsupported chainId: ${chainId}`, }; } const swap = await (0, evm_1.getSwap)({ amount, chainId, tokenIn, tokenOut, maxSlippage, sender: recipient, recipient, }); const chain = (0, evm_1.getEvmChainById)(chainId); if (swap.status === evm_1.RouteStatus.NoWay) { return { swap, message: `No route found to swap ${amount} of ${tokenIn} for ${tokenOut} on ${chain.shortName}`, }; } if (swap.status === evm_1.RouteStatus.Partial) { return { swap, message: `Found a partial quote for ${swap.tokenFrom.symbol} -> ${swap.tokenTo.symbol}. Swapping the full amount is not possible. - AmountIn: ${(0, viem_1.formatUnits)(BigInt(swap.amountIn), swap.tokenFrom.decimals)} - AmountOut: ${(0, viem_1.formatUnits)(BigInt(swap.assumedAmountOut), swap.tokenTo.decimals)}`, }; } return { swap, message: `Found a quote for ${swap.tokenFrom.symbol} (${swap.tokenFrom.address}) -> ${swap.tokenTo.symbol} (${swap.tokenTo.address}) - AmountIn: ${(0, viem_1.formatUnits)(BigInt(swap.amountIn), swap.tokenFrom.decimals)} ${swap.tokenFrom.symbol} - AmountOut: ${(0, viem_1.formatUnits)(BigInt(swap.assumedAmountOut), swap.tokenTo.decimals)} ${swap.tokenTo.symbol}`, }; } /** * * Handles the approval for the token * * @param root0 - The input arguments for the action * @param root0.walletProvider - The wallet provider to handle the approval with * @param root0.token - The token address to approve * @param root0.to - The address to approve the token to * @param root0.amount - The amount to approve * * @returns Either success: true on success or success: false with a message containing the reason for failure */ async function handleApproval({ walletProvider, token, to, amount, }) { // No need to approve if the token is the native token if (token.toLowerCase() === evm_1.nativeAddress) { return { success: true }; } // Check if the wallet already has enough allowance const allowance = (await walletProvider.readContract({ address: token, abi: viem_1.erc20Abi, functionName: "allowance", args: [walletProvider.getAddress(), to], })); if (allowance >= amount) { return { success: true }; } // Exact approval const approvalHash = await walletProvider.sendTransaction({ to: token, data: (0, viem_1.encodeFunctionData)({ abi: viem_1.erc20Abi, functionName: "approve", args: [to, BigInt(amount)], }), }); const approvalReceipt = await walletProvider.waitForTransactionReceipt(approvalHash); const chainId = Number((await walletProvider.getNetwork()).chainId); if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) { return { success: false, message: `Unsupported chainId: ${chainId}`, }; } const chain = (0, evm_1.getEvmChainById)(chainId); if (approvalReceipt.status === "reverted") { return { success: false, message: `Swap failed: Approval Reverted. - Transaction hash: ${approvalHash} - Transaction link: ${chain.getTransactionUrl(approvalHash)}`, }; } return { success: true }; } const sushiRouterActionProvider = () => new SushiRouterActionProvider(); exports.sushiRouterActionProvider = sushiRouterActionProvider;