UNPKG

@coinbase/agentkit

Version:

Coinbase AgentKit core primitives

408 lines (403 loc) 21.6 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 _ZeroXActionProvider_apiKey; Object.defineProperty(exports, "__esModule", { value: true }); exports.zeroXActionProvider = exports.ZeroXActionProvider = void 0; const zod_1 = require("zod"); const actionProvider_1 = require("../actionProvider"); const actionDecorator_1 = require("../actionDecorator"); const schemas_1 = require("./schemas"); const wallet_providers_1 = require("../../wallet-providers"); const viem_1 = require("viem"); const utils_1 = require("./utils"); /** * 0x API Action Provider for token swaps. * Requires a 0x API key. */ class ZeroXActionProvider extends actionProvider_1.ActionProvider { /** * Constructor for the ZeroXActionProvider. * * @param config - Configuration for the provider. */ constructor(config) { super("zerox", []); _ZeroXActionProvider_apiKey.set(this, void 0); /** * Checks if the ZeroX action provider supports the given network. * * @param network - The network to check. * @returns True if the ZeroX action provider supports the network, false otherwise. */ this.supportsNetwork = (network) => network.protocolFamily === "evm"; const apiKey = config.apiKey || process.env.ZEROX_API_KEY; if (!apiKey) { throw new Error("0x API key not provided."); } __classPrivateFieldSet(this, _ZeroXActionProvider_apiKey, apiKey, "f"); } /** * Gets a price quote for swapping one token for another. * * @param walletProvider - The wallet provider to get information from. * @param args - The input arguments for the action. * @returns A message containing the price quote. */ async getSwapPrice(walletProvider, args) { const network = walletProvider.getNetwork(); const chainId = network.chainId; if (!chainId) throw new Error("Chain ID not available from wallet provider"); try { // Get token details const { fromTokenDecimals: sellTokenDecimals, toTokenDecimals: buyTokenDecimals, fromTokenName: sellTokenName, toTokenName: buyTokenName, } = await (0, utils_1.getTokenDetails)(walletProvider, args.sellToken, args.buyToken); // Convert sell amount to base units const sellAmount = (0, viem_1.parseUnits)(args.sellAmount, sellTokenDecimals).toString(); // Create URL for the price API request const url = new URL("https://api.0x.org/swap/permit2/price"); url.searchParams.append("chainId", chainId.toString()); url.searchParams.append("sellToken", args.sellToken); url.searchParams.append("buyToken", args.buyToken); url.searchParams.append("sellAmount", sellAmount); url.searchParams.append("taker", walletProvider.getAddress()); url.searchParams.append("slippageBps", args.slippageBps.toString()); if (args.swapFeeRecipient) { url.searchParams.append("swapFeeRecipient", args.swapFeeRecipient); url.searchParams.append("swapFeeBps", args.swapFeeBps.toString()); url.searchParams.append("swapFeeToken", args.sellToken); } // Make the request const response = await fetch(url.toString(), { method: "GET", headers: { "Content-Type": "application/json", "0x-api-key": __classPrivateFieldGet(this, _ZeroXActionProvider_apiKey, "f"), "0x-version": "v2", }, }); if (!response.ok) { const errorText = await response.text(); return JSON.stringify({ success: false, error: `Error fetching swap price: ${response.status} ${response.statusText} - ${errorText}`, }); } const data = await response.json(); // Format the response const formattedResponse = { success: true, sellAmount: (0, viem_1.formatUnits)(BigInt(sellAmount), sellTokenDecimals), sellTokenName: sellTokenName, sellToken: args.sellToken, buyAmount: (0, viem_1.formatUnits)(data.buyAmount, buyTokenDecimals), minBuyAmount: data.minBuyAmount ? (0, viem_1.formatUnits)(data.minBuyAmount, buyTokenDecimals) : null, buyTokenName: buyTokenName, buyToken: args.buyToken, slippageBps: args.slippageBps, liquidityAvailable: data.liquidityAvailable, balanceEnough: data.issues?.balance === null, priceOfBuyTokenInSellToken: (Number((0, viem_1.formatUnits)(BigInt(sellAmount), sellTokenDecimals)) / Number((0, viem_1.formatUnits)(data.buyAmount, buyTokenDecimals))).toString(), priceOfSellTokenInBuyToken: (Number((0, viem_1.formatUnits)(data.buyAmount, buyTokenDecimals)) / Number((0, viem_1.formatUnits)(BigInt(sellAmount), sellTokenDecimals))).toString(), }; return JSON.stringify(formattedResponse); } catch (error) { return JSON.stringify({ success: false, error: `Error fetching swap price: ${error}`, }); } } /** * Executes a token swap using the 0x API. * * @param walletProvider - The wallet provider to use for the swap. * @param args - The input arguments for the action. * @returns A message containing the result of the swap. */ async executeSwap(walletProvider, args) { // Sanity checks const network = walletProvider.getNetwork(); const chainId = network.chainId; if (!chainId) throw new Error("Chain ID not available from wallet provider"); try { // Get token details const { fromTokenDecimals: sellTokenDecimals, toTokenDecimals: buyTokenDecimals, fromTokenName: sellTokenName, toTokenName: buyTokenName, } = await (0, utils_1.getTokenDetails)(walletProvider, args.sellToken, args.buyToken); // Convert sell amount to base units const sellAmount = (0, viem_1.parseUnits)(args.sellAmount, sellTokenDecimals).toString(); // Get the wallet address const walletAddress = walletProvider.getAddress(); // Fetch price quote first const priceUrl = new URL("https://api.0x.org/swap/permit2/price"); priceUrl.searchParams.append("chainId", chainId.toString()); priceUrl.searchParams.append("sellToken", args.sellToken); priceUrl.searchParams.append("buyToken", args.buyToken); priceUrl.searchParams.append("sellAmount", sellAmount); priceUrl.searchParams.append("taker", walletAddress); priceUrl.searchParams.append("slippageBps", args.slippageBps.toString()); if (args.swapFeeRecipient) { priceUrl.searchParams.append("swapFeeRecipient", args.swapFeeRecipient); priceUrl.searchParams.append("swapFeeBps", args.swapFeeBps.toString()); priceUrl.searchParams.append("swapFeeToken", args.sellToken); } const priceResponse = await fetch(priceUrl.toString(), { method: "GET", headers: { "Content-Type": "application/json", "0x-api-key": __classPrivateFieldGet(this, _ZeroXActionProvider_apiKey, "f"), "0x-version": "v2", }, }); if (!priceResponse.ok) { const errorText = await priceResponse.text(); return JSON.stringify({ success: false, error: `Error fetching swap price: ${priceResponse.status} ${priceResponse.statusText} - ${errorText}`, }); } const priceData = await priceResponse.json(); // Check if liquidity is available if (priceData.liquidityAvailable === false) { return JSON.stringify({ success: false, error: "No liquidity available for this swap.", }); } // Check if balance of sell token is enough if (priceData.balance != null) { return JSON.stringify({ success: false, error: `Insufficient balance of sell token ${priceData.balance.token}. Requested to swap ${priceData.balance.expected}, but balance is only ${priceData.balance.actual}.`, }); } // Check if permit2 approval is needed for ERC20 tokens // Only needed once per token per address let approvalTxHash = null; if (priceData.issues?.allowance) { try { approvalTxHash = await walletProvider.sendTransaction({ to: args.sellToken, data: (0, viem_1.encodeFunctionData)({ abi: viem_1.erc20Abi, functionName: "approve", args: [utils_1.PERMIT2_ADDRESS, viem_1.maxUint256], }), }); await walletProvider.waitForTransactionReceipt(approvalTxHash); } catch (error) { return JSON.stringify({ success: false, error: `Error approving token: ${error}`, }); } } // Fetch the swap quote const quoteUrl = new URL("https://api.0x.org/swap/permit2/quote"); quoteUrl.searchParams.append("chainId", chainId.toString()); quoteUrl.searchParams.append("sellToken", args.sellToken); quoteUrl.searchParams.append("buyToken", args.buyToken); quoteUrl.searchParams.append("sellAmount", sellAmount); quoteUrl.searchParams.append("taker", walletAddress); quoteUrl.searchParams.append("slippageBps", args.slippageBps.toString()); if (args.swapFeeRecipient) { quoteUrl.searchParams.append("swapFeeRecipient", args.swapFeeRecipient); quoteUrl.searchParams.append("swapFeeBps", args.swapFeeBps.toString()); quoteUrl.searchParams.append("swapFeeToken", args.sellToken); } const quoteResponse = await fetch(quoteUrl.toString(), { method: "GET", headers: { "Content-Type": "application/json", "0x-api-key": __classPrivateFieldGet(this, _ZeroXActionProvider_apiKey, "f"), "0x-version": "v2", }, }); if (!quoteResponse.ok) { const errorText = await quoteResponse.text(); return JSON.stringify({ success: false, error: `Error fetching swap quote: ${quoteResponse.status} ${quoteResponse.statusText} - ${errorText}`, }); } const quoteData = await quoteResponse.json(); // Sign Permit2.eip712 returned from quote let signature; if (quoteData.permit2?.eip712) { try { // For LegacyCdpWalletProvider, remove EIP712Domain to avoid ambiguous primary types const types = walletProvider instanceof wallet_providers_1.LegacyCdpWalletProvider ? (() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { EIP712Domain, ...rest } = quoteData.permit2.eip712.types; return rest; })() : quoteData.permit2.eip712.types; const typedData = { domain: quoteData.permit2.eip712.domain, types, primaryType: quoteData.permit2.eip712.primaryType, message: quoteData.permit2.eip712.message, }; signature = await walletProvider.signTypedData(typedData); // Append sig length and sig data to transaction.data if (signature && quoteData.transaction?.data) { const signatureLengthInHex = (0, viem_1.numberToHex)((0, viem_1.size)(signature), { signed: false, size: 32, }); const transactionData = quoteData.transaction.data; const sigLengthHex = signatureLengthInHex; const sig = signature; quoteData.transaction.data = (0, viem_1.concat)([transactionData, sigLengthHex, sig]); } } catch (error) { return JSON.stringify({ success: false, error: `Error signing permit2 message: ${error}`, }); } } // Execute swap try { // Prepare transaction parameters const txParams = { to: quoteData.transaction.to, data: quoteData.transaction.data, ...(quoteData?.transaction.gas ? { gas: BigInt(quoteData.transaction.gas) } : {}), ...(quoteData.transaction.value ? { value: BigInt(quoteData.transaction.value) } : {}), }; // Send transaction const txHash = await walletProvider.sendTransaction(txParams); const receipt = await walletProvider.waitForTransactionReceipt(txHash); if (receipt.status !== "complete" && receipt.status !== "success") { return JSON.stringify({ success: false, ...(approvalTxHash ? { approvalTxHash } : {}), transactionHash: receipt.transactionHash, error: `Swap transaction failed`, }); } // Format the response const formattedResponse = { success: true, ...(approvalTxHash ? { approvalTxHash } : {}), transactionHash: receipt.transactionHash, sellAmount: (0, viem_1.formatUnits)(BigInt(sellAmount), sellTokenDecimals), sellTokenName: sellTokenName, sellToken: args.sellToken, buyAmount: (0, viem_1.formatUnits)(quoteData.buyAmount, buyTokenDecimals), minBuyAmount: quoteData.minBuyAmount ? (0, viem_1.formatUnits)(quoteData.minBuyAmount, buyTokenDecimals) : null, buyTokenName: buyTokenName, buyToken: args.buyToken, slippageBps: args.slippageBps, network: network.networkId, }; return JSON.stringify(formattedResponse); } catch (error) { return JSON.stringify({ success: false, error: `Error sending swap transaction: ${error}`, ...(approvalTxHash ? { approvalTxHash } : {}), }); } } catch (error) { return JSON.stringify({ success: false, error: `Error executing swap: ${error}`, }); } } } exports.ZeroXActionProvider = ZeroXActionProvider; _ZeroXActionProvider_apiKey = new WeakMap(); __decorate([ (0, actionDecorator_1.CreateAction)({ name: "get_swap_price_quote_from_0x", description: ` This tool fetches a price quote for swapping between two tokens using the 0x API. It takes the following inputs: - sellToken: The contract address of the token to sell - buyToken: The contract address of the token to buy - sellAmount: The amount of sellToken to swap in whole units (e.g. 1 ETH or 10 USDC) - slippageBps: (Optional) Maximum allowed slippage in basis points (100 = 1%) - swapFeeRecipient: (Optional) The wallet address to receive affiliate trading fees - swapFeeBps: The amount in basis points (0-1000) to charge as affiliate fees (defaults to 100 = 1%), only used if swapFeeRecipient is provided Important notes: - The contract address for native ETH is "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - This only fetches a price quote and does not execute a swap - Supported on all EVM networks compatible with 0x API - Use sellToken units exactly as provided, do not convert to wei or any other units - Never assume token or address, they have to be provided as inputs. If only token symbol is provided, use the get_token_address tool if available to get the token address first `, schema: schemas_1.GetSwapPriceSchema, }), __metadata("design:type", Function), __metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]), __metadata("design:returntype", Promise) ], ZeroXActionProvider.prototype, "getSwapPrice", null); __decorate([ (0, actionDecorator_1.CreateAction)({ name: "execute_swap_on_0x", description: ` This tool executes a token swap between two tokens using the 0x API. It takes the following inputs: - sellToken: The contract address of the token to sell - buyToken: The contract address of the token to buy - sellAmount: The amount of sellToken to swap in whole units (e.g. 1 ETH or 10 USDC) - slippageBps: (Optional) Maximum allowed slippage in basis points (100 = 1%) - swapFeeRecipient: (Optional) The wallet address to receive affiliate trading fees - swapFeeBps: The amount in basis points (0-1000) to charge as affiliate fees (defaults to 100 = 1%), only used if swapFeeRecipient is provided Important notes: - The contract address for native ETH is "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - This will execute an actual swap transaction that sends tokens from your wallet - If needed, it will automatically approve the permit2 contract to spend the sell token - The approval transaction is only needed once per token - Ensure you have sufficient balance of the sell token before executing - The trade size might influence the excecution price depending on available liquidity - First fetch a price quote and only execute swap if you are happy with the indicated price - Supported on all EVM networks compatible with 0x API - Use sellToken units exactly as provided, do not convert to wei or any other units - Never assume token or address, they have to be provided as inputs. If only token symbol is provided, use the get_token_address tool if available to get the token address first `, schema: schemas_1.ExecuteSwapSchema, }), __metadata("design:type", Function), __metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]), __metadata("design:returntype", Promise) ], ZeroXActionProvider.prototype, "executeSwap", null); /** * Creates a new ZeroXActionProvider with the provided configuration. * * @param config - Optional configuration for the provider. * @returns A new ZeroXActionProvider. */ const zeroXActionProvider = (config) => new ZeroXActionProvider(config); exports.zeroXActionProvider = zeroXActionProvider;