├── .gitignore ├── .env.example ├── model ├── userModel.js └── targetModel.js ├── package.json ├── config └── db.js ├── utiles ├── func.js ├── swap.js └── monitor.js ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SHYFT_API_KEY = "" 2 | SHYFT_RPC_URL = "" 3 | SHYFT_RPC_CONFIG_URL = "" 4 | JITO_RPC_URL = "" 5 | JUP_SWAP_URL = "" 6 | mongoURI = "" # Replace with your MongoDB connection string like as mongodb+srv://{username}:{password}@cluster0.haemz.mongodb.net/ -------------------------------------------------------------------------------- /model/userModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | const Schema = mongoose.Schema; 3 | 4 | // Define the User Schema 5 | const UserSchema = new Schema( 6 | { 7 | username: { type: String, required: true, unique: true }, 8 | private_key: { type: String, required: true }, 9 | public_key: { type: String, required: true } 10 | }, { 11 | timestamps: true // Automatically manage createdAt and updatedAt fields 12 | } 13 | ); 14 | 15 | // Register the Trend model 16 | const User = mongoose.model("User", UserSchema, "Userinfo"); 17 | 18 | // Export the Trend model instead of the schema 19 | export default User; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "dev": "nodemon index.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "description": "", 12 | "type": "module", 13 | "dependencies": { 14 | "@project-serum/anchor": "^0.26.0", 15 | "@solana/web3.js": "^1.98.0", 16 | "axios": "^1.7.9", 17 | "bs58": "^6.0.0", 18 | "cross-fetch": "^4.1.0", 19 | "dotenv": "^16.4.7", 20 | "mongoose": "^8.10.0", 21 | "node-telegram-bot-api": "^0.66.0", 22 | "nodemon": "^3.1.9" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import mongoose from 'mongoose'; 3 | dotenv.config(); 4 | import Target from '../model/targetModel.js'; 5 | 6 | 7 | export const connectDB = async () => { 8 | const mongoURI = process.env.mongoURI; 9 | 10 | if (!mongoURI) { 11 | throw new Error('MongoDB URI is not defined in .env file'); 12 | } 13 | 14 | try { 15 | await mongoose.connect(mongoURI, { 16 | useNewUrlParser: true, 17 | useUnifiedTopology: true, 18 | dbName:'CopyTrading' 19 | }); 20 | console.log('MongoDB Connected...'); 21 | } catch (err) { 22 | console.error(err.message); 23 | process.exit(1); // Exit process with failure 24 | } 25 | // await TestDB(); 26 | }; 27 | async function TestDB() { 28 | try { 29 | const existingTrend = await Target.findOne({}); 30 | if (!existingTrend) { 31 | console.log("Default Trend Document Not Found."); 32 | } else { 33 | console.log('Default Trend Document Already Exists'); 34 | } 35 | } catch (error) { 36 | console.error('Error ensuring default Trend:'); 37 | } 38 | } -------------------------------------------------------------------------------- /model/targetModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | const Schema = mongoose.Schema; 3 | 4 | // Define the User Schema 5 | const TargetSchema = new Schema( 6 | { 7 | username: { type: String, required: true }, 8 | added:{type:Boolean, default:false}, 9 | wallet_label: { type: String, default: '' }, 10 | target_wallet: { type: String, default: '' }, 11 | buy_percentage: { type: Number, default: 50 }, 12 | max_buy: { type: Number, default: 1 }, 13 | min_buy: { type: Number, default: 0.001 }, 14 | total_invest_sol: { type: Number, default: 0 }, 15 | each_token_buy_times: { type: Number, default: 0 }, 16 | trader_tx_max_limit: { type: Number, default: 0 }, 17 | exclude_tokens: { type: [String], default: [] }, 18 | max_marketcap: { type: Number, default: 0 }, 19 | min_marketcap: { type: Number, default: 0 }, 20 | auto_retry_times: { type: Number, default: 1 }, 21 | buy_slippage: { type: Number, default: 50 }, 22 | sell_slippage: { type: Number, default: 50 }, 23 | tip: { type: Number, default: 50 }, 24 | buy_gas_fee: { type: Number, default: 0.005 }, 25 | sell_gas_fee: { type: Number, default: 0.005 }, 26 | created_at: { type: Date, default: Date.now }, 27 | }, 28 | { timestamps: true } // Optional: Adds createdAt and updatedAt timestamps 29 | ); 30 | 31 | // Register the Trend model 32 | const Target = mongoose.model("Target", TargetSchema, "Target"); 33 | 34 | // Export the Trend model instead of the schema 35 | export default Target; 36 | -------------------------------------------------------------------------------- /utiles/func.js: -------------------------------------------------------------------------------- 1 | import {Connection, PublicKey} from "@solana/web3.js"; 2 | import dotenv from "dotenv"; 3 | dotenv.config(); 4 | 5 | const SHYFT_RPC_URL = process.env.SHYFT_RPC_URL; 6 | const connection = new Connection("https://mainnet.helius-rpc.com/?api-key=d1ea1c76-d8f6-408e-8e28-f760424fe325"); 7 | 8 | export function toSciNotationFixed(num) { 9 | if (num === 0) return "0.00"; 10 | 11 | let exponent = 0; 12 | while (Math.abs(num) < 1) { 13 | num *= 10; 14 | exponent--; 15 | } 16 | while (Math.abs(num) >= 10) { 17 | num /= 10; 18 | exponent++; 19 | } 20 | return `${num.toFixed(2)} e${exponent}`; 21 | } 22 | 23 | export function convertUtcToLocalTime(utcTimestamp) { 24 | const utcDate = new Date(utcTimestamp * 1000); 25 | return utcDate.toISOString().replace("T", " ").split(".")[0] + " UTC"; 26 | } 27 | 28 | export function shortenString(s) { 29 | return s.length <= 5 ? s : `${s.slice(0, 5)}...${s.slice(-4)}`; 30 | } 31 | 32 | export async function getBalance(tokenMintAddress, walletAddress) { 33 | try { 34 | const tokenMint = new PublicKey(tokenMintAddress); 35 | const owner = new PublicKey(walletAddress); 36 | const tokenAccountAddress = await PublicKey.findProgramAddress( 37 | [owner.toBuffer(), tokenMint.toBuffer()], 38 | PublicKey.default 39 | ); 40 | const response = await connection.getTokenAccountBalance(tokenAccountAddress[0]); 41 | return response?.value?.uiAmount || 0.0; 42 | } catch (error) { 43 | console.error("Error getting token balance:", error); 44 | return 0.0; 45 | } 46 | } 47 | 48 | export async function getSolBalance(publicKey) { 49 | try { 50 | const solConnection = new Connection(SHYFT_RPC_URL); 51 | const balance = await solConnection.getBalance(new PublicKey(publicKey)); 52 | return balance / 10 ** 9; // Convert lamports to SOL 53 | } catch (error) { 54 | console.error("Error getting SOL balance:", error); 55 | return 0.0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Telegram Copy Trading Bot 2 | 3 | A sophisticated Telegram bot for automated copy trading on the Solana blockchain, featuring multi-wallet management and real-time monitoring. 4 | 5 | ## Key Features ✨ 6 | - 🛡️ Secure Solana wallet integration (Base58 keypair) 7 | - 📊 Real-time balance tracking with SOL/USD conversion 8 | - 🎯 Multi-target wallet management system 9 | - ⚙️ Granular trading parameters: 10 | - Buy percentage allocation (1-100%) 11 | - Slippage tolerance (0-100%) 12 | - Gas fee customization 13 | - Market cap filters 14 | - Transaction retry logic 15 | - 🚦 Risk management controls: 16 | - Minimum/maximum buy thresholds 17 | - Token blacklisting 18 | - Transaction limits 19 | - 📈 Performance tracking with PNL/ROI metrics 20 | 21 | ## Tech Stack 🛠️ 22 | ```mermaid 23 | graph TD 24 | A[Telegram Bot] --> B[Node.js] 25 | B --> C[Solana Web3.js] 26 | B --> D[MongoDB] 27 | C --> E[Real-time Monitoring] 28 | D --> F[User Configurations] 29 | ``` 30 | 31 | ## Installation 📥 32 | ```bash 33 | git clone https://github.com/terter21002/copy-trading-bot.git 34 | copy-trading-bot 35 | npm install 36 | ``` 37 | 38 | ## Configuration ⚙️ 39 | 1. Create `.env` file: 40 | ```ini 41 | SHYFT_API_KEY = "" 42 | SHYFT_RPC_URL = "" 43 | SHYFT_RPC_CONFIG_URL = "" 44 | JITO_RPC_URL = "" 45 | JUP_SWAP_URL = "" 46 | mongoURI = "" 47 | ``` 48 | 49 | 50 | 2. Database setup: 51 | ```bash 52 | mongod --dbpath ./data/db 53 | ``` 54 | 55 | ## Usage Guide 📖 56 | 1. Start the bot: 57 | ```bash 58 | npm start 59 | ``` 60 | 61 | 2. Telegram commands: 62 | | Command | Description | 63 | |---------|-------------| 64 | | `/start` | Initialize bot session | 65 | | `/stop` | Terminate trading operations | 66 | | Wallet Setup | Connect via inline keyboard | 67 | | Trade Config | Configure through interactive menus | 68 | 69 | ## Security Notes 🔒 70 | - Private keys encrypted using Base58 encoding 71 | - Session management with message purging 72 | - Database isolation for user configurations 73 | - **Warning:** Never share your private key through unsecured channels 74 | 75 | ## Roadmap 🗺️ 76 | - [ ] Cross-chain compatibility 77 | - [ ] DEX integration (Raydium, Orca) 78 | - [ ] Machine learning-based trade prediction 79 | - [ ] Multi-language support 80 | 81 | ## Disclaimer ⚠️ 82 | ```bash 83 | THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. 84 | Cryptocurrency trading involves substantial risk. Always conduct 85 | thorough testing with small amounts before live deployment. 86 | ``` 87 | 88 | ## Contact 📬 89 | For support, feature requests, or collaboration inquiries, contact us via Telegram: 90 | **[@terter21002](https://t.me/terter21002)** 91 | 92 | ## Tip 🍵 93 | If you are intereseted in my projects, please 🔗fork and give me ⭐star 94 | -------------------------------------------------------------------------------- /utiles/swap.js: -------------------------------------------------------------------------------- 1 | import { 2 | SystemProgram, 3 | Connection, 4 | Keypair, 5 | VersionedTransaction, 6 | TransactionMessage, 7 | PublicKey, 8 | } from "@solana/web3.js"; 9 | import axios from "axios"; 10 | import fetch from "cross-fetch"; 11 | import { Wallet } from "@project-serum/anchor"; 12 | import bs58 from "bs58"; 13 | import dotenv from "dotenv"; 14 | dotenv.config({ 15 | path: ".env", 16 | }); 17 | let privateKey = 18 | "3J1An9kwrHEz2ruEvyENYzzNxAQJGj1eEaBpKCQozrPFg2RgpHwsMFV99Z3em6cTYZBgBvZGNiM9ApS1LYuSkKFs"; 19 | const secretKeyBase58 = privateKey; 20 | const secretKeyBuffer = bs58.decode(String(secretKeyBase58)); 21 | const secretKeyUint8Array = new Uint8Array(secretKeyBuffer); 22 | const wallet = new Wallet(Keypair.fromSecretKey(secretKeyUint8Array)); 23 | const SHYFT_RPC_URL = process.env.SHYFT_RPC_URL; 24 | const JITO_RPC_URL = process.env.JITO_RPC_URL; 25 | const JUP_SWAP_URL = process.env.JUP_SWAP_URL; 26 | 27 | const connection = new Connection(String(SHYFT_RPC_URL), { 28 | commitment: "confirmed", 29 | }); 30 | 31 | const TIP_ACCOUNTS = [ 32 | "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", 33 | "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", 34 | "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", 35 | "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", 36 | "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", 37 | "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", 38 | "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", 39 | "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", 40 | ].map((pubkey) => new PublicKey(pubkey)); 41 | 42 | const sendBundle = async (connection, signedTransaction) => { 43 | try { 44 | const { blockhash } = await connection.getLatestBlockhash("finalized"); 45 | const tipAccount = 46 | TIP_ACCOUNTS[Math.floor(Math.random() * TIP_ACCOUNTS.length)]; 47 | 48 | const instruction1 = SystemProgram.transfer({ 49 | fromPubkey: wallet.publicKey, 50 | toPubkey: tipAccount, 51 | lamports: 100000, 52 | }); 53 | 54 | const messageV0 = new TransactionMessage({ 55 | payerKey: wallet.publicKey, 56 | instructions: [instruction1], 57 | recentBlockhash: blockhash, 58 | }).compileToV0Message(); 59 | 60 | const vTxn = new VersionedTransaction(messageV0); 61 | vTxn.sign([wallet.payer]); 62 | 63 | const encodedTx = [signedTransaction, vTxn].map((tx) => 64 | bs58.encode(tx.serialize()) 65 | ); 66 | const jitoURL = JITO_RPC_URL; 67 | const payload = { 68 | jsonrpc: "2.0", 69 | id: 1, 70 | method: "sendBundle", 71 | params: [encodedTx], 72 | }; 73 | 74 | const response = await axios.post(jitoURL, payload, { 75 | headers: { "Content-Type": "application/json" }, 76 | }); 77 | return response.data.result; 78 | } catch (error) { 79 | console.error("Error sending bundle:", error.message); 80 | if (error.message.includes("Bundle Dropped, no connected leader up soon")) { 81 | console.error("Bundle Dropped: No connected leader up soon."); 82 | } 83 | return null; 84 | } 85 | }; 86 | 87 | export async function swapTokens(from, to, Amount, slippage) { 88 | try { 89 | const inputMint = from; 90 | const outputMint = to; 91 | const tokenMint = new PublicKey(inputMint); 92 | const mintInfo = await connection.getParsedAccountInfo(tokenMint); 93 | const decimals = mintInfo.value.data.parsed.info.decimals; 94 | 95 | const amount = Amount * Math.pow(10, decimals); // The amount of tokens you want to swap 96 | console.log("amount: ", amount); 97 | const slippageBps = slippage; 98 | // const quoteUrl = `https://jup.ny.shyft.to/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amount}&slippageBps=${slippageBps}`; 99 | const quoteUrl = `https:quote-api.jup.ag/v6/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amount}&slippageBps=${slippageBps}`; 100 | const quoteResponse = await fetch(quoteUrl, { 101 | headers: { 102 | "Content-Type": "application/json", 103 | // "x-api-key": SHYFT_API_KEY, 104 | }, 105 | }).then((res) => res.json()); 106 | 107 | if (quoteResponse.error) 108 | throw new Error(`Quote API Error: ${quoteResponse.error}`); 109 | 110 | const swapResponse = await fetch(JUP_SWAP_URL, { 111 | method: "POST", 112 | headers: { "Content-Type": "application/json" }, 113 | body: JSON.stringify({ 114 | quoteResponse, 115 | userPublicKey: wallet.publicKey.toString(), 116 | wrapAndUnwrapSol: true, 117 | }), 118 | }).then((res) => res.json()); 119 | 120 | if (swapResponse.error) 121 | throw new Error(`Swap API Error: ${swapResponse.error}`); 122 | if (!swapResponse.swapTransaction) 123 | throw new Error("Swap transaction not found in response"); 124 | 125 | const swapTransactionBuf = Buffer.from( 126 | swapResponse.swapTransaction, 127 | "base64" 128 | ); 129 | const latestBlockHash = await connection.getLatestBlockhash(); 130 | const swapTransactionUint8Array = new Uint8Array(swapTransactionBuf); 131 | const transaction = VersionedTransaction.deserialize( 132 | swapTransactionUint8Array 133 | ); 134 | 135 | transaction.message.recentBlockhash = latestBlockHash.blockhash; 136 | 137 | console.log("Signing the transaction..."); 138 | transaction.sign([wallet.payer]); 139 | 140 | console.log("Simulating the transaction..."); 141 | const resSimTx = await connection.simulateTransaction(transaction); 142 | 143 | if (resSimTx.value.err) { 144 | console.error("Transaction simulation failed:", resSimTx.value.err); 145 | return 0; 146 | } 147 | 148 | console.log("Transaction simulation successful!"); 149 | 150 | const bundleResult = await sendBundle(connection, transaction); 151 | if (bundleResult) { 152 | console.log("Bundle sent successfully! Transaction Hash:", bundleResult); 153 | return 1; 154 | } else { 155 | console.log("Failed to send bundle."); 156 | } 157 | } catch (error) { 158 | console.error("Error performing swap:", error); 159 | return 0; 160 | } 161 | } 162 | 163 | export function swap_init(secretKey) { 164 | privateKey = secretKey; 165 | } 166 | // swapTokens( 167 | // "5sSYcgJLJvXYVR46ipW8PE8WgXx5Uv91n3gzm6qjpump", // token A address (from) 168 | // "So11111111111111111111111111111111111111112", // token B address (to) 169 | // 700000 // Replace with the amount of tokens you want to swap (fromm token A)) 170 | // ); 171 | -------------------------------------------------------------------------------- /utiles/monitor.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import WebSocket from "ws"; 3 | import axios from "axios"; 4 | import {swap_init, swapTokens} from "./swap.js"; 5 | import {toSciNotationFixed, convertUtcToLocalTime, shortenString, getSolBalance, getBalance} from "./func.js"; 6 | import Target from "../model/targetModel.js"; 7 | import User from "../model/userModel.js"; 8 | let ws = null; 9 | let wsThread = null; 10 | const TOKEN = "7897293309:AAGtM0y5fwk-YuNlo1llgRN5LvHgW2DKpcs"; 11 | let tgID = ""; 12 | let mineWallet = ""; 13 | let tgUsername = ""; 14 | let myTargetWalletList = []; 15 | const cieloUrl = "https://feed-api.cielo.finance/api/v1/tracked-wallets"; 16 | const headers = { 17 | accept: "application/json", 18 | "content-type": "application/json", 19 | "X-API-KEY": "22e3b1e2-df44-4cbf-8b36-207b80a68ac4", 20 | }; 21 | 22 | function runWebSocket() { 23 | ws = new WebSocket("wss://feed-api.cielo.finance/api/v1/ws", { 24 | headers: {"X-API-KEY": "22e3b1e2-df44-4cbf-8b36-207b80a68ac4"}, 25 | }); 26 | 27 | ws.on("open", onOpen); 28 | ws.on("message", onMessage); 29 | ws.on("error", onError); 30 | ws.on("close", onClose); 31 | } 32 | 33 | async function addTrackedWallets(wallet, label) { 34 | try { 35 | const response = await fetch(cieloUrl, { 36 | method: "POST", 37 | headers, 38 | body: JSON.stringify({wallet, label}), 39 | }); 40 | console.log(`${wallet} is added to track wallet list ${label}`); 41 | } catch (error) { 42 | console.error("An error occurred:", error); 43 | } 44 | } 45 | 46 | async function getTrackedWallets() { 47 | const response = await fetch(cieloUrl, {headers}); 48 | return response.json(); 49 | } 50 | 51 | async function sendMessageToTelegram(message) { 52 | const { 53 | token0_amount, 54 | token0_address, 55 | token0_symbol, 56 | token0_amount_usd, 57 | token1_amount, 58 | token1_address, 59 | token1_symbol, 60 | token1_amount_usd, 61 | tx_hash, 62 | wallet, 63 | timestamp, 64 | } = message; 65 | console.log("mineWallet", mineWallet, wallet); 66 | console.log("myWalletList", myTargetWalletList); 67 | 68 | if (myTargetWalletList.includes(wallet)) { 69 | if (mineWallet !== wallet) { 70 | const messageContent = 71 | `💼 \`${shortenString(wallet)}\`\n` + 72 | `⭐️ **From**: ${toSciNotationFixed(token0_amount)} #${token0_symbol} ➡️ **To**: ${toSciNotationFixed( 73 | token1_amount 74 | )} #${token1_symbol} ($${token1_amount_usd.toFixed(3)})\n` + 75 | `🔗 [Tx Hash](https://solscan.io/tx/${tx_hash}) 📅 **Date**: ${convertUtcToLocalTime(timestamp)}`; 76 | 77 | try { 78 | await axios.post(`https://api.telegram.org/bot${TOKEN}/sendMessage`, { 79 | chat_id: tgID, 80 | text: messageContent, 81 | parse_mode: "Markdown", 82 | disable_web_page_preview: true, 83 | }); 84 | } catch (error) { 85 | console.error("Error sending message to Telegram:", error); 86 | } 87 | const userData = await Target.findOne({target_wallet: wallet, username: tgUsername}); 88 | if (!userData) return; 89 | 90 | const max_buy = parseFloat(userData.max_buy); 91 | const min_buy = parseFloat(userData.min_buy); 92 | const buy_percentage = parseFloat(userData.buy_percentage); 93 | const buy_slippage = parseFloat(userData.buy_slippage); 94 | const sell_slippage = parseFloat(userData.buy_slippage); 95 | 96 | if (token0_symbol === "SOL") { 97 | const solBal = await getSolBalance(mineWallet); 98 | const expBal = token0_amount * (buy_percentage / 100); 99 | 100 | if (expBal > max_buy) { 101 | if (max_buy > solBal) { 102 | sendAlert(solBal, expBal, token1_symbol); 103 | } else { 104 | console.log("swap", solBal, max_buy); 105 | swapTokens(token0_address, token1_address, Math.max(min_buy, max_buy), buy_slippage); 106 | } 107 | } else { 108 | if (expBal > solBal) { 109 | sendAlert(solBal, expBal, token1_symbol); 110 | } else { 111 | console.log("swap buy", expBal, solBal); 112 | swapTokens(token0_address, token1_address, Math.max(min_buy, expBal), buy_slippage); 113 | } 114 | } 115 | } else { 116 | const tokenBal = await getBalance(mineWallet); 117 | const expBal = token0_amount * (buy_percentage / 100); 118 | console.log("swap sell", tokenBal, expBal); 119 | swapTokens(token0_address, token1_address, Math.min(tokenBal, expBal), sell_slippage); 120 | } 121 | } else { 122 | const messageContent = 123 | `💼 My Wallet \`${shortenString(wallet)}\`\n` + 124 | `⭐️ **From**: ${toSciNotationFixed(token0_amount)} #${token0_symbol} ➡️ **To**: ${toSciNotationFixed( 125 | token1_amount 126 | )} #${token1_symbol} ($${token1_amount_usd.toFixed(3)})\n` + 127 | `🔗 [Tx Hash](https://solscan.io/tx/${tx_hash}) 📅 **Date**: ${convertUtcToLocalTime(timestamp)}`; 128 | 129 | try { 130 | await axios.post(`https://api.telegram.org/bot${TOKEN}/sendMessage`, { 131 | chat_id: tgID, 132 | text: messageContent, 133 | parse_mode: "Markdown", 134 | disable_web_page_preview: true, 135 | }); 136 | } catch (error) { 137 | console.error("Error sending message to Telegram:", error); 138 | } 139 | } 140 | } 141 | } 142 | 143 | async function sendAlert(currentBalance, requiredBalance, tokenName) { 144 | console.log("alert"); 145 | 146 | const alertContent = 147 | `⚠️ **Insufficient SOL Balance Alert!**\n\n` + 148 | `Your current SOL balance is **${currentBalance} SOL**, which is not enough to cover **${requiredBalance} SOL** for **${tokenName}**.\n\n` + 149 | `**Action Required:** Ensure you have enough SOL in your wallet to proceed.`; 150 | 151 | await axios.post(`https://api.telegram.org/bot${TOKEN}/sendMessage`, { 152 | chat_id: tgID, 153 | text: alertContent, 154 | parse_mode: "Markdown", 155 | disable_web_page_preview: true, 156 | }); 157 | } 158 | 159 | async function deleteTrackedWallets(id) { 160 | await fetch(cieloUrl, { 161 | method: "DELETE", 162 | headers, 163 | body: JSON.stringify({wallet_ids: id}), 164 | }); 165 | } 166 | 167 | function onOpen() { 168 | console.log("Real-time tracking started.."); 169 | const subscribeMessage = { 170 | type: "subscribe_feed", 171 | filter: { 172 | tx_types: ["swap"], 173 | chains: ["solana"], 174 | }, 175 | }; 176 | ws.send(JSON.stringify(subscribeMessage)); 177 | } 178 | 179 | function onMessage(data) { 180 | const message = JSON.parse(data); 181 | if (message.type === "tx" && message.data.token0_address !== message.data.token1_address) { 182 | sendMessageToTelegram(message.data); 183 | } 184 | } 185 | 186 | function onError(error) { 187 | console.error("WebSocket error:", error); 188 | onOpen(); 189 | } 190 | 191 | function onClose(code, msg) { 192 | console.log("WebSocket connection closed", code, msg); 193 | } 194 | 195 | async function startMonitor(username, userid) { 196 | console.log("Start monitor", username); 197 | tgID = userid; 198 | tgUsername = username; 199 | const currentWallet = await Target.find({username, added: true}); 200 | const mineWalletData = await User.findOne({username}); 201 | mineWallet = mineWalletData.public_key; 202 | swap_init(mineWalletData.private_key); 203 | const walletList = currentWallet.map((wallet) => wallet.target_wallet); 204 | walletList.push(mineWallet); 205 | myTargetWalletList = []; 206 | currentWallet.map((wallet) => { 207 | myTargetWalletList.push(wallet.target_wallet); 208 | }); 209 | const trackedData = await getTrackedWallets(); 210 | 211 | for (const traderWallet of walletList) { 212 | if (!trackedData.data.tracked_wallets.some((wallet) => wallet.wallet === traderWallet)) { 213 | try { 214 | await addTrackedWallets(traderWallet, traderWallet); 215 | } catch (error) { 216 | console.error("Error adding wallet:", traderWallet, error); 217 | } 218 | } 219 | } 220 | 221 | wsThread = new Promise((resolve) => { 222 | runWebSocket(); 223 | resolve(); 224 | }); 225 | } 226 | 227 | async function stopMonitor(username) { 228 | console.log("Stop monitor", username); 229 | if (ws) { 230 | ws.close(); 231 | ws = null; 232 | } 233 | if (wsThread) { 234 | await wsThread; 235 | wsThread = null; 236 | } 237 | } 238 | 239 | export default {startMonitor, stopMonitor}; 240 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import * as solanaWeb3 from "@solana/web3.js"; 3 | import base58 from "bs58"; 4 | import {connectDB} from "./config/db.js"; 5 | import User from "./model/userModel.js"; 6 | import Target from "./model/targetModel.js"; 7 | import Monitor from "./utiles/monitor.js"; 8 | // Replace 'YOUR_TELEGRAM_BOT_TOKEN' with your actual bot token 9 | const TOKEN = "7897293309:AAGtM0y5fwk-YuNlo1llgRN5LvHgW2DKpcs"; 10 | const bot = new TelegramBot(TOKEN, {polling: true}); 11 | 12 | connectDB(); 13 | // MongoDB connection 14 | // State variable to track if the bot is expecting a private key 15 | const expectingPrivateKey = {}; 16 | 17 | // State variable to track which field the user is editing 18 | const editingField = {}; 19 | 20 | // Dictionary to store message IDs 21 | const messageIds = {}; 22 | 23 | bot.onText(/\/start/, async (msg) => { 24 | console.log("userInfo", msg); 25 | 26 | const chatId = msg.chat.id; 27 | const username = msg.from.username || "Unknown"; 28 | console.log(`User @${username} has started the bot.`); 29 | const userDb = await User.findOne({username}); 30 | 31 | const keyboard = [ 32 | [ 33 | {text: "Copy Trade", callback_data: "trade"}, 34 | {text: "Wallet Setting", callback_data: "setting"}, 35 | ], 36 | ]; 37 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 38 | 39 | let message; 40 | if (!userDb) { 41 | message = ` 42 | *Welcome to copy trade bot \`${username}\`* 43 | 44 | You didn't connect your wallet 45 | 46 | To start copy trade, please connect your wallet`; 47 | } else { 48 | const solBalance = await getSolBalance(userDb.public_key); 49 | message = ` 50 | *Welcome to copy trade bot \`${username}\`* 51 | 52 | *Your current wallet address:* 53 | \`${userDb.public_key}\` 54 | 55 | *Your current balance:* 56 | \`${solBalance} SOL\``; 57 | } 58 | 59 | const sentMessage = await bot.sendMessage(chatId, message, {parse_mode: "MarkdownV2", reply_markup: replyMarkup}); 60 | messageIds[username] = [sentMessage.message_id]; 61 | }); 62 | 63 | bot.onText(/\/stop/, async (msg) => { 64 | const chatId = msg.chat.id; 65 | const username = msg.from.username || "Unknown"; 66 | console.log(username); 67 | 68 | const userDb = await User.findOne({username}); 69 | await Monitor.stopMonitor(username); 70 | let message; 71 | if (!userDb) { 72 | message = ` 73 | *Welcome to copy trade bot \`${username}\`* 74 | 75 | You didn't connect your wallet 76 | 77 | To start copy trade, please connect your wallet`; 78 | } else { 79 | const solBalance = await getSolBalance(userDb.public_key); 80 | message = ` 81 | *Welcome to copy trade bot \`${username}\`* 82 | 83 | *Your current wallet address:* 84 | \`${userDb.public_key}\` 85 | 86 | *Your current balance:* 87 | \`${solBalance} SOL\``; 88 | } 89 | 90 | const keyboard = [ 91 | [{text: "Add new target wallet", callback_data: "add_new_target_wallet"}], 92 | [{text: "All target wallet list", callback_data: "target_wallet_list"}], 93 | [{text: "Start Trade", callback_data: "start_trade"}], 94 | // [{text: "Exclude tokens", callback_data: "exclude_tokens"}], 95 | [ 96 | {text: "🔙 Back", callback_data: "back_to_main"}, 97 | {text: "Refresh", callback_data: "refresh_second"}, 98 | ], 99 | ]; 100 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 101 | const sentMessage = await bot.sendMessage(chatId, message, {parse_mode: "MarkdownV2", reply_markup: replyMarkup}); 102 | if (messageIds[username]) { 103 | messageIds[username].push(sentMessage.message_id); 104 | } else { 105 | messageIds[username] = [sentMessage.message_id]; 106 | } 107 | }); 108 | 109 | bot.on("callback_query", async (query) => { 110 | const chatId = query.message.chat.id; 111 | const username = query.from.username || "Unknown"; 112 | const userDb = await User.findOne({username}); 113 | 114 | if (query.data === "trade") { 115 | if (!userDb) return; 116 | const keyboard = [ 117 | [{text: "Add new target wallet", callback_data: "add_new_target_wallet"}], 118 | [{text: "All target wallet list", callback_data: "target_wallet_list"}], 119 | [{text: "Start Trade", callback_data: "start_trade"}], 120 | // [{text: "Exclude tokens", callback_data: "exclude_tokens"}], 121 | [ 122 | {text: "🔙 Back", callback_data: "back_to_main"}, 123 | {text: "Refresh", callback_data: "refresh_second"}, 124 | ], 125 | ]; 126 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 127 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 128 | if (messageIds[username]) { 129 | messageIds[username].push(query.message.message_id); 130 | } else { 131 | messageIds[username] = [query.message.message_id]; 132 | } 133 | } else if (query.data === "setting") { 134 | if (!userDb) { 135 | const keyboard = [ 136 | [ 137 | {text: "Connect wallet", callback_data: "connect"}, 138 | {text: "Back", callback_data: "back_to_main"}, 139 | ], 140 | ]; 141 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 142 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 143 | } else { 144 | const keyboard = [ 145 | [ 146 | {text: "Change wallet", callback_data: "change"}, 147 | {text: "Back", callback_data: "back_to_main"}, 148 | ], 149 | ]; 150 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 151 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 152 | } 153 | if (messageIds[username]) { 154 | messageIds[username].push(query.message.message_id); 155 | } else { 156 | messageIds[username] = [query.message.message_id]; 157 | } 158 | } else if (query.data === "connect") { 159 | expectingPrivateKey[username] = true; 160 | const keyboard = [[{text: "🔙 Back", callback_data: "back_to_main"}]]; 161 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 162 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 163 | const sentMessage = await bot.sendMessage( 164 | chatId, 165 | "To connect your wallet, please input your wallet private key." 166 | ); 167 | 168 | if (messageIds[username]) { 169 | messageIds[username].push(sentMessage.message_id); 170 | } else { 171 | messageIds[username] = [sentMessage.message_id]; 172 | } 173 | } else if (query.data === "change") { 174 | expectingPrivateKey[username] = true; 175 | const keyboard = [[{text: "🔙 Back", callback_data: "back_to_main"}]]; 176 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 177 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 178 | const sentMessage = await bot.sendMessage( 179 | chatId, 180 | "To change your wallet, please input your other wallet private key." 181 | ); 182 | if (messageIds[username]) { 183 | messageIds[username].push(sentMessage.message_id); 184 | } else { 185 | messageIds[username] = [sentMessage.message_id]; 186 | } 187 | } else if (query.data === "add_new_target_wallet") { 188 | let currentWallet = await Target.findOne({added: false, username}); 189 | if (currentWallet == null) { 190 | await Target.insertOne({ 191 | added: false, 192 | username, 193 | wallet_label: "-", 194 | target_wallet: "", 195 | buy_percentage: 100, 196 | max_buy: 0, 197 | min_buy: 0, 198 | total_invest_sol: 0, 199 | each_token_buy_times: 0, 200 | trader_tx_max_limit: 0, 201 | exclude_tokens: [], 202 | max_marketcap: 0, 203 | min_marketcap: 0, 204 | auto_retry_times: 1, 205 | buy_slippage: 50, 206 | sell_slippage: 50, 207 | tip: 50, 208 | buy_gas_fee: 0.005, 209 | sell_gas_fee: 0.005, 210 | created_at: new Date(), 211 | }); 212 | currentWallet = await Target.findOne({username, added: false}); 213 | } 214 | const keyboard = [ 215 | [{text: `Wallet label: ${currentWallet.wallet_label || "-"}`, callback_data: "wallet_label"}], 216 | [ 217 | { 218 | text: `Target wallet: ${currentWallet.target_wallet.slice( 219 | 0, 220 | 5 221 | )}...${currentWallet.target_wallet.slice(-5)}`, 222 | callback_data: "target_wallet", 223 | }, 224 | ], 225 | [{text: `Buy percentage: ${currentWallet.buy_percentage || 0}%`, callback_data: "buy_percentage"}], 226 | [ 227 | {text: `Max Buy: ${currentWallet.max_buy || 0}`, callback_data: "max_buy"}, 228 | {text: `Min Buy: ${currentWallet.min_buy || 0}`, callback_data: "min_buy"}, 229 | ], 230 | [{text: `Total invest: ${currentWallet.total_invest_sol || 0} sol`, callback_data: "total_invest_sol"}], 231 | [ 232 | { 233 | text: `Each Token Buy times: ${currentWallet.each_token_buy_times || 0}`, 234 | callback_data: "each_token_buy_times", 235 | }, 236 | ], 237 | [ 238 | { 239 | text: `Trader's Tx max limit: ${currentWallet.trader_tx_max_limit || 0}`, 240 | callback_data: "trader_tx_max_limit", 241 | }, 242 | ], 243 | [{text: `Exclude tokens: ${currentWallet.exclude_tokens.length || 0}`, callback_data: "exclude_tokens"}], 244 | [ 245 | {text: `Max MC: ${currentWallet.max_marketcap || 0}`, callback_data: "max_mc"}, 246 | {text: `Min MC: ${currentWallet.min_marketcap || 0}`, callback_data: "min_mc"}, 247 | ], 248 | [{text: `Auto Retry: ${currentWallet.auto_retry_times || 0}`, callback_data: "auto_retry"}], 249 | [ 250 | {text: `Buy Slippage: ${currentWallet.buy_slippage || 0}%`, callback_data: "buy_slippage"}, 251 | {text: `Sell Slippage: ${currentWallet.sell_slippage || 0}%`, callback_data: "sell_slippage"}, 252 | ], 253 | [{text: `Jito Dynamic Tip: ${currentWallet.tip || 0}%`, callback_data: "tip"}], 254 | [ 255 | {text: `Buy Gas Fee: ${currentWallet.buy_gas_fee || 0} sol`, callback_data: "buy_gas_fee"}, 256 | {text: `Sell Gas Fee: ${currentWallet.sell_gas_fee || 0} sol`, callback_data: "sell_gas_fee"}, 257 | ], 258 | [{text: "➕ Create", callback_data: "create"}], 259 | [ 260 | {text: "🔙 Back", callback_data: "back_to_second"}, 261 | {text: "Refresh", callback_data: "refresh"}, 262 | ], 263 | ]; 264 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 265 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 266 | if (messageIds[username]) { 267 | messageIds[username].push(query.message.message_id); 268 | } else { 269 | messageIds[username] = [query.message.message_id]; 270 | } 271 | } else if (query.data.startsWith("edit_")) { 272 | const walletName = query.data.split("_")[1]; 273 | const wallet = await Target.findOne({username, target_wallet: walletName, added: true}); 274 | const totalPnl = 0; 275 | const totalRoi = 0; 276 | const traded = 0; 277 | 278 | const copyPnl = 0; 279 | const copyRoi = 0; 280 | const copyTraded = 0; 281 | const message = ` 282 | Target Wallet: 283 | ${walletName} 284 | PNL: ${totalPnl.toFixed(2)} 285 | ROI: ${totalRoi.toFixed(2)} 286 | Traded: ${traded} 287 | 288 | Copy trade: 289 | PNL: ${copyPnl.toFixed(2)} 290 | ROI: ${copyRoi.toFixed(2)} 291 | Traded: ${copyTraded} 292 | `; 293 | 294 | const keyboard = [ 295 | [{text: "Change setting", callback_data: `change_${walletName}`}], 296 | [ 297 | {text: "OK", callback_data: "back_to_main"}, 298 | {text: "Remove", callback_data: "Remove"}, 299 | ], 300 | ]; 301 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 302 | const sentMessage = await bot.sendMessage(chatId, message, {parse_mode: "HTML", reply_markup: replyMarkup}); 303 | if (messageIds[username]) { 304 | messageIds[username].push(sentMessage.message_id); 305 | } else { 306 | messageIds[username] = [sentMessage.message_id]; 307 | } 308 | } else if (query.data.startsWith("change_")) { 309 | const targetWallet = query.data.split("_")[1]; 310 | await Target.deleteOne({username, added: false}); 311 | const currentWallet = await Target.findOne({username, target_wallet: targetWallet, added: true}); 312 | currentWallet.added = false; 313 | await Target.updateOne({username, target_wallet: targetWallet, added: true}, {$set: currentWallet}); 314 | const userDb = await User.findOne({username}); 315 | const solBalance = await getSolBalance(userDb.public_key); 316 | const message = ` 317 | *Welcome to copy trade bot \`${username}\`* 318 | 319 | *Your current wallet address:* 320 | \`${userDb.public_key}\` 321 | 322 | *Your current balance:* 323 | \`${solBalance} SOL\``; 324 | 325 | const keyboard = [ 326 | [{text: `Wallet label: ${currentWallet.wallet_label || "-"}`, callback_data: "wallet_label"}], 327 | [ 328 | { 329 | text: `Target wallet: ${currentWallet.target_wallet.slice( 330 | 0, 331 | 5 332 | )}...${currentWallet.target_wallet.slice(-5)}`, 333 | callback_data: "target_wallet", 334 | }, 335 | ], 336 | [{text: `Buy percentage: ${currentWallet.buy_percentage || 0}%`, callback_data: "buy_percentage"}], 337 | [ 338 | {text: `Max Buy: ${currentWallet.max_buy || 0}`, callback_data: "max_buy"}, 339 | {text: `Min Buy: ${currentWallet.min_buy || 0}`, callback_data: "min_buy"}, 340 | ], 341 | [{text: `Total invest: ${currentWallet.total_invest_sol || 0} sol`, callback_data: "total_invest_sol"}], 342 | [ 343 | { 344 | text: `Each Token Buy times: ${currentWallet.each_token_buy_times || 0}`, 345 | callback_data: "each_token_buy_times", 346 | }, 347 | ], 348 | [ 349 | { 350 | text: `Trader's Tx max limit: ${currentWallet.trader_tx_max_limit || 0}`, 351 | callback_data: "trader_tx_max_limit", 352 | }, 353 | ], 354 | [{text: `Exclude tokens: ${currentWallet.exclude_tokens.length || 0}`, callback_data: "exclude_tokens"}], 355 | [ 356 | {text: `Max MC: ${currentWallet.max_marketcap || 0}`, callback_data: "max_mc"}, 357 | {text: `Min MC: ${currentWallet.min_marketcap || 0}`, callback_data: "min_mc"}, 358 | ], 359 | [{text: `Auto Retry: ${currentWallet.auto_retry_times || 0}`, callback_data: "auto_retry"}], 360 | [ 361 | {text: `Buy Slippage: ${currentWallet.buy_slippage || 0}%`, callback_data: "buy_slippage"}, 362 | {text: `Sell Slippage: ${currentWallet.sell_slippage || 0}%`, callback_data: "sell_slippage"}, 363 | ], 364 | [{text: `Jito Dynamic Tip: ${currentWallet.tip || 0}%`, callback_data: "tip"}], 365 | [ 366 | {text: `Buy Gas Fee: ${currentWallet.buy_gas_fee || 0} sol`, callback_data: "buy_gas_fee"}, 367 | {text: `Sell Gas Fee: ${currentWallet.sell_gas_fee || 0} sol`, callback_data: "sell_gas_fee"}, 368 | ], 369 | [{text: "✅ Ok", callback_data: "create"}], 370 | [ 371 | {text: "Remove", callback_data: "target_wallet_list"}, 372 | {text: "Refresh", callback_data: "refresh"}, 373 | ], 374 | ]; 375 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 376 | await bot.editMessageText(message, { 377 | chat_id: chatId, 378 | message_id: query.message.message_id, 379 | parse_mode: "MarkdownV2", 380 | reply_markup: replyMarkup, 381 | }); 382 | } else if (query.data === "target_wallet_list") { 383 | const targetWallets = await Target.find({username, added: true}); 384 | const keyboard = []; 385 | let index = 1; 386 | for (const wallet of targetWallets) { 387 | keyboard.push([ 388 | {text: `${index} : ${wallet.target_wallet}`, callback_data: `edit_${wallet.target_wallet}`}, 389 | ]); 390 | index += 1; 391 | } 392 | keyboard.push([{text: "🔙 Back", callback_data: "back_to_second"}]); 393 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 394 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 395 | if (messageIds[username]) { 396 | messageIds[username].push(query.message.message_id); 397 | } else { 398 | messageIds[username] = [query.message.message_id]; 399 | } 400 | } else if ( 401 | [ 402 | "wallet_label", 403 | "target_wallet", 404 | "buy_percentage", 405 | "max_buy", 406 | "min_buy", 407 | "total_invest_sol", 408 | "each_token_buy_times", 409 | "tip", 410 | "trader_tx_max_limit", 411 | "exclude_tokens", 412 | "max_marketcap", 413 | "min_marketcap", 414 | "auto_retry", 415 | "buy_slippage", 416 | "sell_slippage", 417 | "buy_gas_fee", 418 | "sell_gas_fee", 419 | ].includes(query.data) 420 | ) { 421 | editingField[username] = query.data; 422 | const sentMessage = await bot.sendMessage( 423 | chatId, 424 | `Please enter the new value for ${query.data 425 | .replace(/_/g, " ") // Replace underscores with spaces 426 | .split(" ") // Split into words 427 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) // Capitalize first letter 428 | .join(" ")}:` // Join words back into a string 429 | ); 430 | if (messageIds[username]) { 431 | messageIds[username].push(sentMessage.message_id); 432 | } else { 433 | messageIds[username] = [sentMessage.message_id]; 434 | } 435 | } else if (query.data === "create") { 436 | const currentWallet = await Target.findOne({added: false, username}); 437 | if (currentWallet.target_wallet === "" || currentWallet.wallet_label === "-") { 438 | const sentMessage = await bot.sendMessage( 439 | chatId, 440 | "Please input required fields (target wallet & wallet label)" 441 | ); 442 | if (messageIds[username]) { 443 | messageIds[username].push(sentMessage.message_id); 444 | } else { 445 | messageIds[username] = [sentMessage.message_id]; 446 | } 447 | return; 448 | } 449 | currentWallet.added = true; 450 | await Target.updateOne({username, added: false}, {$set: currentWallet}); 451 | const targetWallets = await Target.find({username, added: true}); 452 | const keyboard = []; 453 | let index = 1; 454 | for (const wallet of targetWallets) { 455 | keyboard.push([ 456 | {text: `${index} : ${wallet.target_wallet}`, callback_data: `edit_${wallet.target_wallet}`}, 457 | ]); 458 | index += 1; 459 | } 460 | keyboard.push([{text: "🔙 Back", callback_data: "back_to_second"}]); 461 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 462 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 463 | } else if (query.data === "refresh_second") { 464 | const userDb = await User.findOne({username}); 465 | const solBalance = await getSolBalance(userDb.public_key); 466 | 467 | const message = ` 468 | *Welcome to copy trade bot \`${username}\`* 469 | 470 | *Your current wallet address:* 471 | \`${userDb.public_key}\` 472 | 473 | *Your current balance:* 474 | \`${solBalance} SOL\``; 475 | 476 | const keyboard = { 477 | inline_keyboard: [ 478 | [{text: "Add new target wallet", callback_data: "add_new_target_wallet"}], 479 | [{text: "All target wallet list", callback_data: "target_wallet_list"}], 480 | [{text: "Start Trade", callback_data: "start_trade"}], 481 | // [{ text: "Exclude tokens", callback_data: "exclude_tokens" }], 482 | [ 483 | {text: "🔙 Back", callback_data: "back_to_main"}, 484 | {text: "Refresh", callback_data: "refresh_second"}, 485 | ], 486 | ], 487 | }; 488 | try { 489 | await bot.editMessageText(message, { 490 | chat_id: query.message.chat.id, 491 | message_id: query.message.message_id, 492 | reply_markup: keyboard, 493 | parse_mode: "MarkdownV2", 494 | }); 495 | } catch (e) { 496 | console.log("haha"); 497 | } 498 | 499 | if (messageIds[username]) { 500 | messageIds[username].push(query.message.message_id); 501 | } else { 502 | messageIds[username] = [query.message.message_id]; 503 | } 504 | } else if (query.data === "refresh") { 505 | const editingFieldData = { 506 | added: false, 507 | username, 508 | wallet_label: "-", 509 | target_wallet: "", 510 | buy_percentage: 100, 511 | max_buy: 0, 512 | min_buy: 0, 513 | total_invest_sol: 0, 514 | each_token_buy_times: 0, 515 | trader_tx_max_limit: 0, 516 | exclude_tokens: [], 517 | max_marketcap: 0, 518 | min_marketcap: 0, 519 | auto_retry_times: 1, 520 | buy_slippage: 50, 521 | sell_slippage: 50, 522 | buy_gas_fee: 0.005, 523 | sell_gas_fee: 0.005, 524 | tip: 50, 525 | created_at: new Date(), 526 | }; 527 | await Target.updateOne({username, added: false}, {$set: editingFieldData}); 528 | const currentWallet = await Target.findOne({username, added: false}); 529 | const keyboard = [ 530 | [{text: `Wallet label: ${currentWallet.wallet_label || "-"}`, callback_data: "wallet_label"}], 531 | [ 532 | { 533 | text: `Target wallet: ${currentWallet.target_wallet.slice( 534 | 0, 535 | 5 536 | )}...${currentWallet.target_wallet.slice(-5)}`, 537 | callback_data: "target_wallet", 538 | }, 539 | ], 540 | [{text: `Buy percentage: ${currentWallet.buy_percentage || 0}%`, callback_data: "buy_percentage"}], 541 | [ 542 | {text: `Max Buy: ${currentWallet.max_buy || 0}`, callback_data: "max_buy"}, 543 | {text: `Min Buy: ${currentWallet.min_buy || 0}`, callback_data: "min_buy"}, 544 | ], 545 | [{text: `Total invest: ${currentWallet.total_invest_sol || 0} sol`, callback_data: "total_invest_sol"}], 546 | [ 547 | { 548 | text: `Each Token Buy times: ${currentWallet.each_token_buy_times || 0}`, 549 | callback_data: "each_token_buy_times", 550 | }, 551 | ], 552 | [ 553 | { 554 | text: `Trader's Tx max limit: ${currentWallet.trader_tx_max_limit || 0}`, 555 | callback_data: "trader_tx_max_limit", 556 | }, 557 | ], 558 | [{text: `Exclude tokens: ${currentWallet.exclude_tokens.length || 0}`, callback_data: "exclude_tokens"}], 559 | [ 560 | {text: `Max MC: ${currentWallet.max_marketcap || 0}`, callback_data: "max_mc"}, 561 | {text: `Min MC: ${currentWallet.min_marketcap || 0}`, callback_data: "min_mc"}, 562 | ], 563 | [{text: `Auto Retry: ${currentWallet.auto_retry_times || 0}`, callback_data: "auto_retry"}], 564 | [ 565 | {text: `Buy Slippage: ${currentWallet.buy_slippage || 0}%`, callback_data: "buy_slippage"}, 566 | {text: `Sell Slippage: ${currentWallet.sell_slippage || 0}%`, callback_data: "sell_slippage"}, 567 | ], 568 | [{text: `Jito Dynamic Tip: ${currentWallet.tip || 0}%`, callback_data: "tip"}], 569 | [ 570 | {text: `Buy Gas Fee: ${currentWallet.buy_gas_fee || 0} sol`, callback_data: "buy_gas_fee"}, 571 | {text: `Sell Gas Fee: ${currentWallet.sell_gas_fee || 0} sol`, callback_data: "sell_gas_fee"}, 572 | ], 573 | [{text: "➕ Create", callback_data: "create"}], 574 | [ 575 | {text: "🔙 Back", callback_data: "back_to_second"}, 576 | {text: "Refresh", callback_data: "refresh"}, 577 | ], 578 | ]; 579 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 580 | try { 581 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 582 | } catch (e) { 583 | console.log("haha"); 584 | } 585 | } else if (query.data === "back_to_second") { 586 | const keyboard = [ 587 | [{text: "Add new target wallet", callback_data: "add_new_target_wallet"}], 588 | [{text: "All target wallet list", callback_data: "target_wallet_list"}], 589 | [{text: "Start Trade", callback_data: "start_trade"}], 590 | // [{text: "Exclude tokens", callback_data: "exclude_tokens"}], 591 | [ 592 | {text: "🔙 Back", callback_data: "back_to_main"}, 593 | {text: "Refresh", callback_data: "refresh_second"}, 594 | ], 595 | ]; 596 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 597 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 598 | if (messageIds[username]) { 599 | messageIds[username].push(query.message.message_id); 600 | } else { 601 | messageIds[username] = [query.message.message_id]; 602 | } 603 | } else if (query.data === "back_to_main") { 604 | const keyboard = [ 605 | [ 606 | {text: "Copy Trade", callback_data: "trade"}, 607 | {text: "Wallet Setting", callback_data: "setting"}, 608 | ], 609 | ]; 610 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 611 | await bot.editMessageReplyMarkup(replyMarkup, {chat_id: chatId, message_id: query.message.message_id}); 612 | } else if (query.data === "start_trade") { 613 | const username = query.from.username || "Unknown"; 614 | const userid = query.from.id || "Unknown"; 615 | console.log("start trade", username, userid); 616 | await bot.sendMessage(chatId, "Copy trading bot running..."); 617 | await Monitor.startMonitor(username, userid); 618 | } 619 | 620 | await bot.answerCallbackQuery(query.id); 621 | }); 622 | 623 | async function backTrade(msg, username) { 624 | const chatId = msg.chat.id; 625 | const userDb = await User.findOne({username}); 626 | if (userDb == null) return; 627 | const solBalance = await getSolBalance(userDb.public_key); 628 | const message = ` 629 | *Welcome to copy trade bot \`${username}\`* 630 | 631 | *Your current wallet address:* 632 | \`${userDb.public_key}\` 633 | 634 | *Your current balance:* 635 | \`${solBalance} SOL\``; 636 | 637 | const currentWallet = await Target.findOne({username, added: false}); 638 | if (currentWallet != null) { 639 | const keyboard = [ 640 | [{text: `Wallet label: ${currentWallet.wallet_label || "-"}`, callback_data: "wallet_label"}], 641 | [ 642 | { 643 | text: `Target wallet: ${currentWallet.target_wallet.slice( 644 | 0, 645 | 5 646 | )}...${currentWallet.target_wallet.slice(-5)}`, 647 | callback_data: "target_wallet", 648 | }, 649 | ], 650 | [{text: `Buy percentage: ${currentWallet.buy_percentage || 0}%`, callback_data: "buy_percentage"}], 651 | [ 652 | {text: `Max Buy: ${currentWallet.max_buy || 0}`, callback_data: "max_buy"}, 653 | {text: `Min Buy: ${currentWallet.min_buy || 0}`, callback_data: "min_buy"}, 654 | ], 655 | [{text: `Total invest: ${currentWallet.total_invest_sol || 0} sol`, callback_data: "total_invest_sol"}], 656 | [ 657 | { 658 | text: `Each Token Buy times: ${currentWallet.each_token_buy_times || 0}`, 659 | callback_data: "each_token_buy_times", 660 | }, 661 | ], 662 | [ 663 | { 664 | text: `Trader's Tx max limit: ${currentWallet.trader_tx_max_limit || 0}`, 665 | callback_data: "trader_tx_max_limit", 666 | }, 667 | ], 668 | [{text: `Exclude tokens: ${currentWallet.exclude_tokens.length || 0}`, callback_data: "exclude_tokens"}], 669 | [ 670 | {text: `Max MC: ${currentWallet.max_marketcap || 0}`, callback_data: "max_mc"}, 671 | {text: `Min MC: ${currentWallet.min_marketcap || 0}`, callback_data: "min_mc"}, 672 | ], 673 | [{text: `Auto Retry: ${currentWallet.auto_retry_times || 0}`, callback_data: "auto_retry"}], 674 | [ 675 | {text: `Buy Slippage: ${currentWallet.buy_slippage || 0}%`, callback_data: "buy_slippage"}, 676 | {text: `Sell Slippage: ${currentWallet.sell_slippage || 0}%`, callback_data: "sell_slippage"}, 677 | ], 678 | [{text: `Jito Dynamic Tip: ${currentWallet.tip || 0}%`, callback_data: "tip"}], 679 | [ 680 | {text: `Buy Gas Fee: ${currentWallet.buy_gas_fee || 0} sol`, callback_data: "buy_gas_fee"}, 681 | {text: `Sell Gas Fee: ${currentWallet.sell_gas_fee || 0} sol`, callback_data: "sell_gas_fee"}, 682 | ], 683 | [{text: "➕ Create", callback_data: "create"}], 684 | [ 685 | {text: "🔙 Back", callback_data: "back_to_second"}, 686 | {text: "Refresh", callback_data: "refresh"}, 687 | ], 688 | ]; 689 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 690 | const sentMessage = await bot.sendMessage(chatId, message, { 691 | parse_mode: "MarkdownV2", 692 | reply_markup: replyMarkup, 693 | }); 694 | await deletePreviousMessages(chatId, username); 695 | if (messageIds[username]) { 696 | messageIds[username].push(sentMessage.message_id); 697 | } else { 698 | messageIds[username] = [sentMessage.message_id]; 699 | } 700 | } 701 | } 702 | 703 | async function handlePrivateKey(msg, username) { 704 | const chatId = msg.chat.id; 705 | const privateKey = msg.text; 706 | console.log(`Received private key for user @${username}: ${privateKey}`); 707 | await bot.deleteMessage(chatId, msg.message_id); 708 | let sol_public_key_str = ""; 709 | try { 710 | sol_public_key_str = await derive_public_key(privateKey); 711 | await User.findOne({username}); 712 | await User.updateOne( 713 | {username: username}, 714 | { 715 | $set: {public_key: sol_public_key_str, private_key: privateKey}, 716 | }, 717 | {upsert: true} 718 | ); 719 | const solBalance = await getSolBalance(sol_public_key_str); 720 | await deletePreviousMessages(chatId, username); 721 | const keyboard = [ 722 | [ 723 | {text: "Copy Trade", callback_data: "trade"}, 724 | {text: "Wallet Setting", callback_data: "setting"}, 725 | ], 726 | ]; 727 | const replyMarkup = JSON.stringify({inline_keyboard: keyboard}); 728 | const message = ` 729 | *Wallet updated successfully \`${username}\`* 730 | 731 | *Your current wallet address:* 732 | \`${sol_public_key_str}\` 733 | 734 | *Your current balance:* 735 | \`${solBalance} SOL\``; 736 | const sentMessage = await bot.sendMessage(chatId, message, { 737 | parse_mode: "MarkdownV2", 738 | reply_markup: replyMarkup, 739 | }); 740 | if (messageIds[username]) { 741 | messageIds[username].push(sentMessage.message_id); 742 | } else { 743 | messageIds[username] = [sentMessage.message_id]; 744 | } 745 | } catch (e) { 746 | console.error("Error fetching SOL balance:", e); 747 | const sentMessage = await bot.sendMessage(chatId, "Error fetching SOL balance. Please try again."); 748 | if (messageIds[username]) { 749 | messageIds[username].push(sentMessage.message_id); 750 | } else { 751 | messageIds[username] = [sentMessage.message_id]; 752 | } 753 | } 754 | 755 | expectingPrivateKey[username] = false; 756 | } 757 | 758 | async function handleInput(msg, username) { 759 | const chatId = msg.chat.id; 760 | const field = editingField[username]; 761 | const value = msg.text; 762 | 763 | if ( 764 | field === "buy_percentage" || 765 | field === "max_buy" || 766 | field === "min_buy" || 767 | field === "total_invest_sol" || 768 | field === "each_token_buy_times" || 769 | field === "trader_tx_max_limit" || 770 | field === "max_marketcap" || 771 | field === "min_marketcap" || 772 | field === "buy_slippage" || 773 | field === "auto_retry_times" || 774 | field === "buy_slippage" || 775 | field === "sell_slippage" || 776 | field === "tip" || 777 | field === "buy_gas_fee" || 778 | field === "sell_gas_fee" 779 | ) { 780 | // if (!/^\d+$/.test(value) || parseFloat(value) < 0) { 781 | // const sentMessage = await bot.sendMessage(chatId, "Please enter a valid number."); 782 | // if (messageIds[username]) { 783 | // messageIds[username].push(sentMessage.message_id); 784 | // } else { 785 | // messageIds[username] = [sentMessage.message_id]; 786 | // } 787 | // return; 788 | // } 789 | if (!/^\d+(\.\d+)?$/.test(value) || parseFloat(value) < 0) { 790 | const sentMessage = await bot.sendMessage(chatId, "Please enter a valid number."); 791 | if (messageIds[username]) { 792 | messageIds[username].push(sentMessage.message_id); 793 | } else { 794 | messageIds[username] = [sentMessage.message_id]; 795 | } 796 | return; 797 | } 798 | } 799 | 800 | if (field === "wallet_label") { 801 | const existWalletWallet = await Target.findOne({username, wallet_label: value}); 802 | if (existWalletWallet) { 803 | const sentMessage = await bot.sendMessage(chatId, "This wallet label already exists."); 804 | if (messageIds[username]) { 805 | messageIds[username].push(sentMessage.message_id); 806 | } else { 807 | messageIds[username] = [sentMessage.message_id]; 808 | } 809 | return; 810 | } 811 | } 812 | 813 | if (field === "target_wallet" || field === "exclude_tokens") { 814 | if (field === "target_wallet") { 815 | const existWallet = await Target.findOne({username, added: true, target_wallet: value}); 816 | if (existWallet) { 817 | const sentMessage = await bot.sendMessage( 818 | chatId, 819 | "This wallet address already exists in target wallet list." 820 | ); 821 | if (messageIds[username]) { 822 | messageIds[username].push(sentMessage.message_id); 823 | } else { 824 | messageIds[username] = [sentMessage.message_id]; 825 | } 826 | return; 827 | } 828 | } 829 | if (value.length !== 43 && value.length !== 44) { 830 | const sentMessage = await bot.sendMessage(chatId, "Please enter valid target wallet address."); 831 | if (messageIds[username]) { 832 | messageIds[username].push(sentMessage.message_id); 833 | } else { 834 | messageIds[username] = [sentMessage.message_id]; 835 | } 836 | return; 837 | } 838 | const base58Pattern = /^[A-HJ-NP-Za-km-z1-9]+$/; 839 | if (!base58Pattern.test(value)) { 840 | const sentMessage = await bot.sendMessage(chatId, "Please enter valid target wallet address."); 841 | if (messageIds[username]) { 842 | messageIds[username].push(sentMessage.message_id); 843 | } else { 844 | messageIds[username] = [sentMessage.message_id]; 845 | } 846 | return; 847 | } 848 | } 849 | await bot.deleteMessage(chatId, msg.message_id); 850 | await Target.updateOne({username, added: false}, {$set: {[field]: value}}); 851 | editingField[username] = []; 852 | await deletePreviousMessages(chatId, username); 853 | await backTrade(msg, username); 854 | } 855 | 856 | async function derive_public_key(privateKey) { 857 | const privateKeyBytes = base58.decode(privateKey); 858 | if (privateKeyBytes.length !== 64) { 859 | throw new Error("Invalid private key length for Solana."); 860 | } 861 | const solKeypair = solanaWeb3.Keypair.fromSecretKey(privateKeyBytes); 862 | const solPublicKey = solKeypair.publicKey.toBase58(); 863 | // console.log(`Derived Solana public key: ${solPublicKey}`); 864 | return solPublicKey; 865 | } 866 | 867 | async function getSolBalance(publicKey) { 868 | const solClient = new solanaWeb3.Connection(solanaWeb3.clusterApiUrl("mainnet-beta")); 869 | const solBalance = await solClient.getBalance(new solanaWeb3.PublicKey(publicKey)); 870 | const balanceInSol = solBalance / solanaWeb3.LAMPORTS_PER_SOL; 871 | // console.log(`SOL balance for ${publicKey}: ${balanceInSol} SOL`); 872 | return balanceInSol; 873 | } 874 | 875 | bot.on("message", async (msg) => { 876 | const chatId = msg.chat.id; 877 | const username = msg.from.username || "Unknown"; 878 | 879 | if (expectingPrivateKey[username]) { 880 | await handlePrivateKey(msg, username); 881 | } else { 882 | await handleInput(msg, username); 883 | } 884 | }); 885 | 886 | async function deletePreviousMessages(chatId, username) { 887 | console.log("msgIDs", messageIds); 888 | if (messageIds[username]) { 889 | for (const messageId of messageIds[username]) { 890 | try { 891 | await bot.deleteMessage(chatId, messageId); 892 | } catch (e) { 893 | console.error("Failed to delete message"); 894 | } 895 | } 896 | messageIds[username] = []; 897 | } 898 | console.log("aftermsgIDs", messageIds); 899 | } 900 | --------------------------------------------------------------------------------