# Directory Structure ``` ├── .gitignore ├── config.example.json ├── jest.config.mjs ├── package-lock.json ├── package.json ├── README.md ├── README.npm.md ├── scripts │ └── publish.cjs ├── sources │ ├── BUSD_TRANSFER_MCP.mp4 │ └── example.ts ├── src │ ├── addressConfig.ts │ ├── cli │ │ ├── help.ts │ │ ├── init.ts │ │ └── version.ts │ ├── config.ts │ ├── functions │ │ ├── fetchBalanceTool.ts │ │ ├── memeTokenDetails.ts │ │ ├── pancakeAddLiquidityTool.ts │ │ ├── pancakeRemoveLiquidityTool.ts │ │ ├── pancakeSwapPosition.ts │ │ └── pancakeSwapTool.ts │ ├── index.ts │ ├── init.ts │ ├── lib │ │ └── bep20Abi.ts │ ├── main.ts │ ├── PrivateAES.ts │ ├── responseUtils.ts │ ├── test │ │ └── privateAES.test.ts │ ├── tools │ │ ├── buyMemeToken.ts │ │ ├── getWalletInfo.ts │ │ ├── goplusSecurityCheck.ts │ │ ├── pancakeAddLiquidity.ts │ │ ├── pancakeMyPosition.ts │ │ ├── pancakeRemovePosition.ts │ │ ├── pancakeSwap.ts │ │ ├── queryMemeTokenDetails.ts │ │ ├── sellMemeToken.ts │ │ ├── transferBEP20Token.ts │ │ └── transferNativeToken.ts │ ├── types │ │ └── types.ts │ └── util.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # database /prisma/db.sqlite /prisma/db.sqlite-journal db.sqlite # next.js /.next/ /out/ next-env.d.ts # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # local env files # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables .env .env*.local # vercel .vercel # typescript *.tsbuildinfo # idea files .idea yarn.lock .env* dist/ ``` -------------------------------------------------------------------------------- /README.npm.md: -------------------------------------------------------------------------------- ```markdown --- ## 📦 BNBChain MCP – Binance Smart Chain Tool Server (MCP + CLI Ready) > A plug-and-play MCP tool server to **send BNB**, **transfer BEP-20 tokens**, **deploy tokens**, and **interact with smart contracts** on the **Binance Smart Chain (BSC)** — built for **Claude Desktop**, **AI agents**, and **developers.** --- ### ⚙️ Core Capabilities - 🔐 Secure token & native transfers via CLI or MCP - 🧱 Interact with smart contracts (ABI/function-based) - 🔄 PancakeSwap integration for swaps & liquidity - ⚙️ Create meme tokens & deploy BEP-20 smart contracts - 🧠 Native Claude Desktop integration via MCP - 🔧 CLI-ready, MCP-compliant, developer-friendly - 🔑 Password-protected private keys --- ## 🛠 Installation & Setup ### 1. Install ```bash npm install -g bnbchain-mcp ``` ### 2. Run the CLI Setup Wizard ```bash bnbchain-mcp --init ``` You’ll be prompted to enter: - ✅ **BSC Wallet Private Key** *(required)* - ✅ **Wallet Password** *(required, must be 6 characters)* - ✅ **Custom RPC URL** *(optional, defaults to:* `https://bsc-dataseed.binance.org` *) --- ## 🧠 Claude Desktop Integration After CLI setup, the tool can **auto-configure itself into Claude Desktop**. 📍 File modified: ``` ~/Library/Application Support/Claude/claude_desktop_config.json ``` Claude will detect and run this MCP server with your selected tools. --- ## 🔨 Supported MCP Tools | Tool Name | Description | |----------------------|------------------------------------------| | `transferNativeToken` | Send BNB to a wallet | | `transferBEP20Token` | Transfer BEP-20 token via symbol/address | | `pancakeSwap` | Swap tokens via PancakeSwap | | `createFourMeme` | Create meme token on Four.Meme | | `createBEP20Token` | Deploy a BEP-20 contract | | `getBalance` | Get token + native balance | | `callContractFunction`| Custom contract calls via ABI | | `getWalletInfo` | Get wallet info for an address | | `securityCheck` | Check token security of BSC tokens | | `pancakeAddLiquidity` | Add liquidity to PancakeSwap | | `pancakeMyPosition` | View your PancakeSwap positions | | `pancakeRemovePosition`| Remove liquidity from PancakeSwap | | `sellMemeToken` | Sell meme token on Four.Meme | | ...and more coming soon 🔧 | --- ## 🧪 Development Workflow ### Compile TypeScript: ```bash npm run build ``` ### Start MCP Server: ```bash npm start # or node build/index.js ``` ### Re-configure: ```bash bnbchain-mcp --init ``` --- ## 📘 Model Context Protocol (MCP) This project is built on **Model Context Protocol** – a standard to help agents and models interact with structured tool APIs. **MCP Benefits**: - ✅ Structured input/output - ✅ Claude + OpenAI compatible - ✅ Secure + serverless-ready --- ## ✅ Roadmap - [x] CLI Configuration Wizard - [x] Claude Desktop Integration - [x] Token Deploy + Transfer - [ ] Token charting tools (DEXTools, Gecko) - [ ] Telegram auto-trading agent - [ ] AI assistant with BSC on-chain brain --- ## 🤝 Contributing Feel free to fork, PR, or raise issues. We're building **tool-first, AI-ready infrastructure** for the next wave of Web3 agents. Join us! --- ## 🛡️ License MIT — Use freely, contribute openly. --- ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown --- ## 📦 BNBChain MCP – Binance Smart Chain Tool Server (MCP + CLI Ready) > A comprehensive blockchain tool server for BNB, BEP-20 tokens, smart contract deployment and interaction built on BNB Smart Chain (BSC) and compatible with other EVM networks. --- ## Technology Stack - **Blockchain**: BNB Smart Chain (BSC) - **Web3 Libraries**: Viem 2.23.11, PancakeSwap SDK 5.8.8 - **CLI/Backend**: TypeScript, Node.js (ESM) - **Protocol**: Model Context Protocol (MCP) SDK 1.4.0 - **Security**: AES encryption with bcrypt for private key protection - **Token Security**: GoPlus SDK for security checks - **Data Provider**: Moralis SDK 2.27.2 for blockchain data --- ## Supported Networks - **BNB Smart Chain Mainnet** (Chain ID: 56) - RPC: https://bsc-dataseed.binance.org (default) - Custom RPC supported via environment configuration --- ## Contract Addresses | Contract Type | Address | Description | |--------------|---------|-------------| | Four.Meme Try Buy | 0xF251F83e40a78868FcfA3FA4599Dad6494E46034 | Four.Meme token purchase contract | | Four.Meme Buy/Sell AMAP | 0x5c952063c7fc8610FFDB798152D69F0B9550762b | Four.Meme auto-market-adjusted pricing | | Four.Meme Create Token | 0x5c952063c7fc8610FFDB798152D69F0B9550762b | Four.Meme token factory | | PancakeSwap Router V2 | Integrated via SDK | DEX routing and swaps | | PancakeSwap V3 Pools | Accessed via SDK | Liquidity pools management | --- ## Features - **Low-cost BNB & BEP-20 transfers** - Optimized for BSC's low gas fees - **PancakeSwap V2/V3 integration** - Automated swaps, liquidity management, and position tracking - **Four.Meme platform support** - Create, buy, and sell meme tokens directly - **Security-first architecture** - AES-256 encrypted private keys with bcrypt password protection - **Token security analysis** - Built-in GoPlus security checks for token verification - **Gas-efficient operations** - Smart routing for optimal gas usage on BSC - **AI-ready MCP protocol** - Seamless integration with Claude Desktop and AI agents - **Real-time wallet monitoring** - Track balances and positions across multiple tokens --- ## 🛠 Installation & Setup ### 1. Install ```bash npm install -g bnbchain-mcp ``` ### 2. Run the CLI Setup Wizard ```bash bnbchain-mcp --init ``` You’ll be prompted to enter: - ✅ **BSC Wallet Private Key** *(required)* - ✅ **Wallet Password** *(required, must be 6 characters)* - ✅ **Custom RPC URL** *(optional, defaults to:* `https://bsc-dataseed.binance.org` *) --- ## 🧠 Claude Desktop Integration After CLI setup, the tool can **auto-configure itself into Claude Desktop**. 📍 File modified: ``` ~/Library/Application Support/Claude/claude_desktop_config.json ``` Claude will detect and run this MCP server with your selected tools. --- ## 🔨 Supported MCP Tools | Tool Name | Description | |----------------------|------------------------------------------| | `transferNativeToken` | Send BNB to a wallet | | `transferBEP20Token` | Transfer BEP-20 token via symbol/address | | `pancakeSwap` | Swap tokens via PancakeSwap | | `createFourMeme` | Create meme token on Four.Meme | | `createBEP20Token` | Deploy a BEP-20 contract | | `getBalance` | Get token + native balance | | `callContractFunction`| Custom contract calls via ABI | | `getWalletInfo` | Get wallet info for an address | | `securityCheck` | Check token security of BSC tokens | | `pancakeAddLiquidity` | Add liquidity to PancakeSwap | | `pancakeMyPosition` | View your PancakeSwap positions | | `pancakeRemovePosition`| Remove liquidity from PancakeSwap | | `sellMemeToken` | Sell meme token on Four.Meme | | ...and more coming soon 🔧 | --- ## 🧪 Development Workflow ### Compile TypeScript: ```bash npm run build ``` ### Start MCP Server: ```bash npm start # or node build/index.js ``` ### Re-configure: ```bash bnbchain-mcp --init ``` --- ## 📘 Model Context Protocol (MCP) This project is built on **Model Context Protocol** – a standard to help agents and models interact with structured tool APIs. **MCP Benefits**: - ✅ Structured input/output - ✅ Claude + OpenAI compatible - ✅ Secure + serverless-ready --- ## ✅ Roadmap - [x] CLI Configuration Wizard - [x] Claude Desktop Integration - [x] Token Deploy + Transfer - [ ] Token charting tools (DEXTools, Gecko) - [ ] Telegram auto-trading agent - [ ] AI assistant with BSC on-chain brain --- ## 🤝 Contributing Feel free to fork, PR, or raise issues. We're building **tool-first, AI-ready infrastructure** for the next wave of Web3 agents. Join us! --- ## 🛡️ License MIT — Use freely, contribute openly. --- ``` -------------------------------------------------------------------------------- /src/types/types.ts: -------------------------------------------------------------------------------- ```typescript /** * Fetch tokens from PancakeSwap token list * @returns Array of token data */ import { Address } from "viem"; export interface TokenInfo { address: Address; decimals: number; symbol?: string; name?: string; } ``` -------------------------------------------------------------------------------- /src/cli/help.ts: -------------------------------------------------------------------------------- ```typescript // src/cli/help.ts export function printHelp(): void { console.log(` 📦 BNB Chain MCP CLI Usage: bnbchain-mcp [options] Options: -i, --init Initialize configuration -v, --version Show CLI version -h, --help Show help info Examples: bnbchain-mcp --init bnbchain-mcp --version bnbchain-mcp `); } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "Node", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noEmitOnError": false, "isolatedModules": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- ```json { "mcpServers": { "bsc-mcp": { "command": "node", "args": [ "/Users/Username/Desktop/bsc-mpc/build/index.js" ], "env": { "BSC_WALLET_PRIVATE_KEY": "BSC_WALLET_PRIVATE_KEY", "BSC_RPC_URL": "BSC_RPC_URL" }, "disabled": false, "autoApprove": [] } } } ``` -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- ``` export default { transform: { '^.+\\.tsx?$': 'ts-jest', '^.+\\.mjs$': 'ts-jest', }, moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'mjs'], testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', }, extensionsToTreatAsEsm: ['.ts', '.tsx'], globals: { 'ts-jest': { useESM: true, }, }, }; ``` -------------------------------------------------------------------------------- /src/addressConfig.ts: -------------------------------------------------------------------------------- ```typescript import { Address } from "viem" export const AddressConfig: { [key: string]: Address } = { "FourMemeTryBuyContract": "0xF251F83e40a78868FcfA3FA4599Dad6494E46034", "FourMemeBuyTokenAMAPContract": "0x5c952063c7fc8610FFDB798152D69F0B9550762b", "FourMemeSellTokenAMAPContract": "0x5c952063c7fc8610FFDB798152D69F0B9550762b", "FourMemeCreateTokenContract": "0x5c952063c7fc8610FFDB798152D69F0B9550762b", } ``` -------------------------------------------------------------------------------- /src/cli/version.ts: -------------------------------------------------------------------------------- ```typescript import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); let version = '1.0.0'; // fallback try { const pkgPath = path.resolve(__dirname, '../../package.json'); const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); version = pkgJson.version || version; } catch { } export { version }; ``` -------------------------------------------------------------------------------- /src/cli/init.ts: -------------------------------------------------------------------------------- ```typescript import { execSync } from 'child_process'; import { fileURLToPath } from 'url'; import path from 'path'; // __dirname workaround for ES Modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export async function init() { const initPath = path.resolve(__dirname, '../init.js'); try { execSync(`node "${initPath}"`, { stdio: 'inherit' }); } catch (err) { console.error('❌ Failed to run init:', err); process.exit(1); } } ``` -------------------------------------------------------------------------------- /src/test/privateAES.test.ts: -------------------------------------------------------------------------------- ```typescript import { encryptPrivateKey, decryptPrivateKey } from "../PrivateAES.js"; describe.only("privateAES", () => { test("test", async () => { const privateKey = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; const password = '12345678'; const encrypted =await encryptPrivateKey(privateKey, password); console.log(encrypted); const decrypted =await decryptPrivateKey(encrypted, password); console.log(decrypted); expect(decrypted).toBe(privateKey); }) }) ``` -------------------------------------------------------------------------------- /src/lib/bep20Abi.ts: -------------------------------------------------------------------------------- ```typescript export const bep20abi = [ { name: "transfer", type: "function", stateMutability: "nonpayable", inputs: [ { name: "recipient", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ type: "bool" }], }, { name: "decimals", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }], }, { name: "symbol", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string" }], }, { name: "name", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string" }], }, ] as const; ``` -------------------------------------------------------------------------------- /src/functions/fetchBalanceTool.ts: -------------------------------------------------------------------------------- ```typescript import { sanitizeData } from "../responseUtils"; const BALANCE_API_URL = "https://app.termix.ai/api/bscBalanceCheck"; type BalanceData = { address: string; nativeBalance: string; tokenBalances: { token_address: string; symbol: string; name: string; logo: string; decimals: string; balance: string; }[]; }; export async function getBalance(address: string) { const response = await fetch(`${BALANCE_API_URL}?address=${address}`); if (!response.ok) { throw new Error(`Balance Fetch Error: ${response.statusText}`); } const result = await response.json() as BalanceData; return sanitizeData(result, { strictMode: true, maxLength: 200, allowMarkdown: false }); } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node // src/index.ts import { parseArgs } from 'node:util'; import { init } from './cli/init.js'; import { version } from './cli/version.js'; import { printHelp } from './cli/help.js'; import { main } from './main.js'; process.on('uncaughtException', (error: Error) => { console.error('❌ Uncaught exception:', error); }); process.on('unhandledRejection', (error: Error | unknown) => { console.error('❌ Unhandled rejection:', error); }); interface CliOptions { init?: boolean; help?: boolean; version?: boolean; } let values: CliOptions; try { const args = parseArgs({ options: { init: { type: 'boolean', short: 'i' }, help: { type: 'boolean', short: 'h' }, version: { type: 'boolean', short: 'v' }, }, }); values = args.values as CliOptions; } catch (err) { console.error('❌ Unrecognized argument. For help, run `bnbchain-mcp --help`.'); process.exit(1); } if (values.help) { printHelp(); process.exit(0); } if (values.version) { console.log(version); process.exit(0); } if (values.init) { await init(); // run init.js logic } else { main().catch((error: Error) => { console.error('❌ Fatal error in main():', error); process.exit(1); }); } ``` -------------------------------------------------------------------------------- /src/tools/getWalletInfo.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { getBalance } from "../functions/fetchBalanceTool.js"; import { getAccount } from "../config.js"; export function registerGetWalletInfo(server: McpServer) { server.tool("Get_Wallet_Info", "👛View detailed balance and holdings for any wallet address", { address: z.string().optional().describe("When querying the user's own wallet value, it is null"), }, async ({ address }) => { try { if (address === '' || !address || address === 'null') { const account = await getAccount(); address = account.address } const balance = await getBalance(address); return { content: [ { type: "text", text: `Native Balance (BNB): ${balance.nativeBalance}\n\nToken Balances:\n${JSON.stringify(balance.tokenBalances)}\n\nWallet Address: ${address}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Failed to fetch balance: ${errorMessage}` }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /src/tools/pancakeMyPosition.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { myPosition } from "../functions/pancakeSwapPosition.js"; import { bigIntReplacer } from "../util.js"; import { getAccount } from "../config.js"; export function registerPancakeMyPosition(server: McpServer) { server.tool("View_PancakeSwap_Positions", "📊View your active liquidity positions on PancakeSwap", {}, async ({}) => { try { const account = await getAccount(); const positions = await myPosition(account.address); return { content: [ { type: "text", text: `get user potitions successfully. ${JSON.stringify(positions, bigIntReplacer)}` }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `get user potitions failed: ${errorMessage}`, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /src/tools/queryMemeTokenDetails.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { memeTokenDetail } from "../functions/memeTokenDetails.js"; export function registerQueryMemeTokenDetails(server: McpServer) { server.tool( "QueryMemeTokenDetails", "Fetches token details for a given meme token using the four.meme API. Default price in USDT.", { tokenName: z.string().describe("The name of the token to query (e.g., HGUSDT)") }, async ({ tokenName }) => { try { const data = await memeTokenDetail(tokenName); return { content: [ { type: "text", text: `Token details for "${tokenName}": ${JSON.stringify(data, null, 2)}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Failed to fetch token details: ${errorMessage}` } ], isError: true }; } } ); } ``` -------------------------------------------------------------------------------- /src/functions/memeTokenDetails.ts: -------------------------------------------------------------------------------- ```typescript export const memeTokenDetail = async (tokenName: string) => { // Fetch both BNB price and token details in parallel const [bnbPriceRes, tokenRes] = await Promise.all([ fetch("https://api.binance.com/api/v3/ticker/price?symbol=BNBUSDT"), fetch(`https://www.four.meme/meme-api/v1/private/token/query?tokenName=${encodeURIComponent(tokenName)}`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json" } }) ]); // Validate both responses if (!bnbPriceRes.ok) throw new Error(`Failed to fetch BNB price: ${bnbPriceRes.status}`); if (!tokenRes.ok) throw new Error(`Failed to fetch token details: ${tokenRes.status}`); const [bnbPriceJson, tokenJson] = await Promise.all([bnbPriceRes.json(), tokenRes.json()]); const BNB_TO_USDT = parseFloat(bnbPriceJson.price); const token = tokenJson.data?.[0]; if (!token || !token.tokenPrice?.price) { return { content: [{ type: "text", text: "Token not found or price unavailable." }], isError: true }; } const bnbPrice = parseFloat(token.tokenPrice.price); const priceInUsdt = bnbPrice * BNB_TO_USDT; const data = { ...token, tokenPrice: { ...token.tokenPrice, priceInUsdt: priceInUsdt.toFixed(10) } }; return data; }; ``` -------------------------------------------------------------------------------- /src/tools/transferNativeToken.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { parseEther } from "viem"; import { getAccount, walletClient } from "../config.js"; import { buildTxUrl, checkTransactionHash } from "../util.js"; export function registerTransferNativeToken(server: McpServer) { server.tool("Send_BNB", "💎Transfer native token (BNB), Before execution, check the wallet information first", { recipientAddress: z.string(), amount: z.string(), }, async ({ recipientAddress, amount }) => { let txHash = undefined; try { const account = await getAccount(); txHash = await walletClient(account).sendTransaction({ to: recipientAddress as `0x${string}`, value: parseEther(amount), }); const txUrl = await checkTransactionHash(txHash) return { content: [ { type: "text", text: `Transaction sent successfully. ${txUrl}`, url: txUrl, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const txUrl = buildTxUrl(txHash); return { content: [ { type: "text", text: `transaction failed: ${errorMessage}`, url: txUrl, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /src/tools/pancakeSwap.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { pancakeSwap } from "../functions/pancakeSwapTool.js"; import { getAccount, publicClient } from "../config.js"; import { buildTxUrl, checkTransactionHash } from "../util.js"; export function registerPancakeSwap(server: McpServer) { server.tool("PancakeSwap_Token_Exchange", "💱Exchange tokens on BNBChain using PancakeSwap DEX", { inputToken: z.string(), outputToken: z.string(), amount: z.string(), }, async ({ inputToken, outputToken, amount }) => { let txHash = undefined; try { const account = await getAccount(); txHash = await pancakeSwap({ account, inputToken, outputToken, amount, }); const txUrl = await checkTransactionHash(txHash) return { content: [ { type: "text", text: `PancakeSwap transaction sent successfully. ${txUrl}`, url: txUrl, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const txUrl = buildTxUrl(txHash); return { content: [ { type: "text", text: `transaction failed: ${errorMessage}`, url: txUrl, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /src/tools/pancakeRemovePosition.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { removeLiquidityV3 } from "../functions/pancakeRemoveLiquidityTool.js"; import { getAccount } from "../config.js"; import { buildTxUrl, checkTransactionHash } from "../util.js"; export function registerPancakeRemovePosition(server: McpServer) { server.tool("Remove_PancakeSwap_Liquidity", "🔄Withdraw your liquidity from PancakeSwap pools", { positionId: z.string(), percent: z.number().max(100).min(1), }, async ({ positionId, percent }) => { let txHash = undefined; try { const account = await getAccount(); txHash = await removeLiquidityV3(account, BigInt(positionId), percent); const txUrl = await checkTransactionHash(txHash) return { content: [ { type: "text", text: `remove liquidity position on panceke successfully. ${txUrl}`, url: txUrl, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const txUrl = buildTxUrl(txHash); return { content: [ { type: "text", text: `transaction failed: ${errorMessage}`, url: txUrl, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /src/tools/goplusSecurityCheck.ts: -------------------------------------------------------------------------------- ```typescript // @ts-ignore import { GoPlus, ErrorCode } from "@goplus/sdk-node"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { ChainId } from "@pancakeswap/sdk"; import { z } from "zod"; import { sanitizeData } from "../responseUtils"; export function registerGoplusSecurityCheck(server: McpServer) { server.tool("Token_Security_Check", "🔒Analyze BNBChain tokens for potential security risks powered by GoPlus", { tokenAddress: z.string(), }, async ({ tokenAddress }) => { try { const chainId = ChainId.BSC.toString(); // BSC chain ID const addresses = [tokenAddress]; // Call GoPlus API to check token security let res = await (GoPlus as any).tokenSecurity(chainId, addresses, 30); if (res.code !== (ErrorCode as any).SUCCESS) { return { content: [ { type: "text", text: `Security check failed: ${res.message}`, }, ], isError: true, }; } const securityData = res.result[tokenAddress] || {}; const sanitizedData = sanitizeData(securityData, { strictMode: true, maxLength: 200, allowMarkdown: false }); return { content: [ { type: "text", text: `Security check successful for ${tokenAddress}: ${JSON.stringify( sanitizedData, null, 2 )}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Security check failed: ${errorMessage}`, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import dotenv from "dotenv"; dotenv.config(); // Import tool registrations import { registerTransferNativeToken } from "./tools/transferNativeToken.js"; import { registerTransferBEP20Token } from "./tools/transferBEP20Token.js"; import { registerPancakeSwap } from "./tools/pancakeSwap.js"; import { registerGetWalletInfo } from "./tools/getWalletInfo.js"; import { registerBuyMemeToken } from "./tools/buyMemeToken.js"; import { registerSellMemeToken } from "./tools/sellMemeToken.js"; import { registerPancakeAddLiquidity } from "./tools/pancakeAddLiquidity.js"; import { registerPancakeMyPosition } from "./tools/pancakeMyPosition.js"; import { registerPancakeRemovePosition } from "./tools/pancakeRemovePosition.js"; import { registerGoplusSecurityCheck } from "./tools/goplusSecurityCheck.js"; import { registerQueryMemeTokenDetails } from "./tools/queryMemeTokenDetails.js"; // Main server entry export async function main() { const server = new McpServer({ name: "bsc-mcp", version: "1.0.0" }); // Register all tools registerTransferNativeToken(server); registerTransferBEP20Token(server); registerPancakeSwap(server); registerGetWalletInfo(server); registerBuyMemeToken(server); registerSellMemeToken(server); registerPancakeAddLiquidity(server); registerPancakeMyPosition(server); registerPancakeRemovePosition(server); registerGoplusSecurityCheck(server); registerQueryMemeTokenDetails(server); const transport = new StdioServerTransport(); transport.onmessage = (message /** @type {JSONRPCMessage} */) => { console.log("📩 Received message:", JSON.stringify(message, null, 2)); }; transport.onerror = (error) => { console.error("🚨 Transport error:", error); }; await server.connect(transport); } ``` -------------------------------------------------------------------------------- /src/tools/transferBEP20Token.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { parseUnits, getContract, Address, publicActions } from "viem"; import { bep20abi } from "../lib/bep20Abi.js"; import { getAccount, walletClient } from "../config.js"; import { buildTxUrl, checkTransactionHash } from "../util.js"; export function registerTransferBEP20Token(server: McpServer) { server.tool("Send_BEP20_Token", "📤Send any BEP-20 token to another wallet (requires wallet check first)", { recipientAddress: z.string(), amount: z.string(), address: z.string(), }, async ({ recipientAddress, amount, address }) => { let txHash = undefined; try { // Get token details including address and decimals const account = await getAccount(); const client = walletClient(account).extend(publicActions) const contract = getContract({ address: address as Address, abi: bep20abi, client, }); const decimals = await contract.read.decimals(); // Parse the amount based on token decimals const parsedAmount = parseUnits(amount, decimals); txHash = await contract.write.transfer([ `0x${recipientAddress.replace("0x", "")}`, parsedAmount, ], { gas: BigInt(100000), }); const txUrl = await checkTransactionHash(txHash) return { content: [ { type: "text", text: `BEP-20 token transfer sent successfully. ${txUrl}`, url: txUrl, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const txUrl = buildTxUrl(txHash); return { content: [ { type: "text", text: `transaction failed: ${errorMessage}`, url: txUrl, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "bnbchain-mcp", "version": "1.0.12", "main": "index.js", "type": "module", "bin": { "bnbchain-mcp": "build/index.js" }, "scripts": { "test": "node test/testServer.js", "test:privateAES": "npx jest src/test/privateAES.test.ts -t \"test\"", "start": "node build/index.js", "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", "init": "node ./build/init.js", "init:build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\" && node ./build/init.js", "publish:auto": "node scripts/publish.cjs" }, "files": [ "build" ], "keywords": [ "binance-smart-chain", "bsc", "blockchain", "web3", "cryptocurrency", "defi", "model-context-protocol", "mcp", "claude-integration", "ai-agents", "token-transfers", "smart-contracts", "pancakeswap", "bep20", "swap", "liquidity", "wallet-management", "blockchain-tools", "crypto-cli", "defi-tools", "meme-tokens", "token-deployment", "blockchain-development", "web3-infrastructure", "crypto-automation", "token-security", "moralis-integration", "bnb-chain" ], "author": "", "license": "MIT", "description": "", "devDependencies": { "@types/bcrypt": "^5.0.2", "@types/jest": "^29.5.14", "@types/figlet": "^1.7.0", "@types/fs-extra": "^11.0.4", "@types/node": "^22.10.0", "@types/prompts": "^2.4.9", "jest": "^29.7.0", "ts-jest": "^29.3.0", "typescript": "^5.8.2" }, "dependencies": { "@goplus/sdk-node": "^1.0.12", "@modelcontextprotocol/sdk": "^1.4.0", "@pancakeswap/sdk": "^5.8.8", "@pancakeswap/smart-router": "6.1.6", "@pancakeswap/tokens": "^0.6.24", "@pancakeswap/v3-sdk": "^3.9.0", "bcrypt": "^5.1.1", "chalk": "^5.4.1", "dotenv": "^16.4.7", "figlet": "^1.8.0", "fs-extra": "^11.3.0", "graphql-request": "^7.1.2", "moralis": "^2.27.2", "ora": "^8.2.0", "prompts": "^2.4.2", "ts-node-dev": "^2.0.0", "viem": "^2.23.11" } } ``` -------------------------------------------------------------------------------- /src/tools/pancakeAddLiquidity.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { parseUnits, } from "viem"; import { addLiquidityV3 } from "../functions/pancakeAddLiquidityTool.js"; import { CurrencyAmount, } from "@pancakeswap/sdk"; import { FeeAmount } from '@pancakeswap/v3-sdk'; import { getToken } from "../functions/pancakeSwapTool.js"; import { getAccount, } from "../config.js"; import { buildTxUrl, checkTransactionHash } from "../util.js"; export function registerPancakeAddLiquidity(server: McpServer) { server.tool("Add_PancakeSwap_Liquidity", "💧Provide liquidity to PancakeSwap trading pairs", { token0: z.string(), token1: z.string(), token0Amount: z.string(), token1Amount: z.string(), }, async ({ token0, token1, token0Amount, token1Amount }) => { let txHash = undefined; try { // Define tokens const tokenA = await getToken(token0); const tokenB = await getToken(token1); // Amounts to add const amountTokenA = CurrencyAmount.fromRawAmount(tokenA, parseUnits(token0Amount, tokenA.decimals).toString()); const amountTokenB = CurrencyAmount.fromRawAmount(tokenB, parseUnits(token1Amount, tokenB.decimals).toString()); const account = await getAccount(); txHash = await addLiquidityV3( tokenA, tokenB, FeeAmount.MEDIUM, // 0.3% amountTokenA, amountTokenB, account, ); const txUrl = await checkTransactionHash(txHash) return { content: [ { type: "text", text: `add liquidity to pancake successfully. ${txUrl}`, url: txUrl, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const txUrl = buildTxUrl(txHash); return { content: [ { type: "text", text: `transaction failed: ${errorMessage}`, url: txUrl, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /scripts/publish.cjs: -------------------------------------------------------------------------------- ``` #!/usr/bin/env node const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); // Logging helpers const log = (msg) => console.log(`\n🟡 ${msg}`); const success = (msg) => console.log(`✅ ${msg}`); const error = (msg) => { console.error(`❌ ${msg}`); process.exit(1); }; // Paths const pkgPath = path.resolve('package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); const args = process.argv.slice(2); // Run shell command const run = (cmd) => { log(`Running: ${cmd}`); execSync(cmd, { stdio: 'inherit' }); }; // Auto-increment patch version const bumpPatchVersion = (version) => { const [major, minor, patch] = version.split('.').map(Number); return `${major}.${minor}.${patch + 1}`; }; // Swap README for NPM const swapReadmeForNpm = () => { const npmReadme = path.resolve('README.npm.md'); const targetReadme = path.resolve('README.md'); if (fs.existsSync(npmReadme)) { fs.copyFileSync(targetReadme, path.resolve('README.github.md')); fs.copyFileSync(npmReadme, targetReadme); success('Swapped README.npm.md → README.md for NPM publish'); } else { log('No README.npm.md found. Using default README.md'); } }; // Restore original README const restoreReadme = () => { const ghReadme = path.resolve('README.github.md'); const targetReadme = path.resolve('README.md'); if (fs.existsSync(ghReadme)) { fs.copyFileSync(ghReadme, targetReadme); fs.unlinkSync(ghReadme); success('Restored original GitHub README.md'); } }; // Main flow const main = () => { console.log(`📍 Current Git Branch: ${branch}`); if (!fs.existsSync('build/index.js')) { error('Build not found. Run `npm run build` first.'); } // Check for version override const versionFlagIndex = args.indexOf('--version'); let newVersion = ''; if (versionFlagIndex !== -1 && args[versionFlagIndex + 1]) { newVersion = args[versionFlagIndex + 1]; success(`Custom version passed via CLI: ${newVersion}`); } else { newVersion = bumpPatchVersion(pkg.version); success(`Auto-incremented patch version: ${pkg.version} → ${newVersion}`); } pkg.version = newVersion; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); swapReadmeForNpm(); run('npm run build'); const tag = branch === 'develop' ? '--tag alpha' : ''; run(`npm publish ${tag}`); restoreReadme(); success(`Published ${pkg.name}@${newVersion} to NPM (${tag || 'latest'})`); }; main(); ``` -------------------------------------------------------------------------------- /src/PrivateAES.ts: -------------------------------------------------------------------------------- ```typescript import * as crypto from 'crypto'; import * as bcrypt from 'bcrypt'; const algorithm = 'aes-256-gcm'; /** * Encrypts an EVM private key using bcrypt and user password * @param privateKey The EVM private key to encrypt * @param password User-provided password * @returns Encrypted string (Base64 encoded) */ export async function encryptPrivateKey(privateKey: string, password: string): Promise<string> { // bcrypt work factor - higher is more secure but slower const saltRounds = 12; // Generate bcrypt hash as key material const hashedPassword = await bcrypt.hash(password, saltRounds); // Extract bcrypt salt - it's in the first 29 characters of the hash const bcryptSalt = hashedPassword.substring(0, 29); // Generate symmetric encryption key using SHA-256 from bcrypt hash const key = crypto.createHash('sha256').update(hashedPassword).digest(); // Generate random initialization vector const iv = crypto.randomBytes(12); // Create cipher object const cipher = crypto.createCipheriv(algorithm, key, iv); // Encrypt private key let encrypted = cipher.update(privateKey, 'utf8', 'base64'); encrypted += cipher.final('base64'); // Get authentication tag const authTag = cipher.getAuthTag(); // Concatenate all necessary components and encode as Base64 string const result = Buffer.concat([ Buffer.from(bcryptSalt), // Store bcrypt salt iv, authTag, Buffer.from(encrypted, 'base64') ]); return result.toString('base64'); } /** * Decrypts an EVM private key using bcrypt and user password * @param encryptedData Encrypted private key data (Base64 encoded) * @param password User-provided password * @returns Decrypted private key or null (if password is incorrect) */ export async function decryptPrivateKey(encryptedData: string, password: string): Promise<string | null> { try { // Decode Base64 const data = Buffer.from(encryptedData, 'base64'); // Extract components const bcryptSalt = data.subarray(0, 29).toString(); const iv = data.subarray(29, 41); const authTag = data.subarray(41, 57); const encrypted = data.subarray(57).toString('base64'); // Use bcrypt to check password // Reconstruct salt and password to create the same hash as during encryption const hashedPassword = await bcrypt.hash(password, bcryptSalt); // Generate key using the same method from the hash const key = crypto.createHash('sha256').update(hashedPassword).digest(); // Create decipher const decipher = crypto.createDecipheriv(algorithm, key, iv); decipher.setAuthTag(authTag); // Decrypt let decrypted = decipher.update(encrypted, 'base64', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { // Decryption failed (incorrect password or data corruption) return null; } } ``` -------------------------------------------------------------------------------- /src/tools/sellMemeToken.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { parseUnits, type Hex, } from "viem"; import { getAccount, publicClient, walletClient } from "../config.js"; import { AddressConfig } from "../addressConfig.js"; import { buildTxUrl, checkTransactionHash } from "../util.js"; const tokenAbi = [ { "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" }], "name": "allowance", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "approve", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, ] export function registerSellMemeToken(server: McpServer) { server.tool("Sell_Meme_Token", "💰Sell meme tokens for other currencies", { token: z.string(), tokenValue: z.string(), }, async ({ token, tokenValue }) => { let txHash = undefined; try { const account = await getAccount(); const allowanceAmount = await publicClient.readContract({ address: token as Hex, abi: tokenAbi, functionName: 'allowance', args: [account.address, AddressConfig.FourMemeSellTokenAMAPContract], }) as bigint; if (allowanceAmount < parseUnits(tokenValue, 18)) { const hash = await walletClient(account).writeContract({ account, address: token as Hex, abi: tokenAbi, functionName: 'approve', args: [AddressConfig.FourMemeSellTokenAMAPContract, parseUnits(tokenValue, 18)], }); await publicClient.waitForTransactionReceipt({ hash: hash, retryCount: 300, retryDelay: 100, }); } txHash = await walletClient(account).writeContract({ account, address: AddressConfig.FourMemeSellTokenAMAPContract, abi: [{ "inputs": [ { "internalType": "address", "name": "token", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "sellToken", "outputs": [], "stateMutability": "nonpayable", "type": "function" }], functionName: 'sellToken', args: [token as Hex, parseUnits(tokenValue, 18)], }); const txUrl = await checkTransactionHash(txHash) return { content: [ { type: "text", text: `sell meme token successfully. ${txUrl}`, url: txUrl, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const txUrl = buildTxUrl(txHash); return { content: [ { type: "text", text: `transaction failed: ${errorMessage}`, url: txUrl, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript import { Hex, http, publicActions, createWalletClient, createPublicClient, PrivateKeyAccount } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { bsc } from "viem/chains"; import { getPassword, } from "./util.js"; import { decryptPrivateKey, } from "./PrivateAES.js"; export const rpcUrl = process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org"; class ObfuscatedSecureBuffer { private buffers: Uint8Array[] = []; private indexMap: number[] = []; private salt: Uint8Array = new Uint8Array(32); private length: number = 0; private isActive = false; constructor() { } updata(data: Uint8Array | string) { let originalData: Uint8Array; if (typeof data === 'string') { const hexString = data.startsWith('0x') ? data.slice(2) : data; originalData = new Uint8Array(hexString.length / 2); for (let i = 0; i < hexString.length; i += 2) { originalData[i / 2] = parseInt(hexString.substr(i, 2), 16); } } else { originalData = new Uint8Array(data); } this.length = originalData.length; this.salt = new Uint8Array(32); crypto.getRandomValues(this.salt); const bufferCount = 3; this.buffers = []; for (let i = 0; i < bufferCount; i++) { const buffer = new Uint8Array(this.length); crypto.getRandomValues(buffer); this.buffers.push(buffer); } this.indexMap = Array.from({ length: this.length }, (_, i) => i) .sort(() => 0.5 - Math.random()); for (let i = 0; i < this.length; i++) { const targetIndex = this.indexMap[i]; const bufferIndex = i % bufferCount; const saltByte = this.salt[i % this.salt.length]; const obfuscatedByte = originalData[i] ^ saltByte ^ (i & 0xff); this.buffers[bufferIndex][targetIndex] = obfuscatedByte; } this.isActive = true; } getData(): Uint8Array { const result = new Uint8Array(this.length); const bufferCount = this.buffers.length; for (let i = 0; i < this.length; i++) { const targetIndex = this.indexMap[i]; const bufferIndex = i % bufferCount; const saltByte = this.salt[i % this.salt.length]; const obfuscatedByte = this.buffers[bufferIndex][targetIndex]; result[i] = obfuscatedByte ^ saltByte ^ (i & 0xff); } return result; } getHexString(): string { const data = this.getData(); return '0x' + Array.from(data) .map(b => b.toString(16).padStart(2, '0')) .join(''); } zeroize(): void { for (const buffer of this.buffers) { buffer.fill(0); crypto.getRandomValues(buffer); buffer.fill(0xff); buffer.fill(0); } this.salt.fill(0); this.indexMap.fill(0); this.buffers = []; this.indexMap = []; this.isActive = false; } active(): boolean { return this.isActive; } } let obfuscatedPrivateKey = new ObfuscatedSecureBuffer();; export const getAccount = async () => { const BSC_WALLET_PRIVATE_KEY = process.env.BSC_WALLET_PRIVATE_KEY as Hex if (!BSC_WALLET_PRIVATE_KEY) { throw new Error("BSC_WALLET_PRIVATE_KEY is not defined"); } if (obfuscatedPrivateKey.active()) { const pk = obfuscatedPrivateKey.getHexString(); return privateKeyToAccount( pk as Hex ); } const { agreed, value: password } = await getPassword() if (!password) { throw new Error("You did not enter a password."); } const pk = await decryptPrivateKey(BSC_WALLET_PRIVATE_KEY, password); if (agreed) { obfuscatedPrivateKey.updata(pk as string); setTimeout(() => { obfuscatedPrivateKey.zeroize(); }, 1000 * 60 * 60); return privateKeyToAccount( pk as Hex ); } else { return privateKeyToAccount( pk as Hex ); } }; export const getClient = async () => { const account = await getAccount() const client = createWalletClient({ account, chain: bsc, transport: http(rpcUrl), }).extend(publicActions); return client; }; export const publicClient = createPublicClient({ chain: bsc, transport: http(rpcUrl), }); export const walletClient = (account: PrivateKeyAccount) => createWalletClient({ chain: bsc, transport: http(rpcUrl), account: account, }); ``` -------------------------------------------------------------------------------- /src/responseUtils.ts: -------------------------------------------------------------------------------- ```typescript /** * Clean the string, remove or escape characters and patterns that may cause prompt injection * * @param input The input string to be cleaned * @param options Cleaning options * @returns Cleaned safe string */ export function sanitizeString( input: string, options: { maxLength?: number, // Maximum allowed length strictMode?: boolean, // Strict mode (more aggressive filtering) allowMarkdown?: boolean, // Whether to allow markdown syntax escapeQuotes?: boolean // Whether to escape quotes instead of removing } = {} ): string { // Set default values const { maxLength = 500, strictMode = true, allowMarkdown = false, escapeQuotes = true } = options; if (!input || typeof input !== 'string') { return ''; } let sanitized = input; // 1. Remove possible code blocks and formatted text if (!allowMarkdown) { // Remove code blocks sanitized = sanitized.replace(/```[\s\S]*?```/g, "[Code block removed]"); // Remove inline code sanitized = sanitized.replace(/`[^`]*`/g, "[Code removed]"); } // 2. Handle possible closing symbols and instruction patterns // Handle HTML/XML tags sanitized = sanitized.replace(/<[^>]*>/g, ""); // Handle various bracket pairs sanitized = sanitized.replace(/\{[\s\S]*?\}/g, "[Content filtered]"); // Curly brackets sanitized = sanitized.replace(/\[[\s\S]*?\]/g, "[Content filtered]"); // Square brackets sanitized = sanitized.replace(/\([\s\S]*?\)/g, "[Content filtered]"); // Parentheses // 3. Handle potential instruction keywords const aiKeywords = [ "system", "user", "assistant", "model", "prompt", "instruction", "context", "token", "function", "completion", "response", "davinci", "claude", "gpt", "llm", "api", "openai", "anthropic" ]; const keywordPattern = new RegExp(`\\b(${aiKeywords.join('|')})\\b`, 'gi'); sanitized = sanitized.replace(keywordPattern, (match) => `_${match}_`); // 4. Handle quotes (escape or remove) if (escapeQuotes) { // Escape quotes sanitized = sanitized.replace(/"/g, '\\"').replace(/'/g, "\\'"); } else { // Remove quotes sanitized = sanitized.replace(/["']/g, ""); } // 5. Additional processing in strict mode if (strictMode) { // Remove all possible control characters and special characters sanitized = sanitized.replace(/[\u0000-\u001F\u007F-\u009F\u2000-\u200F\u2028-\u202F]/g, ""); // Handle possible injection separators and special patterns sanitized = sanitized.replace(/\.\.\./g, "…"); // Ellipsis sanitized = sanitized.replace(/\-\-\-+/g, "—"); // Em dash sanitized = sanitized.replace(/={2,}/g, "=="); // Equal sign sequence // Handle URL and link patterns sanitized = sanitized.replace(/(https?:\/\/[^\s]+)/g, "[Link removed]"); } // 6. Handle possible JSON structure markers sanitized = sanitized .replace(/(\s*"\w+"\s*:)/g, "【Property】:") // JSON property name .replace(/(\[\s*\]|\{\s*\})/g, "【Empty】"); // Empty array or object // 7. Limit length if (sanitized.length > maxLength) { sanitized = sanitized.substring(0, maxLength) + "..."; } return sanitized; } /** * Recursively process the object, applying the sanitizeString function to all string values * * @param data The data object to be processed * @param options Options to be passed to sanitizeString * @param maxDepth Maximum recursion depth * @param currentDepth Current recursion depth * @returns Processed safe data object */ export function sanitizeData( data: any, options = {}, maxDepth = 5, currentDepth = 0 ): any { // Handle recursion depth limit if (currentDepth >= maxDepth) { return typeof data === 'object' && data !== null ? "[Nested object simplified]" : data; } // Basic types return directly if (data === null || data === undefined) { return data; } // Handle string if (typeof data === 'string') { return sanitizeString(data, options); } // Handle numbers and booleans if (typeof data === 'number' || typeof data === 'boolean') { return data; } // Handle array if (Array.isArray(data)) { return data.map(item => sanitizeData(item, options, maxDepth, currentDepth + 1)); } // Handle object if (typeof data === 'object') { const result: Record<string, any> = {}; for (const key in data) { if (Object.prototype.hasOwnProperty.call(data, key)) { // Also clean the property name const safeKey = key.replace(/[<>{}\[\]]/g, ""); result[safeKey] = sanitizeData(data[key], options, maxDepth, currentDepth + 1); } } return result; } // Other types return String(data); } ``` -------------------------------------------------------------------------------- /src/functions/pancakeSwapTool.ts: -------------------------------------------------------------------------------- ```typescript import { Hash, getContract, Hex, parseUnits, isAddress, PrivateKeyAccount, Address, } from "viem"; import { erc20Abi, hexToBigInt, maxUint256 } from "viem"; import { ChainId, CurrencyAmount, Native, Percent, Token, TradeType, } from "@pancakeswap/sdk"; import { SMART_ROUTER_ADDRESSES, SmartRouter, SmartRouterTrade, SwapRouter, } from "@pancakeswap/smart-router"; import { GraphQLClient } from "graphql-request"; import { publicClient, walletClient } from "../config.js"; import { bep20abi } from "../lib/bep20Abi.js"; export const getToken = async ( token: string, ) => { if (token.toUpperCase() === "BNB") { return Native.onChain(ChainId.BSC) } let address = token.toLowerCase() let decimal; const url = "https://tokens.pancakeswap.finance/pancakeswap-extended.json"; const resp = await fetch(url); const data = await resp.json(); let tokens = data.tokens let symbol; if (!isAddress(address)) { const tokenInfo = tokens.find((item: any) => item.symbol.toLowerCase() === address) if (!tokenInfo) { throw new Error("Token not found"); } address = tokenInfo.address decimal = tokenInfo.decimals symbol = tokenInfo.symbol } else { const tokenInfo = tokens.find((item: any) => item.address.toLowerCase() === address) if (!tokenInfo) { const contract = getContract({ address: address as Address, abi: bep20abi, client: publicClient, }); decimal = await contract.read.decimals(); symbol = await contract.read.symbol(); } else { decimal = tokenInfo.decimals symbol = tokenInfo.symbol } } return new Token( ChainId.BSC, address as Hex, decimal, symbol, ) } export const pancakeSwap = async ({ account, inputToken, outputToken, amount, }: { // privateKey: string; amount: string; inputToken: string; outputToken: string; account: PrivateKeyAccount; }): Promise<Hash> => { const chainId = ChainId.BSC let currencyA = await getToken(inputToken); let currencyB = await getToken(outputToken); let amountDecimal = currencyA.decimals; const parseAmountIn = parseUnits(amount, amountDecimal); const amountValue = CurrencyAmount.fromRawAmount(currencyA, parseAmountIn) if (!currencyA.isNative) { const TokenContract = getContract({ address: currencyA.address, abi: erc20Abi, client: { wallet: walletClient(account), public: publicClient, }, }); if ( !TokenContract.write || !TokenContract.write.approve || !TokenContract.read.allowance ) { throw new Error("Unable to Swap Tokens"); } amountDecimal = await TokenContract.read.decimals(); const smartRouterAddress = SMART_ROUTER_ADDRESSES[ChainId.BSC] const allowance = await TokenContract.read.allowance([account.address, smartRouterAddress]) as bigint if (allowance < parseAmountIn) { const approveResult = await TokenContract.write.approve([smartRouterAddress, parseAmountIn]) await publicClient.waitForTransactionReceipt({ hash: approveResult, }); } } const quoteProvider = SmartRouter.createQuoteProvider({ onChainProvider: () => publicClient, }) const v3SubgraphClient = new GraphQLClient('https://api.thegraph.com/subgraphs/name/pancakeswap/exchange-v3-bsc1') const v2SubgraphClient = new GraphQLClient('https://proxy-worker-api.pancakeswap.com/bsc-exchange1') const [v2Pools, v3Pools] = await Promise.all([ SmartRouter.getV3CandidatePools({ onChainProvider: () => publicClient, // @ts-ignore subgraphProvider: () => v3SubgraphClient, currencyA: currencyA, currencyB: currencyB, subgraphFallback: false, }), SmartRouter.getV2CandidatePools({ onChainProvider: () => publicClient, // @ts-ignore v2SubgraphProvider: () => v2SubgraphClient, // @ts-ignore v3SubgraphProvider: () => v3SubgraphClient, currencyA: currencyA, currencyB: currencyB, }), ]) const pools = [...v2Pools, ...v3Pools] const trade = await SmartRouter.getBestTrade(amountValue, currencyB, TradeType.EXACT_INPUT, { gasPriceWei: () => publicClient.getGasPrice(), maxHops: 2, maxSplits: 2, poolProvider: SmartRouter.createStaticPoolProvider(pools), quoteProvider, quoterOptimization: true, }) as SmartRouterTrade<TradeType> const { value, calldata } = SwapRouter.swapCallParameters(trade, { recipient: account.address, slippageTolerance: new Percent(500, 10000), }) const tx = { account: account.address, // @ts-ignore to: SMART_ROUTER_ADDRESSES[chainId], data: calldata, value: hexToBigInt(value), }; const gasEstimate = await publicClient.estimateGas(tx); const calculateGasMargin = (value: bigint, margin = 1000n): bigint => { return (value * (10000n + margin)) / 10000n; }; const txHash = await walletClient(account).sendTransaction({ account: account, chainId, // @ts-ignore to: SMART_ROUTER_ADDRESSES[chainId], data: calldata, value: hexToBigInt(value), gas: calculateGasMargin(gasEstimate), }); return txHash; }; ``` -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- ```typescript import prompts, { PromptObject } from 'prompts'; import figlet from 'figlet'; import chalk from 'chalk'; import path from 'path'; import fs from 'fs-extra'; import os from 'os'; import { fileURLToPath } from 'url'; import { encryptPrivateKey } from './PrivateAES.js'; import dotenv from "dotenv"; import { Hex } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; dotenv.config(); // Binance Gold Color const yellow = chalk.hex('#F0B90B'); // ESModule __dirname workaround const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Cancel handler const onCancel = () => { console.log(chalk.red('\n❌ Configuration cancelled by user (Ctrl+C or ESC). Exiting...')); process.exit(0); }; // Show Banner const showBanner = () => { const banner = figlet.textSync('BNB Chain MCP ', { font: 'Big' }); console.log(yellow(banner)); console.log(yellow('🚀 Welcome to the BNB Chain MCP Configurator\n')); }; // User Input Types interface UserInputs { walletPassword: string; privateKey: string; rpcUrl?: string; } function validatePassword(password: string) { // At least 8 characters if (password.trim() === '') return 'Wallet Password is required!'; if (password.length < 8 || password.length > 128) return 'Wallet Password must be between 8 and 128 characters!'; // Check if it contains at least one lowercase letter if (!/[a-z]/.test(password)) return 'Wallet Password must contain at least one lowercase letter!'; // Check if it contains at least one uppercase letter if (!/[A-Z]/.test(password)) return 'Wallet Password must contain at least one uppercase letter!'; // Check if it contains at least one number if (!/[0-9]/.test(password)) return 'Wallet Password must contain at least one number!'; // Check if it contains at least one special character if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) return 'Wallet Password must contain at least one special character! (!@#$%^&*()_+-=[]{};\':\\"|,.<>\/?)'; return true; } // Ask for credentials const getInputs = async (): Promise<UserInputs> => { const questions: PromptObject[] = [ { type: 'password', name: 'walletPassword', message: '🔐 Enter your Wallet Password (The password must be between 8 and 128 characters):', validate: (val: string) => { return validatePassword(val); }, }, { type: 'password', name: 'privateKey', message: '🔑 Enter your BNB Chain Wallet Private Key:', validate: (val: string) => val.trim() === '' ? 'Private key is required!' : true, }, { type: 'text', name: 'rpcUrl', message: '🌐 Enter your BNB Chain RPC URL (optional):', }, ]; return await prompts(questions, { onCancel }) as UserInputs; }; // Generate .env file const generateEnvFile = async (privateKey: string, address: string, rpcUrl?: string, ): Promise<void> => { const envContent = ` BSC_WALLET_PRIVATE_KEY=${privateKey} BSC_WALLET_ADDRESS=${address} BSC_RPC_URL=${rpcUrl || ''} `.trim(); await fs.writeFile('.env', envContent); console.log(yellow('✅ .env file generated.')); }; // Generate config object const generateConfig = async (privateKey: string, address: string, rpcUrl?: string, ): Promise<any> => { const indexPath = path.resolve(__dirname, '..', 'build', 'index.js'); // one level up from cli/ return { 'bsc-mcp': { command: 'node', args: [indexPath], env: { BSC_WALLET_PRIVATE_KEY: privateKey, BSC_WALLET_ADDRESS: address, BSC_RPC_URL: rpcUrl || '', }, disabled: false, autoApprove: [] } }; }; // Configure Claude Desktop const configureClaude = async (config: object): Promise<boolean> => { const userHome = os.homedir(); let claudePath; const platform = os.platform(); if (platform == "darwin") { claudePath = path.join(userHome, 'Library/Application Support/Claude/claude_desktop_config.json'); } else if (platform == "win32") { claudePath = path.join(userHome, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'); } else { console.log(chalk.red('❌ Unsupported platform.')); return false; } if (!fs.existsSync(claudePath)) { console.log(chalk.yellow('⚠️ Claude config file not found. Creating a new one with default configuration.')); // Create a default configuration object const defaultConfig = { mcpServers: {} }; // Write the default configuration to the file await fs.writeJSON(claudePath, defaultConfig, { spaces: 2 }); } const jsonData = fs.readFileSync(claudePath, 'utf8'); const data = JSON.parse(jsonData); data.mcpServers = { ...data.mcpServers, ...config, }; await fs.writeJSON(claudePath, data, { spaces: 2 }); console.log(yellow('✅ BNB Chain MCP configured for Claude Desktop. Please RESTART your Claude to enjoy it 🎉')); return true; }; // Save fallback config file const saveFallbackConfig = async (config: object): Promise<void> => { await fs.writeJSON('config.json', config, { spaces: 2 }); console.log(yellow('📁 Saved config.json in root project folder.')); }; // Main logic const init = async () => { showBanner(); const { privateKey, rpcUrl, walletPassword } = await getInputs(); const _0xPrivateKey = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}` const account = privateKeyToAccount( _0xPrivateKey as Hex ); const privateKeyEncrypt = await encryptPrivateKey(_0xPrivateKey, walletPassword); await generateEnvFile(privateKeyEncrypt, account.address, rpcUrl); const config = await generateConfig(privateKeyEncrypt, account.address, rpcUrl); const { setupClaude } = await prompts({ type: 'confirm', name: 'setupClaude', message: '🧠 Do you want to configure in Claude Desktop?', initial: true }, { onCancel }); if (setupClaude) { const success = await configureClaude(config); if (!success) { await saveFallbackConfig(config); } } else { await saveFallbackConfig(config); } }; init(); ``` -------------------------------------------------------------------------------- /src/functions/pancakeSwapPosition.ts: -------------------------------------------------------------------------------- ```typescript import dotenv from 'dotenv'; dotenv.config(); import { TickMath, SqrtPriceMath } from '@pancakeswap/v3-sdk'; import { Address, PrivateKeyAccount, formatUnits, parseAbi, } from "viem"; import { publicClient, } from '../config.js'; const POSITION_MANAGER_ADDRESS = '0x46A15B0b27311cedF172AB29E4f4766fbE7F4364' as Address; const FACTORY_ADDRESS = '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865' as Address; const FACTORY_ABI = parseAbi([ 'function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool)' ]); const POOL_ABI = parseAbi([ 'function liquidity() external view returns (uint128)', 'function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)' ]); const ERC20_ABI = parseAbi([ 'function decimals() external view returns (uint256)', 'function symbol() external view returns (string)', 'function name() external view returns (string)', ]); const masterChefV3ABI = parseAbi([ 'function balanceOf(address account) external view returns (uint256)', 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', 'function positions(uint256) external view returns (uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1)', ]); export const myPosition = async (accountAddress: Address) => { const balance = await publicClient.readContract({ abi: masterChefV3ABI, address: POSITION_MANAGER_ADDRESS, functionName: 'balanceOf', args: [accountAddress], }) if (Number(balance) === 0) { return; } const nftCalls = [] for (let i = 0; i < Number(balance); i++) { const nftCall = { abi: masterChefV3ABI, address: POSITION_MANAGER_ADDRESS, functionName: 'tokenOfOwnerByIndex', args: [accountAddress, BigInt(i)], } nftCalls.push(nftCall) } const nftIds = await publicClient.multicall<BigInt[]>({ contracts: nftCalls, allowFailure: false, }) const positionCalls = nftIds.map((nftId) => { return { abi: masterChefV3ABI, address: POSITION_MANAGER_ADDRESS, functionName: 'positions', args: [nftId], } }) const positions = await publicClient.multicall<any[]>({ contracts: positionCalls, allowFailure: false, }) as any[] const getTokenInfo = async (token: Address) => { const infoCalls = [ { address: token, abi: ERC20_ABI, functionName: 'symbol', args: [], }, { address: token, abi: ERC20_ABI, functionName: 'name', args: [], }, { address: token, abi: ERC20_ABI, functionName: 'decimals', args: [], }, ] const tokenInfo = await publicClient.multicall<any[]>({ contracts: infoCalls, allowFailure: false, }) as any[] return { token, symbol: tokenInfo[0] as string, name: tokenInfo[1] as string, decimals: Number(tokenInfo[2]), } } const poolTokenInfos = await Promise.all(positions.map(async (position) => { const tokenInfos = await Promise.all([ getTokenInfo(position[2] as Address), getTokenInfo(position[3] as Address), ]); return { token0: tokenInfos[0], token1: tokenInfos[1], } })) as any[] const poolCalls = [] for (const position of positions) { const poolCall = { address: FACTORY_ADDRESS, abi: FACTORY_ABI, functionName: 'getPool', args: [ position[2] as Address, position[3] as Address, BigInt(position[4]) ] } poolCalls.push(poolCall); } const pools = await publicClient.multicall<any[]>({ contracts: poolCalls, allowFailure: false, }) as any[] const slot0Calls = [] for (const pool of pools) { const slot0Call = { address: pool as Address, abi: POOL_ABI, functionName: 'slot0', } slot0Calls.push(slot0Call); } const slot0s = await publicClient.multicall<any[]>({ contracts: slot0Calls, allowFailure: false, }) as any[] const positionInfos = [] for (let i = 0; i < pools.length; i++) { const positionInfo = { tickCurrent: slot0s[i][1], tickLower: positions[i][5], tickUpper: positions[i][6], liquidity: positions[i][7], ...poolTokenInfos[i], feeTier: positions[i][4], positionId: nftIds[i], } const sqrtPriceX96 = TickMath.getSqrtRatioAtTick(positionInfo.tickCurrent); const sqrtPriceAX96 = TickMath.getSqrtRatioAtTick(positionInfo.tickLower); const sqrtPriceBX96 = TickMath.getSqrtRatioAtTick(positionInfo.tickUpper); const liquidity = BigInt(positionInfo.liquidity); let amount0; let amount1; if (positionInfo.tickCurrent < positionInfo.tickLower) { amount0 = SqrtPriceMath.getAmount0Delta(sqrtPriceAX96, sqrtPriceBX96, liquidity, false); amount1 = BigInt(0); } else if (positionInfo.tickCurrent > positionInfo.tickUpper) { amount0 = BigInt(0); amount1 = SqrtPriceMath.getAmount1Delta(sqrtPriceAX96, sqrtPriceBX96, liquidity, false); } else { amount0 = SqrtPriceMath.getAmount0Delta(sqrtPriceX96, sqrtPriceBX96, liquidity, false); amount1 = SqrtPriceMath.getAmount1Delta(sqrtPriceAX96, sqrtPriceX96, liquidity, false); } positionInfos.push({ ...positionInfo, amount0, amount1, amount0Format: formatUnits(amount0, Number(positionInfo.token0.decimals)), amount1Format: formatUnits(amount1, Number(positionInfo.token1.decimals)), }); } return positionInfos; } ``` -------------------------------------------------------------------------------- /src/tools/buyMemeToken.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { parseUnits, type Hex, } from "viem"; import { getAccount, publicClient, walletClient } from "../config.js"; import { AddressConfig } from "../addressConfig.js"; import { buildTxUrl, checkTransactionHash } from "../util.js"; export function registerBuyMemeToken(server: McpServer) { server.tool("Buy_Meme_Token", "🚀Purchase meme tokens on BNBChain", { token: z.string(), tokenValue: z.string().default("0"), bnbValue: z.string().default("0"), }, async ({ token, tokenValue, bnbValue }) => { let txHash = undefined; try { const account = await getAccount(); const [,,estimatedAmount,,,amountMsgValue,,] = await publicClient.readContract({ address: AddressConfig.FourMemeTryBuyContract, abi: [ { "inputs": [ { "internalType": "address", "name": "token", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }, { "internalType": "uint256", "name": "funds", "type": "uint256" } ], "name": "tryBuy", "outputs": [ { "internalType": "address", "name": "tokenManager", "type": "address" }, { "internalType": "address", "name": "quote", "type": "address" }, { "internalType": "uint256", "name": "estimatedAmount", "type": "uint256" }, { "internalType": "uint256", "name": "estimatedCost", "type": "uint256" }, { "internalType": "uint256", "name": "estimatedFee", "type": "uint256" }, { "internalType": "uint256", "name": "amountMsgValue", "type": "uint256" }, { "internalType": "uint256", "name": "amountApproval", "type": "uint256" }, { "internalType": "uint256", "name": "amountFunds", "type": "uint256" } ], "stateMutability": "view", "type": "function" }], functionName: 'tryBuy', args: [token as Hex, parseUnits(tokenValue, 18), parseUnits(bnbValue, 18)], }); let outputAmount; let inputAmount; if (tokenValue == "0") { outputAmount = (BigInt(estimatedAmount) * BigInt(100 - 20)) / 100n inputAmount = amountMsgValue } else { outputAmount = estimatedAmount; inputAmount = (BigInt(amountMsgValue) * BigInt(100 + 5)) / 100n } txHash = await walletClient(account).writeContract({ account, address: AddressConfig.FourMemeBuyTokenAMAPContract, abi: [{ "inputs": [ { "internalType": "address", "name": "token", "type": "address" }, { "internalType": "uint256", "name": "funds", "type": "uint256" }, { "internalType": "uint256", "name": "minAmount", "type": "uint256" } ], "name": "buyTokenAMAP", "outputs": [], "stateMutability": "payable", "type": "function" }], functionName: 'buyTokenAMAP', args: [token as Hex, BigInt(inputAmount), outputAmount], value: BigInt(inputAmount), }); const txUrl = await checkTransactionHash(txHash) return { content: [ { type: "text", text: `buy meme token successfully. ${txUrl}`, url: txUrl, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const txUrl = buildTxUrl(txHash); return { content: [ { type: "text", text: `transaction failed: ${errorMessage}`, url: txUrl, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /src/functions/pancakeRemoveLiquidityTool.ts: -------------------------------------------------------------------------------- ```typescript import { ChainId, Native, Percent } from "@pancakeswap/sdk"; import { PositionMath, Multicall } from "@pancakeswap/v3-sdk"; import { Address, Hex, encodeFunctionData, getAddress, zeroAddress, maxUint128, parseAbi, PrivateKeyAccount, publicActions, } from "viem"; import dotenv from 'dotenv'; import { publicClient, walletClient } from "../config.js"; dotenv.config(); const positionManagerABI = [ { inputs: [ { components: [ { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, { internalType: 'uint128', name: 'liquidity', type: 'uint128' }, { internalType: 'uint256', name: 'amount0Min', type: 'uint256' }, { internalType: 'uint256', name: 'amount1Min', type: 'uint256' }, { internalType: 'uint256', name: 'deadline', type: 'uint256' }, ], internalType: 'struct INonfungiblePositionManager.DecreaseLiquidityParams', name: 'params', type: 'tuple', }, ], name: 'decreaseLiquidity', outputs: [ { internalType: 'uint256', name: 'amount0', type: 'uint256' }, { internalType: 'uint256', name: 'amount1', type: 'uint256' }, ], stateMutability: 'payable', type: 'function', }, { inputs: [ { components: [ { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, { internalType: 'address', name: 'recipient', type: 'address' }, { internalType: 'uint128', name: 'amount0Max', type: 'uint128' }, { internalType: 'uint128', name: 'amount1Max', type: 'uint128' }, ], internalType: 'struct INonfungiblePositionManager.CollectParams', name: 'params', type: 'tuple', }, ], name: 'collect', outputs: [ { internalType: 'uint256', name: 'amount0', type: 'uint256' }, { internalType: 'uint256', name: 'amount1', type: 'uint256' }, ], stateMutability: 'payable', type: 'function', }, { inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], name: 'positions', outputs: [ { internalType: 'uint96', name: 'nonce', type: 'uint96' }, { internalType: 'address', name: 'operator', type: 'address' }, { internalType: 'address', name: 'token0', type: 'address' }, { internalType: 'address', name: 'token1', type: 'address' }, { internalType: 'uint24', name: 'fee', type: 'uint24' }, { internalType: 'int24', name: 'tickLower', type: 'int24' }, { internalType: 'int24', name: 'tickUpper', type: 'int24' }, { internalType: 'uint128', name: 'liquidity', type: 'uint128' }, { internalType: 'uint256', name: 'feeGrowthInside0LastX128', type: 'uint256' }, { internalType: 'uint256', name: 'feeGrowthInside1LastX128', type: 'uint256' }, { internalType: 'uint128', name: 'tokensOwed0', type: 'uint128' }, { internalType: 'uint128', name: 'tokensOwed1', type: 'uint128' }, ], stateMutability: 'view', type: 'function', }, ] const FACTORY_ABI = parseAbi([ 'function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool)' ]); const POOL_ABI = parseAbi([ 'function liquidity() external view returns (uint128)', 'function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)' ]); const Payments_ABI = [ { inputs: [ { internalType: 'uint256', name: 'amountMinimum', type: 'uint256', }, { internalType: 'address', name: 'recipient', type: 'address', }, ], name: 'unwrapWETH9', outputs: [], stateMutability: 'payable', type: 'function', }, { inputs: [ { internalType: 'address', name: 'token', type: 'address', }, { internalType: 'uint256', name: 'amountMinimum', type: 'uint256', }, { internalType: 'address', name: 'recipient', type: 'address', }, ], name: 'sweepToken', outputs: [], stateMutability: 'payable', type: 'function', }, ]; const POSITION_MANAGER_ADDRESS = '0x46A15B0b27311cedF172AB29E4f4766fbE7F4364' as Address; const FACTORY_ADDRESS = '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865' as Address; export const removeLiquidityV3 = async (account: PrivateKeyAccount, tokenId: BigInt, percent: number) => { const calldatas: Hex[] = [] const client = walletClient(account).extend(publicActions) const positionInfo = await publicClient.readContract({ address: POSITION_MANAGER_ADDRESS, abi: positionManagerABI, functionName: 'positions', args: [tokenId] }) as any[] const bnb = Native.onChain(ChainId.BSC) const liquidity: bigint = new Percent(percent!, 100).multiply(positionInfo[7]).quotient // remove liquidity calldatas.push(encodeFunctionData({ abi: positionManagerABI, functionName: 'decreaseLiquidity', args: [ { tokenId, liquidity, amount0Min: BigInt(1), amount1Min: BigInt(1), deadline: Math.floor(Date.now() / 1000) + 1200, }, ], })) const involvesETH = getAddress(bnb.wrapped.address) === getAddress(positionInfo[2]) || getAddress(bnb.wrapped.address) === getAddress(positionInfo[3]) calldatas.push(encodeFunctionData({ abi: positionManagerABI, functionName: 'collect', args: [ { tokenId, recipient: involvesETH ? zeroAddress : account.address, amount0Max: maxUint128, amount1Max: maxUint128, }, ], })) if (involvesETH) { const poolAddrs = await client.readContract({ address: FACTORY_ADDRESS, abi: FACTORY_ABI, functionName: 'getPool', args: [ positionInfo[2] as Address, positionInfo[3] as Address, positionInfo[4] ] }) const slot0 = await client.readContract({ address: poolAddrs as Address, abi: POOL_ABI, functionName: 'slot0', }) const token0Amount = PositionMath.getToken0Amount( slot0[1], positionInfo[5], positionInfo[6], slot0[0], positionInfo[7], ) const discountedAmount0 = new Percent(percent!, 100).multiply(token0Amount).quotient const token1Amount = PositionMath.getToken1Amount( slot0[1], positionInfo[5], positionInfo[6], slot0[0], positionInfo[7], ) const discountedAmount1 = new Percent(percent!, 100).multiply(token1Amount).quotient const ethAmount = getAddress(bnb.wrapped.address) === getAddress(positionInfo[2]) ? discountedAmount0 : discountedAmount1 const token = getAddress(bnb.wrapped.address) === getAddress(positionInfo[2]) ? positionInfo[3] : positionInfo[2] const tokenAmount = getAddress(bnb.wrapped.address) === getAddress(positionInfo[2]) ? discountedAmount1 : discountedAmount0 calldatas.push(encodeFunctionData({ abi: Payments_ABI, functionName: 'unwrapWETH9', args: [ethAmount, account.address], })) calldatas.push(encodeFunctionData({ abi: Payments_ABI, functionName: 'sweepToken', args: [token, tokenAmount, account.address], })) } const data = Multicall.encodeMulticall(calldatas) const tx = await client.sendTransaction({ to: POSITION_MANAGER_ADDRESS, data: data, value: BigInt(0), account: account, chain: client.chain as any, }) return tx } ``` -------------------------------------------------------------------------------- /src/functions/pancakeAddLiquidityTool.ts: -------------------------------------------------------------------------------- ```typescript import { parseAbi, type Address, Hex, PrivateKeyAccount, } from 'viem'; import { Pool, Position, nearestUsableTick, FeeAmount, encodeSqrtRatioX96 } from '@pancakeswap/v3-sdk'; import { ChainId, Currency, CurrencyAmount, Percent, Token } from '@pancakeswap/sdk'; import dotenv from 'dotenv'; import { publicClient, walletClient } from '../config.js'; import { TICK_SPACINGS, TickMath } from "@pancakeswap/v3-sdk"; import { FACTORY_ADDRESSES, NFT_POSITION_MANAGER_ADDRESSES } from '@pancakeswap/v3-sdk'; const POSITION_MANAGER_ADDRESS = NFT_POSITION_MANAGER_ADDRESSES[ChainId.BSC]; const FACTORY_ADDRESS = FACTORY_ADDRESSES[ChainId.BSC]; dotenv.config(); // Contract ABI definitions const FACTORY_ABI = parseAbi([ 'function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool)' ]); const POOL_ABI = parseAbi([ 'function liquidity() external view returns (uint128)', 'function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)' ]); const ERC20_ABI = parseAbi([ 'function allowance(address owner, address spender) external view returns (uint256)', 'function approve(address spender, uint256 amount) external returns (bool)', 'function balanceOf(address account) external view returns (uint256)' ]); const POSITION_MANAGER_ABI = [ { "inputs": [{ "components": [{ "internalType": "address", "name": "token0", "type": "address" }, { "internalType": "address", "name": "token1", "type": "address" }, { "internalType": "uint24", "name": "fee", "type": "uint24" }, { "internalType": "int24", "name": "tickLower", "type": "int24" }, { "internalType": "int24", "name": "tickUpper", "type": "int24" }, { "internalType": "uint256", "name": "amount0Desired", "type": "uint256" }, { "internalType": "uint256", "name": "amount1Desired", "type": "uint256" }, { "internalType": "uint256", "name": "amount0Min", "type": "uint256" }, { "internalType": "uint256", "name": "amount1Min", "type": "uint256" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "deadline", "type": "uint256" }], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple" }], "name": "mint", "outputs": [{ "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "uint128", "name": "liquidity", "type": "uint128" }, { "internalType": "uint256", "name": "amount0", "type": "uint256" }, { "internalType": "uint256", "name": "amount1", "type": "uint256" }], "stateMutability": "payable", "type": "function" } ]; async function approveTokensIfNeeded( account: PrivateKeyAccount, token: Currency, spender: Address, amount: string, ): Promise<void> { if (!token.isNative) { const tokenAddress = token.address as Address; const accountAddress = account.address; const allowance = await publicClient.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'allowance', args: [accountAddress, spender] }); if (BigInt(allowance.toString()) < BigInt(amount)) { const hash = await walletClient(account).writeContract({ account: account, address: tokenAddress, abi: ERC20_ABI, functionName: 'approve', args: [spender, BigInt(amount)], }); await publicClient.waitForTransactionReceipt({ hash }); } } } function sortTokens( tokenA: Currency, tokenB: Currency, amountA: CurrencyAmount<Currency>, amountB: CurrencyAmount<Currency>, ): [Token, Token, CurrencyAmount<Currency>, CurrencyAmount<Currency>] { let token0 = tokenA.isNative ? tokenA.wrapped : tokenA; let token1 = tokenB.isNative ? tokenB.wrapped : tokenB; if (token0.sortsBefore(token1)) { return [token0, token1, amountA, amountB]; } else { return [token1, token0, amountB, amountA]; } } async function checkBalance( account: PrivateKeyAccount, token: Currency, amount: CurrencyAmount<Currency> ) { const accountAddress = account.address; if (token.isNative) { const balance = await publicClient.getBalance({ address: accountAddress }); const balanceAmount = CurrencyAmount.fromRawAmount(token, balance.toString()); if (balanceAmount.lessThan(amount)) { throw new Error(`Insufficient balance of ${token.symbol}`); } return } const balance = await publicClient.readContract({ address: token.address as Address, abi: ERC20_ABI, functionName: 'balanceOf', args: [accountAddress] }); const balanceAmount = CurrencyAmount.fromRawAmount(token, balance.toString()); if (balanceAmount.lessThan(amount)) { throw new Error(`Insufficient balance of ${token.symbol}`); } } /** * Add V3 liquidity * @param tokenA first token * @param tokenB second token * @param fee fee tier * @param amountA amount of tokenA * @param amountB amount of tokenB * @param account account * @param slippageTolerance slippage tolerance * @param deadline transaction deadline * @param priceLower lower price bound percentage (default: 80% of current price) * @param priceUpper upper price bound percentage (default: 120% of current price) * @returns transaction receipt */ export async function addLiquidityV3( tokenA: Currency, tokenB: Currency, fee: FeeAmount, amountA: CurrencyAmount<Currency>, amountB: CurrencyAmount<Currency>, account: PrivateKeyAccount, slippageTolerance: Percent = new Percent('50', '10000'), // default 0.5% deadline: number = Math.floor(Date.now() / 1000) + 20 * 60, // default 20 minutes priceLower: number = 0.8, priceUpper: number = 1.2 ): Promise<Hex> { await Promise.all([ approveTokensIfNeeded(account, tokenA, POSITION_MANAGER_ADDRESS, amountA.quotient.toString()), approveTokensIfNeeded(account, tokenB, POSITION_MANAGER_ADDRESS, amountB.quotient.toString()), ]); await checkBalance(account, tokenA, amountA) await checkBalance(account, tokenB, amountB) const [token0, token1, amount0, amount1] = sortTokens(tokenA, tokenB, amountA, amountB); const poolAddress = await publicClient.readContract({ address: FACTORY_ADDRESS, abi: FACTORY_ABI, functionName: 'getPool', args: [ token0.address as Address, token1.address as Address, fee ] }) as Address; if (!poolAddress || poolAddress === '0x0000000000000000000000000000000000000000') { throw new Error(`Pool for ${tokenA.symbol}/${tokenB.symbol} not found`); } const [liquidity, slot0] = await Promise.all([ publicClient.readContract({ address: poolAddress, abi: POOL_ABI, functionName: 'liquidity' }), publicClient.readContract({ address: poolAddress, abi: POOL_ABI, functionName: 'slot0' }) ]); const pool = new Pool( token0, token1, fee, (slot0 as any)[0].toString(), // sqrtPriceX96 liquidity.toString(), (slot0 as any)[1] // tick ); // Retrieve tickSpacing from the SDK constants const tickSpacing = TICK_SPACINGS[fee]; // fee should correspond to a valid // Convert prices to square root ratio and then to ticks const priceLowerRatio = encodeSqrtRatioX96(priceLower * 1e18, 1e18); const priceUpperRatio = encodeSqrtRatioX96(priceUpper * 1e18, 1e18); const lowerPriceTick = TickMath.getTickAtSqrtRatio(priceLowerRatio); const upperPriceTick = TickMath.getTickAtSqrtRatio(priceUpperRatio); // Round ticks to the nearest valid tick const tickLower = nearestUsableTick(lowerPriceTick, tickSpacing); const tickUpper = nearestUsableTick(upperPriceTick, tickSpacing); const position = Position.fromAmounts({ pool, tickLower, tickUpper, amount0: amount0.quotient.toString(), amount1: amount1.quotient.toString(), useFullPrecision: true }); const { amount0: amount0Min, amount1: amount1Min } = position.mintAmountsWithSlippage(slippageTolerance); const value = tokenA.isNative ? amountA.quotient.toString() : tokenB.isNative ? amountB.quotient.toString() : '0'; const mintParams = { token0: token0.address as Address, token1: token1.address as Address, fee, tickLower, tickUpper, amount0Desired: BigInt(amount0.quotient.toString()), amount1Desired: BigInt(amount1.quotient.toString()), amount0Min: BigInt(amount0Min.toString()), amount1Min: BigInt(amount1Min.toString()), recipient: account.address, deadline: BigInt(deadline) }; const hash = await walletClient(account).writeContract({ address: POSITION_MANAGER_ADDRESS, abi: POSITION_MANAGER_ABI, functionName: 'mint', args: [mintParams], value: BigInt(value), account: account }); return hash; } ``` -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- ```typescript import { type Hex } from "viem"; import { exec } from 'child_process'; import fs from 'fs'; import path from 'path'; import os from 'os'; import { decryptPrivateKey, } from "./PrivateAES.js"; import { privateKeyToAccount } from "viem/accounts"; import { publicClient } from "./config.js"; const platform = os.platform(); export function buildTxUrl(txHash: Hex | undefined): string | undefined { if (!txHash) { return undefined; } const txUrl = `https://bscscan.com/tx/${txHash}`; return txUrl } export async function checkTransactionHash(txHash: Hex): Promise<string> { const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash, retryCount: 300, retryDelay: 100, }); const txUrl = `https://bscscan.com/tx/${txHash}`; if (txReceipt.status !== "success") { throw new Error(`Please check the transaction results on bscscan, ${txUrl}`); } return txUrl; } export function bigIntReplacer(key: string, value: any) { return typeof value === 'bigint' ? value.toString() : value; } export interface InputBoxOptions { title?: string; message?: string; defaultValue?: string; termsText?: string; } export interface InputResult { value: string | null; agreed: boolean; } let passwordLock = false export async function getPassword(isRetry?: boolean, num = 0): Promise<InputResult> { if (passwordLock) { throw new Error("Password lock is enabled. Try again in 24 hours"); } if (num > 10) { passwordLock = true; setTimeout(() => { passwordLock = false; }, 1000 * 60 * 60 * 24); throw new Error("You have entered the wrong password too many times."); } const passwordResp = await showInputBoxWithTerms(isRetry); if (!passwordResp.value) { throw new Error("You did not enter a password."); } if (passwordResp.value.length < 8 || passwordResp.value.length > 128) { throw new Error("The password must be between 8 and 128 characters."); } const password = passwordResp.value; const BSC_WALLET_PRIVATE_KEY = process.env.BSC_WALLET_PRIVATE_KEY as Hex if (!BSC_WALLET_PRIVATE_KEY) { throw new Error("BSC_WALLET_PRIVATE_KEY is not defined"); } try { const pk = await decryptPrivateKey(BSC_WALLET_PRIVATE_KEY, password) const account = privateKeyToAccount( pk as Hex ); const address = process.env.BSC_WALLET_ADDRESS if (!address) { throw new Error("BSC_WALLET_ADDRESS is not defined"); } if (account.address != address) { return await getPassword(true, ++num); } } catch (error) { if (error instanceof Error) { if (error.message === "Password lock is enabled. Try again in 24 hours") { throw error; } if (error.message === "You have entered the wrong password too many times.") { throw error; } } return await getPassword(true, ++num); } return passwordResp; } export function showInputBoxWithTerms(isRetry?: boolean): Promise<InputResult> { let message = "Enter your Wallet Password:"; if (isRetry) { message = "Wrong password, please try again:"; } return new Promise((resolve, reject) => { switch (platform) { case 'darwin': // For macOS, we use AppleScript to show a dialog with both input and checkbox // The AppleScript is more complex but allows for a better UX if (isRetry) { message = "❌" + message } const appleScript = ` tell application "System Events" set userPassword to "" set buttonPressed to "" repeat try set userInput to display dialog "${message}" default answer "" with hidden answer buttons {"cancel", "confirm"} default button "confirm" with icon note set userPassword to text returned of userInput set buttonPressed to button returned of userInput if buttonPressed is "cancel" then exit repeat end if if (length of userPassword >= 8) and (length of userPassword <= 128) then exit repeat end if display dialog "Wallet Password must be between 8 and 128 characters!" buttons {"confirm"} default button "confirm" with icon caution on error -- Handle any errors (like when user clicks the red close button) exit repeat end try end repeat if buttonPressed is not "cancel" then set agreeToTerms to button returned of (display dialog "🔒 You will stay signed in for the next hour." buttons {"no", "yes"} default button "no" with icon caution) return userPassword & "============" & agreeToTerms else return "canceled" end if end tell `; exec(`osascript -e '${appleScript}'`, (error, stdout, stderr) => { if (error) { // User cancelled if (error.code === 1 || error.code === 255) { resolve({ value: null, agreed: false }); } else { reject(error); } return; } if (stdout.trim() === "canceled") { reject(new Error("Please enter the password before using ❕")); return; } const [password, agree] = stdout.trim().split("============"); resolve({ value: password, agreed: agree === "yes" }); }); break; case 'win32': const winCommand = ` Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $form = New-Object System.Windows.Forms.Form $form.Text = 'wallet password' $form.Size = New-Object System.Drawing.Size(450,300) $form.StartPosition = 'CenterScreen' $label = New-Object System.Windows.Forms.Label $label.Location = New-Object System.Drawing.Point(10,20) $label.Size = New-Object System.Drawing.Size(380,40) $label.Text = '${message}' $form.Controls.Add($label) # User input label $userLabel = New-Object System.Windows.Forms.Label $userLabel.Location = New-Object System.Drawing.Point(10,70) $userLabel.Size = New-Object System.Drawing.Size(150,20) $userLabel.Text = 'Input Password:' $form.Controls.Add($userLabel) # User input textbox $passwordTextBox = New-Object System.Windows.Forms.TextBox $passwordTextBox.Location = New-Object System.Drawing.Point(160,70) $passwordTextBox.Size = New-Object System.Drawing.Size(250,20) $passwordTextBox.PasswordChar = '*' $form.Controls.Add($passwordTextBox) # Error message label $errorLabel = New-Object System.Windows.Forms.Label $errorLabel.Location = New-Object System.Drawing.Point(160,95) $errorLabel.Size = New-Object System.Drawing.Size(250,20) $errorLabel.ForeColor = [System.Drawing.Color]::Red $errorLabel.Text = '' $form.Controls.Add($errorLabel) $checkbox = New-Object System.Windows.Forms.CheckBox $checkbox.Location = New-Object System.Drawing.Point(10,130) $checkbox.Size = New-Object System.Drawing.Size(350,20) $checkbox.Text = 'You will stay signed in for the next hour.' $form.Controls.Add($checkbox) $button = New-Object System.Windows.Forms.Button $button.Location = New-Object System.Drawing.Point(175,190) $button.Size = New-Object System.Drawing.Size(100,30) $button.Text = 'Confirm' $button.Add_Click({ # Validate password length if ($passwordTextBox.Text.Length -lt 8 -or $passwordTextBox.Text.Length -gt 128) { $errorLabel.Text = 'Wallet Password must be between 8 and 128 characters!' } else { $form.DialogResult = [System.Windows.Forms.DialogResult]::OK $form.Close() } }) $form.Controls.Add($button) $form.AcceptButton = $button $form.Add_Shown({$form.Activate()}) [void]$form.ShowDialog() if ($form.DialogResult -eq [System.Windows.Forms.DialogResult]::OK) { $result = @{ agreed = $checkbox.Checked value = $passwordTextBox.Text } $jsonResult = ConvertTo-Json -InputObject $result Write-Output $jsonResult } exit 0 ` const tempScriptPath = path.join('.', 'terms_form.ps1'); fs.writeFileSync(tempScriptPath, winCommand); exec(`powershell -ExecutionPolicy Bypass -File "${tempScriptPath}"`, (error, stdout, stderr) => { fs.unlinkSync(tempScriptPath); if (error && error.code !== 1) { resolve({ value: null, agreed: false }); return; } if (!stdout) { reject(new Error("Please enter the password before using ❕")); return; } const stdoutJSON = JSON.parse(stdout); resolve({ value: stdoutJSON.value as string, agreed: stdoutJSON.agreed as boolean }); }); break; default: reject(new Error(`Unsupported platform and command-line input is not available: ${platform}`)); } }); } ```