@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
408 lines (403 loc) • 21.6 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 _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;