├── .gitignore ├── public └── sniper.jpg ├── .env.example ├── .vscode └── easycode.ignore ├── config ├── db.ts └── constants.ts ├── bot ├── botAction.ts ├── callbackquery.handler.ts └── message.handler.ts ├── package.json ├── module ├── user.ts ├── swap.ts └── referral.ts ├── utils ├── utils.ts └── web3.ts ├── service ├── userService.ts ├── swapService.ts ├── referralService.ts └── birdeyeService.ts ├── readme.md ├── index.ts ├── components └── inlineKeyboard.ts ├── swap ├── sell.ts └── buy.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | package-lock.json 4 | dist 5 | -------------------------------------------------------------------------------- /public/sniper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greengod63/solana-sniper-bot/HEAD/public/sniper.jpg -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TELEGRAM_BOT_TOKEN="" 2 | BIRDEYE_API_KEY="" 3 | SOLANA_RPC_URL="" 4 | BOT_WALLET_PUBLIC_KEY="" 5 | -------------------------------------------------------------------------------- /.vscode/easycode.ignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | vendor/ 4 | cache/ 5 | .*/ 6 | *.min.* 7 | *.test.* 8 | *.spec.* 9 | *.bundle.* 10 | *.bundle-min.* 11 | *.log 12 | package-lock.json -------------------------------------------------------------------------------- /config/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/sniper-bot" 4 | 5 | // Connect to MongoDB 6 | export async function connectDatabase(){ 7 | try{ 8 | await mongoose.connect(MONGODB_URI); 9 | console.log("Connected to MongoDB!"); 10 | } 11 | catch(error){ 12 | console.error("MongoDB connection error: ", error); 13 | } 14 | } -------------------------------------------------------------------------------- /bot/botAction.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import fs from "fs"; 3 | 4 | export async function sendIKSnipe( 5 | bot: TelegramBot, 6 | chatId: number, 7 | IK_SNIPE: any, 8 | caption?: string 9 | ) { 10 | const image = fs.createReadStream("./public/sniper.jpg"); 11 | 12 | if (!caption) { 13 | caption = `⬇You can create a new snipe or check current active snipes!🔍`; 14 | } 15 | 16 | await bot.sendPhoto(chatId, image, { 17 | parse_mode: "HTML", 18 | caption: caption, 19 | reply_markup: { 20 | inline_keyboard: IK_SNIPE, 21 | }, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-sniper-bot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node index.ts", 8 | "build": "tsc", 9 | "clean": "tsc --build --clean", 10 | "dev": "tsc && node ./dist/index.js" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@solana/web3.js": "^1.98.0", 16 | "axios": "^1.8.1", 17 | "bs58": "^6.0.0", 18 | "dotenv": "^16.4.7", 19 | "fs": "^0.0.1-security", 20 | "mongoose": "^8.10.1", 21 | "node-telegram-bot-api": "^0.66.0", 22 | "typescript": "^5.7.3" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^22.13.5", 26 | "@types/node-telegram-bot-api": "^0.64.7", 27 | "ts-node": "^10.9.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /module/user.ts: -------------------------------------------------------------------------------- 1 | import mongoose, {Schema, Document} from "mongoose"; 2 | 3 | // Define the interface for the User document 4 | export interface IUser extends Document { 5 | chat_id: number; 6 | username?: string; 7 | first_name?: string; 8 | last_name?: string; 9 | public_key: string; 10 | private_key: string; 11 | } 12 | 13 | const userSchema = new Schema({ 14 | chat_id: { 15 | type: Number, 16 | required: true, 17 | unique: true 18 | }, 19 | username: String, 20 | first_name: String, 21 | last_name: String, 22 | public_key: { 23 | type: String, 24 | required: true 25 | }, 26 | private_key: { 27 | type: String, 28 | required: true 29 | }, 30 | }); 31 | 32 | const User = mongoose.model('user', userSchema); 33 | 34 | export default User; -------------------------------------------------------------------------------- /module/swap.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document } from "mongoose"; 2 | 3 | // Define the interface for the User document 4 | export interface ISwap extends Document { 5 | chat_id: number; 6 | token: string; 7 | slippage: number; 8 | snipe_fee: number; 9 | snipe_tip: number; 10 | tp: number; 11 | sl: number; 12 | snipe_amount: number; 13 | type: String; // "manual" | "auto"; 14 | status: String; // "snipping" | "success" | "failure"; 15 | swap_time: number; 16 | } 17 | 18 | const swapSchema = new Schema({ 19 | chat_id: { 20 | type: Number, 21 | required: true, 22 | }, 23 | token: String, 24 | slippage: Number, 25 | snipe_fee: Number, 26 | snipe_tip: Number, 27 | tp: Number, 28 | sl: Number, 29 | snipe_amount: Number, 30 | type: String, 31 | status: String, 32 | swap_time: Number 33 | }); 34 | 35 | const Swap = mongoose.model("swap", swapSchema); 36 | 37 | export default Swap; 38 | -------------------------------------------------------------------------------- /utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const copy2clipboard = (text: string) => { 2 | return `${text}`; 3 | }; 4 | 5 | export const isValidSolanaAddress = async (ca: string) => { 6 | // Remove any whitespace and potential URL components 7 | const cleanCA = ca.trim().split("/").pop() || ""; 8 | 9 | // Solana address regex pattern - matches 32-44 base58 characters 10 | const contractAddressMatch = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/; 11 | 12 | return contractAddressMatch.test(cleanCA); 13 | }; 14 | 15 | export const getShortenedCA = (ca: string) => { 16 | // Get the first 6 letters 17 | const firstSixLetters = ca.slice(0, 6); 18 | 19 | // Get the last 6 letters 20 | const lastSixLetters = ca.slice(-6); 21 | 22 | return firstSixLetters + "..." + lastSixLetters; 23 | }; 24 | 25 | export const isValidSnipeConfig = (snipe_config: any) => { 26 | if ( 27 | snipe_config.token != null && 28 | snipe_config.slippage > 0.0 && 29 | snipe_config.snipe_fee > 0.0 && 30 | snipe_config.snipe_tip > 0.0 && 31 | snipe_config.tp != null && 32 | snipe_config.sl != null && 33 | snipe_config.snipe_amount != null 34 | ) { 35 | return true; 36 | } else { 37 | return false; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /module/referral.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document } from 'mongoose'; 2 | 3 | // Define the interface for the Tier structure 4 | export interface ITier { 5 | users: number; 6 | volume: number; 7 | earnings: number; 8 | } 9 | 10 | // Define the interface for the Referral document 11 | export interface IReferral extends Document { 12 | chat_id: number; 13 | parent_id?: number; 14 | rewards_wallet?: string; 15 | paid_amount?: number; 16 | tier1: ITier; 17 | tier2: ITier; 18 | tier3: ITier; 19 | tier4: ITier; 20 | tier5: ITier; 21 | } 22 | 23 | // Define the schema for the Tier structure 24 | const TierSchema: Schema = new Schema({ 25 | users: { type: Number, default: 0 }, 26 | volume: { type: Number, default: 0 }, 27 | earnings: { type: Number, default: 0 }, 28 | }); 29 | 30 | // Define the schema for the Referral document 31 | const ReferralSchema: Schema = new Schema({ 32 | chat_id: { type: Number, required: true, unique: true }, 33 | parent_id: { type: Number }, 34 | rewards_wallet: { type: String }, 35 | paid_amount: { type: Number, default: 0 }, 36 | tier1: { type: TierSchema, default: () => ({}) }, 37 | tier2: { type: TierSchema, default: () => ({}) }, 38 | tier3: { type: TierSchema, default: () => ({}) }, 39 | tier4: { type: TierSchema, default: () => ({}) }, 40 | tier5: { type: TierSchema, default: () => ({}) }, 41 | }); 42 | 43 | // Create the model 44 | const Referral = mongoose.model('Referral', ReferralSchema); 45 | 46 | export default Referral; -------------------------------------------------------------------------------- /config/constants.ts: -------------------------------------------------------------------------------- 1 | export const BOT_FEE_PERCENT = 1; // 1% 2 | 3 | export enum BotCaption { 4 | strInputTokenAddress = `💰 Enter Token Address`, 5 | strInvalidSolanaTokenAddress = `⚠️ Invalid Solana Token Address! Again enter correct Token Address!`, 6 | strInputSwapSolAmount = `💰 Enter Swap SOL Amount`, 7 | strInvalidSolAmount = `⚠️ Invalid Swap SOL Amount ⚠️`, 8 | HelpCaption = `🚀 TG Solana Trading Bot 🚀`, 9 | strWelcome = `Welcome to Solana Trading bot 🎉\n`, 10 | SET_PRIORITY_FEE = `💸 Priority Fee SOL Amount \n\n💲 Enter SOL Value in format "0.0X"`, 11 | SET_JITOTIP = `💸 Jito Tip SOL Amount \n\n💲 Enter SOL Value in format "0.0X"`, 12 | SET_SNIPE_AMOUNT = `💰 Snipe Amount \n\n💲 Enter Snipe Amount in format "0.0X"`, 13 | SET_SLIPPAGE = `⚖ Slippage \n\n💲 Enter Slippage in format "xx%"`, 14 | SET_TakeProfit = `⚖ Take Profit \n\n💲 Enter Take Profit in format "xx%"`, 15 | SET_StopLoss = `⚖ Stop Loss \n\n💲 Enter Stop Loss in format "xx%"`, 16 | strInvalidInput = `⚠️ Invalid Input ⚠️`, 17 | SET_PK = `🔑 Private KEY \n\n💲 Enter Wallet Private KEY`, 18 | SET_DES = `⚙ User Setting.\nYou can set any settings on here. You can set any settings on here.`, 19 | SWAP_FAILED = `⚠️ Swap Failed ⚠️`, 20 | SNIPE_CONFIG_FAILED = `⚠️ Snipe Configuration Failed ⚠️`, 21 | AUTO_SWAP_ON = "🔔 Auto Swap ON", 22 | AUTO_SWAP_OFF = "🔕 Auto Swap OFF", 23 | strAlreadyRefer = `👍 You have already referred a friend.`, 24 | strReferSuccess = `👍 You have successfully referred a friend.`, 25 | strInvalidReferUser = `⚠️ Invalid User ⚠️`, 26 | } 27 | -------------------------------------------------------------------------------- /service/userService.ts: -------------------------------------------------------------------------------- 1 | import User, { IUser } from "../module/user"; 2 | import { Keypair } from "@solana/web3.js"; 3 | import bs58 from "bs58"; 4 | 5 | /** 6 | * Add a new user. 7 | * @param userData - The user data to add. 8 | */ 9 | export async function addUser(userData: Partial): Promise { 10 | try { 11 | const private_key = bs58.encode(Keypair.generate().secretKey); 12 | const public_key = Keypair.fromSecretKey( 13 | bs58.decode(private_key) 14 | ).publicKey.toBase58(); 15 | 16 | const newUser = new User({ 17 | ...userData, 18 | private_key: private_key, 19 | public_key: public_key, 20 | }); 21 | await newUser.save(); 22 | return newUser; 23 | } catch (error) { 24 | console.error("Error adding user:", error); 25 | throw error; 26 | } 27 | } 28 | 29 | /** 30 | * Get a user by chat_id. 31 | * @param chat_id - The user ID to search for. 32 | */ 33 | export async function getUserById(chat_id: number): Promise { 34 | try { 35 | const user = await User.findOne({ chat_id }); 36 | return user; 37 | } catch (error) { 38 | console.error("Error fetching user:", error); 39 | throw error; 40 | } 41 | } 42 | 43 | /** 44 | * Update a user's data. 45 | * @param chat_id - The user ID to update. 46 | * @param updates - The updates to apply. 47 | */ 48 | export async function updateUser( 49 | chat_id: number, 50 | updates: Partial> 51 | ): Promise { 52 | try { 53 | const user = await User.findOneAndUpdate({ chat_id }, updates, { 54 | new: true, 55 | }); 56 | return user; 57 | } catch (error) { 58 | console.error("Error updating user:", error); 59 | throw error; 60 | } 61 | } 62 | 63 | /** 64 | * Delete a user by chat_id. 65 | * @param chat_id - The user ID to delete. 66 | */ 67 | export async function deleteUser(chat_id: number): Promise { 68 | try { 69 | const user = await User.findOneAndDelete({ chat_id }); 70 | return user; 71 | } catch (error) { 72 | console.error("Error deleting user:", error); 73 | throw error; 74 | } 75 | } 76 | 77 | /** 78 | * Get all users. 79 | */ 80 | export async function getAllUsers(): Promise { 81 | try { 82 | const users = await User.find({}); 83 | return users; 84 | } catch (error) { 85 | console.error("Error fetching all users:", error); 86 | throw error; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Lucky Solana Sniper bot 2 | 3 | ## Features 4 | - ⚡️ Typescript 5 | - ⚛️ Telegram Bot using node-telegram-bot-api 6 | - ✨ Birdeye API 7 | - 💨 Solana Web3 8 | 9 | 10 | ## Getting Started 11 | 12 | ### 1. Clone this template using one of the three ways 13 | 14 | ```bash 15 | git clone https://github.com/greengod63/solana-sniper-bot.git 16 | cd solana-sniper-bot 17 | ``` 18 | 19 | ### 2. Install dependencies 20 | 21 | I used **npm** as a node package manager. 22 | 23 | ```bash 24 | npm install 25 | ``` 26 | 27 | ### 3. Set .env 28 | Create a `.env` file in the project root: 29 | ```env 30 | TELEGRAM_BOT_TOKEN="" 31 | BIRDEYE_API_KEY="" 32 | SOLANA_RPC_URL="" 33 | BOT_WALLET_PUBLIC_KEY="" 34 | ``` 35 | 36 | > **💡 Pro Tip:** 37 | > - Obtain an API key from Helius or another Solana RPC provider for optimal performance 38 | > - Default RPC endpoints are available as fallback options 39 | 40 | ### 4. Configure project 41 | 42 | `config` folder contains several main config settings for this project. 43 | 44 | ### 5. Run the project 45 | 46 | You can start the server using this command: 47 | 48 | ```bash 49 | npm start 50 | ``` 51 | 52 | ### 6. Project Structure 53 | - `index.ts`: Main code for this project. 54 | - `config/`: Contains the settings for this project. 55 | - `bot/`: Contains the code such as `MessageHandler` or `CallBackQueryHandler` to control the Telegram bot. 56 | - `components/`: Defines InlineKeyboards that are main parts of Telegram Bot UI. 57 | - `module/`: Defines MongoDB Schema. 58 | - `service/`: CRUD actions to interact with MongoDB. 59 | - `public/`: Public Assets such as images. 60 | - `swap/`: Buy/Sell Script SPL tokens on Solana. 61 | - `utils/`: Various useful functions. 62 | - `.env`: Needed Env values. 63 | - `package.json`: Project metadata and dependencies. 64 | - `tsconfig.json`: TypeScript configuration file. 65 | 66 | ## Contributing 67 | Contributions are welcome! Please fork the repository and submit a pull request with your improvements. 68 | 69 | 1. Fork the repository 70 | 2. Create your feature branch (`git checkout -b feature/YourFeature`) 71 | 3. Commit your changes (`git commit -m 'Add some feature'`) 72 | 4. Push to the branch (`git push origin feature/YourFeature`) 73 | 5. Open a pull request 74 | 75 | ## License 76 | This project is licensed under the MIT License - see the LICENSE file for details. 77 | 78 | ## Tips 79 | `hYn1ZbfAdhSgwezgVADHR6nNzGWe7F71JGVdFvqk8L3` 80 | 81 | 82 | > **🎯 This is not a completed project. Now I am developing continuously.** 83 | -------------------------------------------------------------------------------- /service/swapService.ts: -------------------------------------------------------------------------------- 1 | import Swap, { ISwap } from "../module/swap"; 2 | import { Keypair } from "@solana/web3.js"; 3 | import bs58 from "bs58"; 4 | 5 | /** 6 | * Add a new swap. 7 | * @param swapData - The swap data to add. 8 | */ 9 | export async function addSwap( 10 | swapData: Partial, 11 | chatId: number 12 | ): Promise { 13 | try { 14 | const newSwap = new Swap({ 15 | ...swapData, 16 | chat_id: chatId, 17 | type: "manual", 18 | status: "snipping", 19 | }); 20 | await newSwap.save(); 21 | return newSwap; 22 | } catch (error) { 23 | console.error("Error adding swap:", error); 24 | throw error; 25 | } 26 | } 27 | 28 | /** 29 | * Get a swap by chat ID. 30 | * @param chat_id - The chat ID to search for. 31 | */ 32 | export async function getSwapByChatId(chat_id: number): Promise { 33 | try { 34 | const swap = await Swap.findOne({ chat_id }); 35 | return swap; 36 | } catch (error) { 37 | console.error("Error fetching swap:", error); 38 | throw error; 39 | } 40 | } 41 | 42 | /** 43 | * Update a swap's data. 44 | * @param chat_id - The chat ID to update. 45 | * @param updates - The updates to apply. 46 | */ 47 | export async function updateSwap( 48 | chat_id: number, 49 | updates: Partial> 50 | ): Promise { 51 | try { 52 | const swap = await Swap.findOneAndUpdate({ chat_id }, updates, { 53 | new: true, 54 | }); 55 | return swap; 56 | } catch (error) { 57 | console.error("Error updating swap:", error); 58 | throw error; 59 | } 60 | } 61 | 62 | /** 63 | * Delete a swap by chat ID. 64 | * @param chat_id - The chat ID to delete. 65 | */ 66 | export async function deleteSwap(chat_id: number): Promise { 67 | try { 68 | const swap = await Swap.findOneAndDelete({ chat_id }); 69 | return swap; 70 | } catch (error) { 71 | console.error("Error deleting swap:", error); 72 | throw error; 73 | } 74 | } 75 | 76 | /** 77 | * Get all swaps. 78 | */ 79 | export async function getAllSwaps(): Promise { 80 | try { 81 | const swaps = await Swap.find({}); 82 | return swaps; 83 | } catch (error) { 84 | console.error("Error fetching all swaps:", error); 85 | throw error; 86 | } 87 | } 88 | 89 | /** 90 | * Get swaps by status. 91 | * @param status - The status to filter by (e.g., "snipping", "success", "failure"). 92 | */ 93 | export async function getSwapsByStatus(status: string): Promise { 94 | try { 95 | const swaps = await Swap.find({ status }); 96 | return swaps; 97 | } catch (error) { 98 | console.error("Error fetching swaps by status:", error); 99 | throw error; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /utils/web3.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import bs58 from "bs58"; 3 | import { 4 | LAMPORTS_PER_SOL, 5 | Transaction, 6 | SystemProgram, 7 | sendAndConfirmTransaction, 8 | Keypair, 9 | Connection, 10 | PublicKey, 11 | clusterApiUrl, 12 | ComputeBudgetProgram, 13 | TransactionMessage, 14 | VersionedTransaction, 15 | TransactionInstruction, 16 | } from "@solana/web3.js"; 17 | 18 | dotenv.config(); 19 | 20 | // ✅ Solana Mainnet Connection 21 | const SOLANA_RPC_URL = 22 | process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; 23 | const connection = new Connection(SOLANA_RPC_URL, "confirmed"); 24 | 25 | export const getSolBalance = async (walletAddress: string) => { 26 | const balance = await connection.getBalance(new PublicKey(walletAddress)); 27 | console.log("Sol Balance", balance); 28 | 29 | return balance / 1e9; 30 | }; 31 | 32 | export const getTokenInfo_Decimals_Supply = async (tokenAddress: string) => { 33 | const tokenInfo = await connection.getParsedAccountInfo( 34 | new PublicKey(tokenAddress) 35 | ); 36 | // console.log("tokenInfo------>", JSON.stringify(tokenInfo, null, 2)); 37 | const splData = tokenInfo.value?.data; 38 | if (splData && "parsed" in splData) { 39 | const parsed = splData.parsed; 40 | // console.log("Parsed----->", JSON.stringify(parsed.info, null, 2)) 41 | const decimals = parsed.info.decimals; 42 | const supply = parsed.info.supply; 43 | console.log("Parsed----->", decimals, supply); 44 | 45 | return { decimals: decimals, supply: supply }; 46 | } 47 | }; 48 | 49 | export const tansferSOL = async ( 50 | senderPrivateKeyString: string, 51 | receiverPublicKeyString: string, 52 | amount: number 53 | ) => { 54 | const senderSecretKey = bs58.decode(senderPrivateKeyString); 55 | const sender = Keypair.fromSecretKey(senderSecretKey); 56 | 57 | // Get receiver public key 58 | const receiverPublicKey = new PublicKey(receiverPublicKeyString); 59 | 60 | try { 61 | const trasnferInstruction = SystemProgram.transfer({ 62 | fromPubkey: sender.publicKey, 63 | toPubkey: receiverPublicKey, 64 | lamports: amount * 1e9, 65 | }); 66 | 67 | const transaction = new Transaction().add(trasnferInstruction); 68 | 69 | const transactionSignature = await sendAndConfirmTransaction( 70 | connection, 71 | transaction, 72 | [sender] 73 | ); 74 | 75 | console.log( 76 | "Transaction Signature: ", 77 | `https://solscan.io/tx/${transactionSignature}` 78 | ); 79 | return { status: "success", tx_hash: transactionSignature }; 80 | } catch (error: any) { 81 | console.error("❌ Transfer SOL error", error.message); 82 | return { status: "failed", tx_hash: null }; 83 | } 84 | }; 85 | 86 | export const getTokenPrice = async (tokenAddressArray: string[]) => { 87 | const mergedMintAddresses = tokenAddressArray.join(","); 88 | const solAddress = "So11111111111111111111111111111111111111112"; 89 | const priceResponseShowExtraInfo = await fetch( 90 | `https://api.jup.ag/price/v2?ids=${solAddress},${mergedMintAddresses}` 91 | ); 92 | // const priceResponseShowExtraInfo = await fetch(`https://api.jup.ag/price/v2?ids=${mergedMintAddresses}&showExtraInfo=true`); 93 | const priceDataShowExtraInfo = await priceResponseShowExtraInfo.json(); 94 | return priceDataShowExtraInfo; 95 | }; 96 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { connectDatabase } from "./config/db"; 3 | import { addUser, getUserById } from "./service/userService"; 4 | import { IK_START, getIKSnipe } from "./components/inlineKeyboard"; 5 | import { messageHandler } from "./bot/message.handler"; 6 | import { callbackQueryHandler } from "./bot/callbackquery.handler"; 7 | import fs from "fs"; 8 | 9 | import dotenv from "dotenv"; 10 | 11 | dotenv.config(); 12 | 13 | const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; 14 | 15 | const BotMenu = [ 16 | { 17 | command: "start", 18 | description: "💥 Start", 19 | }, 20 | { 21 | command: "setting", 22 | description: "⚙️ setting", 23 | }, 24 | { 25 | command: "position", 26 | description: "💰 Position", 27 | }, 28 | { 29 | command: "referral", 30 | description: "📊 Referral Stats", 31 | }, 32 | { command: "help", description: "❓ Help" }, 33 | ]; 34 | 35 | const bot = new TelegramBot(TELEGRAM_BOT_TOKEN!, { 36 | polling: true, 37 | webHook: false, 38 | onlyFirstMatch: true, 39 | filepath: false, 40 | }); 41 | 42 | const userSnipeConfig = new Map(); 43 | 44 | const startBot = () => { 45 | // Connect Database 46 | connectDatabase(); 47 | 48 | bot.setMyCommands(BotMenu); 49 | 50 | bot.onText(/^\/start$/, async (msg: TelegramBot.Message) => { 51 | console.log("🚀 input start cmd:"); 52 | 53 | const chatId = msg.chat.id; 54 | let user; 55 | const existingUser = await getUserById(chatId); 56 | if (existingUser) { 57 | console.log("User already exist: ", chatId); 58 | user = existingUser; 59 | } 60 | else { 61 | console.log("New User: ", chatId); 62 | 63 | const userChat = msg.chat; 64 | user = await addUser({ 65 | chat_id: userChat.id, 66 | username: userChat.username, 67 | first_name: userChat.first_name, 68 | last_name: userChat.last_name 69 | }); 70 | } 71 | 72 | // Snipe Config Init 73 | let snipe_config:any = { 74 | token: null, 75 | slippage: 50, 76 | snipe_fee: 0.005, 77 | snipe_tip: 0.005, 78 | tp: null, 79 | sl: null, 80 | snipe_amount: null, 81 | }; 82 | 83 | userSnipeConfig.set(chatId, snipe_config); 84 | 85 | const image = fs.createReadStream("./public/sniper.jpg"); 86 | const caption = `Welcome to Lucky Sniper Bot!✨\n⬇You can deposit SOL to your wallet and start sniping!🔍\n\n💰Your Wallet:\n${user.public_key}`; 87 | await bot.sendPhoto(msg.chat.id, image, { 88 | parse_mode: "HTML", 89 | caption: caption, 90 | reply_markup: { 91 | inline_keyboard: IK_START, 92 | }, 93 | }); 94 | }); 95 | 96 | bot.onText(/^\/snipe/, async (msg: TelegramBot.Message) => { 97 | 98 | }); 99 | 100 | bot.on("message", (msg: TelegramBot.Message) => { 101 | console.log("message handler"); 102 | // bot.sendMessage(msg.chat.id, "hhhhhhhh"); 103 | messageHandler(bot, msg, userSnipeConfig); 104 | }); 105 | 106 | bot.on("callback_query", async (cb_query: TelegramBot.CallbackQuery) => { 107 | console.log("callback_query handler"); 108 | callbackQueryHandler(bot, cb_query, userSnipeConfig); 109 | }); 110 | }; 111 | 112 | startBot(); 113 | -------------------------------------------------------------------------------- /components/inlineKeyboard.ts: -------------------------------------------------------------------------------- 1 | import { getShortenedCA } from "../utils/utils"; 2 | 3 | // Start Inline Keyboard 4 | export const IK_START = [ 5 | [ 6 | { 7 | text: "📥 Buy", 8 | callback_data: "BUY", 9 | }, 10 | { 11 | text: "📤 Sell", 12 | callback_data: "SELL", 13 | }, 14 | ], 15 | [ 16 | { 17 | text: "⚙ Settings", 18 | callback_data: "SETTINGS", 19 | }, 20 | ], 21 | [ 22 | { 23 | text: "🔎 Snipe", 24 | callback_data: "SNIPE_SETTINGS", 25 | }, 26 | ], 27 | ]; 28 | 29 | // Snipe Inline Keyboard 30 | export function getIKSnipe({ 31 | token = null, 32 | slippage = 50, 33 | snipe_fee = 0.005, 34 | snipe_tip = 0.005, 35 | tp = null, 36 | sl = null, 37 | snipe_amount = null, 38 | }: { 39 | token: string | null; 40 | slippage: number; 41 | snipe_fee: number; 42 | snipe_tip: number; 43 | tp: number | null; 44 | sl: number | null; 45 | snipe_amount: number | null; 46 | }): any { 47 | const IK_SNIPE = [ 48 | [ 49 | { 50 | text: "🔙 Back", 51 | callback_data: "BACK", 52 | }, 53 | { 54 | text: "🔃 Refresh", 55 | callback_data: "REFRESH", 56 | }, 57 | ], 58 | [ 59 | { 60 | text: `${token ? "🟢" : "🔴"} Token: ${ 61 | token ? getShortenedCA(token) : "---" 62 | }`, 63 | callback_data: `TOKEN-${token}`, 64 | }, 65 | ], 66 | [ 67 | { 68 | text: `Snipe Fee: ${snipe_fee} SOL`, 69 | callback_data: `SNIPE_FEE-${snipe_fee}`, 70 | }, 71 | { 72 | text: `Snipe Tip: ${snipe_tip} SOL`, 73 | callback_data: `SNIPE_TIP-${snipe_tip}`, 74 | }, 75 | ], 76 | [ 77 | { 78 | text: `Slippage: ${slippage}%`, 79 | callback_data: `SLIPPAGE-${slippage}%`, 80 | }, 81 | ], 82 | [ 83 | { 84 | text: `${tp ? "🟢" : "🔴"} Take Profit(TP): ${tp ? tp : "---"} %`, 85 | callback_data: `TP-${tp ? tp : "null"}`, 86 | }, 87 | { 88 | text: `${sl ? "🟢" : "🔴"} Stop Loss(SL): ${sl ? sl : "---"} %`, 89 | callback_data: `SL-${sl ? sl : "null"}`, 90 | }, 91 | ], 92 | [ 93 | { 94 | text: `${snipe_amount == 0.2 ? "✅ " : ""}Snipe 0.2 SOL`, 95 | callback_data: `SNIPE-0.2`, 96 | }, 97 | { 98 | text: `${snipe_amount == 0.5 ? "✅ " : ""}Snipe 0.5 SOL`, 99 | callback_data: `SNIPE-0.5`, 100 | }, 101 | ], 102 | [ 103 | { 104 | text: `${snipe_amount == 1 ? "✅ " : ""}Snipe 1 SOL`, 105 | callback_data: `SNIPE-1`, 106 | }, 107 | { 108 | text: `${ 109 | snipe_amount && 110 | snipe_amount != 0.2 && 111 | snipe_amount != 0.5 && 112 | snipe_amount != 1 113 | ? "✅ " 114 | : "" 115 | }Snipe ${snipe_amount ? snipe_amount : "X"} SOL`, 116 | callback_data: `SNIPE-${snipe_amount}`, 117 | }, 118 | ], 119 | [ 120 | { 121 | text: `🎯 Create a snipe`, 122 | callback_data: `CREATE_SNIPE`, 123 | }, 124 | ], 125 | [ 126 | { 127 | text: `📃 Created Snipes`, 128 | callback_data: `LIST_SNIPE`, 129 | }, 130 | ], 131 | ]; 132 | 133 | return IK_SNIPE; 134 | } 135 | -------------------------------------------------------------------------------- /service/referralService.ts: -------------------------------------------------------------------------------- 1 | import Referral, { IReferral, ITier } from "../module/referral"; 2 | 3 | 4 | /** 5 | * Add a new referral record and update tier data for parent and ancestors. 6 | * @param chat_id - The chat ID of the new referral. 7 | * @param parent_id - The chat ID of the parent referral (optional). 8 | * @param rewards_wallet - The rewards wallet address (optional). 9 | */ 10 | export async function addReferral( 11 | chat_id: number, 12 | parent_id?: number, 13 | rewards_wallet?: string 14 | ): Promise { 15 | try { 16 | // Create the new referral 17 | const newReferral = new Referral({ 18 | chat_id, 19 | parent_id, 20 | rewards_wallet, 21 | }); 22 | await newReferral.save(); 23 | 24 | // Update tier data for parent and ancestors 25 | let tmpUserId = parent_id; 26 | let idx = 0; 27 | while (tmpUserId && idx < 5) { 28 | const parentReferral = await Referral.findOne({ 29 | chat_id: tmpUserId, 30 | }); 31 | if (parentReferral && parentReferral.chat_id !== chat_id) { 32 | const tier = `tier${idx + 1}` as keyof IReferral; // e.g., "tier1", "tier2", etc. 33 | await incrementTierData(tmpUserId, tier, "users", 1); 34 | tmpUserId = parentReferral.parent_id; 35 | idx++; 36 | } else { 37 | break; 38 | } 39 | } 40 | 41 | return newReferral; 42 | } catch (error) { 43 | console.error("Error adding referral:", error); 44 | throw error; 45 | } 46 | } 47 | 48 | /** 49 | * Get referral data by chat ID. 50 | * @param chat_id - The chat ID of the referral. 51 | */ 52 | export async function getReferralByChatId( 53 | chat_id: number 54 | ): Promise { 55 | try { 56 | const referral = await Referral.findOne({ chat_id }); 57 | return referral; 58 | } catch (error) { 59 | console.error("Error fetching referral data:", error); 60 | throw error; 61 | } 62 | } 63 | 64 | /** 65 | * Update tier data for a specific referral. 66 | * @param chat_id - The chat ID of the referral. 67 | * @param tier - The tier to update (e.g., "tier1"). 68 | * @param updates - The updates to apply (users, volume, earnings). 69 | */ 70 | export async function updateTierData( 71 | chat_id: number, 72 | tier: keyof IReferral, 73 | updates: Partial 74 | ): Promise { 75 | try { 76 | const referral = await Referral.findOneAndUpdate( 77 | { chat_id }, 78 | { $set: { [`${tier}`]: updates } }, 79 | { new: true } 80 | ); 81 | return referral; 82 | } catch (error) { 83 | console.error("Error updating tier data:", error); 84 | throw error; 85 | } 86 | } 87 | 88 | /** 89 | * Increment a specific field in a tier. 90 | * @param chat_id - The chat ID of the referral. 91 | * @param tier - The tier to update (e.g., "tier1"). 92 | * @param field - The field to increment (e.g., "users", "volume", "earnings"). 93 | * @param value - The value to increment by. 94 | */ 95 | export async function incrementTierData( 96 | chat_id: number, 97 | tier: keyof IReferral, 98 | field: keyof ITier, 99 | value: number 100 | ): Promise { 101 | try { 102 | const referral = await Referral.findOneAndUpdate( 103 | { chat_id }, 104 | { $inc: { [`${tier}.${field}`]: value } }, 105 | { new: true } 106 | ); 107 | return referral; 108 | } catch (error) { 109 | console.error("Error incrementing tier data:", error); 110 | throw error; 111 | } 112 | } 113 | 114 | /** 115 | * Calculate unpaid amount for a referral. 116 | * @param chat_id - The chat ID of the referral. 117 | */ 118 | export async function getUnpaidAmount(chat_id: number): Promise { 119 | try { 120 | const referral = await Referral.findOne({ chat_id }); 121 | if (!referral) throw new Error("Referral not found"); 122 | 123 | const unpaidAmount = 124 | referral.tier1.earnings + 125 | referral.tier2.earnings + 126 | referral.tier3.earnings + 127 | referral.tier4.earnings + 128 | referral.tier5.earnings; 129 | 130 | return unpaidAmount; 131 | } catch (error) { 132 | console.error("Error calculating unpaid amount:", error); 133 | throw error; 134 | } 135 | } 136 | 137 | /** 138 | * Update paid amount and reset tier earnings. 139 | * @param chat_id - The chat ID of the referral. 140 | * @param payment_amount - The amount to add to paid_amount. 141 | */ 142 | export async function updatePaymentAndResetTiers( 143 | chat_id: number, 144 | payment_amount: number 145 | ): Promise { 146 | try { 147 | const referral = await Referral.findOneAndUpdate( 148 | { chat_id }, 149 | { 150 | $inc: { paid_amount: payment_amount }, 151 | $set: { 152 | "tier1.earnings": 0, 153 | "tier2.earnings": 0, 154 | "tier3.earnings": 0, 155 | "tier4.earnings": 0, 156 | "tier5.earnings": 0, 157 | }, 158 | }, 159 | { new: true } 160 | ); 161 | return referral; 162 | } catch (error) { 163 | console.error("Error updating payment and resetting tiers:", error); 164 | throw error; 165 | } 166 | } 167 | 168 | /** 169 | * Delete a referral record. 170 | * @param chat_id - The chat ID of the referral. 171 | */ 172 | export async function deleteReferral( 173 | chat_id: number 174 | ): Promise { 175 | try { 176 | const referral = await Referral.findOneAndDelete({ chat_id }); 177 | return referral; 178 | } catch (error) { 179 | console.error("Error deleting referral:", error); 180 | throw error; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /bot/callbackquery.handler.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { getUserById, addUser } from "../service/userService"; 3 | import { getIKSnipe } from "../components/inlineKeyboard"; 4 | import fs from "fs"; 5 | import { BotCaption } from "../config/constants"; 6 | import { sendIKSnipe } from "./botAction"; 7 | import { addSwap } from "../service/swapService"; 8 | import { isValidSnipeConfig } from "../utils/utils"; 9 | import buyToken from "../swap/buy"; 10 | 11 | export async function callbackQueryHandler( 12 | bot: TelegramBot, 13 | cb_query: TelegramBot.CallbackQuery, 14 | userSnipeConfig: Map 15 | ) { 16 | const cb_query_cmd = cb_query.data; 17 | const chatId = cb_query.message?.chat.id; 18 | const messageId = cb_query.message?.message_id || 0; 19 | if (!cb_query_cmd || !chatId) return; 20 | 21 | let user; 22 | const existingUser = await getUserById(chatId); 23 | if (existingUser) { 24 | console.log("User already exist: ", chatId); 25 | user = existingUser; 26 | } else { 27 | console.log("New User: ", chatId); 28 | user = await addUser({ 29 | chat_id: chatId, 30 | username: cb_query.from.username, 31 | first_name: cb_query.from.first_name, 32 | last_name: cb_query.from.last_name, 33 | }); 34 | } 35 | 36 | switch (cb_query_cmd.split("-")[0]) { 37 | case "SNIPE_SETTINGS": // Snipe Button 38 | const snipe_config = userSnipeConfig.get(chatId); 39 | console.log("Callback snipe_config: ", snipe_config); 40 | 41 | const IK_SNIPE = getIKSnipe(snipe_config); 42 | sendIKSnipe(bot, chatId, IK_SNIPE); 43 | break; 44 | case "BACK": //Back Button 45 | bot.deleteMessage(chatId, messageId); 46 | return; 47 | case "TOKEN": //Token Button 48 | await bot.sendMessage(chatId, BotCaption.strInputTokenAddress, { 49 | parse_mode: "HTML", 50 | reply_markup: { 51 | force_reply: true, 52 | selective: true, 53 | }, 54 | }); 55 | break; 56 | case "SNIPE_FEE": //Snipe fee Button 57 | await bot.sendMessage(chatId, BotCaption.SET_PRIORITY_FEE, { 58 | parse_mode: "HTML", 59 | reply_markup: { 60 | force_reply: true, 61 | selective: true, 62 | }, 63 | }); 64 | break; 65 | case "SNIPE_TIP": //Snipe tip Button 66 | await bot.sendMessage(chatId, BotCaption.SET_JITOTIP, { 67 | parse_mode: "HTML", 68 | reply_markup: { 69 | force_reply: true, 70 | selective: true, 71 | }, 72 | }); 73 | break; 74 | case "SLIPPAGE": //Slippage Button 75 | await bot.sendMessage(chatId, BotCaption.SET_SLIPPAGE, { 76 | parse_mode: "HTML", 77 | reply_markup: { 78 | force_reply: true, 79 | selective: true, 80 | }, 81 | }); 82 | break; 83 | case "TP": //TP Button 84 | await bot.sendMessage(chatId, BotCaption.SET_TakeProfit, { 85 | parse_mode: "HTML", 86 | reply_markup: { 87 | force_reply: true, 88 | selective: true, 89 | }, 90 | }); 91 | break; 92 | case "SL": //SL Button 93 | await bot.sendMessage(chatId, BotCaption.SET_StopLoss, { 94 | parse_mode: "HTML", 95 | reply_markup: { 96 | force_reply: true, 97 | selective: true, 98 | }, 99 | }); 100 | break; 101 | case "SNIPE": //Snipe-[x] Button 102 | const amount = cb_query_cmd.split("-")[1]; 103 | if (amount == "0.2" || amount == "0.5" || amount == "1") { 104 | const snipe_config = userSnipeConfig.get(chatId); 105 | const updated_config = { 106 | ...snipe_config, 107 | snipe_amount: parseFloat(amount), 108 | }; 109 | console.log("Message snipe_config: ", updated_config); 110 | userSnipeConfig.set(chatId, updated_config); 111 | const IK_SNIPE = getIKSnipe(updated_config); 112 | sendIKSnipe(bot, chatId, IK_SNIPE); 113 | } else { 114 | await bot.sendMessage(chatId, BotCaption.SET_SNIPE_AMOUNT, { 115 | parse_mode: "HTML", 116 | reply_markup: { 117 | force_reply: true, 118 | selective: true, 119 | }, 120 | }); 121 | } 122 | break; 123 | case "REFRESH": //Refresh Button 124 | // Snipe Config Init 125 | const init_snipe_config: any = { 126 | token: null, 127 | slippage: 50, 128 | snipe_fee: 0.005, 129 | snipe_tip: 0.005, 130 | tp: null, 131 | sl: null, 132 | snipe_amount: null, 133 | }; 134 | 135 | console.log("Message snipe_config Refresh: ", init_snipe_config); 136 | userSnipeConfig.set(chatId, init_snipe_config); 137 | const INIT_IK_SNIPE = getIKSnipe(init_snipe_config); 138 | sendIKSnipe(bot, chatId, INIT_IK_SNIPE); 139 | break; 140 | case "CREATE_SNIPE": //Create Snipe Button 141 | const completed_snipe_config = userSnipeConfig.get(chatId); 142 | const isValid = isValidSnipeConfig(completed_snipe_config); 143 | if (isValid) { 144 | // buyToken 145 | const result = await buyToken( 146 | chatId, 147 | user.private_key, 148 | completed_snipe_config.snipe_amount, 149 | completed_snipe_config.token, 150 | completed_snipe_config.snipe_fee, 151 | completed_snipe_config.slippage 152 | ); 153 | if (result && result.status == "success") { 154 | await bot.sendMessage( 155 | chatId, 156 | `Success Buy Token!🎉\nTxID: ${result.tx_hash}`, 157 | { parse_mode: "HTML" } 158 | ); 159 | // await updateTiersVolume(chatId, result.amount); 160 | } else { 161 | await bot.sendMessage(chatId, "Failed Buy Token!"); 162 | } 163 | await addSwap(completed_snipe_config, chatId); 164 | } else { 165 | await bot.sendMessage(chatId, BotCaption.SNIPE_CONFIG_FAILED, { 166 | parse_mode: "HTML", 167 | }); 168 | } 169 | break; 170 | default: 171 | break; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /service/birdeyeService.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | const BIRDEYE_API_KEY = process.env.BIRDEYE_API_KEY; 7 | 8 | /********************************** Token Data Example 9 | { 10 | "data": { 11 | "address": "EZ6QdXHkzi7MKj2u3Fwd7gUbq2pgJTDW1CbhoMhxpump", 12 | "decimals": 6, 13 | "symbol": "TTB", 14 | "name": "Trump The Builder", 15 | "marketCap": 4757.61438649097, 16 | "fdv": 4757.61438649097, 17 | "extensions": { 18 | "twitter": "https://x.com/realDonaldTrump", 19 | "website": "https://x.com/realDonaldTrump", 20 | "description": "Just A Building Trump" 21 | }, 22 | "logoURI": "https://ipfs.io/ipfs/QmYKZitwMVAtUqRTNEA7C5VKVvMQzWii6d2h75Wohcw5rE", 23 | "liquidity": 0.435679538309231, 24 | "lastTradeUnixTime": 1739926864, 25 | "lastTradeHumanTime": "2025-02-19T01:01:04", 26 | "price": 0.0000047576190138903505, 27 | "history30mPrice": null, 28 | "priceChange30mPercent": null, 29 | "history1hPrice": null, 30 | "priceChange1hPercent": null, 31 | "history2hPrice": null, 32 | "priceChange2hPercent": null, 33 | "history4hPrice": null, 34 | "priceChange4hPercent": null, 35 | "history6hPrice": null, 36 | "priceChange6hPercent": null, 37 | "history8hPrice": null, 38 | "priceChange8hPercent": null, 39 | "history12hPrice": null, 40 | "priceChange12hPercent": null, 41 | "history24hPrice": null, 42 | "priceChange24hPercent": null, 43 | "uniqueWallet30m": 0, 44 | "uniqueWalletHistory30m": 0, 45 | "uniqueWallet30mChangePercent": null, 46 | "uniqueWallet1h": 0, 47 | "uniqueWalletHistory1h": 0, 48 | "uniqueWallet1hChangePercent": null, 49 | "uniqueWallet2h": 0, 50 | "uniqueWalletHistory2h": 0, 51 | "uniqueWallet2hChangePercent": null, 52 | "uniqueWallet4h": 0, 53 | "uniqueWalletHistory4h": 0, 54 | "uniqueWallet4hChangePercent": null, 55 | "uniqueWallet8h": 0, 56 | "uniqueWalletHistory8h": 0, 57 | "uniqueWallet8hChangePercent": null, 58 | "uniqueWallet24h": 0, 59 | "uniqueWalletHistory24h": 0, 60 | "uniqueWallet24hChangePercent": null, 61 | "supply": 999999027.370757, 62 | "totalSupply": 999999027.370757, 63 | "mc": 4757.61438649097, 64 | "circulatingSupply": 999999027.370757, 65 | "realMc": 4757.61438649097, 66 | "holder": 1, 67 | "trade30m": 0, 68 | "tradeHistory30m": 0, 69 | "trade30mChangePercent": null, 70 | "sell30m": 0, 71 | "sellHistory30m": 0, 72 | "sell30mChangePercent": null, 73 | "buy30m": 0, 74 | "buyHistory30m": 0, 75 | "buy30mChangePercent": null, 76 | "v30m": 0, 77 | "v30mUSD": 0, 78 | "vHistory30m": 0, 79 | "vHistory30mUSD": 0, 80 | "v30mChangePercent": null, 81 | "vBuy30m": 0, 82 | "vBuy30mUSD": 0, 83 | "vBuyHistory30m": 0, 84 | "vBuyHistory30mUSD": 0, 85 | "vBuy30mChangePercent": null, 86 | "vSell30m": 0, 87 | "vSell30mUSD": 0, 88 | "vSellHistory30m": 0, 89 | "vSellHistory30mUSD": 0, 90 | "vSell30mChangePercent": null, 91 | "trade1h": 0, 92 | "tradeHistory1h": 0, 93 | "trade1hChangePercent": null, 94 | "sell1h": 0, 95 | "sellHistory1h": 0, 96 | "sell1hChangePercent": null, 97 | "buy1h": 0, 98 | "buyHistory1h": 0, 99 | "buy1hChangePercent": null, 100 | "v1h": 0, 101 | "v1hUSD": 0, 102 | "vHistory1h": 0, 103 | "vHistory1hUSD": 0, 104 | "v1hChangePercent": null, 105 | "vBuy1h": 0, 106 | "vBuy1hUSD": 0, 107 | "vBuyHistory1h": 0, 108 | "vBuyHistory1hUSD": 0, 109 | "vBuy1hChangePercent": null, 110 | "vSell1h": 0, 111 | "vSell1hUSD": 0, 112 | "vSellHistory1h": 0, 113 | "vSellHistory1hUSD": 0, 114 | "vSell1hChangePercent": null, 115 | "trade2h": 0, 116 | "tradeHistory2h": 0, 117 | "trade2hChangePercent": null, 118 | "sell2h": 0, 119 | "sellHistory2h": 0, 120 | "sell2hChangePercent": null, 121 | "buy2h": 0, 122 | "buyHistory2h": 0, 123 | "buy2hChangePercent": null, 124 | "v2h": 0, 125 | "v2hUSD": 0, 126 | "vHistory2h": 0, 127 | "vHistory2hUSD": 0, 128 | "v2hChangePercent": null, 129 | "vBuy2h": 0, 130 | "vBuy2hUSD": 0, 131 | "vBuyHistory2h": 0, 132 | "vBuyHistory2hUSD": 0, 133 | "vBuy2hChangePercent": null, 134 | "vSell2h": 0, 135 | "vSell2hUSD": 0, 136 | "vSellHistory2h": 0, 137 | "vSellHistory2hUSD": 0, 138 | "vSell2hChangePercent": null, 139 | "trade4h": 0, 140 | "tradeHistory4h": 0, 141 | "trade4hChangePercent": null, 142 | "sell4h": 0, 143 | "sellHistory4h": 0, 144 | "sell4hChangePercent": null, 145 | "buy4h": 0, 146 | "buyHistory4h": 0, 147 | "buy4hChangePercent": null, 148 | "v4h": 0, 149 | "v4hUSD": 0, 150 | "vHistory4h": 0, 151 | "vHistory4hUSD": 0, 152 | "v4hChangePercent": null, 153 | "vBuy4h": 0, 154 | "vBuy4hUSD": 0, 155 | "vBuyHistory4h": 0, 156 | "vBuyHistory4hUSD": 0, 157 | "vBuy4hChangePercent": null, 158 | "vSell4h": 0, 159 | "vSell4hUSD": 0, 160 | "vSellHistory4h": 0, 161 | "vSellHistory4hUSD": 0, 162 | "vSell4hChangePercent": null, 163 | "trade8h": 0, 164 | "tradeHistory8h": 0, 165 | "trade8hChangePercent": null, 166 | "sell8h": 0, 167 | "sellHistory8h": 0, 168 | "sell8hChangePercent": null, 169 | "buy8h": 0, 170 | "buyHistory8h": 0, 171 | "buy8hChangePercent": null, 172 | "v8h": 0, 173 | "v8hUSD": 0, 174 | "vHistory8h": 0, 175 | "vHistory8hUSD": 0, 176 | "v8hChangePercent": null, 177 | "vBuy8h": 0, 178 | "vBuy8hUSD": 0, 179 | "vBuyHistory8h": 0, 180 | "vBuyHistory8hUSD": 0, 181 | "vBuy8hChangePercent": null, 182 | "vSell8h": 0, 183 | "vSell8hUSD": 0, 184 | "vSellHistory8h": 0, 185 | "vSellHistory8hUSD": 0, 186 | "vSell8hChangePercent": null, 187 | "trade24h": 0, 188 | "tradeHistory24h": 0, 189 | "trade24hChangePercent": null, 190 | "sell24h": 0, 191 | "sellHistory24h": 0, 192 | "sell24hChangePercent": null, 193 | "buy24h": 0, 194 | "buyHistory24h": 0, 195 | "buy24hChangePercent": null, 196 | "v24h": 0, 197 | "v24hUSD": 0, 198 | "vHistory24h": 0, 199 | "vHistory24hUSD": 0, 200 | "v24hChangePercent": null, 201 | "vBuy24h": 0, 202 | "vBuy24hUSD": 0, 203 | "vBuyHistory24h": 0, 204 | "vBuyHistory24hUSD": 0, 205 | "vBuy24hChangePercent": null, 206 | "vSell24h": 0, 207 | "vSell24hUSD": 0, 208 | "vSellHistory24h": 0, 209 | "vSellHistory24hUSD": 0, 210 | "vSell24hChangePercent": null, 211 | "numberMarkets": 1 212 | }, 213 | "success": true 214 | } 215 | ***********************************/ 216 | 217 | export async function getTokenOverview(tokenAddress: string) { 218 | const res = await axios.get( 219 | "https://public-api.birdeye.so/defi/token_overview", 220 | { 221 | params: { address: tokenAddress }, 222 | headers: { 223 | accept: "application/json", 224 | "x-chain": "solana", 225 | "x-api-key": BIRDEYE_API_KEY, 226 | }, 227 | } 228 | ); 229 | if (res.data?.success) { 230 | return res.data?.data; 231 | } else { 232 | return null; 233 | } 234 | } 235 | 236 | export async function getWalletTokenList(walletAddress: string) { 237 | const res = await axios.get( 238 | "https://public-api.birdeye.so/v1/wallet/token_list", 239 | { 240 | params: { wallet: walletAddress }, 241 | headers: { 242 | accept: "application/json", 243 | "x-chain": "solana", 244 | "x-api-key": BIRDEYE_API_KEY, 245 | }, 246 | } 247 | ); 248 | if (res.data?.success) { 249 | return res.data?.data; 250 | } else { 251 | return null; 252 | } 253 | } -------------------------------------------------------------------------------- /bot/message.handler.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { getUserById } from "../service/userService"; 3 | import { isValidSolanaAddress } from "../utils/utils"; 4 | import { BotCaption } from "../config/constants"; 5 | import { getIKSnipe } from "../components/inlineKeyboard"; 6 | import { sendIKSnipe } from "./botAction"; 7 | import { getTokenOverview } from "../service/birdeyeService"; 8 | 9 | export async function messageHandler( 10 | bot: TelegramBot, 11 | msg: TelegramBot.Message, 12 | userSnipeConfig: Map 13 | ) { 14 | try { 15 | if (!msg.text) return; 16 | 17 | const chatId = msg.chat.id; 18 | const existingUser = await getUserById(chatId); 19 | if (!existingUser) { 20 | return; 21 | } 22 | // console.log("hello------------->", msg.text, msg) 23 | const { reply_to_message } = msg; 24 | if (reply_to_message && reply_to_message.text) { 25 | const { text } = reply_to_message; 26 | 27 | const regex = /^[0-9]+(\.[0-9]+)?$/; 28 | const isNumber = regex.test(msg.text) === true; 29 | 30 | console.log("isNumber------------>,", isNumber, text); 31 | const reply_message_id = reply_to_message.message_id; 32 | 33 | switch (text) { 34 | case BotCaption.strInputTokenAddress: 35 | case BotCaption.strInvalidSolanaTokenAddress: 36 | const isCA = await isValidSolanaAddress(msg.text); 37 | if (isCA) { 38 | const snipe_config = userSnipeConfig.get(chatId); 39 | console.log("Message snipe_config: ", snipe_config); 40 | const updated_config = { ...snipe_config, token: msg.text }; 41 | userSnipeConfig.set(chatId, updated_config); 42 | // snipe_config.token = msg.text; 43 | const IK_SNIPE = getIKSnipe(updated_config); 44 | sendIKSnipe(bot, chatId, IK_SNIPE); 45 | } else { 46 | await bot.deleteMessage(chatId, msg.message_id); 47 | await bot.deleteMessage(chatId, reply_message_id); 48 | 49 | await bot.sendMessage( 50 | chatId, 51 | BotCaption.strInvalidSolanaTokenAddress, 52 | { 53 | parse_mode: "HTML", 54 | reply_markup: { 55 | force_reply: true, 56 | selective: true, 57 | }, 58 | } 59 | ); 60 | return; 61 | } 62 | break; 63 | case BotCaption.SET_PRIORITY_FEE.replace(/<[^>]*>/g, ""): 64 | console.log("priority fee"); 65 | if (isNumber) { 66 | const snipe_config = userSnipeConfig.get(chatId); 67 | const updated_config = { 68 | ...snipe_config, 69 | snipe_fee: parseFloat(msg.text), 70 | }; 71 | console.log("Message snipe_config: ", updated_config); 72 | userSnipeConfig.set(chatId, updated_config); 73 | const IK_SNIPE = getIKSnipe(updated_config); 74 | sendIKSnipe(bot, chatId, IK_SNIPE); 75 | } else { 76 | await bot.deleteMessage(chatId, msg.message_id); 77 | await bot.deleteMessage(chatId, reply_message_id); 78 | 79 | await bot.sendMessage(chatId, BotCaption.strInvalidInput); 80 | } 81 | break; 82 | case BotCaption.SET_JITOTIP.replace(/<[^>]*>/g, ""): 83 | console.log("jito tip"); 84 | if (isNumber) { 85 | const snipe_config = userSnipeConfig.get(chatId); 86 | const updated_config = { 87 | ...snipe_config, 88 | snipe_tip: parseFloat(msg.text), 89 | }; 90 | console.log("Message snipe_config: ", updated_config); 91 | userSnipeConfig.set(chatId, updated_config); 92 | const IK_SNIPE = getIKSnipe(updated_config); 93 | sendIKSnipe(bot, chatId, IK_SNIPE); 94 | } else { 95 | await bot.deleteMessage(chatId, msg.message_id); 96 | await bot.deleteMessage(chatId, reply_message_id); 97 | 98 | await bot.sendMessage(chatId, BotCaption.strInvalidInput); 99 | } 100 | break; 101 | case BotCaption.SET_SLIPPAGE.replace(/<[^>]*>/g, ""): 102 | console.log("slippage"); 103 | if (isNumber) { 104 | const snipe_config = userSnipeConfig.get(chatId); 105 | const updated_config = { 106 | ...snipe_config, 107 | slippage: parseFloat(msg.text), 108 | }; 109 | console.log("Message snipe_config: ", updated_config); 110 | userSnipeConfig.set(chatId, updated_config); 111 | const IK_SNIPE = getIKSnipe(updated_config); 112 | sendIKSnipe(bot, chatId, IK_SNIPE); 113 | } else { 114 | await bot.deleteMessage(chatId, msg.message_id); 115 | await bot.deleteMessage(chatId, reply_message_id); 116 | 117 | await bot.sendMessage(chatId, BotCaption.strInvalidInput); 118 | } 119 | break; 120 | case BotCaption.SET_TakeProfit.replace(/<[^>]*>/g, ""): 121 | console.log("take profit"); 122 | if (isNumber) { 123 | const snipe_config = userSnipeConfig.get(chatId); 124 | const updated_config = { 125 | ...snipe_config, 126 | tp: parseFloat(msg.text), 127 | }; 128 | console.log("Message snipe_config: ", updated_config); 129 | userSnipeConfig.set(chatId, updated_config); 130 | const IK_SNIPE = getIKSnipe(updated_config); 131 | sendIKSnipe(bot, chatId, IK_SNIPE); 132 | } else { 133 | await bot.deleteMessage(chatId, msg.message_id); 134 | await bot.deleteMessage(chatId, reply_message_id); 135 | 136 | await bot.sendMessage(chatId, BotCaption.strInvalidInput); 137 | } 138 | break; 139 | case BotCaption.SET_StopLoss.replace(/<[^>]*>/g, ""): 140 | console.log("priority fee"); 141 | if (isNumber) { 142 | const snipe_config = userSnipeConfig.get(chatId); 143 | const updated_config = { 144 | ...snipe_config, 145 | sl: parseFloat(msg.text), 146 | }; 147 | console.log("Message snipe_config: ", updated_config); 148 | userSnipeConfig.set(chatId, updated_config); 149 | const IK_SNIPE = getIKSnipe(updated_config); 150 | sendIKSnipe(bot, chatId, IK_SNIPE); 151 | } else { 152 | await bot.deleteMessage(chatId, msg.message_id); 153 | await bot.deleteMessage(chatId, reply_message_id); 154 | 155 | await bot.sendMessage(chatId, BotCaption.strInvalidInput); 156 | } 157 | break; 158 | case BotCaption.SET_SNIPE_AMOUNT.replace(/<[^>]*>/g, ""): 159 | console.log("snipe amount"); 160 | if (isNumber) { 161 | const snipe_config = userSnipeConfig.get(chatId); 162 | const updated_config = { 163 | ...snipe_config, 164 | snipe_amount: parseFloat(msg.text), 165 | }; 166 | console.log("Message snipe_config: ", updated_config); 167 | userSnipeConfig.set(chatId, updated_config); 168 | const IK_SNIPE = getIKSnipe(updated_config); 169 | sendIKSnipe(bot, chatId, IK_SNIPE); 170 | } else { 171 | await bot.deleteMessage(chatId, msg.message_id); 172 | await bot.deleteMessage(chatId, reply_message_id); 173 | 174 | await bot.sendMessage(chatId, BotCaption.strInvalidInput); 175 | } 176 | break; 177 | } 178 | } else { 179 | const isCA = await isValidSolanaAddress(msg.text); 180 | if (isCA) { 181 | const tokenInfo = await getTokenOverview(msg.text); 182 | 183 | const caption = `Name (Symbol): ${tokenInfo.name} (${tokenInfo.symbol})\nPrice: ${tokenInfo.price}\nMarketCap: ${tokenInfo.marketCap}`; 184 | 185 | const snipe_config = userSnipeConfig.get(chatId); 186 | console.log("Message snipe_config: ", snipe_config); 187 | const updated_config = { ...snipe_config, token: msg.text }; 188 | userSnipeConfig.set(chatId, updated_config); 189 | // snipe_config.token = msg.text; 190 | const IK_SNIPE = getIKSnipe(updated_config); 191 | sendIKSnipe(bot, chatId, IK_SNIPE, caption); 192 | } else { 193 | return; 194 | } 195 | } 196 | } catch (error) {} 197 | } 198 | -------------------------------------------------------------------------------- /swap/sell.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | Connection, 4 | Keypair, 5 | VersionedTransaction, 6 | PublicKey, 7 | SystemProgram, 8 | AddressLookupTableAccount, 9 | TransactionMessage, 10 | TransactionInstruction, 11 | } from "@solana/web3.js"; 12 | import bs58 from "bs58"; 13 | // import { addSwap } from "../service/swap.service"; 14 | import { getTokenOverview } from "../service/birdeyeService"; 15 | import { getTokenInfo_Decimals_Supply, getTokenPrice } from "../utils/web3"; 16 | import dotenv from "dotenv"; 17 | import { BOT_FEE_PERCENT } from "../config/constants"; 18 | 19 | dotenv.config(); 20 | 21 | // ✅ Solana Mainnet Connection 22 | const SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com"; 23 | const connection = new Connection(SOLANA_RPC_URL, "confirmed"); 24 | const BOT_FEE_WALLET_ADDRESS = process.env.BOT_FEE_WALLET_ADDRESS as string; 25 | 26 | const botFeeWalletPublicKey = new PublicKey(BOT_FEE_WALLET_ADDRESS); // Replace with your wallet address 27 | 28 | // ✅ **Get Swap Route from Jupiter API with Retry Logic** 29 | async function getJupiterSwapRoute( 30 | retries = 3, 31 | amountInToken: number, 32 | slippage: number, 33 | tokenAddress: string, 34 | decimals: number 35 | ) { 36 | while (retries > 0) { 37 | try { 38 | const url = `https://quote-api.jup.ag/v6/quote?inputMint=${tokenAddress}&outputMint=So11111111111111111111111111111111111111112&amount=${Math.floor( 39 | amountInToken * Math.pow(10, decimals) 40 | )}&slippageBps=${slippage * 100}`; 41 | console.log(`🔍 Requesting swap route from Jupiter API: ${url}`); 42 | 43 | const quoteResponse = (await axios.get(url)).data; 44 | console.log(quoteResponse); 45 | 46 | return quoteResponse; 47 | } catch (error: any) { 48 | console.error(`❌ Swap Route Error: ${error.message}`); 49 | retries--; 50 | if (retries === 0) process.exit(1); 51 | console.log(`🔄 Retrying... Attempts left: ${retries}`); 52 | await new Promise((res) => setTimeout(res, 2000)); 53 | } 54 | } 55 | } 56 | 57 | function deserializeInstruction(instruction: any) { 58 | return new TransactionInstruction({ 59 | programId: new PublicKey(instruction.programId), 60 | keys: instruction.accounts.map((key: any) => ({ 61 | pubkey: new PublicKey(key.pubkey), 62 | isSigner: key.isSigner, 63 | isWritable: key.isWritable, 64 | })), 65 | data: Buffer.from(instruction.data, "base64"), 66 | }); 67 | } 68 | 69 | // ✅ **Execute Swap Transaction** 70 | async function executeSwap( 71 | routeData: any, 72 | ownerAddress: string, 73 | wallet: Keypair, 74 | gasFee: number, 75 | tokenAddress: string, 76 | chatId: number, 77 | amountInToken: number, 78 | botFeeSOL: number 79 | ) { 80 | try { 81 | const instructions = ( 82 | await axios.post( 83 | "https://quote-api.jup.ag/v6/swap-instructions", 84 | { 85 | quoteResponse: routeData, 86 | userPublicKey: ownerAddress, 87 | wrapAndUnwrapSol: true, 88 | computeUnitPriceMicroLamports: Math.floor(gasFee * 1e9), // ✅ Convert SOL Gas Fee to MicroLamports 89 | }, 90 | { 91 | headers: { 92 | "Content-Type": "application/json", 93 | }, 94 | } 95 | ) 96 | ).data; 97 | 98 | const { 99 | tokenLedgerInstruction, // If you are using `useTokenLedger = true`. 100 | computeBudgetInstructions, // The necessary instructions to setup the compute budget. 101 | setupInstructions, // Setup missing ATA for the users. 102 | swapInstruction: swapInstructionPayload, // The actual swap instruction. 103 | cleanupInstruction, // Unwrap the SOL if `wrapAndUnwrapSol = true`. 104 | addressLookupTableAddresses, // The lookup table addresses that you can use if you are using versioned transaction. 105 | } = instructions; 106 | 107 | const getAddressLookupTableAccounts = async ( 108 | keys: string[] 109 | ): Promise => { 110 | const addressLookupTableAccountInfos = 111 | await connection.getMultipleAccountsInfo( 112 | keys.map((key) => new PublicKey(key)) 113 | ); 114 | 115 | return addressLookupTableAccountInfos.reduce( 116 | (acc, accountInfo, index) => { 117 | const addressLookupTableAddress = keys[index]; 118 | if (accountInfo) { 119 | const addressLookupTableAccount = new AddressLookupTableAccount({ 120 | key: new PublicKey(addressLookupTableAddress), 121 | state: AddressLookupTableAccount.deserialize(accountInfo.data), 122 | }); 123 | acc.push(addressLookupTableAccount); 124 | } 125 | 126 | return acc; 127 | }, 128 | new Array() 129 | ); 130 | }; 131 | 132 | const addressLookupTableAccounts = []; 133 | addressLookupTableAccounts.push( 134 | ...(await getAddressLookupTableAccounts(addressLookupTableAddresses)) 135 | ); 136 | 137 | // ✅ Add Transfer Instruction for Bot Fee 138 | const botFee = Math.floor(botFeeSOL * 1e9); // bot fee SOL, adjust as needed 139 | 140 | const transferInstruction = SystemProgram.transfer({ 141 | fromPubkey: wallet.publicKey, 142 | toPubkey: botFeeWalletPublicKey, 143 | lamports: botFee, 144 | }); 145 | 146 | // ✅ Fetch Fresh Blockhash Before Signing 147 | const { blockhash } = await connection.getLatestBlockhash("finalized"); 148 | 149 | if (!blockhash) { 150 | console.error("Failed to retrieve blockhash from cache"); 151 | throw new Error("Failed to retrieve blockhash from cache"); 152 | } 153 | const messageV0 = new TransactionMessage({ 154 | payerKey: wallet.publicKey, 155 | recentBlockhash: blockhash, 156 | instructions: [ 157 | ...setupInstructions.map(deserializeInstruction), 158 | deserializeInstruction(swapInstructionPayload), 159 | deserializeInstruction(cleanupInstruction), 160 | transferInstruction, 161 | ], 162 | }).compileToV0Message(addressLookupTableAccounts); 163 | 164 | const transaction = new VersionedTransaction(messageV0); 165 | 166 | transaction.sign([wallet]); 167 | 168 | // ✅ **Simulate Transaction Before Sending** 169 | const simulationResult = await connection.simulateTransaction(transaction); 170 | if (simulationResult.value.err) { 171 | console.error("❌ Simulation Error:", simulationResult.value.err); 172 | process.exit(1); 173 | } 174 | 175 | console.log(`🚀 Sending Signed Transaction...`); 176 | const txid = await connection.sendRawTransaction(transaction.serialize(), { 177 | skipPreflight: false, 178 | preflightCommitment: "confirmed", 179 | }); 180 | 181 | const tokenInfo = await getTokenInfo_Decimals_Supply(tokenAddress); 182 | const sol_amount = parseFloat(routeData.outAmount) / 1e9; 183 | const token_amount = 184 | parseFloat(routeData.inAmount) / Math.pow(10, tokenInfo?.decimals); 185 | const token_price = parseFloat(routeData.swapUsdValue) / token_amount; 186 | 187 | // await addSwap({ 188 | // chat_id: chatId, 189 | // token_mint_address: tokenAddress, 190 | // tx_hash: txid, 191 | // sol_amount: sol_amount, 192 | // token_amount: token_amount, 193 | // entry_sol_price: parseFloat(routeData.swapUsdValue) / sol_amount, 194 | // avg_entry_price: token_price, 195 | // avg_entry_mc: 196 | // (tokenInfo.supply * token_price) / Math.pow(10, tokenInfo.decimals), 197 | // swap: "SELL", 198 | // tx_time: Date.now(), 199 | // }); 200 | console.log(`✅ Swap Submitted! Transaction ID: ${txid}`); 201 | console.log(`🔗 View on SolScan: https://solscan.io/tx/${txid}`); 202 | return { status: "success", tx_hash: txid, amount: sol_amount }; 203 | } catch (error: any) { 204 | console.error(`❌ Swap Execution Failed: ${error.message}`); 205 | return { status: "failed", tx_hash: null }; 206 | } 207 | } 208 | 209 | export async function sellToken( 210 | chatId: number, 211 | private_key: string, 212 | amountInToken: number, 213 | tokenAddress: string, 214 | gasFee: number, 215 | slippage: number 216 | ) { 217 | try { 218 | let privateKeyString = private_key; 219 | 220 | // ✅ Decode and Validate Base58 Private Key 221 | let privateKey; 222 | try { 223 | privateKey = bs58.decode(privateKeyString); 224 | if (privateKey.length !== 64) { 225 | throw new Error( 226 | `❌ Invalid private key length! Expected 64 bytes but got ${privateKey.length}.` 227 | ); 228 | } 229 | } catch (e: any) { 230 | console.error(`❌ Failed to decode private key: ${e.message}`); 231 | return { status: "failed", tx_hash: null }; 232 | } 233 | 234 | const wallet = Keypair.fromSecretKey(privateKey); 235 | const ownerAddress = wallet.publicKey.toString(); 236 | 237 | console.log(`🚀 Wallet: ${ownerAddress}`); 238 | console.log(`💰 Selling Token: ${tokenAddress}`); 239 | console.log(`🔄 Amount: ${amountInToken} Tokens`); 240 | console.log(`⛽ Gas Fee: ${gasFee} SOL`); 241 | console.log(`📊 Slippage: ${slippage}%`); 242 | console.log("🔄 Fetching swap route..."); 243 | 244 | const tokenPrices = await getTokenPrice([tokenAddress]); 245 | const tokenPrice = parseFloat(tokenPrices.data[tokenAddress].price); 246 | const solPrice = parseFloat( 247 | tokenPrices.data["So11111111111111111111111111111111111111112"].price 248 | ); 249 | const botFeeSOL = 250 | ((amountInToken * tokenPrice) / solPrice) * (BOT_FEE_PERCENT / 100); 251 | const solBalance = await connection.getBalance(wallet.publicKey); 252 | 253 | if (solBalance < botFeeSOL) { 254 | console.error(`❌ Swap Execution Failed: Insufficient SOL balance`); 255 | return { status: "failed", tx_hash: null }; 256 | } 257 | 258 | // Get Decimals and Supply 259 | const tokenInfo = await getTokenInfo_Decimals_Supply(tokenAddress); 260 | const decimals = tokenInfo?.decimals; 261 | 262 | // ✅ **Execute Swap Process** 263 | const swapRoute = await getJupiterSwapRoute( 264 | 3, 265 | amountInToken, 266 | slippage, 267 | tokenAddress, 268 | decimals 269 | ); 270 | const result = await executeSwap( 271 | swapRoute, 272 | ownerAddress, 273 | wallet, 274 | gasFee, 275 | tokenAddress, 276 | chatId, 277 | amountInToken, 278 | botFeeSOL 279 | ); 280 | return result; 281 | } catch (error: any) { 282 | console.error(`❌ Error: ${error.message}`); 283 | return { status: "failed", tx_hash: null }; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /swap/buy.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | Connection, 4 | Keypair, 5 | VersionedTransaction, 6 | PublicKey, 7 | SystemProgram, 8 | AddressLookupTableAccount, 9 | TransactionMessage, 10 | TransactionInstruction, 11 | } from "@solana/web3.js"; 12 | import bs58 from "bs58"; 13 | // import { addSwap } from "../service/swap.service"; 14 | import { getTokenOverview } from "../service/birdeyeService"; 15 | import { getTokenInfo_Decimals_Supply } from "../utils/web3"; 16 | import dotenv from "dotenv"; 17 | import { BOT_FEE_PERCENT } from "../config/constants"; 18 | 19 | dotenv.config(); 20 | 21 | // ✅ Solana Mainnet Connection 22 | const SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com"; 23 | const connection = new Connection(SOLANA_RPC_URL, "confirmed"); 24 | const BOT_WALLET_PUBLIC_KEY = process.env.BOT_WALLET_PUBLIC_KEY as string; 25 | 26 | const botFeeWalletPublicKey = new PublicKey(BOT_WALLET_PUBLIC_KEY); // Replace with your wallet address 27 | 28 | // ✅ Get CLI Arguments (Private Key, Amount, Token, Gas Fee, Slippage) 29 | // const args = process.argv.slice(2); 30 | // if (args.length < 5) { 31 | // console.error( 32 | // "❌ Missing arguments! Usage: node buy_token.js " 33 | // ); 34 | // process.exit(1); 35 | // } 36 | 37 | // ✅ **Get Swap Route from Jupiter API with Retry Logic** 38 | async function getJupiterSwapRoute( 39 | retries = 3, 40 | amountInSol: number, 41 | slippage: number, 42 | tokenAddress: string 43 | ) { 44 | while (retries > 0) { 45 | try { 46 | const url = `https://quote-api.jup.ag/v6/quote?inputMint=So11111111111111111111111111111111111111112&outputMint=${tokenAddress}&amount=${Math.floor( 47 | amountInSol * 1e9 48 | )}&slippageBps=${slippage * 100}`; 49 | console.log(`🔍 Requesting swap route from Jupiter API: ${url}`); 50 | 51 | const quoteResponse = (await axios.get(url)).data; 52 | console.log(quoteResponse); 53 | 54 | return quoteResponse; 55 | } catch (error: any) { 56 | console.error(`❌ Swap Route Error: ${error.message}`); 57 | retries--; 58 | if (retries === 0) process.exit(1); 59 | console.log(`🔄 Retrying... Attempts left: ${retries}`); 60 | await new Promise((res) => setTimeout(res, 2000)); 61 | } 62 | } 63 | } 64 | 65 | function deserializeInstruction(instruction: any) { 66 | return new TransactionInstruction({ 67 | programId: new PublicKey(instruction.programId), 68 | keys: instruction.accounts.map((key: any) => ({ 69 | pubkey: new PublicKey(key.pubkey), 70 | isSigner: key.isSigner, 71 | isWritable: key.isWritable, 72 | })), 73 | data: Buffer.from(instruction.data, "base64"), 74 | }); 75 | } 76 | 77 | // ✅ **Execute Swap Transaction** 78 | async function executeSwap( 79 | routeData: any, 80 | ownerAddress: string, 81 | wallet: Keypair, 82 | gasFee: number, 83 | tokenAddress: string, 84 | chatId: number, 85 | amountInSol: number 86 | ) { 87 | try { 88 | const instructions = ( 89 | await axios.post( 90 | "https://quote-api.jup.ag/v6/swap-instructions", 91 | { 92 | quoteResponse: routeData, 93 | userPublicKey: ownerAddress, 94 | wrapAndUnwrapSol: true, 95 | computeUnitPriceMicroLamports: Math.floor(gasFee * 1e9), // ✅ Convert SOL Gas Fee to MicroLamports 96 | }, 97 | { 98 | headers: { 99 | "Content-Type": "application/json", 100 | }, 101 | } 102 | ) 103 | ).data; 104 | 105 | const { 106 | tokenLedgerInstruction, // If you are using `useTokenLedger = true`. 107 | computeBudgetInstructions, // The necessary instructions to setup the compute budget. 108 | setupInstructions, // Setup missing ATA for the users. 109 | swapInstruction: swapInstructionPayload, // The actual swap instruction. 110 | cleanupInstruction, // Unwrap the SOL if `wrapAndUnwrapSol = true`. 111 | addressLookupTableAddresses, // The lookup table addresses that you can use if you are using versioned transaction. 112 | } = instructions; 113 | 114 | const getAddressLookupTableAccounts = async ( 115 | keys: string[] 116 | ): Promise => { 117 | const addressLookupTableAccountInfos = 118 | await connection.getMultipleAccountsInfo( 119 | keys.map((key) => new PublicKey(key)) 120 | ); 121 | 122 | return addressLookupTableAccountInfos.reduce( 123 | (acc, accountInfo, index) => { 124 | const addressLookupTableAddress = keys[index]; 125 | if (accountInfo) { 126 | const addressLookupTableAccount = new AddressLookupTableAccount({ 127 | key: new PublicKey(addressLookupTableAddress), 128 | state: AddressLookupTableAccount.deserialize(accountInfo.data), 129 | }); 130 | acc.push(addressLookupTableAccount); 131 | } 132 | 133 | return acc; 134 | }, 135 | new Array() 136 | ); 137 | }; 138 | 139 | const addressLookupTableAccounts = []; 140 | addressLookupTableAccounts.push( 141 | ...(await getAddressLookupTableAccounts(addressLookupTableAddresses)) 142 | ); 143 | 144 | // ✅ Add Transfer Instruction for Bot Fee 145 | const botFee = Math.floor(amountInSol * (BOT_FEE_PERCENT / 100) * 1e9); // bot fee SOL, adjust as needed 146 | 147 | const transferInstruction = SystemProgram.transfer({ 148 | fromPubkey: wallet.publicKey, 149 | toPubkey: botFeeWalletPublicKey, 150 | lamports: botFee, 151 | }); 152 | 153 | // ✅ Fetch Fresh Blockhash Before Signing 154 | const { blockhash } = await connection.getLatestBlockhash("finalized"); 155 | 156 | if (!blockhash) { 157 | console.error("Failed to retrieve blockhash from cache"); 158 | throw new Error("Failed to retrieve blockhash from cache"); 159 | } 160 | const messageV0 = new TransactionMessage({ 161 | payerKey: wallet.publicKey, 162 | recentBlockhash: blockhash, 163 | instructions: [ 164 | ...setupInstructions.map(deserializeInstruction), 165 | deserializeInstruction(swapInstructionPayload), 166 | deserializeInstruction(cleanupInstruction), 167 | transferInstruction, 168 | ], 169 | }).compileToV0Message(addressLookupTableAccounts); 170 | 171 | const transaction = new VersionedTransaction(messageV0); 172 | 173 | transaction.sign([wallet]); 174 | 175 | // ✅ **Simulate Transaction Before Sending** 176 | const simulationResult = await connection.simulateTransaction(transaction); 177 | if (simulationResult.value.err) { 178 | console.error("❌ Simulation Error:", simulationResult.value.err); 179 | process.exit(1); 180 | } 181 | 182 | console.log(`🚀 Sending Signed Transaction...`); 183 | const txid = await connection.sendRawTransaction(transaction.serialize(), { 184 | skipPreflight: false, 185 | preflightCommitment: "confirmed", 186 | }); 187 | 188 | // const tokenOverview = await getTokenOverview(tokenAddress); 189 | const tokenInfo = await getTokenInfo_Decimals_Supply(tokenAddress); 190 | // console.log("tokenOverview", tokenOverview); 191 | const sol_amount = parseFloat(routeData.inAmount) / 1e9; 192 | const token_amount = 193 | parseFloat(routeData.outAmount) / Math.pow(10, tokenInfo?.decimals); 194 | const token_price = parseFloat(routeData.swapUsdValue) / token_amount; 195 | 196 | // await addSwap({ 197 | // chat_id: chatId, 198 | // token_mint_address: tokenAddress, 199 | // tx_hash: txid, 200 | // sol_amount: sol_amount, 201 | // token_amount: token_amount, 202 | // entry_sol_price: parseFloat(routeData.swapUsdValue) / sol_amount, 203 | // avg_entry_price: token_price, 204 | // avg_entry_mc: 205 | // (tokenInfo.supply * token_price) / Math.pow(10, tokenInfo.decimals), 206 | // swap: "BUY", 207 | // tx_time: Date.now(), 208 | // }); 209 | console.log(`✅ Swap Submitted! Transaction ID: ${txid}`); 210 | console.log(`🔗 View on SolScan: https://solscan.io/tx/${txid}`); 211 | return { status: "success", tx_hash: txid, amount: sol_amount }; 212 | } catch (error: any) { 213 | console.error(`❌ Swap Execution Failed: ${error.message}`); 214 | return { status: "failed", tx_hash: null }; 215 | // process.exit(1); 216 | } 217 | } 218 | 219 | async function buyToken( 220 | chatId: number, 221 | private_key: string, 222 | amountInSol: number, 223 | tokenAddress: string, 224 | gasFee: number, 225 | slippage: number 226 | ) { 227 | try { 228 | // let privateKeyString = args[0].trim(); 229 | let privateKeyString = private_key; 230 | 231 | // ✅ Decode and Validate Base58 Private Key 232 | let privateKey: Uint8Array; 233 | try { 234 | privateKey = bs58.decode(privateKeyString); 235 | if (privateKey.length !== 64) { 236 | throw new Error( 237 | `❌ Invalid private key length! Expected 64 bytes but got ${privateKey.length}.` 238 | ); 239 | } 240 | } catch (e: any) { 241 | console.error(`❌ Failed to decode private key: ${e.message}`); 242 | process.exit(1); 243 | } 244 | 245 | // ✅ Extract Swap Parameters 246 | // const amountInSol = parseFloat(args[1]); 247 | // const tokenAddress = args[2].trim(); 248 | // const gasFee = parseFloat(args[3]); 249 | // const slippage = parseFloat(args[4]); 250 | 251 | const wallet = Keypair.fromSecretKey(privateKey); 252 | const ownerAddress = wallet.publicKey.toString(); 253 | 254 | console.log(`🚀 Wallet: ${ownerAddress}`); 255 | console.log(`💰 Buying Token: ${tokenAddress}`); 256 | console.log(`🔄 Amount: ${amountInSol} SOL`); 257 | console.log(`⛽ Gas Fee: ${gasFee} SOL`); 258 | console.log(`📊 Slippage: ${slippage}%`); 259 | console.log("🔄 Fetching swap route..."); 260 | 261 | const botFee = amountInSol * (BOT_FEE_PERCENT / 100); 262 | const solBalance = await connection.getBalance(wallet.publicKey); 263 | 264 | if (solBalance < botFee) { 265 | console.error(`❌ Swap Execution Failed: Insufficient SOL balance`); 266 | return { status: "failed", tx_hash: null }; 267 | } 268 | 269 | // ✅ **Execute Swap Process** 270 | const swapRoute = await getJupiterSwapRoute( 271 | 3, 272 | amountInSol, 273 | slippage, 274 | tokenAddress 275 | ); 276 | const result = await executeSwap( 277 | swapRoute, 278 | ownerAddress, 279 | wallet, 280 | gasFee, 281 | tokenAddress, 282 | chatId, 283 | amountInSol 284 | ); 285 | return result; 286 | } catch (error: any) { 287 | console.error(`❌ Error: ${error.message}`); 288 | process.exit(1); 289 | } 290 | } 291 | 292 | export default buyToken; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 40 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 41 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 42 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 43 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "noEmit": true, /* Disable emitting files from a compilation. */ 60 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 61 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 62 | // "removeComments": true, /* Disable emitting comments. */ 63 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 64 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 65 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 66 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 69 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 72 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 74 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 80 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 81 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 82 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 83 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 84 | 85 | /* Type Checking */ 86 | "strict": true, /* Enable all strict type-checking options. */ 87 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 88 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 89 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 90 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 91 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 92 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 93 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 94 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 95 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 96 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 97 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 98 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 99 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 100 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 101 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 102 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 103 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 104 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 105 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 106 | 107 | /* Completeness */ 108 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 109 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 110 | } 111 | } 112 | --------------------------------------------------------------------------------