Skip to main content
Glama

agentek-eth

by NaniDAO
intents.ts8.63 kB
import { z } from "zod"; import { AgentekClient, createTool, Intent } from "../client.js"; import { arbitrum, base, mainnet, sepolia } from "viem/chains"; import { Address, encodeFunctionData, erc20Abi, Hex, parseUnits, PublicClient, } from "viem"; import { addressSchema } from "../utils.js"; import { resolveENSTool } from "../ens/tools.js"; const intentTransferChains = [mainnet, arbitrum, base, sepolia]; const intentTransferParameters = z.object({ token: addressSchema.describe("The token address"), amount: z.string().describe("The amount to transfer"), to: z.string().describe("The recipient address or ENS name"), chainId: z.number().optional().describe("Optional specify chainId to use"), }); export const ETH_ADDRESS = "0x0000000000000000000000000000000000000000" as Address; const getTokenDecimals = async ( publicClient: PublicClient, token: Address, ): Promise<number> => { if (token == ETH_ADDRESS) { return 18; } else { return await publicClient.readContract({ abi: erc20Abi, functionName: "decimals", address: token, }); } }; const getTokenBalance = async ( publicClient: PublicClient, token: Address, from: Address, ): Promise<bigint> => { if (token == ETH_ADDRESS) { return publicClient.getBalance({ address: from, }); } else { return await publicClient.readContract({ abi: erc20Abi, functionName: "balanceOf", address: token, args: [from], }); } }; export const intentTransferTool = createTool({ name: "intentTransfer", description: "Creates an intent to transfer tokens", supportedChains: intentTransferChains, parameters: intentTransferParameters, execute: async ( client: AgentekClient, args: z.infer<typeof intentTransferParameters>, ): Promise<Intent> => { let { token, amount, to, chainId } = args; const chains = client.filterSupportedChains(intentTransferChains, chainId); const from = await client.getAddress(); // if `to` is an ENS name, resolve it to an address if (to.includes('.')) { to = await resolveENSTool.execute(client, { name: to, }).catch(() => { throw new Error(`Failed to resolve ENS name ${to}`); }); } const chainsWithBalance = ( await Promise.all( chains.map(async (chain) => { const publicClient = client.getPublicClient(chain.id); const decimals = await getTokenDecimals( publicClient, token as Address, ); const balance = await getTokenBalance( publicClient, token as Address, from, ); const amountBigInt = parseUnits(amount, decimals); if (balance >= amountBigInt) { return { chain, balance, amount: amountBigInt, decimals, }; } return null; }), ) ).filter((chain): chain is NonNullable<typeof chain> => chain !== null); if (chainsWithBalance.length === 0) { throw new Error( `${from} doesn't have enough ${token} balance on any of the supported chains - ${chains.map((chain) => chain.name)}`, ); } const cheapestChain = await Promise.all( chainsWithBalance.map(async (chainInfo) => { const publicClient = client.getPublicClient(chainInfo.chain.id); const gasPrice = await publicClient.getGasPrice(); return { ...chainInfo, gasPrice, }; }), ).then((chains) => chains.reduce((cheapest, current) => current.gasPrice < cheapest.gasPrice ? current : cheapest, ), ); const walletClient = client.getWalletClient(cheapestChain.chain.id); let ops = []; let tokenSymbol = ""; if (token === ETH_ADDRESS) { tokenSymbol = "ETH"; ops.push({ target: to as Address, value: cheapestChain.amount.toString(), data: "0x" as Hex, }); } else { tokenSymbol = await client.getPublicClient(cheapestChain.chain.id).readContract({ address: token as Address, abi: erc20Abi, functionName: "symbol", }); ops.push({ target: token as Address, value: "0", data: encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [to as Address, cheapestChain.amount], }), }); } if (!walletClient) { return { intent: `send ${amount.toString()} ${tokenSymbol} to ${to}`, ops, chain: cheapestChain.chain.id, }; } else { const hash = await client.executeOps(ops, cheapestChain.chain.id); return { intent: `send ${amount.toString()} ${tokenSymbol} to ${to}`, ops, chain: cheapestChain.chain.id, hash: hash, }; } }, }); const intentTransferFromParameters = z.object({ token: z.string().describe("The token address"), amount: z.string().describe("The amount to transfer"), from: z.string().describe("The address to transfer from"), to: z.string().describe("The recipient address or ENS"), chainId: z.number().optional().describe("Optional specific chain to use"), }); const intentTransferFromChains = [mainnet, arbitrum, base, sepolia]; export const intentTransferFromTool = createTool({ name: "intentTransferFrom", description: "Creates an intent to transfer tokens from another address", supportedChains: intentTransferFromChains, parameters: intentTransferFromParameters, execute: async ( client: AgentekClient, args: z.infer<typeof intentTransferFromParameters>, ): Promise<Intent> => { let { token, amount, to, from, chainId } = args; const chains = client.filterSupportedChains(intentTransferChains, chainId); // if `to` is an ENS name, resolve it to an address if (to.includes('.')) { to = await resolveENSTool.execute(client, { name: to, }).catch(() => { throw new Error(`Failed to resolve ENS name ${to}`); }); } const chainsWithBalance = ( await Promise.all( chains.map(async (chain) => { const publicClient = client.getPublicClient(chain.id); const decimals = await getTokenDecimals( publicClient, token as Address, ); const balance = await getTokenBalance( publicClient, token as Address, from as Address, ); const amountBigInt = parseUnits(amount, decimals); if (balance >= amountBigInt) { return { chain, balance, amount: amountBigInt, decimals, }; } return null; }), ) ).filter((chain): chain is NonNullable<typeof chain> => chain !== null); if (chainsWithBalance.length === 0) { throw new Error( `${from} doesn't have enough ${token} balance on any of the supported chains - ${chains.map((chain) => chain.name)}`, ); } const cheapestChain = await Promise.all( chainsWithBalance.map(async (chainInfo) => { const publicClient = client.getPublicClient(chainInfo.chain.id); const gasPrice = await publicClient.getGasPrice(); return { ...chainInfo, gasPrice, }; }), ).then((chains) => chains.reduce((cheapest, current) => current.gasPrice < cheapest.gasPrice ? current : cheapest, ), ); const publicClient = client.getPublicClient(cheapestChain.chain.id); const walletClient = client.getWalletClient(cheapestChain.chain.id); let ops = []; if (token === ETH_ADDRESS) { // @TODO gracefully fallback to weth if eth mentioned throw new Error("ETH transferFrom not supported"); } else { ops.push({ target: token as Address, value: "0", data: encodeFunctionData({ abi: erc20Abi, functionName: "transferFrom", args: [from as Address, to as Address, cheapestChain.amount], }), }); } if (!walletClient) { return { intent: `send ${amount.toString()} ${token} from ${from} to ${to}`, ops, chain: cheapestChain.chain.id, }; } else { const hash = await client.executeOps(ops, cheapestChain.chain.id); return { intent: `send ${amount.toString()} ${token} from ${from} to ${to}`, ops, chain: cheapestChain.chain.id, hash: hash, }; } }, });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NaniDAO/agentek'

If you have feedback or need assistance with the MCP directory API, please join our Discord server