Skip to main content
Glama
index.ts55.6 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { createPublicClient, formatUnits, http, getContract, decodeEventLog, formatEther } from "viem"; import { monadTestnet } from "viem/chains"; import { ethers } from "ethers"; import * as dotenv from "dotenv"; import * as path from "path"; import * as fs from "fs"; import { ERC20_ABI, COINFLIP_ABI, APRMON_ABI, UNISWAP_ROUTER_ABI, UNISWAP_FACTORY_ABI, UNISWAP_PAIR_ABI, WMON_ABI } from "./abis"; import { TOKEN_LIST } from "./tokens"; // Muat file .env dari direktori proyek const envPath: string = path.resolve(__dirname, "..", ".env"); if (!fs.existsSync(envPath)) { console.error(`File ${envPath} does not exist.`); console.error(`Please create ${envPath} with: PRIVATE_KEY=0x... (64 hexadecimal characters starting with 0x).`); console.error(`Current working directory: ${process.cwd()}`); console.error(`Expected .env in project root directory, one level above: ${__dirname}`); throw new Error(`File ${envPath} does not exist`); } dotenv.config({ path: envPath }); // Validasi private key const PRIVATE_KEY: string | undefined = process.env.PRIVATE_KEY; if (!PRIVATE_KEY) { console.error(`PRIVATE_KEY is not set in ${envPath}.`); console.error(`Ensure ${envPath} contains: PRIVATE_KEY=0x... (64 hexadecimal characters starting with 0x).`); throw new Error(`PRIVATE_KEY is not set in ${envPath}`); } if (!/^0x[a-fA-F0-9]{64}$/.test(PRIVATE_KEY)) { console.error(`Invalid PRIVATE_KEY format in ${envPath}. Must be 0x followed by 64 hexadecimal characters.`); throw new Error("Invalid PRIVATE_KEY format"); } // Validasi alamat Uniswap const UNISWAP_ROUTER_ADDRESS: string = process.env.UNISWAP_ROUTER_ADDRESS!; const UNISWAP_FACTORY_ADDRESS: string = process.env.UNISWAP_FACTORY_ADDRESS!; const WMON_ADDRESS: string = process.env.WMON_ADDRESS!; if (!UNISWAP_ROUTER_ADDRESS || !UNISWAP_FACTORY_ADDRESS || !WMON_ADDRESS) { console.error(`UNISWAP_ROUTER_ADDRESS, UNISWAP_FACTORY_ADDRESS, and WMON_ADDRESS must be set in ${envPath}.`); throw new Error("Missing Uniswap configuration in .env"); } // Validasi file abis.js dan tokens.ts const abisPath: string = path.resolve(__dirname, "abis.js"); const tokensPath: string = path.resolve(__dirname, "tokens.js"); if (!fs.existsSync(abisPath)) { console.error(`File ${abisPath} does not exist.`); console.error(`Please create ${abisPath} with ERC20_ABI, COINFLIP_ABI, APRMON_ABI, UNISWAP_ROUTER_ABI, UNISWAP_FACTORY_ABI, UNISWAP_PAIR_ABI, and WMON_ABI definitions.`); console.error(`Expected abis.js in: ${__dirname}`); throw new Error(`File ${abisPath} does not exist`); } if (!fs.existsSync(tokensPath)) { console.error(`File ${tokensPath} does not exist.`); console.error(`Please create ${tokensPath} with TOKEN_LIST definition.`); console.error(`Expected tokens.js in: ${__dirname}`); throw new Error(`File ${tokensPath} does not exist`); } // Konfigurasi RPC dengan FallbackProvider const rpcUrls: string[] = [ monadTestnet.rpcUrls.default.http[0], ]; const providers: ethers.providers.JsonRpcProvider[] = rpcUrls.map(url => new ethers.providers.JsonRpcProvider(url)); const provider: ethers.providers.FallbackProvider = new ethers.providers.FallbackProvider(providers, 1); const wallet: ethers.Wallet = new ethers.Wallet(PRIVATE_KEY, provider); // Create a public client const publicClient = createPublicClient({ chain: monadTestnet, transport: http(), }); // Update server capabilities const server: McpServer = new McpServer({ name: "monad-testnet", version: "0.0.1", capabilities: [ "get-mon-balance", "get-token-balance", "get-transaction-details", "get-gas-price", "get-latest-block", "get-multiple-balances", "send-mon", "send-token", "play-coinflip", "get-coinflip-history", "stake-aprmon", "unstake-aprmon", "claim-aprmon", "get-aprmon-balance", "get-aprmon-rate", "get-aprmon-requests", "swap" ] }); // Tool untuk mendapatkan saldo MON server.tool( "get-mon-balance", "Get MON balance for an address on Monad testnet", { address: z.string().describe("Monad testnet address to check balance for"), }, async ({ address }: { address: string }) => { try { const balance = await publicClient.getBalance({ address: address as `0x${string}`, }); return { content: [ { type: "text", text: `Balance for ${address}: ${formatUnits(balance, 18)} MON`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to retrieve balance for address: ${address}. Error: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Tool untuk mendapatkan saldo token server.tool( "get-token-balance", "Get token balance for an address from a specific token contract", { address: z.string().describe("Address to check balance for"), tokenContract: z.string().describe("Token contract address"), }, async ({ address, tokenContract }: { address: string; tokenContract: string }) => { try { const contract = getContract({ address: tokenContract as `0x${string}`, abi: ERC20_ABI, client: publicClient, }); const [balance, decimals, symbol] = await Promise.all([ contract.read.balanceOf([address as `0x${string}`]) as Promise<bigint>, contract.read.decimals() as Promise<number>, contract.read.symbol() as Promise<string>, ]); return { content: [ { type: "text", text: `Token balance for ${address}: ${formatUnits(balance, decimals)} ${symbol}`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to retrieve token balance. Error: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Tool untuk detail transaksi server.tool( "get-transaction-details", "Get detailed information about a transaction", { hash: z.string().describe("Transaction hash to check"), }, async ({ hash }: { hash: string }) => { try { const tx = await publicClient.getTransaction({ hash: hash as `0x${string}` }); const receipt = await publicClient.getTransactionReceipt({ hash: hash as `0x${string}` }); const block = await publicClient.getBlock({ blockHash: receipt.blockHash }); let transactionType: string = "Native MON Transfer"; let from: string = tx.from; let to: string | null = tx.to; let value: bigint = tx.value; let tokenSymbol: string = "MON"; let decimals: number = 18; if (receipt.logs.length > 0) { try { const log = receipt.logs[0]; const decoded = decodeEventLog({ abi: ERC20_ABI, data: log.data, topics: log.topics, }); if (decoded.eventName === "Transfer") { const contract = getContract({ address: log.address, abi: ERC20_ABI, client: publicClient, }); const [symbol, tokenDecimals] = await Promise.all([ contract.read.symbol() as Promise<string>, contract.read.decimals() as Promise<number>, ]); transactionType = "Token Transfer"; from = decoded.args.from; to = decoded.args.to; value = decoded.args.value; tokenSymbol = symbol; decimals = tokenDecimals; } } catch (e) { // Jika decoding gagal, gunakan detail transfer native } } const timestamp: Date = new Date(Number(block.timestamp) * 1000); const formattedValue: string = transactionType === "Native MON Transfer" ? formatEther(value) : formatUnits(value, decimals); return { content: [ { type: "text", text: `Transaction Details: Type: ${transactionType} From: ${from} To: ${to} Amount: ${formattedValue} ${tokenSymbol} Date: ${timestamp.toLocaleString()} Status: ${receipt.status === "success" ? "Success" : "Failed"} Block: ${receipt.blockNumber} Gas Used: ${receipt.gasUsed} wei` }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to retrieve transaction details. Error: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Tool untuk harga gas server.tool( "get-gas-price", "Get current gas price on Monad testnet", { }, async () => { try { const gasPrice: bigint = await publicClient.getGasPrice(); return { content: [{ type: "text", text: `Current Gas Price: ${formatUnits(gasPrice, 9)} Gwei`, }], }; } catch (error: unknown) { return { content: [{ type: "text", text: `Failed to get gas price. Error: ${error instanceof Error ? error.message : String(error)}` }], }; } } ); // Tool untuk blok terbaru server.tool( "get-latest-block", "Get information about the latest block on Monad testnet", { }, async () => { try { const block = await publicClient.getBlock(); const timestamp: Date = new Date(Number(block.timestamp) * 1000); return { content: [{ type: "text", text: `Latest Block Information: Block Number: ${block.number} Timestamp: ${timestamp.toLocaleString()} Hash: ${block.hash} Parent Hash: ${block.parentHash} Transactions Count: ${block.transactions.length} Gas Used: ${formatUnits(block.gasUsed, 9)} Gwei Gas Limit: ${formatUnits(block.gasLimit, 9)} Gwei` }], }; } catch (error: unknown) { return { content: [{ type: "text", text: `Failed to get latest block info. Error: ${error instanceof Error ? error.message : String(error)}` }], }; } } ); // Tool untuk saldo multiple token server.tool( "get-multiple-balances", "Get balances for multiple tokens at once", { address: z.string().describe("Address to check balances for"), tokenContracts: z.array(z.string()).describe("Array of token contract addresses"), }, async ({ address, tokenContracts }: { address: string; tokenContracts: string[] }) => { try { const balances = await Promise.all( tokenContracts.map(async (tokenContract: string) => { const contract = getContract({ address: tokenContract as `0x${string}`, abi: ERC20_ABI, client: publicClient, }); const [balance, symbol, decimals] = await Promise.all([ contract.read.balanceOf([address as `0x${string}`]) as Promise<bigint>, contract.read.symbol() as Promise<string>, contract.read.decimals() as Promise<number>, ]); return { symbol, balance: formatUnits(balance, decimals), contract: tokenContract }; }) ); const balanceText: string = balances .map(b => `${b.symbol}: ${b.balance} (${b.contract})`) .join('\n'); return { content: [{ type: "text", text: `Token Balances for ${address}:\n${balanceText}`, }], }; } catch (error: unknown) { return { content: [{ type: "text", text: `Failed to get multiple token balances. Error: ${error instanceof Error ? error.message : String(error)}` }], }; } } ); // Tool untuk mengirim MON server.tool( "send-mon", "Send MON tokens to a specified address on Monad testnet", { toAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { message: "Invalid Ethereum address" }).describe("Recipient address"), amount: z.string().regex(/^\d+(\.\d+)?$/, { message: "Invalid amount, must be a positive number" }).describe("Amount of MON to send"), }, async ({ toAddress, amount }: { toAddress: string; amount: string }) => { try { const amountWei: ethers.BigNumber = ethers.utils.parseEther(amount); const balance: ethers.BigNumber = await provider.getBalance(wallet.address); if (balance.lt(amountWei)) { throw new Error("Insufficient MON balance"); } const gasLimit: ethers.BigNumber = await provider.estimateGas({ to: toAddress, value: amountWei, }); const bufferedGasLimit: ethers.BigNumber = gasLimit.mul(120).div(100); const tx: ethers.providers.TransactionResponse = await wallet.sendTransaction({ to: toAddress, value: amountWei, gasLimit: bufferedGasLimit, gasPrice: await provider.getGasPrice(), }); const receipt: ethers.providers.TransactionReceipt = await tx.wait(); return { content: [ { type: "text", text: `Successfully sent ${amount} MON to ${receipt.to}\nTransaction Hash: ${receipt.transactionHash}\nStatus: ${receipt.status === 1 ? "Success" : "Failed"}`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to send MON. Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool untuk mengirim token ERC-20 server.tool( "send-token", "Send ERC-20 tokens to a specified address from a token contract", { toAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { message: "Invalid Ethereum address" }).describe("Recipient address"), tokenContract: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { message: "Invalid token contract address" }).describe("Token contract address"), amount: z.string().regex(/^\d+(\.\d+)?$/, { message: "Invalid amount, must be a positive number" }).describe("Amount of tokens to send"), }, async ({ toAddress, tokenContract, amount }: { toAddress: string; tokenContract: string; amount: string }) => { try { const code: string = await provider.getCode(tokenContract); if (code === "0x") { throw new Error("Invalid token contract: no code found"); } const contract: ethers.Contract = new ethers.Contract(tokenContract, ERC20_ABI, wallet); const [decimals, symbol]: [number, string] = await Promise.all([ contract.decimals(), contract.symbol() ]); const amountWei: ethers.BigNumber = ethers.utils.parseUnits(amount, decimals); const balance: ethers.BigNumber = await contract.balanceOf(wallet.address); if (balance.lt(amountWei)) { throw new Error(`Insufficient ${symbol} balance`); } const gasLimit: ethers.BigNumber = await contract.estimateGas.transfer(toAddress, amountWei); const bufferedGasLimit: ethers.BigNumber = gasLimit.mul(120).div(100); const tx: ethers.ContractTransaction = await contract.transfer(toAddress, amountWei, { gasLimit: bufferedGasLimit, gasPrice: await provider.getGasPrice(), }); const receipt: ethers.ContractReceipt = await tx.wait(); return { content: [ { type: "text", text: `Successfully sent ${amount} ${symbol} to ${receipt.to}\nTransaction Hash: ${receipt.transactionHash}\nStatus: ${receipt.status === 1 ? "Success" : "Failed"}`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to send token. Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool untuk bermain CoinflipGame server.tool( "play-coinflip", "Play a coinflip game on Monad testnet by betting MON on Heads or Tails", { amount: z.string() .regex(/^\d+(\.\d+)?$/, { message: "Invalid amount, must be a positive number" }) .describe("Amount to bet in MON (minimum 0.01 MON)"), choice: z.enum(["head", "tail"]) .describe("Choose 'head' or 'tail' for the coinflip"), }, async ({ amount, choice }: { amount: string; choice: "head" | "tail" }) => { try { const amountWei: ethers.BigNumber = ethers.utils.parseEther(amount); const minBet: ethers.BigNumber = ethers.utils.parseEther("0.01"); if (amountWei.lt(minBet)) { throw new Error("Bet must be at least 0.01 MON"); } const balance: ethers.BigNumber = await provider.getBalance(wallet.address); if (balance.lt(amountWei)) { throw new Error("Insufficient MON balance for bet and gas"); } const contract: ethers.Contract = new ethers.Contract( "0x664e248c39cd70Fa333E9b2544beEd6A7a2De09b", COINFLIP_ABI, wallet ); const totalPool: ethers.BigNumber = await contract.getTotalBalance(); const requiredPool: ethers.BigNumber = amountWei.mul(2); if (totalPool.lt(requiredPool)) { throw new Error("Insufficient contract pool to pay potential winnings"); } const choiceEnum: number = choice.toLowerCase() === "head" ? 0 : 1; const gasLimit: ethers.BigNumber = await contract.estimateGas.flipCoin(choiceEnum, { value: amountWei, }); const bufferedGasLimit: ethers.BigNumber = gasLimit.mul(120).div(100); const tx: ethers.ContractTransaction = await contract.flipCoin(choiceEnum, { value: amountWei, gasLimit: bufferedGasLimit, gasPrice: await provider.getGasPrice(), }); const receipt: ethers.ContractReceipt = await tx.wait(); let resultText: string = "Unknown result"; for (const log of receipt.logs) { try { const parsedLog = contract.interface.parseLog(log); if (parsedLog.name === "FlipResult") { const { playerChoice, result, won, amount, bet } = parsedLog.args; const choiceStr: string = playerChoice === 0 ? "Head" : "Tail"; const resultStr: string = result ? "Head" : "Tail"; const betMon: string = ethers.utils.formatEther(bet); if (won) { const winningsMon: string = ethers.utils.formatEther(amount); resultText = `You chose ${choiceStr}. Result: ${resultStr}. You won! Winnings: ${winningsMon} MON`; } else { resultText = `You chose ${choiceStr}. Result: ${resultStr}. You lost. Bet: ${betMon} MON`; } break; } } catch (e) { // Lanjutkan jika log bukan FlipResult } } return { content: [ { type: "text", text: `${resultText}\nTransaction Hash: ${receipt.transactionHash}`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to play coinflip. Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool untuk melihat riwayat Coinflip server.tool( "get-coinflip-history", "Get the history of coinflip games for an address on Monad testnet", { address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { message: "Invalid Ethereum address" }) .optional() .describe("Address to check game history for (defaults to your wallet address)"), limit: z.number().int().min(1).max(100).optional().default(50) .describe("Maximum number of games to retrieve (1-100, default 50)"), }, async ({ address, limit }: { address?: string; limit?: number }) => { try { const playerAddress: string = address || wallet.address; const contract: ethers.Contract = new ethers.Contract( "0x664e248c39cd70Fa333E9b2544beEd6A7a2De09b", COINFLIP_ABI, provider ); const latestBlock = await provider.getBlockNumber(); const fromBlock = Math.max(0, latestBlock - 1000); const filter = { address: "0x664e248c39cd70Fa333E9b2544beEd6A7a2De09b", topics: [ contract.interface.getEventTopic("FlipResult"), ethers.utils.hexZeroPad(playerAddress, 32), ], fromBlock: fromBlock, toBlock: "latest" }; const logs = await provider.getLogs(filter); const games = logs.slice(0, limit).map((log) => { const parsedLog = contract.interface.parseLog(log); const { playerChoice, result, won, amount, bet } = parsedLog.args; return { choice: playerChoice === 0 ? "Head" : "Tail", result: result ? "Head" : "Tail", won, amount: ethers.utils.formatEther(amount), bet: ethers.utils.formatEther(bet), }; }); const totalWins: number = games.filter(g => g.won).length; const totalLosses: number = games.length - totalWins; const totalWinnings: number = games .filter(g => g.won) .reduce((sum, g) => sum + parseFloat(g.amount), 0); const totalBets: number = games .reduce((sum, g) => sum + parseFloat(g.bet), 0); const profit: number = totalWinnings - totalBets; const historyText: string = games.length > 0 ? games.map((g, i) => `- Game ${i + 1}: Chose ${g.choice}, Result: ${g.result}, ${g.won ? `Won: ${g.amount} MON` : `Lost, Bet: ${g.bet} MON`}` ).join('\n') : "No games found in the recent block range."; return { content: [ { type: "text", text: `Coinflip History for ${playerAddress}:\n` + `Total Wins: ${totalWins}\n` + `Total Losses: ${totalLosses}\n` + `Profit: ${profit.toFixed(4)} MON\n` + `Games:\n${historyText}`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to retrieve coinflip history. Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool untuk stake aprMON server.tool( "stake-aprmon", "Stake MON to receive aprMON tokens on Monad testnet", { amount: z.string() .regex(/^\d+(\.\d+)?$/, { message: "Invalid amount, must be a positive number" }) .describe("Amount of MON to stake (e.g., 1.0)"), receiver: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { message: "Invalid Ethereum address" }) .optional() .describe("Address to receive aprMON tokens (defaults to your wallet address)") }, async ({ amount, receiver }: { amount: string; receiver?: string }) => { try { const amountWei: ethers.BigNumber = ethers.utils.parseEther(amount); const balance: ethers.BigNumber = await provider.getBalance(wallet.address); if (balance.lt(amountWei)) { throw new Error("Insufficient MON balance for staking and gas"); } const contract: ethers.Contract = new ethers.Contract( "0xb2f82D0f38dc453D596Ad40A37799446Cc89274A", APRMON_ABI, wallet ); const receiverAddress: string = receiver || wallet.address; const gasLimit: ethers.BigNumber = await contract.estimateGas.deposit(amountWei, receiverAddress, { value: amountWei }); const bufferedGasLimit: ethers.BigNumber = gasLimit.mul(120).div(100); const tx: ethers.ContractTransaction = await contract.deposit(amountWei, receiverAddress, { value: amountWei, gasLimit: bufferedGasLimit, gasPrice: await provider.getGasPrice(), }); const receipt: ethers.ContractReceipt = await tx.wait(); let sharesReceived: string = "0"; for (const log of receipt.logs) { try { const parsedLog = contract.interface.parseLog(log); if (parsedLog.name === "Deposit") { sharesReceived = ethers.utils.formatEther(parsedLog.args.shares); break; } } catch (e) { // Lanjutkan jika log bukan Deposit } } return { content: [ { type: "text", text: `Successfully staked ${amount} MON. Received ${sharesReceived} aprMON.\nTransaction Hash: ${receipt.transactionHash}`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to stake aprMON. Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool untuk unstake aprMON server.tool( "unstake-aprmon", "Request withdrawal of aprMON tokens on Monad testnet", { amount: z.string() .regex(/^\d+(\.\d+)?$/, { message: "Invalid amount, must be a positive number" }) .describe("Amount of aprMON to withdraw (e.g., 1.0)"), controller: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { message: "Invalid Ethereum address" }) .optional() .describe("Address to control the withdrawal (defaults to your wallet address)") }, async ({ amount, controller }: { amount: string; controller?: string }) => { try { const amountWei: ethers.BigNumber = ethers.utils.parseEther(amount); const contract: ethers.Contract = new ethers.Contract( "0xb2f82D0f38dc453D596Ad40A37799446Cc89274A", APRMON_ABI, wallet ); const balance: ethers.BigNumber = await contract.balanceOf(wallet.address); if (balance.lt(amountWei)) { throw new Error("Insufficient aprMON balance"); } const controllerAddress: string = controller || wallet.address; const gasLimit: ethers.BigNumber = await contract.estimateGas.requestRedeem( amountWei, controllerAddress, wallet.address ); const bufferedGasLimit: ethers.BigNumber = gasLimit.mul(120).div(100); const tx: ethers.ContractTransaction = await contract.requestRedeem( amountWei, controllerAddress, wallet.address, { gasLimit: bufferedGasLimit, gasPrice: await provider.getGasPrice(), } ); const receipt: ethers.ContractReceipt = await tx.wait(); let requestId: string = "0"; for (const log of receipt.logs) { try { const parsedLog = contract.interface.parseLog(log); if (parsedLog.name === "RedeemRequest") { requestId = parsedLog.args.requestId.toString(); break; } } catch (e) { // Lanjutkan jika log bukan RedeemRequest } } return { content: [ { type: "text", text: `Unstake request submitted successfully.\n` + `Request ID: ${requestId}\n` + `Transaction Hash: ${receipt.transactionHash}\n` + `Wait 10 minutes to claim with 'claim aprmon' using the Request ID above.` }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to request unstake aprMON. Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool untuk klaim aprMON server.tool( "claim-aprmon", "Claim MON tokens from a specific aprMON withdrawal request on Monad testnet", { requestId: z.number().int().min(1, { message: "Request ID must be a positive integer" }) .describe("Request ID of the withdrawal to claim"), receiver: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { message: "Invalid Ethereum address" }) .optional() .describe("Address to receive MON tokens (defaults to your wallet address)") }, async ({ requestId, receiver }: { requestId: number; receiver?: string }) => { try { const contract: ethers.Contract = new ethers.Contract( "0xb2f82D0f38dc453D596Ad40A37799446Cc89274A", APRMON_ABI, wallet ); // Periksa apakah permintaan masih tertunda const shares: ethers.BigNumber = await contract.pendingRedeemRequest(requestId, wallet.address); if (shares.eq(0)) { return { content: [ { type: "text", text: `No pending aprMON redeem request found for Request ID ${requestId}. It may have been claimed or does not exist.`, }, ], }; } const receiverAddress: string = receiver || wallet.address; const gasLimit: ethers.BigNumber = await contract.estimateGas.redeem(requestId, receiverAddress); const bufferedGasLimit: ethers.BigNumber = gasLimit.mul(120).div(100); const tx: ethers.ContractTransaction = await contract.redeem(requestId, receiverAddress, { gasLimit: bufferedGasLimit, gasPrice: await provider.getGasPrice(), }); const receipt: ethers.ContractReceipt = await tx.wait(); let assetsClaimed: string = "0"; let fee: string = "0"; for (const log of receipt.logs) { try { const parsedLog = contract.interface.parseLog(log); if (parsedLog.name === "Redeem") { assetsClaimed = ethers.utils.formatEther(parsedLog.args.assets); fee = ethers.utils.formatEther(parsedLog.args.fee); break; } } catch (e) { // Lanjutkan jika log bukan Redeem } } return { content: [ { type: "text", text: `Successfully claimed ${assetsClaimed} MON for Request ID ${requestId}.\n` + `Fee: ${fee} MON\n` + `Transaction Hash: ${receipt.transactionHash}` }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to claim aprMON for Request ID ${requestId}. ` + `Error: ${error instanceof Error ? error.message : String(error)}` }, ], }; } } ); // Tool untuk mendapatkan saldo aprMON server.tool( "get-aprmon-balance", "Get aprMON balance for an address on Monad testnet", { address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { message: "Invalid Ethereum address" }) .optional() .describe("Address to check aprMON balance for (defaults to your wallet address)") }, async ({ address }: { address?: string }) => { try { const checkAddress: string = address || wallet.address; const contract = getContract({ address: "0xb2f82D0f38dc453D596Ad40A37799446Cc89274A" as `0x${string}`, abi: APRMON_ABI, client: publicClient, }); const balance = await contract.read.balanceOf([checkAddress as `0x${string}`]); const assets = await contract.read.convertToAssets([balance]); const balanceMon: string = formatUnits(balance, 18); const assetsMon: string = formatUnits(assets, 18); return { content: [ { type: "text", text: `Your aprMON balance: ${balanceMon} aprMON (worth ${assetsMon} MON)`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to retrieve aprMON balance. Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool untuk mendapatkan rasio pertukaran aprMON server.tool( "get-aprmon-rate", "Get the current aprMON/MON exchange rate on Monad testnet", { amount: z.string() .regex(/^\d+(\.\d+)?$/, { message: "Invalid amount, must be a positive number" }) .optional() .describe("Amount to calculate rate for (default is 1)") }, async ({ amount = "1" }: { amount?: string }) => { try { const amountWei: ethers.BigNumber = ethers.utils.parseEther(amount); const amountBigInt: bigint = BigInt(amountWei.toString()); const contract = getContract({ address: "0xb2f82D0f38dc453D596Ad40A37799446Cc89274A" as `0x${string}`, abi: APRMON_ABI, client: publicClient, }); const assets = await contract.read.convertToAssets([amountBigInt]); const shares = await contract.read.convertToShares([amountBigInt]); const assetsMon: string = formatUnits(assets, 18); const sharesMon: string = formatUnits(shares, 18); return { content: [ { type: "text", text: `Current aprMON rate: ${amount} aprMON = ${assetsMon} MON, ${amount} MON = ${sharesMon} aprMON`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to retrieve aprMON rate. Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool untuk mendapatkan daftar permintaan redeem aprMON server.tool( "get-aprmon-requests", "Get list of pending aprMON redeem requests for an address on Monad testnet", { address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { message: "Invalid Ethereum address" }) .optional() .describe("Address to check redeem requests for (defaults to your wallet address)"), limit: z.number().int().min(1).max(100).optional().default(50) .describe("Maximum number of requests to retrieve (1-100, default 50)") }, async ({ address, limit }: { address?: string; limit?: number }) => { try { const checkAddress: string = address || wallet.address; const contract: ethers.Contract = new ethers.Contract( "0xb2f82D0f38dc453D596Ad40A37799446Cc89274A", APRMON_ABI, provider ); // Ambil blok terbaru dan tetapkan rentang untuk getLogs const latestBlock = await provider.getBlockNumber(); const fromBlock = Math.max(0, latestBlock - 1000); // Ambil event RedeemRequest const filter = { address: "0xb2f82D0f38dc453D596Ad40A37799446Cc89274A", topics: [ contract.interface.getEventTopic("RedeemRequest"), null, ethers.utils.hexZeroPad(checkAddress, 32), ], fromBlock: fromBlock, toBlock: "latest" }; const logs = await provider.getLogs(filter); // Filter permintaan yang masih tertunda const pendingRequests: { requestId: number; shares: string; created: string }[] = []; for (const log of logs.slice(0, limit)) { try { const parsedLog = contract.interface.parseLog(log); if (parsedLog.name === "RedeemRequest") { const requestId = Number(parsedLog.args.requestId); const shares: ethers.BigNumber = await contract.pendingRedeemRequest(requestId, checkAddress); if (!shares.eq(0)) { const block = await provider.getBlock(log.blockNumber); const timestamp = new Date(block.timestamp * 1000).toLocaleString(); pendingRequests.push({ requestId, shares: ethers.utils.formatEther(shares), created: timestamp, }); } } } catch (e) { // Lanjutkan jika log tidak valid } } const requestText: string = pendingRequests.length > 0 ? pendingRequests .map(r => `- Request ID: ${r.requestId}, Amount: ${r.shares} aprMON, Created: ${r.created}`) .join('\n') : "No pending redeem requests found in the recent block range."; return { content: [ { type: "text", text: `Pending aprMON redeem requests for ${checkAddress}:\n${requestText}\nTotal: ${pendingRequests.length} requests`, }, ], }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to retrieve aprMON redeem requests. Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool untuk swap token server.tool( "swap", "Swap tokens or MON on Uniswap V2 on Monad testnet", { amount: z.string().regex(/^\d+(\.\d+)?%?$/, { message: "Invalid amount, must be a positive number or percentage (e.g., '0.1' or '50%')" }) .describe("Amount to swap (e.g., '0.1' or '50%')"), tokenIn: z.string().optional() .describe("Input token address or name (e.g., 'MON', 'WMON', 'WETH'), defaults to MON"), tokenOut: z.string().describe("Output token address or name (e.g., 'MON', 'WMON', 'WETH', or contract address like '0x...')"), slippage: z.number().min(0).max(10).optional() .describe("Slippage percentage (0-10, default: 1%)") }, async ({ amount, tokenIn, tokenOut, slippage }: { amount: string; tokenIn?: string; tokenOut: string; slippage?: number }) => { try { // Resolusi tokenIn dan tokenOut let tokenInAddress: string = TOKEN_LIST.MON; // Default ke MON let tokenOutAddress: string; let tokenInName: string = "MON"; let tokenOutName: string = tokenOut; // Proses tokenIn if (tokenIn) { if (/^0x[a-fA-F0-9]{40}$/.test(tokenIn)) { tokenInAddress = ethers.utils.getAddress(tokenIn); const code = await provider.getCode(tokenInAddress); if (code === "0x") { throw new Error(`Invalid tokenIn contract: ${tokenIn}. No code found.`); } const contract = new ethers.Contract(tokenInAddress, ERC20_ABI, provider); tokenInName = await contract.symbol(); } else { const address = TOKEN_LIST[tokenIn.toUpperCase()]; if (!address) { throw new Error(`Invalid tokenIn: ${tokenIn}. Use token address or supported name (MON, WMON, WETH).`); } tokenInAddress = address; tokenInName = tokenIn.toUpperCase(); } } // Proses tokenOut if (/^0x[a-fA-F0-9]{40}$/.test(tokenOut)) { tokenOutAddress = ethers.utils.getAddress(tokenOut); const code = await provider.getCode(tokenOutAddress); if (code === "0x") { throw new Error(`Invalid tokenOut contract: ${tokenOut}. No code found.`); } const contract = new ethers.Contract(tokenOutAddress, ERC20_ABI, provider); tokenOutName = await contract.symbol(); } else { const address = TOKEN_LIST[tokenOut.toUpperCase()]; if (!address) { throw new Error(`Invalid tokenOut: ${tokenOut}. Use token address or supported name (MON, WMON, WETH).`); } tokenOutAddress = address; tokenOutName = tokenOut.toUpperCase(); } // Validasi tokenIn dan tokenOut tidak sama if (tokenInAddress.toLowerCase() === tokenOutAddress.toLowerCase()) { throw new Error("Cannot swap a token with itself"); } // Proses jumlah let amountWei: ethers.BigNumber; if (amount.endsWith("%")) { const percentage = parseFloat(amount.slice(0, -1)) / 100; if (isNaN(percentage) || percentage <= 0 || percentage > 1) { throw new Error("Invalid percentage, must be between 0% and 100%"); } if (tokenInAddress === TOKEN_LIST.MON) { const balance = await provider.getBalance(wallet.address); amountWei = balance.mul(Math.floor(percentage * 10000)).div(10000); } else { const contract = new ethers.Contract(tokenInAddress, ERC20_ABI, wallet); const balance = await contract.balanceOf(wallet.address); amountWei = balance.mul(Math.floor(percentage * 10000)).div(10000); } } else { if (tokenInAddress === TOKEN_LIST.MON) { amountWei = ethers.utils.parseEther(amount); } else { const contract = new ethers.Contract(tokenInAddress, ERC20_ABI, wallet); const decimals = await contract.decimals(); amountWei = ethers.utils.parseUnits(amount, decimals); } } // Validasi saldo if (tokenInAddress === TOKEN_LIST.MON) { const balance = await provider.getBalance(wallet.address); if (balance.lt(amountWei)) { throw new Error(`Insufficient MON balance: ${ethers.utils.formatEther(balance)} MON available`); } } else { const contract = new ethers.Contract(tokenInAddress, ERC20_ABI, wallet); const balance = await contract.balanceOf(wallet.address); if (balance.lt(amountWei)) { throw new Error(`Insufficient ${tokenInName} balance: ${ethers.utils.formatUnits(balance, await contract.decimals())} ${tokenInName} available`); } } // Periksa pasangan perdagangan const factory = new ethers.Contract(UNISWAP_FACTORY_ADDRESS, UNISWAP_FACTORY_ABI, provider); const pairAddress = await factory.getPair( tokenInAddress === TOKEN_LIST.MON ? WMON_ADDRESS : tokenInAddress, tokenOutAddress === TOKEN_LIST.MON ? WMON_ADDRESS : tokenOutAddress ); if (pairAddress === "0x0000000000000000000000000000000000000000") { throw new Error(`No trading pair exists for ${tokenInName}/${tokenOutName}`); } // Periksa likuiditas const pairContract = new ethers.Contract(pairAddress, UNISWAP_PAIR_ABI, provider); const reserves = await pairContract.getReserves(); if (reserves[0].eq(0) || reserves[1].eq(0)) { throw new Error(`Insufficient liquidity for ${tokenInName}/${tokenOutName}`); } // Siapkan Uniswap Router const router = new ethers.Contract(UNISWAP_ROUTER_ADDRESS, UNISWAP_ROUTER_ABI, wallet); const deadline = Math.floor(Date.now() / 1000) + 300; // 5 menit const path = [ tokenInAddress === TOKEN_LIST.MON ? WMON_ADDRESS : tokenInAddress, tokenOutAddress === TOKEN_LIST.MON ? WMON_ADDRESS : tokenOutAddress ]; // Hitung amountOutMin const effectiveSlippage = slippage !== undefined ? slippage : 1; // Default 1% let amountOut: ethers.BigNumber; try { amountOut = (await router.getAmountsOut(amountWei, path))[1]; } catch (error: unknown) { throw new Error(`Failed to get amounts out: ${error instanceof Error ? error.message : String(error)}`); } const amountOutMin = amountOut.mul(100 - effectiveSlippage * 100).div(100); // Lakukan approve jika tokenIn bukan MON let approvalTxHash: string | null = null; if (tokenInAddress !== TOKEN_LIST.MON) { const contract = new ethers.Contract(tokenInAddress, ERC20_ABI, wallet); const allowance = await contract.allowance(wallet.address, UNISWAP_ROUTER_ADDRESS); if (allowance.lt(amountWei)) { const gasLimit = (await contract.estimateGas.approve(UNISWAP_ROUTER_ADDRESS, amountWei)).mul(120).div(100); const tx = await contract.approve(UNISWAP_ROUTER_ADDRESS, amountWei, { gasLimit }); const receipt = await tx.wait(); approvalTxHash = receipt.transactionHash; } } // Hitung saldo sebelum swap let balanceBefore: ethers.BigNumber; if (tokenOutAddress === TOKEN_LIST.MON) { balanceBefore = await provider.getBalance(wallet.address); } else { const contract = new ethers.Contract(tokenOutAddress, ERC20_ABI, provider); balanceBefore = await contract.balanceOf(wallet.address); } // Lakukan swap let tx: ethers.ContractTransaction; let swapFunction: string; if (tokenInAddress === TOKEN_LIST.MON) { swapFunction = "swapExactETHForTokens"; tx = await router[swapFunction]( amountOutMin, path, wallet.address, deadline, { value: amountWei, gasLimit: (await router.estimateGas[swapFunction](amountOutMin, path, wallet.address, deadline, { value: amountWei })).mul(120).div(100) } ); } else if (tokenOutAddress === TOKEN_LIST.MON) { swapFunction = "swapExactTokensForETH"; tx = await router[swapFunction]( amountWei, amountOutMin, path, wallet.address, deadline, { gasLimit: (await router.estimateGas[swapFunction](amountWei, amountOutMin, path, wallet.address, deadline)).mul(120).div(100) } ); } else { swapFunction = "swapExactTokensForTokens"; tx = await router[swapFunction]( amountWei, amountOutMin, path, wallet.address, deadline, { gasLimit: (await router.estimateGas[swapFunction](amountWei, amountOutMin, path, wallet.address, deadline)).mul(120).div(100) } ); } const receipt = await tx.wait(); if (receipt.status !== 1) { throw new Error(`Swap transaction failed: ${receipt.transactionHash}`); } // Hitung token yang diterima let tokensReceived: string; if (tokenOutAddress === TOKEN_LIST.MON) { const balanceAfter = await provider.getBalance(wallet.address); tokensReceived = ethers.utils.formatEther(balanceAfter.sub(balanceBefore)); } else { const contract = new ethers.Contract(tokenOutAddress, ERC20_ABI, provider); const decimals = await contract.decimals(); const balanceAfter = await contract.balanceOf(wallet.address); tokensReceived = ethers.utils.formatUnits(balanceAfter.sub(balanceBefore), decimals); } let outputText = `Successfully swapped ${amount} ${tokenInName} to ${tokensReceived} ${tokenOutName}.\n` + `Transaction Hash: ${receipt.transactionHash}\n` + `Slippage: ${effectiveSlippage}%`; if (approvalTxHash) { outputText += `\nApproval Transaction Hash: ${approvalTxHash}`; } return { content: [ { type: "text", text: outputText }, ], }; } catch (error: unknown) { console.error(`Swap error: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Failed to swap tokens. Error: ${error instanceof Error ? error.message : String(error)}` }, ], isError: true }; } } ); /** * Main function to start the MCP server * Uses stdio for communication with LLM clients */ async function main(): Promise<void> { console.error("Starting MCP server..."); console.error("Server process started with args:", process.argv); console.error("Current working directory:", process.cwd()); console.error(`Loaded .env from: ${envPath}`); console.error(`Loaded abis from: ${abisPath}`); console.error(`Loaded tokens from: ${tokensPath}`); const transport: StdioServerTransport = new StdioServerTransport(); console.error("Transport created"); await server.connect(transport); console.error("Monad testnet MCP Server running on stdio"); } // Start the server and handle any fatal errors main().catch((error: unknown) => { console.error("Fatal error in main():", error); process.exit(1); });

Latest Blog Posts

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/Semutireng22/mcp-monad'

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