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