diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 7f2b0233a..77dbbb13d 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -18,6 +18,7 @@ export * from "./erc721"; export * from "./farcaster"; export * from "./jupiter"; export * from "./messari"; +export * from "./pumpclaw"; export * from "./pyth"; export * from "./moonwell"; export * from "./morpho"; diff --git a/typescript/agentkit/src/action-providers/pumpclaw/README.md b/typescript/agentkit/src/action-providers/pumpclaw/README.md new file mode 100644 index 000000000..fb42c8523 --- /dev/null +++ b/typescript/agentkit/src/action-providers/pumpclaw/README.md @@ -0,0 +1,47 @@ +# PumpClaw Action Provider + +This directory contains the **PumpclawActionProvider** implementation, which provides actions to interact with the **PumpClaw protocol** on Base mainnet. + +## Directory Structure + +``` +pumpclaw/ +├── pumpclawActionProvider.ts # Main provider with PumpClaw functionality +├── pumpclawActionProvider.test.ts # Test file for PumpClaw provider +├── constants.ts # PumpClaw contract constants and ABIs +├── schemas.ts # PumpClaw action schemas +├── index.ts # Main exports +└── README.md # This file +``` + +## Actions + +- `create_token`: Create a new token via PumpClaw factory with Uniswap V4 liquidity +- `get_token_info`: Get detailed information about a PumpClaw token +- `list_tokens`: List all tokens created on PumpClaw +- `buy_token`: Buy tokens with ETH via SwapRouter +- `sell_token`: Sell tokens for ETH via SwapRouter +- `set_image_url`: Update token image (creator only) + +## Adding New Actions + +To add new PumpClaw actions: + +1. Define your action schema in `schemas.ts` +2. Implement the action in `pumpclawActionProvider.ts` +3. Add tests in `pumpclawActionProvider.test.ts` + +## Network Support + +The PumpClaw provider supports Base mainnet only. + +## Contract Addresses + +- **Factory**: `0xe5bCa0eDe9208f7Ee7FCAFa0415Ca3DC03e16a90` (Base mainnet) +- **SwapRouter**: `0x3A9c65f4510de85F1843145d637ae895a2Fe04BE` (Base mainnet) + +## Notes + +PumpClaw is a token launcher on Base that uses Uniswap V4 for liquidity provisioning. Liquidity is locked at creation time and trading fees are split between the token creator and protocol. + +For more information, visit [pumpclaw.com](https://pumpclaw.com). diff --git a/typescript/agentkit/src/action-providers/pumpclaw/constants.ts b/typescript/agentkit/src/action-providers/pumpclaw/constants.ts new file mode 100644 index 000000000..c169721b1 --- /dev/null +++ b/typescript/agentkit/src/action-providers/pumpclaw/constants.ts @@ -0,0 +1,196 @@ +import type { Abi } from "abitype"; + +export const SUPPORTED_NETWORKS = ["base-mainnet"]; + +/** + * PumpClaw Factory contract ABI - minimal functions needed for PumpClaw interactions. + */ +export const PUMPCLAW_FACTORY_ABI: Abi = [ + { + type: "function", + name: "createToken", + inputs: [ + { name: "name", type: "string", internalType: "string" }, + { name: "symbol", type: "string", internalType: "string" }, + { name: "imageUrl", type: "string", internalType: "string" }, + { name: "totalSupply", type: "uint256", internalType: "uint256" }, + { name: "initialFdv", type: "uint256", internalType: "uint256" }, + { name: "creator", type: "address", internalType: "address" }, + ], + outputs: [{ name: "token", type: "address", internalType: "address" }], + stateMutability: "payable", + }, + { + type: "function", + name: "getTokenInfo", + inputs: [{ name: "token", type: "address", internalType: "address" }], + outputs: [ + { name: "name", type: "string", internalType: "string" }, + { name: "symbol", type: "string", internalType: "string" }, + { name: "imageUrl", type: "string", internalType: "string" }, + { name: "totalSupply", type: "uint256", internalType: "uint256" }, + { name: "creator", type: "address", internalType: "address" }, + { name: "pool", type: "address", internalType: "address" }, + { name: "createdAt", type: "uint256", internalType: "uint256" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getTokenCount", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "getTokens", + inputs: [ + { name: "offset", type: "uint256", internalType: "uint256" }, + { name: "limit", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "", type: "address[]", internalType: "address[]" }], + stateMutability: "view", + }, + { + type: "function", + name: "setImageUrl", + inputs: [ + { name: "token", type: "address", internalType: "address" }, + { name: "imageUrl", type: "string", internalType: "string" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, +] as const; + +/** + * PumpClaw SwapRouter contract ABI - minimal functions needed for swap interactions. + */ +export const PUMPCLAW_SWAPROUTER_ABI: Abi = [ + { + type: "function", + name: "buyTokens", + inputs: [ + { name: "token", type: "address", internalType: "address" }, + { name: "minTokensOut", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "sellTokens", + inputs: [ + { name: "token", type: "address", internalType: "address" }, + { name: "tokensIn", type: "uint256", internalType: "uint256" }, + { name: "minEthOut", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, +] as const; + +/** + * ERC20 token ABI - standard functions needed for token interactions. + */ +export const ERC20_ABI: Abi = [ + { + type: "function", + name: "symbol", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "decimals", + inputs: [], + outputs: [{ name: "", type: "uint8", internalType: "uint8" }], + stateMutability: "view", + }, + { + type: "function", + name: "totalSupply", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "balanceOf", + inputs: [{ name: "account", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "approve", + inputs: [ + { name: "spender", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "allowance", + inputs: [ + { name: "owner", type: "address", internalType: "address" }, + { name: "spender", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, +] as const; + +/** + * Contract addresses on Base mainnet. + */ +export const PUMPCLAW_CONTRACT_ADDRESSES = { + "base-mainnet": { + Factory: "0xe5bCa0eDe9208f7Ee7FCAFa0415Ca3DC03e16a90", + SwapRouter: "0x3A9c65f4510de85F1843145d637ae895a2Fe04BE", + }, +} as const; + +/** + * Gets the PumpClaw Factory contract address for the specified network. + * + * @param network - The network ID to get the contract address for. + * @returns The contract address for the specified network. + * @throws Error if the specified network is not supported. + */ +export function getFactoryAddress(network: string): string { + const addresses = + PUMPCLAW_CONTRACT_ADDRESSES[ + network.toLowerCase() as keyof typeof PUMPCLAW_CONTRACT_ADDRESSES + ]; + if (!addresses) { + throw new Error( + `Unsupported network: ${network}. Supported: ${Object.keys(PUMPCLAW_CONTRACT_ADDRESSES).join(", ")}`, + ); + } + return addresses.Factory; +} + +/** + * Gets the PumpClaw SwapRouter contract address for the specified network. + * + * @param network - The network ID to get the contract address for. + * @returns The contract address for the specified network. + * @throws Error if the specified network is not supported. + */ +export function getSwapRouterAddress(network: string): string { + const addresses = + PUMPCLAW_CONTRACT_ADDRESSES[ + network.toLowerCase() as keyof typeof PUMPCLAW_CONTRACT_ADDRESSES + ]; + if (!addresses) { + throw new Error( + `Unsupported network: ${network}. Supported: ${Object.keys(PUMPCLAW_CONTRACT_ADDRESSES).join(", ")}`, + ); + } + return addresses.SwapRouter; +} diff --git a/typescript/agentkit/src/action-providers/pumpclaw/index.ts b/typescript/agentkit/src/action-providers/pumpclaw/index.ts new file mode 100644 index 000000000..8b00d3cd9 --- /dev/null +++ b/typescript/agentkit/src/action-providers/pumpclaw/index.ts @@ -0,0 +1,2 @@ +export * from "./schemas"; +export * from "./pumpclawActionProvider"; diff --git a/typescript/agentkit/src/action-providers/pumpclaw/pumpclawActionProvider.test.ts b/typescript/agentkit/src/action-providers/pumpclaw/pumpclawActionProvider.test.ts new file mode 100644 index 000000000..07662321d --- /dev/null +++ b/typescript/agentkit/src/action-providers/pumpclaw/pumpclawActionProvider.test.ts @@ -0,0 +1,359 @@ +import { EvmWalletProvider } from "../../wallet-providers"; +import { PumpclawActionProvider } from "./pumpclawActionProvider"; +import { getFactoryAddress, getSwapRouterAddress } from "./constants"; + +describe("PumpclawActionProvider", () => { + const MOCK_TOKEN_ADDRESS = + "0x1234567890123456789012345678901234567890" as `0x${string}`; + const MOCK_POOL_ADDRESS = + "0x2345678901234567890123456789012345678901" as `0x${string}`; + const MOCK_CREATOR_ADDRESS = + "0x3456789012345678901234567890123456789012" as `0x${string}`; + const MOCK_WALLET_ADDRESS = + "0x9876543210987654321098765432109876543210" as `0x${string}`; + const MOCK_TX_HASH = "0xabcdef1234567890"; + const MOCK_TOTAL_SUPPLY = "1000000000000000000000000000"; // 1B tokens + const MOCK_INITIAL_FDV = "10000000000000000000"; // 10 ETH + + let provider: PumpclawActionProvider; + let mockWallet: jest.Mocked; + + beforeEach(() => { + mockWallet = { + getAddress: jest.fn().mockReturnValue(MOCK_WALLET_ADDRESS), + getNetwork: jest + .fn() + .mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet" }), + sendTransaction: jest + .fn() + .mockResolvedValue(MOCK_TX_HASH as `0x${string}`), + waitForTransactionReceipt: jest.fn().mockResolvedValue({}), + readContract: jest.fn(), + } as unknown as jest.Mocked; + + provider = new PumpclawActionProvider(); + }); + + describe("supportsNetwork", () => { + it("should support base-mainnet", () => { + expect( + provider.supportsNetwork({ + protocolFamily: "evm", + networkId: "base-mainnet", + }), + ).toBe(true); + }); + + it("should not support base-sepolia", () => { + expect( + provider.supportsNetwork({ + protocolFamily: "evm", + networkId: "base-sepolia", + }), + ).toBe(false); + }); + + it("should not support non-EVM networks", () => { + expect( + provider.supportsNetwork({ + protocolFamily: "bitcoin", + networkId: "base-mainnet", + }), + ).toBe(false); + }); + }); + + describe("createToken", () => { + it("should create a token successfully", async () => { + const response = await provider.createToken(mockWallet, { + name: "Test Token", + symbol: "TEST", + imageUrl: "https://example.com/image.png", + totalSupply: MOCK_TOTAL_SUPPLY, + initialFdv: MOCK_INITIAL_FDV, + }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + to: getFactoryAddress("base-mainnet"), + }), + ); + expect(response).toContain("Successfully created"); + expect(response).toContain("Test Token"); + expect(response).toContain("TEST"); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should handle creation failure", async () => { + mockWallet.sendTransaction.mockRejectedValue(new Error("create failed")); + + const response = await provider.createToken(mockWallet, { + name: "Test Token", + symbol: "TEST", + imageUrl: "https://example.com/image.png", + totalSupply: MOCK_TOTAL_SUPPLY, + initialFdv: MOCK_INITIAL_FDV, + }); + expect(response).toContain("Error creating"); + }); + }); + + describe("getTokenInfo", () => { + beforeEach(() => { + mockWallet.readContract.mockImplementation((params: any) => { + if (params.functionName === "getTokenInfo") { + return Promise.resolve([ + "Test Token", + "TEST", + "https://example.com/image.png", + BigInt(MOCK_TOTAL_SUPPLY), + MOCK_CREATOR_ADDRESS, + MOCK_POOL_ADDRESS, + BigInt(1640995200), + ]); + } + if (params.functionName === "decimals") { + return Promise.resolve(18); + } + return Promise.resolve(null); + }); + }); + + it("should return token information", async () => { + const response = await provider.getTokenInfo(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + }); + + expect(mockWallet.readContract).toHaveBeenCalled(); + expect(response).toContain("Test Token"); + expect(response).toContain("TEST"); + expect(response).toContain("https://example.com/image.png"); + expect(response).toContain(MOCK_CREATOR_ADDRESS); + expect(response).toContain(MOCK_POOL_ADDRESS); + }); + + it("should handle non-existent token", async () => { + mockWallet.readContract.mockImplementation((params: any) => { + if (params.functionName === "getTokenInfo") { + return Promise.resolve([ + "", + "", + "", + BigInt(0), + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + BigInt(0), + ]); + } + return Promise.resolve(null); + }); + + const response = await provider.getTokenInfo(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + }); + expect(response).toContain("not a valid PumpClaw token"); + }); + + it("should handle errors", async () => { + mockWallet.readContract.mockRejectedValue(new Error("read failed")); + + const response = await provider.getTokenInfo(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + }); + expect(response).toContain("Error getting token information"); + }); + }); + + describe("listTokens", () => { + it("should list tokens successfully", async () => { + mockWallet.readContract.mockImplementation((params: any) => { + if (params.functionName === "getTokenCount") { + return Promise.resolve(BigInt(5)); + } + if (params.functionName === "getTokens") { + return Promise.resolve([ + MOCK_TOKEN_ADDRESS, + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", + ]); + } + return Promise.resolve(null); + }); + + const response = await provider.listTokens(mockWallet, { + offset: 0, + limit: 10, + }); + + expect(response).toContain("showing 3 of 5 total"); + expect(response).toContain(MOCK_TOKEN_ADDRESS); + }); + + it("should handle empty list", async () => { + mockWallet.readContract.mockImplementation((params: any) => { + if (params.functionName === "getTokenCount") { + return Promise.resolve(BigInt(0)); + } + return Promise.resolve(null); + }); + + const response = await provider.listTokens(mockWallet, { + offset: 0, + limit: 10, + }); + expect(response).toContain("No PumpClaw tokens"); + }); + + it("should handle offset beyond token count", async () => { + mockWallet.readContract.mockImplementation((params: any) => { + if (params.functionName === "getTokenCount") { + return Promise.resolve(BigInt(5)); + } + if (params.functionName === "getTokens") { + return Promise.resolve([]); + } + return Promise.resolve(null); + }); + + const response = await provider.listTokens(mockWallet, { + offset: 10, + limit: 10, + }); + expect(response).toContain("No tokens found at offset 10"); + }); + }); + + describe("buyToken", () => { + it("should buy tokens successfully", async () => { + const response = await provider.buyToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + ethAmount: "1000000000000000000", + minTokensOut: "0", + }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + to: getSwapRouterAddress("base-mainnet"), + value: BigInt("1000000000000000000"), + }), + ); + expect(response).toContain("Successfully bought"); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should handle buy failure", async () => { + mockWallet.sendTransaction.mockRejectedValue(new Error("buy failed")); + + const response = await provider.buyToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + ethAmount: "1000000000000000000", + minTokensOut: "0", + }); + expect(response).toContain("Error buying"); + }); + }); + + describe("sellToken", () => { + beforeEach(() => { + mockWallet.readContract.mockImplementation((params: any) => { + if (params.functionName === "balanceOf") { + return Promise.resolve(BigInt("2000000000000000000")); + } + if (params.functionName === "allowance") { + return Promise.resolve(BigInt(0)); + } + return Promise.resolve(null); + }); + }); + + it("should sell tokens successfully with approval", async () => { + const response = await provider.sellToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensIn: "1000000000000000000", + minEthOut: "0", + }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(2); // approve + sell + expect(response).toContain("Successfully sold"); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should sell tokens without approval if already approved", async () => { + mockWallet.readContract.mockImplementation((params: any) => { + if (params.functionName === "balanceOf") { + return Promise.resolve(BigInt("2000000000000000000")); + } + if (params.functionName === "allowance") { + return Promise.resolve(BigInt("2000000000000000000")); + } + return Promise.resolve(null); + }); + + const response = await provider.sellToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensIn: "1000000000000000000", + minEthOut: "0", + }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1); // only sell + expect(response).toContain("Successfully sold"); + }); + + it("should reject when balance is insufficient", async () => { + mockWallet.readContract.mockImplementation((params: any) => { + if (params.functionName === "balanceOf") { + return Promise.resolve(BigInt("100")); + } + return Promise.resolve(null); + }); + + const response = await provider.sellToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensIn: "1000000000000000000", + minEthOut: "0", + }); + expect(response).toContain("Insufficient balance"); + }); + + it("should handle sell failure", async () => { + mockWallet.sendTransaction.mockRejectedValue(new Error("sell failed")); + + const response = await provider.sellToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensIn: "1000000000000000000", + minEthOut: "0", + }); + expect(response).toContain("Error selling"); + }); + }); + + describe("setImageUrl", () => { + it("should set image URL successfully", async () => { + const response = await provider.setImageUrl(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + imageUrl: "https://example.com/new-image.png", + }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + to: getFactoryAddress("base-mainnet"), + }), + ); + expect(response).toContain("Successfully updated"); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should handle non-creator error", async () => { + mockWallet.sendTransaction.mockRejectedValue( + new Error("Only creator can update"), + ); + + const response = await provider.setImageUrl(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + imageUrl: "https://example.com/new-image.png", + }); + expect(response).toContain("Error updating"); + expect(response).toContain("Only the token creator"); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/pumpclaw/pumpclawActionProvider.ts b/typescript/agentkit/src/action-providers/pumpclaw/pumpclawActionProvider.ts new file mode 100644 index 000000000..e085c3f1c --- /dev/null +++ b/typescript/agentkit/src/action-providers/pumpclaw/pumpclawActionProvider.ts @@ -0,0 +1,444 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { + SUPPORTED_NETWORKS, + PUMPCLAW_FACTORY_ABI, + PUMPCLAW_SWAPROUTER_ABI, + ERC20_ABI, + getFactoryAddress, + getSwapRouterAddress, +} from "./constants"; +import { encodeFunctionData, formatUnits } from "viem"; +import { + PumpclawCreateTokenInput, + PumpclawGetTokenInfoInput, + PumpclawListTokensInput, + PumpclawBuyTokenInput, + PumpclawSellTokenInput, + PumpclawSetImageUrlInput, +} from "./schemas"; + +/** + * PumpclawActionProvider is an action provider for PumpClaw protocol interactions. + * + * PumpClaw is a token launcher on Base that deploys ERC20 tokens with + * full-range Uniswap V4 liquidity. Liquidity is locked at creation time. + * + * @see https://pumpclaw.com + */ +export class PumpclawActionProvider extends ActionProvider { + /** + * Constructor for the PumpclawActionProvider class. + */ + constructor() { + super("pumpclaw", []); + } + + /** + * Creates a new token via PumpClaw factory. + * + * @param walletProvider - The wallet provider to create the token from. + * @param args - The input arguments for the action. + * @returns A message containing the token creation details. + */ + @CreateAction({ + name: "create_token", + description: ` +This tool creates a new ERC20 token on Base via PumpClaw with Uniswap V4 liquidity. + +Inputs: +- Token name and symbol +- Image URL for the token +- Total supply (default: 1B tokens) +- Initial FDV in ETH (default: 10 ETH) +- Creator address (optional, defaults to sender) + +Important notes: +- Amounts are in wei (no decimal points) +- Default total supply: 1,000,000,000 tokens (1B) +- Default initial FDV: 10 ETH +- Liquidity is locked at creation time +- Only supported on Base mainnet`, + schema: PumpclawCreateTokenInput, + }) + async createToken( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const factoryAddress = getFactoryAddress(walletProvider.getNetwork().networkId!); + const creator = args.creator || walletProvider.getAddress(); + + const createData = encodeFunctionData({ + abi: PUMPCLAW_FACTORY_ABI, + functionName: "createToken", + args: [ + args.name, + args.symbol, + args.imageUrl, + BigInt(args.totalSupply), + BigInt(args.initialFdv), + creator as `0x${string}`, + ], + }); + + const txHash = await walletProvider.sendTransaction({ + to: factoryAddress as `0x${string}`, + data: createData, + }); + + await walletProvider.waitForTransactionReceipt(txHash); + + return `Successfully created PumpClaw token "${args.name}" (${args.symbol}). + +Transaction hash: ${txHash} + +The token contract address can be found in the transaction logs (TokenCreated event).`; + } catch (error) { + return `Error creating PumpClaw token: ${error}`; + } + } + + /** + * Gets detailed information about a PumpClaw token. + * + * @param walletProvider - The wallet provider to get token information from. + * @param args - The input arguments for the action. + * @returns A message containing the token information. + */ + @CreateAction({ + name: "get_token_info", + description: ` +This tool gets detailed information about a PumpClaw token on Base. + +Inputs: +- Token contract address + +Returns token details including name, symbol, image URL, total supply, +creator address, pool address, and creation timestamp. + +Important notes: +- Only works with tokens created via PumpClaw factory +- Supported on Base mainnet only`, + schema: PumpclawGetTokenInfoInput, + }) + async getTokenInfo( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const factoryAddress = getFactoryAddress(walletProvider.getNetwork().networkId!); + + const tokenInfo = (await walletProvider.readContract({ + address: factoryAddress as `0x${string}`, + abi: PUMPCLAW_FACTORY_ABI, + functionName: "getTokenInfo", + args: [args.tokenAddress as `0x${string}`], + })) as [string, string, string, bigint, string, string, bigint]; + + if (tokenInfo[6] === 0n) { + return `Error: ${args.tokenAddress} is not a valid PumpClaw token (not found in registry).`; + } + + const [name, symbol, imageUrl, totalSupply, creator, pool, createdAt] = tokenInfo; + + // Get decimals + const decimals = (await walletProvider.readContract({ + address: args.tokenAddress as `0x${string}`, + abi: ERC20_ABI, + functionName: "decimals", + args: [], + })) as number; + + const formattedSupply = formatUnits(totalSupply, decimals); + + return `Token Information for ${args.tokenAddress}: + +Name: ${name} +Symbol: ${symbol} +Image URL: ${imageUrl} +Total Supply: ${formattedSupply} ${symbol} +Creator: ${creator} +Pool Address: ${pool} +Created: ${new Date(Number(createdAt) * 1000).toISOString()}`; + } catch (error) { + return `Error getting token information: ${error}`; + } + } + + /** + * Lists all tokens created on PumpClaw. + * + * @param walletProvider - The wallet provider to list tokens from. + * @param args - The input arguments for the action. + * @returns A message containing the list of tokens. + */ + @CreateAction({ + name: "list_tokens", + description: ` +This tool lists all tokens created on PumpClaw. + +Inputs: +- Offset: starting index (default: 0) +- Limit: number of tokens to return (default: 10, max: 100) + +Returns a list of token contract addresses. + +Important notes: +- Tokens are returned in creation order (oldest first) +- Use offset for pagination +- Supported on Base mainnet only`, + schema: PumpclawListTokensInput, + }) + async listTokens( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const factoryAddress = getFactoryAddress(walletProvider.getNetwork().networkId!); + + const tokenCount = (await walletProvider.readContract({ + address: factoryAddress as `0x${string}`, + abi: PUMPCLAW_FACTORY_ABI, + functionName: "getTokenCount", + args: [], + })) as bigint; + + if (tokenCount === 0n) { + return "No PumpClaw tokens have been created yet."; + } + + const tokens = (await walletProvider.readContract({ + address: factoryAddress as `0x${string}`, + abi: PUMPCLAW_FACTORY_ABI, + functionName: "getTokens", + args: [BigInt(args.offset), BigInt(args.limit)], + })) as string[]; + + if (tokens.length === 0) { + return `No tokens found at offset ${args.offset}. Total token count: ${tokenCount.toString()}`; + } + + let result = `PumpClaw Tokens (showing ${tokens.length} of ${tokenCount.toString()} total):\n\n`; + + for (let i = 0; i < tokens.length; i++) { + result += `${args.offset + i + 1}. ${tokens[i]}\n`; + } + + return result; + } catch (error) { + return `Error listing tokens: ${error}`; + } + } + + /** + * Buys PumpClaw tokens with ETH via SwapRouter. + * + * @param walletProvider - The wallet provider to buy tokens with. + * @param args - The input arguments for the action. + * @returns A message containing the purchase details. + */ + @CreateAction({ + name: "buy_token", + description: ` +This tool buys PumpClaw tokens with ETH via SwapRouter on Base. +Do not use this tool for buying other types of tokens. + +Inputs: +- Token contract address +- Amount of ETH to spend (in wei) +- Minimum tokens to receive (in wei, for slippage protection) + +Important notes: +- Amounts are in wei (no decimal points). 1 ETH = 10^18 wei. +- The minTokensOut protects against slippage — the transaction reverts if received amount is less. +- Only supported on Base mainnet`, + schema: PumpclawBuyTokenInput, + }) + async buyToken( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const swapRouterAddress = getSwapRouterAddress( + walletProvider.getNetwork().networkId!, + ); + + const buyData = encodeFunctionData({ + abi: PUMPCLAW_SWAPROUTER_ABI, + functionName: "buyTokens", + args: [args.tokenAddress as `0x${string}`, BigInt(args.minTokensOut)], + }); + + const txHash = await walletProvider.sendTransaction({ + to: swapRouterAddress as `0x${string}`, + data: buyData, + value: BigInt(args.ethAmount), + }); + + await walletProvider.waitForTransactionReceipt(txHash); + + return `Successfully bought PumpClaw tokens. Transaction hash: ${txHash}`; + } catch (error) { + return `Error buying PumpClaw tokens: ${error}`; + } + } + + /** + * Sells PumpClaw tokens for ETH via SwapRouter. + * + * @param walletProvider - The wallet provider to sell tokens from. + * @param args - The input arguments for the action. + * @returns A message containing the sale details. + */ + @CreateAction({ + name: "sell_token", + description: ` +This tool sells PumpClaw tokens for ETH via SwapRouter on Base. +Do not use this tool for selling other types of tokens. + +Inputs: +- Token contract address +- Amount of tokens to sell (in wei) +- Minimum ETH to receive (in wei, for slippage protection) + +Important notes: +- Amounts are in wei (no decimal points). 1 token = 10^decimals wei. +- The minEthOut protects against slippage — the transaction reverts if received amount is less. +- Token approval for the SwapRouter is handled automatically. +- Only supported on Base mainnet`, + schema: PumpclawSellTokenInput, + }) + async sellToken( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const swapRouterAddress = getSwapRouterAddress( + walletProvider.getNetwork().networkId!, + ); + + // Check balance + const balance = (await walletProvider.readContract({ + address: args.tokenAddress as `0x${string}`, + abi: ERC20_ABI, + functionName: "balanceOf", + args: [walletProvider.getAddress() as `0x${string}`], + })) as bigint; + + if (balance < BigInt(args.tokensIn)) { + return `Error: Insufficient balance. You have ${balance.toString()} wei but are trying to sell ${args.tokensIn} wei.`; + } + + // Check and handle token approval + const allowance = (await walletProvider.readContract({ + address: args.tokenAddress as `0x${string}`, + abi: ERC20_ABI, + functionName: "allowance", + args: [ + walletProvider.getAddress() as `0x${string}`, + swapRouterAddress as `0x${string}`, + ], + })) as bigint; + + if (allowance < BigInt(args.tokensIn)) { + const approveData = encodeFunctionData({ + abi: ERC20_ABI, + functionName: "approve", + args: [swapRouterAddress as `0x${string}`, BigInt(args.tokensIn)], + }); + + const approveTxHash = await walletProvider.sendTransaction({ + to: args.tokenAddress as `0x${string}`, + data: approveData, + }); + + await walletProvider.waitForTransactionReceipt(approveTxHash); + } + + const sellData = encodeFunctionData({ + abi: PUMPCLAW_SWAPROUTER_ABI, + functionName: "sellTokens", + args: [ + args.tokenAddress as `0x${string}`, + BigInt(args.tokensIn), + BigInt(args.minEthOut), + ], + }); + + const txHash = await walletProvider.sendTransaction({ + to: swapRouterAddress as `0x${string}`, + data: sellData, + }); + + await walletProvider.waitForTransactionReceipt(txHash); + + return `Successfully sold PumpClaw tokens. Transaction hash: ${txHash}`; + } catch (error) { + return `Error selling PumpClaw tokens: ${error}`; + } + } + + /** + * Updates the image URL of a PumpClaw token (creator only). + * + * @param walletProvider - The wallet provider to update the token from. + * @param args - The input arguments for the action. + * @returns A message containing the update details. + */ + @CreateAction({ + name: "set_image_url", + description: ` +This tool updates the image URL of a PumpClaw token on Base. + +Inputs: +- Token contract address +- New image URL + +Important notes: +- Only the token creator can update the image URL +- The transaction will revert if called by a non-creator +- Only supported on Base mainnet`, + schema: PumpclawSetImageUrlInput, + }) + async setImageUrl( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const factoryAddress = getFactoryAddress(walletProvider.getNetwork().networkId!); + + const setImageData = encodeFunctionData({ + abi: PUMPCLAW_FACTORY_ABI, + functionName: "setImageUrl", + args: [args.tokenAddress as `0x${string}`, args.imageUrl], + }); + + const txHash = await walletProvider.sendTransaction({ + to: factoryAddress as `0x${string}`, + data: setImageData, + }); + + await walletProvider.waitForTransactionReceipt(txHash); + + return `Successfully updated image URL for token ${args.tokenAddress}. Transaction hash: ${txHash}`; + } catch (error) { + return `Error updating image URL: ${error}. Note: Only the token creator can update the image URL.`; + } + } + + /** + * Checks if the PumpClaw action provider supports the given network. + * + * @param network - The network to check. + * @returns True if the network is supported, false otherwise. + */ + supportsNetwork = (network: Network) => + network.protocolFamily === "evm" && + SUPPORTED_NETWORKS.includes(network.networkId!); +} + +export const pumpclawActionProvider = () => new PumpclawActionProvider(); diff --git a/typescript/agentkit/src/action-providers/pumpclaw/schemas.ts b/typescript/agentkit/src/action-providers/pumpclaw/schemas.ts new file mode 100644 index 000000000..ba1e8126c --- /dev/null +++ b/typescript/agentkit/src/action-providers/pumpclaw/schemas.ts @@ -0,0 +1,145 @@ +import { z } from "zod"; +import { isAddress } from "viem"; + +const ethereumAddress = z.custom<`0x${string}`>( + (val) => typeof val === "string" && isAddress(val), + "Invalid Ethereum address", +); + +/** + * Input schema for creating a token. + */ +export const PumpclawCreateTokenInput = z + .object({ + name: z + .string() + .min(1) + .describe("The name of the token to create (e.g., 'My Token')"), + symbol: z + .string() + .min(1) + .describe("The symbol of the token to create (e.g., 'MTK')"), + imageUrl: z + .string() + .url() + .describe("The image URL for the token (must be a valid URL)"), + totalSupply: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .default("1000000000000000000000000000") + .describe( + "Total supply in wei (default: 1B tokens = 1000000000000000000000000000)", + ), + initialFdv: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .default("10000000000000000000") + .describe( + "Initial FDV in wei (default: 10 ETH = 10000000000000000000)", + ), + creator: ethereumAddress + .optional() + .describe( + "Address of the token creator (defaults to sender if omitted)", + ), + }) + .strip() + .describe("Instructions for creating a new PumpClaw token"); + +/** + * Input schema for getting token information. + */ +export const PumpclawGetTokenInfoInput = z + .object({ + tokenAddress: ethereumAddress.describe( + "The PumpClaw token contract address to get information for", + ), + }) + .strip() + .describe("Instructions for getting PumpClaw token information"); + +/** + * Input schema for listing tokens. + */ +export const PumpclawListTokensInput = z + .object({ + offset: z + .number() + .int() + .min(0) + .default(0) + .describe("Starting index for token list (default: 0)"), + limit: z + .number() + .int() + .min(1) + .max(100) + .default(10) + .describe("Maximum number of tokens to return (default: 10, max: 100)"), + }) + .strip() + .describe("Instructions for listing PumpClaw tokens"); + +/** + * Input schema for buying tokens. + */ +export const PumpclawBuyTokenInput = z + .object({ + tokenAddress: ethereumAddress.describe( + "The PumpClaw token contract address to buy", + ), + ethAmount: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .describe("Amount of ETH to spend in wei (e.g., '1000000000000000000' for 1 ETH)"), + minTokensOut: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .default("0") + .describe( + "Minimum tokens to receive in wei (slippage protection, default: 0)", + ), + }) + .strip() + .describe("Instructions for buying PumpClaw tokens with ETH"); + +/** + * Input schema for selling tokens. + */ +export const PumpclawSellTokenInput = z + .object({ + tokenAddress: ethereumAddress.describe( + "The PumpClaw token contract address to sell", + ), + tokensIn: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .describe( + "Amount of tokens to sell in wei (e.g., '1000000000000000000' for 1 token with 18 decimals)", + ), + minEthOut: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .default("0") + .describe( + "Minimum ETH to receive in wei (slippage protection, default: 0)", + ), + }) + .strip() + .describe("Instructions for selling PumpClaw tokens for ETH"); + +/** + * Input schema for setting image URL. + */ +export const PumpclawSetImageUrlInput = z + .object({ + tokenAddress: ethereumAddress.describe( + "The PumpClaw token contract address to update", + ), + imageUrl: z + .string() + .url() + .describe("The new image URL for the token (must be a valid URL)"), + }) + .strip() + .describe("Instructions for updating PumpClaw token image URL");