@agentek/tools
Version:
Blockchain tools for AI agents
169 lines • 7.87 kB
JavaScript
import { z } from "zod";
import { parseUnits, maxUint256, erc20Abi, encodeFunctionData, } from "viem";
import { createTool } from "../client.js";
import { mainnet, optimism, arbitrum, base } from "viem/chains";
/** Native ETH sentinel address used by 0x v2. */
const NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
/** Supported chain IDs for 0x Swap API v2. */
const SUPPORTED_CHAIN_IDS = new Set([
mainnet.id,
optimism.id,
arbitrum.id,
base.id,
]);
/**
* Helper to handle 'ETH' vs. token addresses.
* Returns the 0x native token sentinel for ETH, otherwise the address as-is.
*/
function normalize(token) {
if (!token)
return "";
if (token.toLowerCase() === "eth")
return NATIVE_TOKEN;
return token;
}
const matchaSwapChains = [mainnet, optimism, arbitrum, base];
/**
* A tool that performs token swaps across multiple networks (Mainnet, Optimism,
* Arbitrum and Base) via the 0x/Matcha aggregator (v2 AllowanceHolder API).
*
* If a wallet client is available, it will execute the swap immediately.
* If no wallet client is present, it will return a RequestIntent.
*/
export const createMatchSwapTool = ({ zeroxApiKey, swapFeeRecipient, swapFeeBps, swapFeeToken, }) => {
return createTool({
name: "intent0xSwap",
description: "Swap tokens on Ethereum, Optimism, Arbitrum, or Base via the 0x/Matcha aggregator. Automatically handles ERC20 approval if needed. Checks balance before swapping.",
supportedChains: matchaSwapChains,
parameters: z.object({
chainId: z.number().describe("Chain ID (e.g. 1, 10, 42161, 8453)"),
fromToken: z
.string()
.describe('Source token contract address (0x...), or "ETH" for native ETH'),
toToken: z.string().describe("Destination token contract address (0x...)"),
amount: z.string().describe("Amount of source token to swap in human-readable units (e.g. '1.5' for 1.5 tokens). Decimals are resolved automatically."),
slippageBps: z
.number()
.int()
.min(1)
.max(1000)
.default(100)
.describe("Max slippage in basis points (default 100 = 1%)"),
}),
execute: async (client, args) => {
const { chainId, fromToken, toToken, amount, slippageBps } = args;
if (!SUPPORTED_CHAIN_IDS.has(chainId)) {
throw new Error(`Chain ID ${chainId} is not supported by 0x aggregator. Supported: 1, 10, 42161, 8453.`);
}
// Prepare addresses
const sellToken = normalize(fromToken);
const buyToken = normalize(toToken);
const isNativeETH = sellToken === NATIVE_TOKEN;
// Retrieve the relevant wallet + public clients
const walletClient = client.getWalletClient(chainId);
const publicClient = client.getPublicClient(chainId);
const swapIntentDescription = `Swap ${amount} of ${fromToken} for ${toToken} on chainId ${chainId}`;
try {
// Determine decimals
const sellDecimals = isNativeETH
? 18
: (await publicClient.readContract({
address: sellToken,
abi: erc20Abi,
functionName: "decimals",
})) || 18;
const sellAmount = parseUnits(`${amount}`, sellDecimals);
const ops = [];
const userAddress = await client.getAddress();
// Check user's balance
const userBalance = isNativeETH
? await publicClient.getBalance({ address: userAddress })
: (await publicClient.readContract({
address: sellToken,
abi: erc20Abi,
functionName: "balanceOf",
args: [userAddress],
}));
if (userBalance < sellAmount) {
throw new Error(`Insufficient balance: You have ${userBalance.toString()} ${sellToken} but trying to sell ${sellAmount.toString()} ${sellToken}`);
}
// Fetch quote from 0x v2 (AllowanceHolder)
const params = new URLSearchParams({
chainId: String(chainId),
sellToken,
buyToken,
sellAmount: sellAmount.toString(),
taker: userAddress,
slippageBps: String(slippageBps),
});
// Integrator/affiliate fee
if (swapFeeRecipient && swapFeeBps) {
params.set("swapFeeRecipient", swapFeeRecipient);
params.set("swapFeeBps", String(swapFeeBps));
params.set("swapFeeToken", swapFeeToken || "sellToken");
}
const quoteUrl = `https://api.0x.org/swap/allowance-holder/quote?${params}`;
const quoteResp = await fetch(quoteUrl, {
headers: {
"0x-api-key": zeroxApiKey,
"0x-version": "v2",
},
});
if (!quoteResp.ok) {
const errorBody = await quoteResp.text();
throw new Error(`Failed to get swap quote: ${quoteResp.status} ${errorBody}`);
}
const quote = await quoteResp.json();
if (!quote?.transaction) {
throw new Error(quote?.message || "Failed to retrieve a valid swap quote");
}
// Check allowance if selling ERC-20 (v2: issues.allowance tells us the spender)
if (!isNativeETH && quote.issues?.allowance) {
const spender = quote.issues.allowance.spender;
const currentAllowance = BigInt(quote.issues.allowance.actual || "0");
if (sellAmount > currentAllowance) {
ops.push({
target: sellToken,
value: "0",
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [spender, maxUint256],
}),
});
}
}
// Build swap call from the v2 nested transaction object
ops.push({
target: quote.transaction.to,
value: quote.transaction.value || "0",
data: quote.transaction.data,
});
// If no wallet client, return an unexecuted intent
if (!walletClient) {
return {
intent: swapIntentDescription,
ops,
chain: chainId,
buyAmount: quote.buyAmount,
minBuyAmount: quote.minBuyAmount,
};
}
// If walletClient is present, execute ops
const hash = await client.executeOps(ops, chainId);
return {
intent: swapIntentDescription,
ops,
chain: chainId,
hash,
buyAmount: quote.buyAmount,
minBuyAmount: quote.minBuyAmount,
};
}
catch (error) {
throw new Error(`Matcha Swap Failed: ${error instanceof Error ? error.message : error}`);
}
},
});
};
//# sourceMappingURL=intents.js.map