From cfcd318cf817022437593e97b7576476ebf4ef0c Mon Sep 17 00:00:00 2001 From: h1-hunt Date: Fri, 13 Feb 2026 15:49:24 +0000 Subject: [PATCH 1/2] feat: add Mint Club V2 action provider Add action provider for Mint Club V2 bonding curve protocol on Base. Actions: - get_token_info: Token details and bonding curve information - get_token_price: Current price in reserve tokens and USD - buy_token: Mint tokens via bonding curve - sell_token: Burn tokens via bonding curve - create_token: Create new token with bonding curve Uses viem for direct contract interaction with MCV2_Bond. Includes auto ERC20 approval, slippage protection, and USD pricing via 1inch Spot Price Aggregator. Docs: https://docs.mint.club Protocol: https://mint.club --- .../agentkit/src/action-providers/index.ts | 1 + .../src/action-providers/mintclub/README.md | 42 ++ .../action-providers/mintclub/constants.ts | 252 ++++++++++ .../src/action-providers/mintclub/index.ts | 2 + .../mintclub/mintclubActionProvider.test.ts | 370 ++++++++++++++ .../mintclub/mintclubActionProvider.ts | 461 ++++++++++++++++++ .../src/action-providers/mintclub/schemas.ts | 147 ++++++ .../src/action-providers/mintclub/utils.ts | 244 +++++++++ 8 files changed, 1519 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/mintclub/README.md create mode 100644 typescript/agentkit/src/action-providers/mintclub/constants.ts create mode 100644 typescript/agentkit/src/action-providers/mintclub/index.ts create mode 100644 typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/mintclub/schemas.ts create mode 100644 typescript/agentkit/src/action-providers/mintclub/utils.ts diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 7f2b0233a..efc6bef4c 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 "./mintclub"; export * from "./pyth"; export * from "./moonwell"; export * from "./morpho"; diff --git a/typescript/agentkit/src/action-providers/mintclub/README.md b/typescript/agentkit/src/action-providers/mintclub/README.md new file mode 100644 index 000000000..458df5623 --- /dev/null +++ b/typescript/agentkit/src/action-providers/mintclub/README.md @@ -0,0 +1,42 @@ +# Mint Club V2 Action Provider + +This directory contains the **MintclubActionProvider** implementation, which provides actions to interact with the **Mint Club V2 protocol** on Base mainnet. + +## Directory Structure + +``` +mintclub/ +├── mintclubActionProvider.ts # Main provider with Mint Club V2 functionality +├── mintclubActionProvider.test.ts # Test file for Mint Club provider +├── constants.ts # Mint Club contract constants and ABIs +├── schemas.ts # Mint Club action schemas +├── utils.ts # Mint Club utility functions +├── index.ts # Main exports +└── README.md # This file +``` + +## Actions + +- `get_token_info`: Get detailed information about a Mint Club token including bonding curve details +- `get_token_price`: Get the current price of a Mint Club token in reserve tokens and USD +- `buy_token`: Buy Mint Club tokens via the bonding curve mechanism +- `sell_token`: Sell Mint Club tokens via the bonding curve mechanism +- `create_token`: Create a new Mint Club token with bonding curve + +## Adding New Actions + +To add new Mint Club actions: + +1. Define your action schema in `schemas.ts` +2. Implement the action in `mintclubActionProvider.ts` +3. Add tests in `mintclubActionProvider.test.ts` + +## Network Support + +The Mint Club provider supports Base mainnet only. + +## Notes + +Mint Club V2 is a bonding curve token protocol that allows anyone to create tokens backed by reserve assets. The protocol uses mathematical curves to determine token prices based on supply and demand. + +For more information on the **Mint Club V2 protocol**, visit [Mint Club Documentation](https://mint.club/). \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/mintclub/constants.ts b/typescript/agentkit/src/action-providers/mintclub/constants.ts new file mode 100644 index 000000000..9a8d61018 --- /dev/null +++ b/typescript/agentkit/src/action-providers/mintclub/constants.ts @@ -0,0 +1,252 @@ +import type { Abi } from "abitype"; + +export const SUPPORTED_NETWORKS = ["base-mainnet"]; + +/** + * MCV2 Bond contract ABI - minimal functions needed for Mint Club V2 interactions. + */ +export const MCV2_BOND_ABI: Abi = [ + { + type: "function", + name: "tokenBond", + inputs: [{ name: "token", type: "address", internalType: "address" }], + outputs: [ + { name: "creator", type: "address", internalType: "address" }, + { name: "mintRoyalty", type: "uint16", internalType: "uint16" }, + { name: "burnRoyalty", type: "uint16", internalType: "uint16" }, + { name: "createdAt", type: "uint40", internalType: "uint40" }, + { name: "reserveToken", type: "address", internalType: "address" }, + { name: "reserveBalance", type: "uint256", internalType: "uint256" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getReserveForToken", + inputs: [ + { name: "token", type: "address", internalType: "address" }, + { name: "tokensToMint", type: "uint256", internalType: "uint256" }, + ], + outputs: [ + { name: "reserveAmount", type: "uint256", internalType: "uint256" }, + { name: "royalty", type: "uint256", internalType: "uint256" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getRefundForTokens", + inputs: [ + { name: "token", type: "address", internalType: "address" }, + { name: "tokensToBurn", type: "uint256", internalType: "uint256" }, + ], + outputs: [ + { name: "refundAmount", type: "uint256", internalType: "uint256" }, + { name: "royalty", type: "uint256", internalType: "uint256" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "mint", + inputs: [ + { name: "token", type: "address", internalType: "address" }, + { name: "tokensToMint", type: "uint256", internalType: "uint256" }, + { name: "maxReserveAmount", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "burn", + inputs: [ + { name: "token", type: "address", internalType: "address" }, + { name: "tokensToBurn", type: "uint256", internalType: "uint256" }, + { name: "minRefund", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "createToken", + inputs: [ + { + name: "tp", + type: "tuple", + internalType: "struct MCV2_Bond.TokenParams", + components: [ + { name: "name", type: "string", internalType: "string" }, + { name: "symbol", type: "string", internalType: "string" }, + ], + }, + { + name: "bp", + type: "tuple", + internalType: "struct MCV2_Bond.BondParams", + components: [ + { name: "mintRoyalty", type: "uint16", internalType: "uint16" }, + { name: "burnRoyalty", type: "uint16", internalType: "uint16" }, + { name: "reserveToken", type: "address", internalType: "address" }, + { name: "maxSupply", type: "uint128", internalType: "uint128" }, + { name: "stepRanges", type: "uint128[]", internalType: "uint128[]" }, + { name: "stepPrices", type: "uint128[]", internalType: "uint128[]" }, + ], + }, + ], + outputs: [{ name: "token", type: "address", internalType: "address" }], + stateMutability: "payable", + }, + { + type: "function", + name: "creationFee", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, +] 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; + +/** + * 1inch Spot Price Aggregator ABI for USD pricing. + */ +export const SPOT_PRICE_AGGREGATOR_ABI: Abi = [ + { + type: "function", + name: "getRate", + inputs: [ + { name: "srcToken", type: "address", internalType: "contract IERC20" }, + { name: "dstToken", type: "address", internalType: "contract IERC20" }, + { name: "useWrappers", type: "bool", internalType: "bool" }, + ], + outputs: [{ name: "weightedRate", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, +] as const; + +/** + * Contract addresses on Base mainnet. + */ +export const MINTCLUB_CONTRACT_ADDRESSES = { + "base-mainnet": { + MCV2_Bond: "0xc5a076cad94176c2996B32d8466Be1cE757FAa27", + SpotPriceAggregator: "0x00000000000D6FFc74A8feb35aF5827bf57f6786", + USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + }, +} as const; + +/** + * Gets the MCV2 Bond 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 getBondAddress(network: string): string { + const addresses = + MINTCLUB_CONTRACT_ADDRESSES[ + network.toLowerCase() as keyof typeof MINTCLUB_CONTRACT_ADDRESSES + ]; + if (!addresses) { + throw new Error( + `Unsupported network: ${network}. Supported: ${Object.keys(MINTCLUB_CONTRACT_ADDRESSES).join(", ")}`, + ); + } + return addresses.MCV2_Bond; +} + +/** + * Gets the Spot Price Aggregator 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 getSpotPriceAggregatorAddress(network: string): string { + const addresses = + MINTCLUB_CONTRACT_ADDRESSES[ + network.toLowerCase() as keyof typeof MINTCLUB_CONTRACT_ADDRESSES + ]; + if (!addresses) { + throw new Error( + `Unsupported network: ${network}. Supported: ${Object.keys(MINTCLUB_CONTRACT_ADDRESSES).join(", ")}`, + ); + } + return addresses.SpotPriceAggregator; +} + +/** + * Gets the USDC 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 getUsdcAddress(network: string): string { + const addresses = + MINTCLUB_CONTRACT_ADDRESSES[ + network.toLowerCase() as keyof typeof MINTCLUB_CONTRACT_ADDRESSES + ]; + if (!addresses) { + throw new Error( + `Unsupported network: ${network}. Supported: ${Object.keys(MINTCLUB_CONTRACT_ADDRESSES).join(", ")}`, + ); + } + return addresses.USDC; +} diff --git a/typescript/agentkit/src/action-providers/mintclub/index.ts b/typescript/agentkit/src/action-providers/mintclub/index.ts new file mode 100644 index 000000000..9379c974a --- /dev/null +++ b/typescript/agentkit/src/action-providers/mintclub/index.ts @@ -0,0 +1,2 @@ +export * from "./schemas"; +export * from "./mintclubActionProvider"; \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.test.ts b/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.test.ts new file mode 100644 index 000000000..4ea6a1a8f --- /dev/null +++ b/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.test.ts @@ -0,0 +1,370 @@ +import { encodeFunctionData } from "viem"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { MintclubActionProvider } from "./mintclubActionProvider"; +import { MCV2_BOND_ABI, ERC20_ABI, getBondAddress } from "./constants"; +import { + getTokenBond, + getTokenInfo, + getBuyQuote, + getSellQuote, + getUsdRate, + needsApproval, +} from "./utils"; +import { + MintclubGetTokenInfoInput, + MintclubGetTokenPriceInput, + MintclubBuyTokenInput, + MintclubSellTokenInput, + MintclubCreateTokenInput, +} from "./schemas"; + +jest.mock("./utils", () => ({ + getTokenBond: jest.fn(), + getTokenInfo: jest.fn(), + getBuyQuote: jest.fn(), + getSellQuote: jest.fn(), + getUsdRate: jest.fn(), + needsApproval: jest.fn(), +})); + +describe("MintclubActionProvider", () => { + const MOCK_TOKEN_ADDRESS = + "0x1234567890123456789012345678901234567890" as `0x${string}`; + const MOCK_RESERVE_TOKEN = + "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`; + const MOCK_TOKENS_WEI = "1000000000000000000"; + const MOCK_RESERVE_AMOUNT = "1000000"; + const MOCK_TX_HASH = "0xabcdef1234567890"; + const MOCK_WALLET_ADDRESS = + "0x9876543210987654321098765432109876543210" as `0x${string}`; + const MOCK_MAX_SUPPLY = "1000000000000000000000000"; + + let provider: MintclubActionProvider; + 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 MintclubActionProvider(); + + (getTokenInfo as jest.Mock).mockResolvedValue({ + symbol: "TEST", + decimals: 18, + totalSupply: "1000000000000000000000", + formattedSupply: "1000.0", + }); + + (getTokenBond as jest.Mock).mockResolvedValue({ + creator: MOCK_WALLET_ADDRESS, + mintRoyalty: 100, + burnRoyalty: 100, + createdAt: 1640995200, + reserveToken: MOCK_RESERVE_TOKEN, + reserveBalance: "1000000000000000000", + }); + + (getBuyQuote as jest.Mock).mockResolvedValue({ + reserveAmount: MOCK_RESERVE_AMOUNT, + royalty: "10000", + }); + + (getSellQuote as jest.Mock).mockResolvedValue({ + refundAmount: MOCK_RESERVE_AMOUNT, + royalty: "10000", + }); + + (getUsdRate as jest.Mock).mockResolvedValue(1.0); + (needsApproval as jest.Mock).mockResolvedValue(false); + }); + + describe("Input Validation", () => { + it("should reject invalid address in getTokenInfo", () => { + const result = MintclubGetTokenInfoInput.safeParse({ + tokenAddress: "0xinvalid", + }); + expect(result.success).toBe(false); + }); + + it("should accept valid getTokenInfo input", () => { + const result = MintclubGetTokenInfoInput.safeParse({ + tokenAddress: MOCK_TOKEN_ADDRESS, + }); + expect(result.success).toBe(true); + }); + + it("should reject invalid amount in getTokenPrice", () => { + const result = MintclubGetTokenPriceInput.safeParse({ + tokenAddress: MOCK_TOKEN_ADDRESS, + amount: "abc", + }); + expect(result.success).toBe(false); + }); + + it("should reject decimal wei in buyToken", () => { + const result = MintclubBuyTokenInput.safeParse({ + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensToMint: "1.5", + maxReserveAmount: MOCK_RESERVE_AMOUNT, + }); + expect(result.success).toBe(false); + }); + + it("should accept valid buyToken input", () => { + const result = MintclubBuyTokenInput.safeParse({ + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensToMint: MOCK_TOKENS_WEI, + maxReserveAmount: MOCK_RESERVE_AMOUNT, + }); + expect(result.success).toBe(true); + }); + + it("should reject royalty exceeding max in createToken", () => { + const result = MintclubCreateTokenInput.safeParse({ + name: "Test", + symbol: "TST", + reserveToken: MOCK_RESERVE_TOKEN, + maxSupply: MOCK_MAX_SUPPLY, + stepRanges: [MOCK_MAX_SUPPLY], + stepPrices: ["1000000000000000"], + mintRoyalty: 6000, + burnRoyalty: 100, + }); + expect(result.success).toBe(false); + }); + + it("should accept valid createToken input", () => { + const result = MintclubCreateTokenInput.safeParse({ + name: "Test Token", + symbol: "TST", + reserveToken: MOCK_RESERVE_TOKEN, + maxSupply: MOCK_MAX_SUPPLY, + stepRanges: ["500000000000000000000000", MOCK_MAX_SUPPLY], + stepPrices: ["1000000000000000", "10000000000000000"], + mintRoyalty: 100, + burnRoyalty: 100, + }); + expect(result.success).toBe(true); + }); + }); + + 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("getTokenInfo", () => { + it("should return token and bond information", async () => { + const response = await provider.getTokenInfo(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + }); + + expect(getTokenInfo).toHaveBeenCalledWith(mockWallet, MOCK_TOKEN_ADDRESS); + expect(getTokenBond).toHaveBeenCalledWith(mockWallet, MOCK_TOKEN_ADDRESS); + expect(response).toContain("TEST"); + expect(response).toContain("1000.0"); + expect(response).toContain("1.00%"); + }); + + it("should handle non-existent token", async () => { + (getTokenInfo as jest.Mock).mockResolvedValue(null); + + const response = await provider.getTokenInfo(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + }); + expect(response).toContain("Error"); + }); + + it("should handle non-Mint Club token", async () => { + (getTokenBond as jest.Mock).mockResolvedValue(null); + + const response = await provider.getTokenInfo(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + }); + expect(response).toContain("not a Mint Club V2 token"); + }); + }); + + describe("getTokenPrice", () => { + it("should return price with USD value", async () => { + const response = await provider.getTokenPrice(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + amount: "1.0", + }); + + expect(getBuyQuote).toHaveBeenCalled(); + expect(response).toContain("Price for 1.0 TEST"); + expect(response).toContain("USD Value"); + }); + + it("should handle missing quote", async () => { + (getBuyQuote as jest.Mock).mockResolvedValue(null); + + const response = await provider.getTokenPrice(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + amount: "1.0", + }); + expect(response).toContain("Error"); + }); + }); + + describe("buyToken", () => { + it("should buy tokens without needing approval", async () => { + const response = await provider.buyToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensToMint: MOCK_TOKENS_WEI, + maxReserveAmount: MOCK_RESERVE_AMOUNT, + }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1); + expect(response).toContain("Successfully minted"); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should handle approval + buy when needed", async () => { + (needsApproval as jest.Mock).mockResolvedValue(true); + + const response = await provider.buyToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensToMint: MOCK_TOKENS_WEI, + maxReserveAmount: MOCK_RESERVE_AMOUNT, + }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(2); + expect(response).toContain("Successfully minted"); + }); + + it("should handle non-Mint Club token", async () => { + (getTokenBond as jest.Mock).mockResolvedValue(null); + + const response = await provider.buyToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensToMint: MOCK_TOKENS_WEI, + maxReserveAmount: MOCK_RESERVE_AMOUNT, + }); + expect(response).toContain("not a valid Mint Club V2 token"); + }); + + it("should handle transaction failure", async () => { + mockWallet.sendTransaction.mockRejectedValue(new Error("tx failed")); + + const response = await provider.buyToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensToMint: MOCK_TOKENS_WEI, + maxReserveAmount: MOCK_RESERVE_AMOUNT, + }); + expect(response).toContain("Error buying"); + }); + }); + + describe("sellToken", () => { + beforeEach(() => { + mockWallet.readContract.mockResolvedValue( + BigInt("2000000000000000000"), + ); + }); + + it("should sell tokens successfully", async () => { + const response = await provider.sellToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensToBurn: MOCK_TOKENS_WEI, + minRefund: MOCK_RESERVE_AMOUNT, + }); + + expect(response).toContain("Successfully sold"); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should reject when balance is insufficient", async () => { + mockWallet.readContract.mockResolvedValue(BigInt("100")); + + const response = await provider.sellToken(mockWallet, { + tokenAddress: MOCK_TOKEN_ADDRESS, + tokensToBurn: MOCK_TOKENS_WEI, + minRefund: MOCK_RESERVE_AMOUNT, + }); + expect(response).toContain("Insufficient balance"); + }); + }); + + describe("createToken", () => { + beforeEach(() => { + // Mock creationFee + mockWallet.readContract.mockResolvedValue(BigInt(0)); + }); + + it("should create a token successfully", async () => { + const response = await provider.createToken(mockWallet, { + name: "Test Token", + symbol: "TST", + reserveToken: MOCK_RESERVE_TOKEN, + maxSupply: MOCK_MAX_SUPPLY, + stepRanges: [MOCK_MAX_SUPPLY], + stepPrices: ["1000000000000000"], + mintRoyalty: 100, + burnRoyalty: 100, + }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + to: getBondAddress("base-mainnet"), + value: BigInt(0), + }), + ); + expect(response).toContain("Successfully created"); + expect(response).toContain("Test Token"); + expect(response).toContain("TST"); + }); + + it("should handle creation failure", async () => { + mockWallet.sendTransaction.mockRejectedValue( + new Error("create failed"), + ); + + const response = await provider.createToken(mockWallet, { + name: "Test Token", + symbol: "TST", + reserveToken: MOCK_RESERVE_TOKEN, + maxSupply: MOCK_MAX_SUPPLY, + stepRanges: [MOCK_MAX_SUPPLY], + stepPrices: ["1000000000000000"], + mintRoyalty: 100, + burnRoyalty: 100, + }); + expect(response).toContain("Error creating"); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.ts b/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.ts new file mode 100644 index 000000000..21e23e6a4 --- /dev/null +++ b/typescript/agentkit/src/action-providers/mintclub/mintclubActionProvider.ts @@ -0,0 +1,461 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { + SUPPORTED_NETWORKS, + MCV2_BOND_ABI, + ERC20_ABI, + getBondAddress, +} from "./constants"; +import { + getTokenBond, + getTokenInfo, + getBuyQuote, + getSellQuote, + getUsdRate, + needsApproval, +} from "./utils"; +import { encodeFunctionData, formatUnits } from "viem"; +import { + MintclubGetTokenInfoInput, + MintclubGetTokenPriceInput, + MintclubBuyTokenInput, + MintclubSellTokenInput, + MintclubCreateTokenInput, +} from "./schemas"; + +/** + * MintclubActionProvider is an action provider for Mint Club V2 protocol interactions. + * + * Mint Club V2 is a permissionless bonding curve protocol on Base. Tokens are created + * with programmable price curves backed by reserve assets. The protocol handles minting, + * burning, and price discovery through smart contracts. + * + * @see https://mint.club + * @see https://docs.mint.club + */ +export class MintclubActionProvider extends ActionProvider { + /** + * Constructor for the MintclubActionProvider class. + */ + constructor() { + super("mintclub", []); + } + + /** + * Gets detailed information about a Mint Club token including bonding curve details. + * + * @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 Mint Club V2 bonding curve token on Base. + +Inputs: +- Token contract address + +Returns token details (symbol, decimals, supply) and bonding curve details +(creator, reserve token, reserve balance, royalties). + +Important notes: +- Only works with Mint Club V2 tokens that have bonding curves +- Supported on Base mainnet only`, + schema: MintclubGetTokenInfoInput, + }) + async getTokenInfo( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const [tokenInfo, bondInfo] = await Promise.all([ + getTokenInfo(walletProvider, args.tokenAddress), + getTokenBond(walletProvider, args.tokenAddress), + ]); + + if (!tokenInfo) { + return `Error: Could not fetch token information for ${args.tokenAddress}. Verify this is a valid ERC20 token address.`; + } + + if (!bondInfo) { + return `Error: ${args.tokenAddress} is not a Mint Club V2 token (no bonding curve found).`; + } + + const reserveTokenInfo = await getTokenInfo(walletProvider, bondInfo.reserveToken); + const reserveSymbol = reserveTokenInfo?.symbol || "Unknown"; + const reserveDecimals = reserveTokenInfo?.decimals || 18; + const formattedReserveBalance = formatUnits( + BigInt(bondInfo.reserveBalance), + reserveDecimals, + ); + + return `Token Information for ${args.tokenAddress}: + +Token: ${tokenInfo.symbol} +Decimals: ${tokenInfo.decimals} +Total Supply: ${tokenInfo.formattedSupply} +Creator: ${bondInfo.creator} +Reserve Token: ${reserveSymbol} (${bondInfo.reserveToken}) +Reserve Balance: ${formattedReserveBalance} ${reserveSymbol} +Mint Royalty: ${(bondInfo.mintRoyalty / 100).toFixed(2)}% +Burn Royalty: ${(bondInfo.burnRoyalty / 100).toFixed(2)}% +Created: ${new Date(bondInfo.createdAt * 1000).toISOString()}`; + } catch (error) { + return `Error getting token information: ${error}`; + } + } + + /** + * Gets the current price of a Mint Club token. + * + * @param walletProvider - The wallet provider to get price information from. + * @param args - The input arguments for the action. + * @returns A message containing the token price information. + */ + @CreateAction({ + name: "get_token_price", + description: ` +This tool gets the current price of a Mint Club V2 token on Base. + +Inputs: +- Token contract address +- Amount of tokens to price (in whole units, e.g., "1" or "100") + +Returns the cost in reserve tokens (including royalty) and USD estimate. + +Important notes: +- Price depends on the bonding curve position — larger amounts have higher price impact +- Includes royalty fees in the total cost +- Supported on Base mainnet only`, + schema: MintclubGetTokenPriceInput, + }) + async getTokenPrice( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const [tokenInfo, bondInfo] = await Promise.all([ + getTokenInfo(walletProvider, args.tokenAddress), + getTokenBond(walletProvider, args.tokenAddress), + ]); + + if (!tokenInfo || !bondInfo) { + return `Error: ${args.tokenAddress} is not a valid Mint Club V2 token.`; + } + + const tokensInWei = BigInt( + Math.floor(Number(args.amount) * 10 ** tokenInfo.decimals), + ).toString(); + + const buyQuote = await getBuyQuote(walletProvider, args.tokenAddress, tokensInWei); + if (!buyQuote) { + return `Error: Could not get price quote. The bonding curve may have insufficient remaining supply.`; + } + + const reserveTokenInfo = await getTokenInfo(walletProvider, bondInfo.reserveToken); + const reserveSymbol = reserveTokenInfo?.symbol || "Unknown"; + const reserveDecimals = reserveTokenInfo?.decimals || 18; + + const reserveCost = formatUnits(BigInt(buyQuote.reserveAmount), reserveDecimals); + const royalty = formatUnits(BigInt(buyQuote.royalty), reserveDecimals); + const totalCost = formatUnits( + BigInt(buyQuote.reserveAmount) + BigInt(buyQuote.royalty), + reserveDecimals, + ); + + // Get USD rate for the reserve token + const usdRate = await getUsdRate(walletProvider, bondInfo.reserveToken); + + let result = `Price for ${args.amount} ${tokenInfo.symbol}: + +Reserve Cost: ${reserveCost} ${reserveSymbol} +Royalty: ${royalty} ${reserveSymbol} +Total: ${totalCost} ${reserveSymbol}`; + + if (usdRate !== null && usdRate > 0) { + const totalReserve = + Number(buyQuote.reserveAmount) + Number(buyQuote.royalty); + const usdValue = (totalReserve / 10 ** reserveDecimals) * usdRate; + result += `\nUSD Value: ~$${usdValue.toFixed(2)}`; + } + + return result; + } catch (error) { + return `Error getting token price: ${error}`; + } + } + + /** + * Buys Mint Club tokens via the bonding curve. + * + * @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 (mints) Mint Club V2 tokens via the bonding curve on Base. +Do not use this tool for buying other types of tokens. + +Inputs: +- Token contract address +- Amount of tokens to mint (in wei, e.g., "1000000000000000000" for 1 token with 18 decimals) +- Maximum reserve to spend (in wei, for slippage protection) + +Important notes: +- Amounts are in wei (no decimal points). 1 token = 10^decimals wei. +- The max reserve amount protects against slippage — the transaction reverts if the cost exceeds this. +- If the reserve token is an ERC20, approval is handled automatically. +- Only supported on Base mainnet.`, + schema: MintclubBuyTokenInput, + }) + async buyToken( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const bondAddress = getBondAddress(walletProvider.getNetwork().networkId!); + const recipient = args.recipient || walletProvider.getAddress(); + + const bondInfo = await getTokenBond(walletProvider, args.tokenAddress); + if (!bondInfo) { + return `Error: ${args.tokenAddress} is not a valid Mint Club V2 token.`; + } + + // Check and handle ERC20 approval for reserve token + const approvalNeeded = await needsApproval( + walletProvider, + bondInfo.reserveToken, + walletProvider.getAddress(), + args.maxReserveAmount, + ); + + if (approvalNeeded) { + const approveData = encodeFunctionData({ + abi: ERC20_ABI, + functionName: "approve", + args: [bondAddress as `0x${string}`, BigInt(args.maxReserveAmount)], + }); + + const approveTxHash = await walletProvider.sendTransaction({ + to: bondInfo.reserveToken as `0x${string}`, + data: approveData, + }); + + await walletProvider.waitForTransactionReceipt(approveTxHash); + } + + const mintData = encodeFunctionData({ + abi: MCV2_BOND_ABI, + functionName: "mint", + args: [ + args.tokenAddress as `0x${string}`, + BigInt(args.tokensToMint), + BigInt(args.maxReserveAmount), + recipient as `0x${string}`, + ], + }); + + const txHash = await walletProvider.sendTransaction({ + to: bondAddress as `0x${string}`, + data: mintData, + }); + + await walletProvider.waitForTransactionReceipt(txHash); + + return `Successfully minted Mint Club tokens. Transaction hash: ${txHash}`; + } catch (error) { + return `Error buying Mint Club tokens: ${error}`; + } + } + + /** + * Sells Mint Club tokens via the bonding curve. + * + * @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 (burns) Mint Club V2 tokens via the bonding curve on Base. +Do not use this tool for selling other types of tokens. + +Inputs: +- Token contract address +- Amount of tokens to burn (in wei) +- Minimum refund to receive (in wei, for slippage protection) + +Important notes: +- Amounts are in wei (no decimal points). 1 token = 10^decimals wei. +- The min refund protects against slippage — the transaction reverts if refund is less. +- Token approval for the Bond contract is handled automatically. +- Only supported on Base mainnet.`, + schema: MintclubSellTokenInput, + }) + async sellToken( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const bondAddress = getBondAddress(walletProvider.getNetwork().networkId!); + const recipient = args.recipient || walletProvider.getAddress(); + + // Verify sufficient 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.tokensToBurn)) { + return `Error: Insufficient balance. You have ${balance.toString()} wei but are trying to sell ${args.tokensToBurn} wei.`; + } + + // Check and handle token approval + const approvalNeeded = await needsApproval( + walletProvider, + args.tokenAddress, + walletProvider.getAddress(), + args.tokensToBurn, + ); + + if (approvalNeeded) { + const approveData = encodeFunctionData({ + abi: ERC20_ABI, + functionName: "approve", + args: [bondAddress as `0x${string}`, BigInt(args.tokensToBurn)], + }); + + const approveTxHash = await walletProvider.sendTransaction({ + to: args.tokenAddress as `0x${string}`, + data: approveData, + }); + + await walletProvider.waitForTransactionReceipt(approveTxHash); + } + + const burnData = encodeFunctionData({ + abi: MCV2_BOND_ABI, + functionName: "burn", + args: [ + args.tokenAddress as `0x${string}`, + BigInt(args.tokensToBurn), + BigInt(args.minRefund), + recipient as `0x${string}`, + ], + }); + + const txHash = await walletProvider.sendTransaction({ + to: bondAddress as `0x${string}`, + data: burnData, + }); + + await walletProvider.waitForTransactionReceipt(txHash); + + return `Successfully sold Mint Club tokens. Transaction hash: ${txHash}`; + } catch (error) { + return `Error selling Mint Club tokens: ${error}`; + } + } + + /** + * Creates a new Mint Club token with a bonding curve. + * + * @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 Mint Club V2 ERC20 token with a bonding curve on Base. + +Inputs: +- Token name and symbol +- Reserve token address (the asset that backs the bonding curve) +- Maximum supply (in wei) +- Step ranges and step prices arrays (define the bonding curve shape) +- Mint and burn royalty (in basis points, e.g., 100 = 1%) + +The bonding curve is defined by parallel arrays of step ranges and prices: +- stepRanges: cumulative supply thresholds (e.g., ["500000000000000000000000", "1000000000000000000000000"]) +- stepPrices: price per token at each step (e.g., ["10000000000000000", "100000000000000000"]) +The last stepRange must equal maxSupply. + +Important notes: +- May require a creation fee (paid in ETH) +- Royalties are in basis points: 100 = 1%, 10000 = 100% +- Only supported on Base mainnet`, + schema: MintclubCreateTokenInput, + }) + async createToken( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const bondAddress = getBondAddress(walletProvider.getNetwork().networkId!); + + // Check creation fee + const creationFee = (await walletProvider.readContract({ + address: bondAddress as `0x${string}`, + abi: MCV2_BOND_ABI, + functionName: "creationFee", + args: [], + })) as bigint; + + const tokenParams = { + name: args.name, + symbol: args.symbol, + }; + + const bondParams = { + mintRoyalty: args.mintRoyalty, + burnRoyalty: args.burnRoyalty, + reserveToken: args.reserveToken as `0x${string}`, + maxSupply: BigInt(args.maxSupply), + stepRanges: args.stepRanges.map((r: string) => BigInt(r)), + stepPrices: args.stepPrices.map((p: string) => BigInt(p)), + }; + + const createData = encodeFunctionData({ + abi: MCV2_BOND_ABI, + functionName: "createToken", + args: [tokenParams, bondParams], + }); + + const txHash = await walletProvider.sendTransaction({ + to: bondAddress as `0x${string}`, + data: createData, + value: creationFee, + }); + + await walletProvider.waitForTransactionReceipt(txHash); + + return `Successfully created Mint Club 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 Mint Club token: ${error}`; + } + } + + /** + * Checks if the Mint Club 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 mintclubActionProvider = () => new MintclubActionProvider(); diff --git a/typescript/agentkit/src/action-providers/mintclub/schemas.ts b/typescript/agentkit/src/action-providers/mintclub/schemas.ts new file mode 100644 index 000000000..b977fadae --- /dev/null +++ b/typescript/agentkit/src/action-providers/mintclub/schemas.ts @@ -0,0 +1,147 @@ +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 getting token information. + */ +export const MintclubGetTokenInfoInput = z + .object({ + tokenAddress: ethereumAddress.describe( + "The Mint Club V2 token contract address to get information for", + ), + }) + .strip() + .describe("Instructions for getting Mint Club token information"); + +/** + * Input schema for getting token price. + */ +export const MintclubGetTokenPriceInput = z + .object({ + tokenAddress: ethereumAddress.describe( + "The Mint Club V2 token contract address to get price for", + ), + amount: z + .string() + .regex(/^\d+(\.\d+)?$/, "Must be a valid number") + .describe( + "Amount of tokens to price in whole units (e.g., '1' or '100.5')", + ), + }) + .strip() + .describe("Instructions for getting Mint Club token price"); + +/** + * Input schema for buying tokens. + */ +export const MintclubBuyTokenInput = z + .object({ + tokenAddress: ethereumAddress.describe( + "The Mint Club V2 token contract address to buy", + ), + tokensToMint: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .describe( + "Amount of tokens to mint in wei (e.g., '1000000000000000000' for 1 token with 18 decimals)", + ), + maxReserveAmount: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .describe( + "Maximum reserve tokens to spend in wei (slippage protection — reverts if cost exceeds this)", + ), + recipient: ethereumAddress + .optional() + .describe( + "Address to receive the minted tokens (defaults to sender if omitted)", + ), + }) + .strip() + .describe("Instructions for buying Mint Club tokens via bonding curve"); + +/** + * Input schema for selling tokens. + */ +export const MintclubSellTokenInput = z + .object({ + tokenAddress: ethereumAddress.describe( + "The Mint Club V2 token contract address to sell", + ), + tokensToBurn: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .describe( + "Amount of tokens to burn in wei (e.g., '1000000000000000000' for 1 token with 18 decimals)", + ), + minRefund: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .describe( + "Minimum reserve tokens to receive in wei (slippage protection — reverts if refund is less)", + ), + recipient: ethereumAddress + .optional() + .describe( + "Address to receive the reserve token refund (defaults to sender if omitted)", + ), + }) + .strip() + .describe("Instructions for selling Mint Club tokens via bonding curve"); + +/** + * Input schema for creating tokens. + */ +export const MintclubCreateTokenInput = 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')"), + reserveToken: ethereumAddress.describe( + "The reserve token address that backs the bonding curve (e.g., HUNT, USDC, or WETH address)", + ), + maxSupply: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount (no decimals)") + .describe("Maximum token supply in wei"), + stepRanges: z + .array(z.string().regex(/^\d+$/, "Must be a valid wei amount")) + .min(1) + .describe( + "Cumulative supply thresholds for each bonding curve step in wei. Last value must equal maxSupply.", + ), + stepPrices: z + .array(z.string().regex(/^\d+$/, "Must be a valid wei amount")) + .min(1) + .describe( + "Price per token at each bonding curve step (multiplied by 10^18 for precision). Must have same length as stepRanges.", + ), + mintRoyalty: z + .number() + .int() + .min(0) + .max(5000) + .describe( + "Mint royalty in basis points (0–5000, where 100 = 1%). Max 50%.", + ), + burnRoyalty: z + .number() + .int() + .min(0) + .max(5000) + .describe( + "Burn royalty in basis points (0–5000, where 100 = 1%). Max 50%.", + ), + }) + .strip() + .describe("Instructions for creating a new Mint Club V2 token with bonding curve"); diff --git a/typescript/agentkit/src/action-providers/mintclub/utils.ts b/typescript/agentkit/src/action-providers/mintclub/utils.ts new file mode 100644 index 000000000..0b75ea9a8 --- /dev/null +++ b/typescript/agentkit/src/action-providers/mintclub/utils.ts @@ -0,0 +1,244 @@ +import { EvmWalletProvider } from "../../wallet-providers"; +import { + MCV2_BOND_ABI, + ERC20_ABI, + SPOT_PRICE_AGGREGATOR_ABI, + getBondAddress, + getSpotPriceAggregatorAddress, + getUsdcAddress, +} from "./constants"; +import { formatUnits } from "viem"; + +/** + * Gets detailed information about a token bond. + * + * @param wallet - The wallet provider to use for contract calls. + * @param tokenAddress - Address of the token contract. + * @returns Token bond information or null if not a Mint Club token. + */ +export async function getTokenBond( + wallet: EvmWalletProvider, + tokenAddress: string, +): Promise<{ + creator: string; + mintRoyalty: number; + burnRoyalty: number; + createdAt: number; + reserveToken: string; + reserveBalance: string; +} | null> { + try { + const bondAddress = getBondAddress(wallet.getNetwork().networkId!); + + const bondInfo = (await wallet.readContract({ + address: bondAddress as `0x${string}`, + abi: MCV2_BOND_ABI, + functionName: "tokenBond", + args: [tokenAddress as `0x${string}`], + })) as [string, number, number, number, string, bigint]; + + // createdAt == 0 means the token is not registered on Mint Club + if (bondInfo[3] === 0) { + return null; + } + + return { + creator: bondInfo[0], + mintRoyalty: bondInfo[1], + burnRoyalty: bondInfo[2], + createdAt: bondInfo[3], + reserveToken: bondInfo[4], + reserveBalance: bondInfo[5].toString(), + }; + } catch { + return null; + } +} + +/** + * Gets basic token information (symbol, decimals, total supply). + * + * @param wallet - The wallet provider to use for contract calls. + * @param tokenAddress - Address of the token contract. + * @returns Token information or null if the contract call fails. + */ +export async function getTokenInfo( + wallet: EvmWalletProvider, + tokenAddress: string, +): Promise<{ + symbol: string; + decimals: number; + totalSupply: string; + formattedSupply: string; +} | null> { + try { + const [symbol, decimals, totalSupply] = await Promise.all([ + wallet.readContract({ + address: tokenAddress as `0x${string}`, + abi: ERC20_ABI, + functionName: "symbol", + args: [], + }) as Promise, + wallet.readContract({ + address: tokenAddress as `0x${string}`, + abi: ERC20_ABI, + functionName: "decimals", + args: [], + }) as Promise, + wallet.readContract({ + address: tokenAddress as `0x${string}`, + abi: ERC20_ABI, + functionName: "totalSupply", + args: [], + }) as Promise, + ]); + + return { + symbol, + decimals, + totalSupply: totalSupply.toString(), + formattedSupply: formatUnits(totalSupply, decimals), + }; + } catch { + return null; + } +} + +/** + * Gets quote for buying tokens from the bonding curve. + * + * @param wallet - The wallet provider to use for contract calls. + * @param tokenAddress - Address of the token contract. + * @param tokensToMint - Amount of tokens to mint (in wei). + * @returns The quote containing reserve amount and royalty, or null on failure. + */ +export async function getBuyQuote( + wallet: EvmWalletProvider, + tokenAddress: string, + tokensToMint: string, +): Promise<{ + reserveAmount: string; + royalty: string; +} | null> { + try { + const bondAddress = getBondAddress(wallet.getNetwork().networkId!); + + const quote = (await wallet.readContract({ + address: bondAddress as `0x${string}`, + abi: MCV2_BOND_ABI, + functionName: "getReserveForToken", + args: [tokenAddress as `0x${string}`, BigInt(tokensToMint)], + })) as [bigint, bigint]; + + return { + reserveAmount: quote[0].toString(), + royalty: quote[1].toString(), + }; + } catch { + return null; + } +} + +/** + * Gets quote for selling tokens to the bonding curve. + * + * @param wallet - The wallet provider to use for contract calls. + * @param tokenAddress - Address of the token contract. + * @param tokensToBurn - Amount of tokens to burn (in wei). + * @returns The quote containing refund amount and royalty, or null on failure. + */ +export async function getSellQuote( + wallet: EvmWalletProvider, + tokenAddress: string, + tokensToBurn: string, +): Promise<{ + refundAmount: string; + royalty: string; +} | null> { + try { + const bondAddress = getBondAddress(wallet.getNetwork().networkId!); + + const quote = (await wallet.readContract({ + address: bondAddress as `0x${string}`, + abi: MCV2_BOND_ABI, + functionName: "getRefundForTokens", + args: [tokenAddress as `0x${string}`, BigInt(tokensToBurn)], + })) as [bigint, bigint]; + + return { + refundAmount: quote[0].toString(), + royalty: quote[1].toString(), + }; + } catch { + return null; + } +} + +/** + * Gets the USD price of a reserve token using 1inch Spot Price Aggregator. + * Returns the rate as USD per one full unit of the reserve token. + * + * @param wallet - The wallet provider to use for contract calls. + * @param reserveTokenAddress - Address of the reserve token. + * @returns USD price per token or null if unable to fetch. + */ +export async function getUsdRate( + wallet: EvmWalletProvider, + reserveTokenAddress: string, +): Promise { + try { + const aggregatorAddress = getSpotPriceAggregatorAddress( + wallet.getNetwork().networkId!, + ); + const usdcAddress = getUsdcAddress(wallet.getNetwork().networkId!); + + // getRate returns USDC-denominated rate per 1 full unit of srcToken + // The returned value has the same decimals as dstToken (USDC = 6) + const rate = (await wallet.readContract({ + address: aggregatorAddress as `0x${string}`, + abi: SPOT_PRICE_AGGREGATOR_ABI, + functionName: "getRate", + args: [ + reserveTokenAddress as `0x${string}`, + usdcAddress as `0x${string}`, + false, + ], + })) as bigint; + + // Rate is in USDC units (6 decimals) + return Number(rate) / 1e6; + } catch { + return null; + } +} + +/** + * Checks if approval is needed for the Bond contract to spend tokens. + * + * @param wallet - The wallet provider to use for contract calls. + * @param tokenAddress - Address of the token to check allowance for. + * @param ownerAddress - Address of the token owner. + * @param amountNeeded - Amount that needs to be approved (in wei). + * @returns True if approval is needed, false otherwise. + */ +export async function needsApproval( + wallet: EvmWalletProvider, + tokenAddress: string, + ownerAddress: string, + amountNeeded: string, +): Promise { + try { + const bondAddress = getBondAddress(wallet.getNetwork().networkId!); + + const allowance = (await wallet.readContract({ + address: tokenAddress as `0x${string}`, + abi: ERC20_ABI, + functionName: "allowance", + args: [ownerAddress as `0x${string}`, bondAddress as `0x${string}`], + })) as bigint; + + return allowance < BigInt(amountNeeded); + } catch { + return true; + } +} From c559b1b764dde1f09cbf603b8d316dd914a2b14e Mon Sep 17 00:00:00 2001 From: h1-hunt Date: Fri, 13 Feb 2026 16:47:33 +0000 Subject: [PATCH 2/2] fix: add trailing newlines to README.md and index.ts --- typescript/agentkit/src/action-providers/mintclub/README.md | 2 +- typescript/agentkit/src/action-providers/mintclub/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript/agentkit/src/action-providers/mintclub/README.md b/typescript/agentkit/src/action-providers/mintclub/README.md index 458df5623..181791c65 100644 --- a/typescript/agentkit/src/action-providers/mintclub/README.md +++ b/typescript/agentkit/src/action-providers/mintclub/README.md @@ -39,4 +39,4 @@ The Mint Club provider supports Base mainnet only. Mint Club V2 is a bonding curve token protocol that allows anyone to create tokens backed by reserve assets. The protocol uses mathematical curves to determine token prices based on supply and demand. -For more information on the **Mint Club V2 protocol**, visit [Mint Club Documentation](https://mint.club/). \ No newline at end of file +For more information on the **Mint Club V2 protocol**, visit [Mint Club Documentation](https://mint.club/). diff --git a/typescript/agentkit/src/action-providers/mintclub/index.ts b/typescript/agentkit/src/action-providers/mintclub/index.ts index 9379c974a..fe8322bd9 100644 --- a/typescript/agentkit/src/action-providers/mintclub/index.ts +++ b/typescript/agentkit/src/action-providers/mintclub/index.ts @@ -1,2 +1,2 @@ export * from "./schemas"; -export * from "./mintclubActionProvider"; \ No newline at end of file +export * from "./mintclubActionProvider";