├── .env.backup ├── src ├── test │ ├── rugCheckHandler.test.ts │ └── sniperooHandler.test.ts ├── utils │ ├── notification.ts │ ├── env-validator.ts │ ├── handlers │ │ ├── sniperooHandler.ts │ │ ├── signatureHandler.ts │ │ ├── tokenHandler.ts │ │ └── rugCheckHandler.ts │ └── managers │ │ └── websocketManager.ts ├── types.ts ├── config.ts ├── tracker │ └── db.ts └── index.ts ├── eslint.config.mjs ├── tsconfig.json ├── .gitignore ├── package.json ├── CHANGELOG.md └── README.md /.env.backup: -------------------------------------------------------------------------------- 1 | # Sniperoo Config 2 | SNIPEROO_API_KEY="" 3 | SNIPEROO_PUBKEY="" 4 | 5 | # Helius Config 6 | HELIUS_HTTPS_URI="https://mainnet.helius-rpc.com/?api-key=" 7 | HELIUS_WSS_URI="wss://mainnet.helius-rpc.com/?api-key=" 8 | 9 | -------------------------------------------------------------------------------- /src/test/rugCheckHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { getRugCheckConfirmed } from "../utils/handlers/rugCheckHandler"; 2 | 3 | (async () => { 4 | const testId = ""; 5 | if (testId) { 6 | const res = await getRugCheckConfirmed(testId); 7 | console.log("result:", res); 8 | } 9 | })(); 10 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import tseslint from "typescript-eslint"; 3 | 4 | 5 | /** @type {import('eslint').Linter.Config[]} */ 6 | export default [ 7 | {files: ["**/*.{js,mjs,cjs,ts}"]}, 8 | {languageOptions: { globals: globals.browser }}, 9 | ...tseslint.configs.recommended, 10 | ]; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | *.pem 16 | 17 | # debug 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # local env files 23 | .env*.local 24 | .env 25 | .todo 26 | 27 | # vercel 28 | .vercel 29 | 30 | # typescript 31 | *.tsbuildinfo 32 | next-env.d.ts 33 | 34 | # Project Personal Files 35 | tokens.db 36 | myConfig.ts -------------------------------------------------------------------------------- /src/utils/notification.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import { config } from "../config"; 3 | 4 | export function playSound(speech?: string) { 5 | const text = speech ? speech : config.token_buy.play_sound_text; 6 | const command = `powershell -Command "(New-Object -com SAPI.SpVoice).speak('${text}')"`; 7 | exec(command, (error, stdout, stderr) => { 8 | if (error) { 9 | console.error(`Error: ${error.message}`); 10 | return false; 11 | } 12 | if (stderr) { 13 | console.error(`stderr: ${stderr}`); 14 | return false; 15 | } 16 | console.log("Speech executed successfully"); 17 | return true; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sol_token_sniper", 3 | "version": "2.0.0", 4 | "main": "dist/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node dist/index.js", 8 | "start:tracker": "node dist/tracker/index.js", 9 | "dev": "ts-node src/index.ts", 10 | "test:sniperooHandler": "ts-node src/test/sniperooHandler.test.ts", 11 | "test:rugCheckHandler": "ts-node src/test/rugCheckHandler.test.ts", 12 | "clean": "rm -rf dist" 13 | }, 14 | "keywords": [], 15 | "author": "https://x.com/digbenjamins", 16 | "license": "ISC", 17 | "description": "", 18 | "devDependencies": { 19 | "@types/luxon": "^3.4.2", 20 | "@types/node": "^22.10.1", 21 | "@types/ws": "^8.5.13", 22 | "eslint": "^9.15.0", 23 | "globals": "^15.12.0", 24 | "ts-node": "^10.9.2", 25 | "typescript": "^5.7.2", 26 | "typescript-eslint": "^8.16.0" 27 | }, 28 | "dependencies": { 29 | "@project-serum/anchor": "^0.26.0", 30 | "@solana/spl-token": "^0.4.13", 31 | "@solana/web3.js": "^1.95.5", 32 | "axios": "^1.7.8", 33 | "bs58": "^6.0.0", 34 | "dotenv": "^16.4.5", 35 | "luxon": "^3.5.0", 36 | "sqlite": "^5.1.1", 37 | "sqlite3": "^5.1.7", 38 | "ws": "^8.18.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/env-validator.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | // Load environment variables 3 | dotenv.config(); 4 | 5 | export interface EnvConfig { 6 | HELIUS_HTTPS_URI: string; 7 | HELIUS_WSS_URI: string; 8 | SNIPEROO_API_KEY: string; 9 | SNIPEROO_PUBKEY: string; 10 | } 11 | 12 | export function validateEnv(): EnvConfig { 13 | const requiredEnvVars = ["HELIUS_HTTPS_URI", "HELIUS_WSS_URI"] as const; 14 | 15 | const missingVars = requiredEnvVars.filter((envVar) => { 16 | return !process.env[envVar]; 17 | }); 18 | 19 | if (missingVars.length > 0) { 20 | throw new Error(`🚫 Missing required environment variables: ${missingVars.join(", ")}`); 21 | } 22 | 23 | const validateUrl = (envVar: string, protocol: string, checkApiKey: boolean = false) => { 24 | const value = process.env[envVar]; 25 | if (!value) return; 26 | 27 | const url = new URL(value); 28 | if (value && url.protocol !== protocol) { 29 | throw new Error(`🚫 ${envVar} must start with ${protocol}`); 30 | } 31 | if (checkApiKey && value) { 32 | const apiKey = url.searchParams.get("api-key"); 33 | if (!apiKey || apiKey.trim() === "") { 34 | throw new Error(`🚫 The 'api-key' parameter is missing or empty in the URL: ${value}`); 35 | } 36 | } 37 | }; 38 | 39 | validateUrl("HELIUS_HTTPS_URI", "https:", true); 40 | validateUrl("HELIUS_WSS_URI", "wss:", true); 41 | 42 | return { 43 | HELIUS_HTTPS_URI: process.env.HELIUS_HTTPS_URI!, 44 | HELIUS_WSS_URI: process.env.HELIUS_WSS_URI!, 45 | SNIPEROO_API_KEY: process.env.SNIPEROO_API_KEY!, 46 | SNIPEROO_PUBKEY: process.env.SNIPEROO_PUBKEY!, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface NewTokenRecord { 2 | id?: number; // Optional because it's added by the database 3 | time: number; 4 | name: string; 5 | mint: string; 6 | creator: string; 7 | } 8 | export interface MintsDataReponse { 9 | tokenMint?: string; 10 | solMint?: string; 11 | } 12 | export interface RugResponseExtended { 13 | mint: string; 14 | tokenProgram: string; 15 | creator: string; 16 | token: { 17 | mintAuthority: string | null; 18 | supply: number; 19 | decimals: number; 20 | isInitialized: boolean; 21 | freezeAuthority: string | null; 22 | }; 23 | token_extensions: unknown | null; 24 | tokenMeta: { 25 | name: string; 26 | symbol: string; 27 | uri: string; 28 | mutable: boolean; 29 | updateAuthority: string; 30 | }; 31 | topHolders: { 32 | address: string; 33 | amount: number; 34 | decimals: number; 35 | pct: number; 36 | uiAmount: number; 37 | uiAmountString: string; 38 | owner: string; 39 | insider: boolean; 40 | }[]; 41 | freezeAuthority: string | null; 42 | mintAuthority: string | null; 43 | risks: { 44 | name: string; 45 | value: string; 46 | description: string; 47 | score: number; 48 | level: string; 49 | }[]; 50 | score: number; 51 | fileMeta: { 52 | description: string; 53 | name: string; 54 | symbol: string; 55 | image: string; 56 | }; 57 | lockerOwners: Record; 58 | lockers: Record; 59 | lpLockers: unknown | null; 60 | markets: { 61 | pubkey: string; 62 | marketType: string; 63 | mintA: string; 64 | mintB: string; 65 | mintLP: string; 66 | liquidityA: string; 67 | liquidityB: string; 68 | }[]; 69 | totalMarketLiquidity: number; 70 | totalLPProviders: number; 71 | rugged: boolean; 72 | } 73 | -------------------------------------------------------------------------------- /src/test/sniperooHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { buyToken } from "../utils/handlers/sniperooHandler"; 2 | 3 | /** 4 | * Simple test function for the buyToken functionality 5 | */ 6 | const tokenAddress = ""; // Replace with a real token address for testing 7 | const amount = 0.01; // Small amount for testing 8 | async function testBuyToken(): Promise { 9 | console.log("=== Testing Sniperoo buyToken Function ==="); 10 | 11 | // Test case 1: Valid parameters 12 | try { 13 | console.log("\nTest 1: Valid parameters"); 14 | console.log("Buying token with valid parameters..."); 15 | console.log(`Token Address: ${tokenAddress}`); 16 | console.log(`Amount: ${amount} SOL`); 17 | const result = await buyToken(tokenAddress, amount); 18 | console.log(`Result: ${result ? "SUCCESS ✅" : "FAILED ❌"}`); 19 | } catch (error) { 20 | console.error("Test 1 Error:", error instanceof Error ? error.message : "Unknown error"); 21 | } 22 | 23 | // Test case 2: Invalid token address 24 | try { 25 | console.log("\nTest 2: Invalid token address"); 26 | console.log("Buying token with empty address..."); 27 | const result = await buyToken("", 0.01); 28 | console.log(`Result: ${result ? "SUCCESS ✅" : "FAILED ❌"}`); 29 | } catch (error) { 30 | console.log(`Error caught as expected: ${error instanceof Error ? error.message : "Unknown error"} ✅`); 31 | } 32 | 33 | // Test case 3: Invalid amount 34 | try { 35 | console.log("\nTest 3: Invalid amount"); 36 | console.log("Buying token with zero amount..."); 37 | const tokenAddress = "YOUR_TEST_TOKEN_ADDRESS"; // Replace with a real token address 38 | const result = await buyToken(tokenAddress, 0); 39 | console.log(`Result: ${result ? "SUCCESS ✅" : "FAILED ❌"}`); 40 | } catch (error) { 41 | console.log(`Error caught as expected: ${error instanceof Error ? error.message : "Unknown error"} ✅`); 42 | } 43 | console.log("\n=== Test Complete ==="); 44 | } 45 | 46 | // Run the test 47 | testBuyToken().catch((error) => { 48 | console.error("Unhandled error in test:", error); 49 | }); 50 | -------------------------------------------------------------------------------- /src/utils/handlers/sniperooHandler.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { validateEnv } from "../env-validator"; 3 | 4 | /** 5 | * Buys a token using the Sniperoo API 6 | * @param tokenAddress The token's mint address 7 | * @param inputAmount Amount of SOL to spend 8 | * @returns Boolean indicating if the purchase was successful 9 | */ 10 | export async function buyToken(tokenAddress: string, inputAmount: number, sell: boolean, tp: number, sl: number): Promise { 11 | try { 12 | const env = validateEnv(); 13 | 14 | // Validate inputs 15 | if (!tokenAddress || typeof tokenAddress !== "string" || tokenAddress.trim() === "") { 16 | return false; 17 | } 18 | 19 | if (inputAmount <= 0) { 20 | return false; 21 | } 22 | 23 | if (!tp || !sl) { 24 | sell = false; 25 | } 26 | 27 | // Prepare request body 28 | const requestBody = { 29 | walletAddresses: [env.SNIPEROO_PUBKEY], 30 | tokenAddress: tokenAddress, 31 | inputAmount: inputAmount, 32 | autoSell: { 33 | enabled: sell, 34 | strategy: { 35 | strategyName: "simple", 36 | profitPercentage: tp, 37 | stopLossPercentage: sl, 38 | }, 39 | }, 40 | }; 41 | 42 | // Make API request using axios 43 | const response = await axios.post("https://api.sniperoo.app/trading/buy-token?toastFrontendId=0", requestBody, { 44 | headers: { 45 | Authorization: `Bearer ${env.SNIPEROO_API_KEY}`, 46 | "Content-Type": "application/json", 47 | }, 48 | }); 49 | 50 | // Axios automatically throws an error for non-2xx responses, 51 | // so if we get here, the request was successful 52 | return true; 53 | } catch (error) { 54 | // Handle axios errors 55 | if (axios.isAxiosError(error)) { 56 | console.error(`Sniperoo API error (${error.response?.status || "unknown"}):`, error.response?.data || error.message); 57 | } else { 58 | console.error("Error buying token:", error instanceof Error ? error.message : "Unknown error"); 59 | } 60 | return false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | liquidity_pool: [ 3 | { 4 | enabled: true, 5 | id: "pump1", 6 | name: "pumpswap", 7 | program: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", 8 | instruction: "Program log: Instruction: CreatePool", 9 | }, 10 | { 11 | enabled: false, 12 | id: "rad1", 13 | name: "Raydium", 14 | program: "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", 15 | instruction: "Program log: initialize2: InitializeInstruction2", 16 | }, 17 | ], 18 | concurrent_transactions: 1, // Number of simultaneous transactions 19 | wsol_pc_mint: "So11111111111111111111111111111111111111112", 20 | db: { 21 | pathname: "src/tracker/tokens.db", // Sqlite Database location 22 | }, 23 | token_buy: { 24 | provider: "sniperoo", 25 | sol_amount: 0.1, // Amount of SOL to spend 26 | play_sound: true, // Works only on windows 27 | play_sound_text: "Order Filled!", 28 | }, 29 | token_sell: { 30 | enabled: true, // If set to true, the bot will sell the token via Sniperoo API 31 | stop_loss_percent: 15, 32 | take_profit_percent: 50, 33 | }, 34 | checks: { 35 | simulation_mode: false, 36 | mode: "full", // snipe=Minimal Checks, full=Full Checks based on Rug Check, none=No Checks 37 | verbose_logs: false, 38 | settings: { 39 | // Dangerous (Checked in snipe mode) 40 | allow_mint_authority: false, // The mint authority is the address that has permission to mint (create) new tokens. Strongly Advised to set to false. 41 | allow_freeze_authority: false, // The freeze authority is the address that can freeze token transfers, effectively locking up funds. Strongly Advised to set to false 42 | // Critical 43 | max_alowed_pct_topholders: 50, // Max allowed percentage an individual topholder might hold 44 | exclude_lp_from_topholders: true, // If true, Liquidity Pools will not be seen as top holders 45 | block_returning_token_names: true, 46 | block_returning_token_creators: true, 47 | allow_insider_topholders: false, // Allow inseder accounts to be part of the topholders 48 | allow_not_initialized: false, // This indicates whether the token account is properly set up on the blockchain. Strongly Advised to set to false 49 | allow_rugged: false, 50 | allow_mutable: false, 51 | block_symbols: ["XXX"], 52 | block_names: ["XXX"], 53 | // Warning 54 | min_total_lp_providers: 999, 55 | min_total_markets: 999, 56 | min_total_market_Liquidity: 5000, 57 | // Misc 58 | ignore_ends_with_pump: true, 59 | max_score: 1, // Set to 0 to ignore 60 | }, 61 | }, 62 | axios: { 63 | get_timeout: 10000, // Timeout for API requests 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - 23-mar-2025:19: Add sound notification when the order has been filled 4 | - 23-mar-2025:19: Added Sniperoo Integration for sniping tokens via utils/sniperooHandler.ts 5 | - 23-mar-2025:19: Removed Jupiter Swap API functionality 6 | - 23-mar-2025:19: Removed Tracker functionality 7 | - 23-mar-2025:19: Updated .env file 8 | - 23-mar-2025:19: Renamed "holdings" database to "tokens" database 9 | - 23-mar-2025:19: Added multiple program websocket subscription posibility 10 | - 23-mar-2025:19: Added websocket manager to manage and maintain websocket connection 11 | - 23-mar-2025:19: Added "@solana/spl-token" library for token authorities lookup 12 | - 23-mar-2025:19: Removed Legacy rug options from full rug check 13 | - 23-mar-2025:19: Added option for full rug check, small snipe check or no check handled via utils/handlers/tokenHandler.ts 14 | 15 | ## Legacy Sniper updates (branch: legacy-sniper-jupiter-swap-api) 16 | 17 | - 19-mar-2025:21: Replace Helius "Enhanced Transactions API" with getParsedTransaction Solana RPC method (@solana/web3.js) 18 | - 27-feb-2025:13: Updated the Jupiter API endpoints in .envbackup to reflect new endpoints. 19 | - 27-feb-2025:13: Updated multiple project dependencies 20 | - 10-jan-2025:21: Added Dexscreener Tokens API as price source for tracker with option in config. 21 | - 09-jan-2025:21: Added token wallet balance, an amount mismatch check before trying to sell TP or SL to prevent quoting fees. 22 | - 09-jan-2025:15: Added .env validator to check if all .env variables are properly set 23 | - 06-jan-2025:11: Add rugcheck option: Exclude LP from topholders 24 | - 02-jan-2025:16: Change Metadata RCP request to local database lookup 25 | - 02-jan-2025:16: Expanded Rug check 26 | - 02-jan-2025:16: Added new token tracker for rug check duplicates functionality and meta data 27 | - 02-jan-2025:16: Added Simulation Mode to skip the actual swap 28 | - 02-jan-2025:16: Added logsUnsubscribe before subscribing to RPC logsSubscribe method 29 | - 02-jan-2025:16: Improved fetchTransactionDetails() error handling 30 | - 02-jan-2025:16: Updated fetchTransactionDetails() to use retry based on count instead of time 31 | - 02-jan-2025:16: Process transaction asynchronously and add max concurrent transactions 32 | - 02-jan-2025:16: Revert back to native punycode as libraries are identical. 33 | - 30-dec-2024:21: Added patch-package dev dependency to apply patches/tr46+0.0.3.patch 34 | - 30-dec-2024:21: Added punycode.js package to resolve [issue](https://github.com/mathiasbynens/punycode.js/issues/137). 35 | - 21-dec-2024:19: Added createSellTransaction() in transactions.ts to sell SL and TP tokens. 36 | - 21-dec-2024:19: Added Retry logic for Swap Quote requests 37 | - 21-dec-2024:19: Added Verbose loging option 38 | - 18-dec-2024-22: Added tracker functionality in "src\tracker\index.ts". 39 | - 18-dec-2024-22: Updated fetchAndSaveSwapDetails() in transactions.ts to use sqlite3. 40 | - 18-dec-2024-22: Updated config.ts: Addded sell parameters 41 | - 18-dec-2024-22: Added packages: luxon, sqlite, sqlite3 42 | - 17-dec-2024-13: Added fetchAndSaveSwapDetails() in transactions.ts to track confirmed swaps. 43 | - 17-dec-2024-13: Updated test.ts 44 | - 17-dec-2024-13: Added JUP_HTTPS_PRICE_URI to .env.backup 45 | - 17-dec-2024-13: Web3.js updated from 1.95.8 to 1.98.0 46 | - 06-dec-2024-00: Initial Commit: Solana Sniper Bot 47 | -------------------------------------------------------------------------------- /src/utils/handlers/signatureHandler.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from "@solana/web3.js"; 2 | import { validateEnv } from "../env-validator"; 3 | import { config } from "../../config"; 4 | 5 | // Constants 6 | const WSOL_MINT = config.wsol_pc_mint; 7 | 8 | /** 9 | * SignatureHandler class optimized for speed 10 | */ 11 | export class SignatureHandler { 12 | private connection: Connection; 13 | 14 | constructor(connection?: Connection) { 15 | const env = validateEnv(); 16 | this.connection = connection || new Connection(env.HELIUS_HTTPS_URI, "confirmed"); 17 | } 18 | 19 | /** 20 | * Get the mint address from a transaction signature - optimized for speed 21 | * @param signature Transaction signature 22 | * @returns Promise resolving to mint address or null 23 | */ 24 | public async getMintFromSignature(signature: string): Promise { 25 | if (!signature || typeof signature !== "string" || signature.trim() === "") { 26 | return null; // Invalid signature, return null immediately 27 | } 28 | 29 | try { 30 | // Fetch transaction with minimal options 31 | let tx = await this.connection.getParsedTransaction(signature, { 32 | maxSupportedTransactionVersion: 0, 33 | commitment: "confirmed", 34 | }); 35 | 36 | // Quick validation with retry 37 | if (!tx?.meta) { 38 | // Wait 200ms and try one more time 39 | await new Promise((resolve) => setTimeout(resolve, 200)); 40 | tx = await this.connection.getParsedTransaction(signature, { 41 | maxSupportedTransactionVersion: 0, 42 | commitment: "confirmed", 43 | }); 44 | 45 | // If still no meta data, return null 46 | if (!tx?.meta) return null; 47 | } 48 | 49 | // Get token balances - prefer postTokenBalances as they're more likely to contain the new token 50 | const tokenBalances = tx.meta.postTokenBalances || tx.meta.preTokenBalances; 51 | if (!tokenBalances?.length) return null; 52 | 53 | // Fast path: If we have exactly 2 token balances, one is likely WSOL and the other is the token 54 | if (tokenBalances.length === 2) { 55 | const mint1 = tokenBalances[0].mint; 56 | const mint2 = tokenBalances[1].mint; 57 | 58 | // If mint1 is WSOL, return mint2 (unless it's also WSOL) 59 | if (mint1 === WSOL_MINT) { 60 | return mint2 === WSOL_MINT ? null : mint2; 61 | } 62 | 63 | // If mint2 is WSOL, return mint1 64 | if (mint2 === WSOL_MINT) { 65 | return mint1; 66 | } 67 | 68 | // If neither is WSOL, return the first one 69 | return mint1; 70 | } 71 | 72 | // For more than 2 balances, find the first non-WSOL mint 73 | for (const balance of tokenBalances) { 74 | if (balance.mint !== WSOL_MINT) { 75 | return balance.mint; 76 | } 77 | } 78 | 79 | // If we only found WSOL mints, return null 80 | return null; 81 | } catch (error) { 82 | // Minimal error logging for speed 83 | return null; 84 | } 85 | } 86 | } 87 | 88 | // Create a singleton instance for better performance 89 | const signatureHandler = new SignatureHandler(); 90 | 91 | /** 92 | * Get the mint address from a transaction signature (optimized for speed) 93 | * @param signature Transaction signature 94 | * @returns Mint address or null 95 | */ 96 | export async function getMintFromSignature(signature: string): Promise { 97 | return signatureHandler.getMintFromSignature(signature); 98 | } 99 | -------------------------------------------------------------------------------- /src/tracker/db.ts: -------------------------------------------------------------------------------- 1 | import * as sqlite3 from "sqlite3"; 2 | import { open } from "sqlite"; 3 | import { config } from "./../config"; 4 | import { NewTokenRecord } from "../types"; 5 | 6 | // New token duplicates tracker 7 | export async function createTableNewTokens(database: any): Promise { 8 | try { 9 | await database.exec(` 10 | CREATE TABLE IF NOT EXISTS tokens ( 11 | id INTEGER PRIMARY KEY AUTOINCREMENT, 12 | time INTEGER NOT NULL, 13 | name TEXT NOT NULL, 14 | mint TEXT NOT NULL, 15 | creator TEXT NOT NULL 16 | ); 17 | `); 18 | return true; 19 | } catch (error: any) { 20 | return false; 21 | } 22 | } 23 | 24 | export async function insertNewToken(newToken: NewTokenRecord) { 25 | const db = await open({ 26 | filename: config.db.pathname, 27 | driver: sqlite3.Database, 28 | }); 29 | 30 | // Create Table if not exists 31 | const newTokensTableExist = await createTableNewTokens(db); 32 | if (!newTokensTableExist) { 33 | await db.close(); 34 | } 35 | 36 | // Proceed with adding holding 37 | if (newTokensTableExist) { 38 | const { time, name, mint, creator } = newToken; 39 | 40 | await db.run( 41 | ` 42 | INSERT INTO tokens (time, name, mint, creator) 43 | VALUES (?, ?, ?, ?); 44 | `, 45 | [time, name, mint, creator] 46 | ); 47 | 48 | await db.close(); 49 | } 50 | } 51 | 52 | export async function selectTokenByNameAndCreator(name: string, creator: string): Promise { 53 | // Open the database 54 | const db = await open({ 55 | filename: config.db.pathname, 56 | driver: sqlite3.Database, 57 | }); 58 | 59 | // Create Table if not exists 60 | const newTokensTableExist = await createTableNewTokens(db); 61 | if (!newTokensTableExist) { 62 | await db.close(); 63 | return []; 64 | } 65 | 66 | // Query the database for matching tokens 67 | const tokens = await db.all( 68 | ` 69 | SELECT * 70 | FROM tokens 71 | WHERE name = ? OR creator = ?; 72 | `, 73 | [name, creator] 74 | ); 75 | 76 | // Close the database 77 | await db.close(); 78 | 79 | // Return the results 80 | return tokens; 81 | } 82 | 83 | export async function selectTokenByMint(mint: string): Promise { 84 | // Open the database 85 | const db = await open({ 86 | filename: config.db.pathname, 87 | driver: sqlite3.Database, 88 | }); 89 | 90 | // Create Table if not exists 91 | const newTokensTableExist = await createTableNewTokens(db); 92 | if (!newTokensTableExist) { 93 | await db.close(); 94 | return []; 95 | } 96 | 97 | // Query the database for matching tokens 98 | const tokens = await db.all( 99 | ` 100 | SELECT * 101 | FROM tokens 102 | WHERE mint = ?; 103 | `, 104 | [mint] 105 | ); 106 | 107 | // Close the database 108 | await db.close(); 109 | 110 | // Return the results 111 | return tokens; 112 | } 113 | 114 | export async function selectAllTokens(): Promise { 115 | // Open the database 116 | const db = await open({ 117 | filename: config.db.pathname, 118 | driver: sqlite3.Database, 119 | }); 120 | 121 | // Create Table if not exists 122 | const newTokensTableExist = await createTableNewTokens(db); 123 | if (!newTokensTableExist) { 124 | await db.close(); 125 | return []; 126 | } 127 | 128 | // Query the database for matching tokens 129 | const tokens = await db.all( 130 | ` 131 | SELECT * 132 | FROM tokens; 133 | ` 134 | ); 135 | 136 | // Close the database 137 | await db.close(); 138 | 139 | // Return the results 140 | return tokens; 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains all the code "as is", following the "Solana PumpSwap Sniper Trading Bot in TypeScript" on YouTube provided by [DigitalBenjamins](https://x.com/digbenjamins). 2 | 3 | Solana PumpSwap Sniper Trading Bot in TypeScript | Buy fast with JITO and Sell | pump.fun migration 4 | 5 | [![Solana Sniper Trading Bot in TypeScript](https://img.youtube.com/vi/eQ8osFo5Df4/0.jpg)](https://www.youtube.com/watch?v=eQ8osFo5Df4) 6 | 7 | ## Project Description 8 | 9 | The Solana PumpSwap trading sniper 2025 is a TypeScript (node.js) bot designed to automate the buying of (meme) tokens on the Solana blockchain. 10 | It is configured to execute trades based on predefined checks and parameters like amount, slipage, rug check and priority. It checks for migration from pumpfun to pumpswap 11 | 12 | With customizable parameters, you can tailor the strategy to suit your needs. The primary goal of this project is to educate users about the essential components required to develop a simple token sniper, offering insights into its functionality and implementation! 13 | 14 | ### Features 15 | 16 | - Token Sniper for PumpSwap and Raydium for the Solana blockchain 17 | - Rug check using a third party service rugcheck.xyz 18 | - Possibility to skip pump.fun tokens 19 | - Auto-buy with parameters for amount, slippage and priority using JITO 20 | - Sell automatically using stop loss and Take profit 21 | - Possibility to set own RPC nodes 22 | - Snipe using JITO sniper Sniperoo 23 | 24 | ### Prerequisites, Installation and Usage Instructions 25 | 26 | 1. Ensure [Node.js](https://nodejs.org/en) is installed on your computer. 27 | 2. Clone the repository to your local machine. 28 | 3. Navigate to the project folder and run the following command to install all dependencies: "npm i" 29 | 4. To start the sniper, run: "npm run dev" 30 | 5. To start the tracker, run: "npm run tracker" 31 | 6. Optional: To start the sniper and tracker after being compiled, run: "npm run start" and "npm run start:tracker" 32 | 33 | ### Third Party documentation 34 | 35 | - [Helius RPC nodes](https://docs.helius.dev) 36 | - [Sniperoo](https://www.sniperoo.app/signup?ref=IZ7ZYZEV) 37 | - [Rugcheck API](https://api.rugcheck.xyz/swagger/index.html) 38 | - [Solana](https://solana.com/docs) 39 | - [Solscan](https://solscan.io) 40 | 41 | ### Disclaimer 42 | 43 | The course videos accompanying this project are provided free of charge and are intended solely for educational purposes. This software does not guarantee profitability or financial success and is not designed to generate profitable trades. 44 | 45 | You are solely responsible for your own financial decisions. Before making any trades or investments, it is strongly recommended that you consult with a qualified financial professional. 46 | 47 | By using this software, you acknowledge that the creators and contributors of this project shall not be held liable for any financial losses, damages, or other consequences resulting from its use. Use the software at your own risk. 48 | 49 | The software (code in this repository) must not be used to engage in any form of market manipulation, fraud, illegal activities, or unethical behavior. The creators of this project do not endorse or support malicious use cases, such as front-running, exploiting contracts, or harming other users. Users are expected to adhere to ethical trading practices and comply with applicable laws and regulations. 50 | 51 | The software (code in this repository) is intended solely to facilitate learning and enhance the educational experience provided by the accompanying videos. Any other use is strictly prohibited. 52 | 53 | All trading involves risk and may not be suitable for all individuals. You should carefully consider your investment objectives, level of experience, and risk appetite before engaging in any trading activities. Past performance is not indicative of future results, and there is no guarantee that any trading strategy, algorithm or tool discussed will result in profits or avoid losses. 54 | 55 | I am not a licensed financial advisor or a registered broker-dealer. The content shared is based solely on personal experience and knowledge and should not be relied upon as financial advice or a guarantee of success. Always conduct your own research and consult with a professional financial advisor before making any investment decisions. 56 | -------------------------------------------------------------------------------- /src/utils/handlers/tokenHandler.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from "@solana/web3.js"; 2 | import { getMint } from "@solana/spl-token"; 3 | import { config } from "../../config"; 4 | import { validateEnv } from "../env-validator"; 5 | 6 | /** 7 | * TokenCheckManager class for verifying token security properties 8 | */ 9 | export class TokenCheckManager { 10 | private connection: Connection; 11 | 12 | constructor(connection?: Connection) { 13 | const env = validateEnv(); 14 | this.connection = connection || new Connection(env.HELIUS_HTTPS_URI, "confirmed"); 15 | } 16 | 17 | /** 18 | * Check if a token's mint and freeze authorities are still enabled 19 | * @param mintAddress The token's mint address (contract address) 20 | * @returns Object containing authority status and details 21 | */ 22 | public async getTokenAuthorities(mintAddress: string): Promise { 23 | try { 24 | // Validate mint address 25 | if (!mintAddress || typeof mintAddress !== "string" || mintAddress.trim() === "") { 26 | throw new Error("Invalid mint address"); 27 | } 28 | 29 | const mintPublicKey = new PublicKey(mintAddress); 30 | const mintInfo = await getMint(this.connection, mintPublicKey); 31 | 32 | // Check if mint authority exists (is not null) 33 | const hasMintAuthority = mintInfo.mintAuthority !== null; 34 | 35 | // Check if freeze authority exists (is not null) 36 | const hasFreezeAuthority = mintInfo.freezeAuthority !== null; 37 | 38 | // Get the addresses as strings if they exist 39 | const mintAuthorityAddress = mintInfo.mintAuthority ? mintInfo.mintAuthority.toBase58() : null; 40 | const freezeAuthorityAddress = mintInfo.freezeAuthority ? mintInfo.freezeAuthority.toBase58() : null; 41 | 42 | return { 43 | mintAddress: mintAddress, 44 | hasMintAuthority, 45 | hasFreezeAuthority, 46 | mintAuthorityAddress, 47 | freezeAuthorityAddress, 48 | isSecure: !hasMintAuthority && !hasFreezeAuthority, 49 | details: { 50 | supply: mintInfo.supply.toString(), 51 | decimals: mintInfo.decimals, 52 | }, 53 | }; 54 | } catch (error) { 55 | console.error(`Error checking token authorities for ${mintAddress}:`, error); 56 | throw error; 57 | } 58 | } 59 | 60 | /** 61 | * Simplified check that returns only whether the token passes security checks 62 | * based on the configuration settings 63 | * @param mintAddress The token's mint address 64 | * @returns Boolean indicating if the token passes security checks 65 | */ 66 | public async isTokenSecure(mintAddress: string): Promise { 67 | try { 68 | const authorityStatus = await this.getTokenAuthorities(mintAddress); 69 | 70 | // Check against configuration settings 71 | const allowMintAuthority = config.checks.settings.allow_mint_authority; 72 | const allowFreezeAuthority = config.checks.settings.allow_freeze_authority; 73 | 74 | // Token is secure if: 75 | // 1. It has no mint authority OR mint authority is allowed in config 76 | // 2. It has no freeze authority OR freeze authority is allowed in config 77 | return (!authorityStatus.hasMintAuthority || allowMintAuthority) && (!authorityStatus.hasFreezeAuthority || allowFreezeAuthority); 78 | } catch (error) { 79 | console.error(`Error checking if token is secure: ${mintAddress}`, error); 80 | return false; // Consider token insecure if there's an error 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * Interface for token authority check results 87 | */ 88 | export interface TokenAuthorityStatus { 89 | mintAddress: string; 90 | hasMintAuthority: boolean; 91 | hasFreezeAuthority: boolean; 92 | mintAuthorityAddress: string | null; 93 | freezeAuthorityAddress: string | null; 94 | isSecure: boolean; 95 | details: { 96 | supply: string; 97 | decimals: number; 98 | }; 99 | } 100 | 101 | // Create a singleton instance for better performance 102 | const tokenCheckManager = new TokenCheckManager(); 103 | 104 | /** 105 | * Check if a token's mint and freeze authorities are still enabled 106 | * @param mintAddress The token's mint address 107 | * @returns Object containing authority status and details 108 | */ 109 | export async function getTokenAuthorities(mintAddress: string): Promise { 110 | return tokenCheckManager.getTokenAuthorities(mintAddress); 111 | } 112 | 113 | /** 114 | * Check if a token passes security checks based on configuration 115 | * @param mintAddress The token's mint address 116 | * @returns Boolean indicating if the token passes security checks 117 | */ 118 | export async function isTokenSecure(mintAddress: string): Promise { 119 | return tokenCheckManager.isTokenSecure(mintAddress); 120 | } 121 | -------------------------------------------------------------------------------- /src/utils/managers/websocketManager.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from "ws"; 2 | import { EventEmitter } from "events"; 3 | 4 | // Connection states 5 | export enum ConnectionState { 6 | DISCONNECTED = "disconnected", 7 | CONNECTING = "connecting", 8 | CONNECTED = "connected", 9 | RECONNECTING = "reconnecting", 10 | ERROR = "error", 11 | } 12 | 13 | export interface WebSocketManagerOptions { 14 | url: string; 15 | initialBackoff?: number; 16 | maxBackoff?: number; 17 | maxRetries?: number; 18 | debug?: boolean; 19 | } 20 | export interface WebSocketRequest { 21 | jsonrpc: string; 22 | id: number; 23 | method: string; 24 | params: unknown[]; 25 | } 26 | 27 | export class WebSocketManager extends EventEmitter { 28 | private ws: WebSocket | null = null; 29 | private state: ConnectionState = ConnectionState.DISCONNECTED; 30 | private retryCount = 0; 31 | private backoffTime: number; 32 | private maxBackoff: number; 33 | private maxRetries: number; 34 | private reconnectTimer: NodeJS.Timeout | null = null; 35 | private url: string; 36 | private debug: boolean; 37 | 38 | constructor(options: WebSocketManagerOptions) { 39 | super(); 40 | this.url = options.url; 41 | this.backoffTime = options.initialBackoff || 1000; 42 | this.maxBackoff = options.maxBackoff || 30000; 43 | this.maxRetries = options.maxRetries || Infinity; 44 | this.debug = options.debug || false; 45 | } 46 | 47 | // Get current connection state 48 | public getState(): ConnectionState { 49 | return this.state; 50 | } 51 | 52 | // Connect to WebSocket server 53 | public connect(): void { 54 | if (this.state === ConnectionState.CONNECTING || this.state === ConnectionState.CONNECTED) { 55 | this.log("Already connected or connecting"); 56 | return; 57 | } 58 | 59 | this.setState(ConnectionState.CONNECTING); 60 | this.log(`Connecting to WebSocket at ${this.url}`); 61 | 62 | try { 63 | this.ws = new WebSocket(this.url); 64 | this.setupEventListeners(); 65 | } catch (error) { 66 | this.handleError(error instanceof Error ? error : new Error("Unknown error during connection")); 67 | } 68 | } 69 | 70 | // Send data through the WebSocket 71 | public send(data: WebSocketRequest | string): boolean { 72 | if (this.state !== ConnectionState.CONNECTED || !this.ws) { 73 | this.log("Cannot send: WebSocket not connected", "error"); 74 | return false; 75 | } 76 | 77 | try { 78 | const message = typeof data === "string" ? data : JSON.stringify(data); 79 | this.ws.send(message); 80 | return true; 81 | } catch (error) { 82 | this.handleError(error instanceof Error ? error : new Error("Error sending message")); 83 | return false; 84 | } 85 | } 86 | 87 | // Disconnect WebSocket 88 | public disconnect(): void { 89 | this.log("Manually disconnecting WebSocket"); 90 | this.cleanUp(); 91 | this.setState(ConnectionState.DISCONNECTED); 92 | } 93 | 94 | // Set up WebSocket event listeners 95 | private setupEventListeners(): void { 96 | if (!this.ws) return; 97 | 98 | this.ws.on("open", () => { 99 | this.setState(ConnectionState.CONNECTED); 100 | this.retryCount = 0; 101 | this.backoffTime = 1000; // Reset backoff time on successful connection 102 | this.emit("open"); 103 | this.log("WebSocket connection established"); 104 | }); 105 | 106 | this.ws.on("message", (data: WebSocket.Data) => { 107 | this.emit("message", data); 108 | }); 109 | 110 | this.ws.on("error", (error: Error) => { 111 | this.handleError(error); 112 | }); 113 | 114 | this.ws.on("close", (code: number, reason: string) => { 115 | this.log(`WebSocket closed: ${code} - ${reason}`); 116 | this.cleanUp(); 117 | 118 | if (this.state !== ConnectionState.DISCONNECTED) { 119 | this.attemptReconnect(); 120 | } 121 | }); 122 | } 123 | 124 | // Handle WebSocket errors 125 | private handleError(error: Error): void { 126 | this.log(`WebSocket error: ${error.message}`, "error"); 127 | this.setState(ConnectionState.ERROR); 128 | this.emit("error", error); 129 | 130 | // Don't attempt reconnect here - let the close handler do it 131 | // as an error is typically followed by a close event 132 | } 133 | 134 | // Attempt to reconnect with exponential backoff 135 | private attemptReconnect(): void { 136 | if (this.retryCount >= this.maxRetries) { 137 | this.log(`Maximum retry attempts (${this.maxRetries}) reached. Giving up.`, "error"); 138 | this.setState(ConnectionState.DISCONNECTED); 139 | this.emit("max_retries_reached"); 140 | return; 141 | } 142 | 143 | this.setState(ConnectionState.RECONNECTING); 144 | this.retryCount++; 145 | 146 | // Calculate backoff with jitter to prevent thundering herd 147 | const jitter = Math.random() * 0.3 + 0.85; // Random between 0.85 and 1.15 148 | const delay = Math.min(this.backoffTime * jitter, this.maxBackoff); 149 | 150 | this.log(`Attempting to reconnect in ${Math.round(delay)}ms (attempt ${this.retryCount})`); 151 | 152 | this.reconnectTimer = setTimeout(() => { 153 | this.connect(); 154 | // Increase backoff for next attempt 155 | this.backoffTime = Math.min(this.backoffTime * 1.5, this.maxBackoff); 156 | }, delay); 157 | } 158 | 159 | // Clean up resources 160 | private cleanUp(): void { 161 | if (this.reconnectTimer) { 162 | clearTimeout(this.reconnectTimer); 163 | this.reconnectTimer = null; 164 | } 165 | 166 | if (this.ws) { 167 | // Remove all listeners to prevent memory leaks 168 | this.ws.removeAllListeners(); 169 | 170 | // Close the connection if it's still open 171 | if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) { 172 | try { 173 | this.ws.close(); 174 | } catch (e) { 175 | // Ignore errors during close 176 | } 177 | } 178 | 179 | this.ws = null; 180 | } 181 | } 182 | 183 | // Update connection state and emit event 184 | private setState(state: ConnectionState): void { 185 | if (this.state !== state) { 186 | this.state = state; 187 | this.emit("state_change", state); 188 | } 189 | } 190 | 191 | // Logging helper 192 | private log(message: string, level: "info" | "error" = "info"): void { 193 | if (this.debug) { 194 | if (level === "error") { 195 | console.error(`[WebSocketManager] ${message}`); 196 | } else { 197 | console.log(`[WebSocketManager] ${message}`); 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/utils/handlers/rugCheckHandler.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import dotenv from "dotenv"; 3 | import { config } from "../../config"; 4 | import { RugResponseExtended, NewTokenRecord } from "../../types"; 5 | import { insertNewToken, selectTokenByNameAndCreator } from "../../tracker/db"; 6 | 7 | // Load environment variables from the .env file 8 | dotenv.config(); 9 | 10 | /** 11 | * Checks if a token passes all rug check criteria 12 | * @param tokenMint The token's mint address 13 | * @returns Promise indicating if the token passes all checks 14 | */ 15 | export async function getRugCheckConfirmed(tokenMint: string): Promise { 16 | try { 17 | const rugResponse = await axios.get(`https://api.rugcheck.xyz/v1/tokens/${tokenMint}/report`, { 18 | timeout: config.axios.get_timeout, 19 | }); 20 | 21 | if (!rugResponse.data) return false; 22 | 23 | // For debugging purposes, log the full response data 24 | if (config.checks.verbose_logs) { 25 | console.log("📜 [Rug Check Handler] Rug check response data:", rugResponse.data); 26 | } 27 | 28 | // Extract information from the token report 29 | const tokenReport: RugResponseExtended = rugResponse.data; 30 | const tokenCreator = tokenReport.creator ? tokenReport.creator : tokenMint; 31 | const mintAuthority = tokenReport.token.mintAuthority; 32 | const freezeAuthority = tokenReport.token.freezeAuthority; 33 | const isInitialized = tokenReport.token.isInitialized; 34 | const tokenName = tokenReport.tokenMeta.name; 35 | const tokenSymbol = tokenReport.tokenMeta.symbol; 36 | const tokenMutable = tokenReport.tokenMeta.mutable; 37 | let topHolders = tokenReport.topHolders; 38 | const marketsLength = tokenReport.markets ? tokenReport.markets.length : 0; 39 | const totalLPProviders = tokenReport.totalLPProviders; 40 | const totalMarketLiquidity = tokenReport.totalMarketLiquidity; 41 | const isRugged = tokenReport.rugged; 42 | const rugScore = tokenReport.score; 43 | 44 | // Update topholders if liquidity pools are excluded 45 | if (config.checks.settings.exclude_lp_from_topholders) { 46 | // local types 47 | type Market = { 48 | liquidityA?: string; 49 | liquidityB?: string; 50 | }; 51 | 52 | const markets: Market[] | undefined = tokenReport.markets; 53 | if (markets) { 54 | // Safely extract liquidity addresses from markets 55 | const liquidityAddresses: string[] = (markets ?? []) 56 | .flatMap((market) => [market.liquidityA, market.liquidityB]) 57 | .filter((address): address is string => !!address); 58 | 59 | // Filter out topHolders that match any of the liquidity addresses 60 | topHolders = topHolders.filter((holder) => !liquidityAddresses.includes(holder.address)); 61 | } 62 | } 63 | 64 | // Get config settings 65 | const rugCheckSettings = config.checks.settings; 66 | 67 | // Set conditions for token validation 68 | const conditions = [ 69 | { 70 | check: !rugCheckSettings.allow_mint_authority && mintAuthority !== null, 71 | message: "🚫 Mint authority should be null", 72 | }, 73 | { 74 | check: !rugCheckSettings.allow_not_initialized && !isInitialized, 75 | message: "🚫 Token is not initialized", 76 | }, 77 | { 78 | check: !rugCheckSettings.allow_freeze_authority && freezeAuthority !== null, 79 | message: "🚫 Freeze authority should be null", 80 | }, 81 | { 82 | check: !rugCheckSettings.allow_mutable && tokenMutable !== false, 83 | message: "🚫 Mutable should be false", 84 | }, 85 | { 86 | check: !rugCheckSettings.allow_insider_topholders && topHolders.some((holder) => holder.insider), 87 | message: "🚫 Insider accounts should not be part of the top holders", 88 | }, 89 | { 90 | check: topHolders.some((holder) => holder.pct > rugCheckSettings.max_alowed_pct_topholders), 91 | message: "🚫 An individual top holder cannot hold more than the allowed percentage of the total supply", 92 | }, 93 | { 94 | check: totalLPProviders < rugCheckSettings.min_total_lp_providers, 95 | message: "🚫 Not enough LP Providers.", 96 | }, 97 | { 98 | check: marketsLength < rugCheckSettings.min_total_markets, 99 | message: "🚫 Not enough Markets.", 100 | }, 101 | { 102 | check: totalMarketLiquidity < rugCheckSettings.min_total_market_Liquidity, 103 | message: "🚫 Not enough Market Liquidity.", 104 | }, 105 | { 106 | check: !rugCheckSettings.allow_rugged && isRugged, 107 | message: "🚫 Token is rugged", 108 | }, 109 | { 110 | check: rugCheckSettings.block_symbols.includes(tokenSymbol), 111 | message: "🚫 Symbol is blocked", 112 | }, 113 | { 114 | check: rugCheckSettings.block_names.includes(tokenName), 115 | message: "🚫 Name is blocked", 116 | }, 117 | { 118 | check: rugScore > rugCheckSettings.max_score && rugCheckSettings.max_score !== 0, 119 | message: "🚫 Rug score too high.", 120 | }, 121 | { 122 | check: rugCheckSettings.ignore_ends_with_pump && tokenMint.toLowerCase().endsWith("pump"), 123 | message: "🚫 Token name ends with 'pump' which is blocked by configuration.", 124 | }, 125 | ]; 126 | 127 | // Check for duplicate tokens if tracking is enabled 128 | if (rugCheckSettings.block_returning_token_names || rugCheckSettings.block_returning_token_creators) { 129 | try { 130 | // Get duplicates based on token name and creator 131 | const duplicate = await selectTokenByNameAndCreator(tokenName, tokenCreator); 132 | 133 | // Verify if duplicate token or creator was returned 134 | if (duplicate.length !== 0) { 135 | if (rugCheckSettings.block_returning_token_names && duplicate.some((token) => token.name === tokenName)) { 136 | console.log("🚫 Token with this name was already created"); 137 | return false; 138 | } 139 | if (rugCheckSettings.block_returning_token_creators && duplicate.some((token) => token.creator === tokenCreator)) { 140 | console.log("🚫 Token from this creator was already created"); 141 | return false; 142 | } 143 | } 144 | } catch (error) { 145 | console.error("Error checking for duplicate tokens:", error); 146 | // Continue with other checks even if this one fails 147 | } 148 | } 149 | 150 | // Create new token record for tracking 151 | const newToken: NewTokenRecord = { 152 | time: Date.now(), 153 | mint: tokenMint, 154 | name: tokenName, 155 | creator: tokenCreator, 156 | }; 157 | 158 | try { 159 | await insertNewToken(newToken); 160 | } catch (err) { 161 | if (rugCheckSettings.block_returning_token_names || rugCheckSettings.block_returning_token_creators) { 162 | console.error("⛔ Unable to store new token for tracking duplicate tokens:", err); 163 | } 164 | // Continue with other checks even if this one fails 165 | } 166 | 167 | // Validate all conditions 168 | for (const condition of conditions) { 169 | if (condition.check) { 170 | console.log(condition.message); 171 | return false; 172 | } 173 | } 174 | 175 | return true; 176 | } catch (error) { 177 | console.error(`Error in rug check for token ${tokenMint}:`, error); 178 | return false; // Consider token unsafe if there's an error 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from "ws"; // Node.js websocket library 2 | import { config } from "./config"; // Configuration parameters for our bot 3 | import { validateEnv } from "./utils/env-validator"; 4 | import { WebSocketManager, ConnectionState, WebSocketRequest } from "./utils/managers/websocketManager"; 5 | import { getMintFromSignature } from "./utils/handlers/signatureHandler"; 6 | import { getTokenAuthorities, TokenAuthorityStatus } from "./utils/handlers/tokenHandler"; 7 | import { buyToken } from "./utils/handlers/sniperooHandler"; 8 | import { getRugCheckConfirmed } from "./utils/handlers/rugCheckHandler"; 9 | import { playSound } from "./utils/notification"; 10 | 11 | // Regional Variables 12 | let activeTransactions = 0; 13 | const MAX_CONCURRENT = config.concurrent_transactions; 14 | const CHECK_MODE = config.checks.mode || "full"; 15 | const BUY_PROVIDER = config.token_buy.provider; 16 | const BUY_AMOUNT = config.token_buy.sol_amount; 17 | const SUBSCRIBE_LP = config.liquidity_pool; 18 | const SIM_MODE = config.checks.simulation_mode || false; 19 | const PLAY_SOUND = config.token_buy.play_sound || false; 20 | 21 | // Sell Options 22 | const SELL_ENABLED = config.token_sell.enabled || false; 23 | const SELL_STOP_LOSS = config.token_sell.stop_loss_percent || 15; 24 | const SELL_TAKE_PROFIT = config.token_sell.take_profit_percent || 50; 25 | 26 | // current handled mint 27 | let CURRENT_MINT: string = ""; 28 | 29 | // Function used to handle the transaction once a new pool creation is found 30 | async function processTransaction(signature: string): Promise { 31 | console.log("================================================================"); 32 | console.log("💦 [Process Transaction] New Liquidity Pool signature found"); 33 | console.log("⌛ [Process Transaction] Extracting token CA from signature..."); 34 | console.log("https://solscan.io/tx/" + signature); 35 | 36 | /** 37 | * Extract the token CA from the transaction signature 38 | */ 39 | const returnedMint = await getMintFromSignature(signature); 40 | if (!returnedMint) { 41 | console.log("❌ [Process Transaction] No valid token CA could be extracted"); 42 | console.log("🔎 [Process Transaction] Looking for new Liquidity Pools again\n"); 43 | return; 44 | } 45 | console.log("✅ [Process Transaction] Token CA extracted successfully"); 46 | 47 | /** 48 | * Check if the mint address is the same as the current one to prevent failed logs from spam buying 49 | */ 50 | if (CURRENT_MINT === returnedMint) { 51 | console.log("⏭️ [Process Transaction] Skipping duplicate mint to prevent mint spamming"); 52 | console.log("🔎 [Process Transaction] Looking for new Liquidity Pools again\n"); 53 | return; 54 | } 55 | CURRENT_MINT = returnedMint; 56 | 57 | /** 58 | * Perform checks based on selected level of rug check 59 | */ 60 | if (CHECK_MODE === "snipe") { 61 | console.log(`🔍 [Process Transaction] Performing ${CHECK_MODE} check`); 62 | const tokenAuthorityStatus: TokenAuthorityStatus = await getTokenAuthorities(returnedMint); 63 | if (!tokenAuthorityStatus.isSecure) { 64 | /** 65 | * Token is not secure, check if we should skip based on preferences 66 | */ 67 | const allowMintAuthority = config.checks.settings.allow_mint_authority || false; 68 | const allowFreezeAuthority = config.checks.settings.allow_freeze_authority || false; 69 | if (!allowMintAuthority && tokenAuthorityStatus.hasMintAuthority) { 70 | console.log("❌ [Process Transaction] Token has mint authority, skipping..."); 71 | console.log("🔎 [Process Transaction] Looking for new Liquidity Pools again\n"); 72 | return; 73 | } 74 | if (!allowFreezeAuthority && tokenAuthorityStatus.hasFreezeAuthority) { 75 | console.log("❌ [Process Transaction] Token has freeze authority, skipping..."); 76 | console.log("🔎 [Process Transaction] Looking for new Liquidity Pools again\n"); 77 | return; 78 | } 79 | } 80 | console.log("✅ [Process Transaction] Snipe check passed successfully"); 81 | } else if (CHECK_MODE === "full") { 82 | /** 83 | * Perform full check 84 | */ 85 | if (returnedMint.trim().toLowerCase().endsWith("pump") && config.checks.settings.ignore_ends_with_pump) { 86 | console.log("❌ [Process Transaction] Token ends with pump, skipping..."); 87 | console.log("🔎 [Process Transaction] Looking for new Liquidity Pools again\n"); 88 | return; 89 | } 90 | // Check rug check 91 | const isRugCheckPassed = await getRugCheckConfirmed(returnedMint); 92 | if (!isRugCheckPassed) { 93 | console.log("❌ [Process Transaction] Full rug check not passed, skipping..."); 94 | console.log("🔎 [Process Transaction] Looking for new Liquidity Pools again\n"); 95 | return; 96 | } 97 | } 98 | 99 | /** 100 | * Perform Swap Transaction 101 | */ 102 | if (BUY_PROVIDER === "sniperoo" && !SIM_MODE) { 103 | console.log("🔫 [Process Transaction] Sniping token using Sniperoo..."); 104 | const result = await buyToken(returnedMint, BUY_AMOUNT, SELL_ENABLED, SELL_TAKE_PROFIT, SELL_STOP_LOSS); 105 | if (!result) { 106 | CURRENT_MINT = ""; // Reset the current mint 107 | console.log("❌ [Process Transaction] Token not swapped. Sniperoo failed."); 108 | console.log("🔎 [Process Transaction] Looking for new Liquidity Pools again\n"); 109 | return; 110 | } 111 | if (PLAY_SOUND) playSound(); 112 | console.log("✅ [Process Transaction] Token swapped successfully using Sniperoo"); 113 | } 114 | 115 | /** 116 | * Check if Simopulation Mode is enabled in order to output the warning 117 | */ 118 | if (SIM_MODE) { 119 | console.log("🧻 [Process Transaction] Token not swapped! Simulation Mode turned on."); 120 | if (PLAY_SOUND) playSound("Token found in simulation mode"); 121 | } 122 | 123 | /** 124 | * Output token mint address 125 | */ 126 | console.log("👽 GMGN: https://gmgn.ai/sol/token/" + returnedMint); 127 | console.log("😈 BullX: https://neo.bullx.io/terminal?chainId=1399811149&address=" + returnedMint); 128 | } 129 | 130 | // Main function to start the application 131 | async function main(): Promise { 132 | console.clear(); 133 | console.log("🚀 Starting Solana Token Sniper..."); 134 | 135 | // Load environment variables from the .env file 136 | const env = validateEnv(); 137 | 138 | // Create WebSocket manager 139 | const wsManager = new WebSocketManager({ 140 | url: env.HELIUS_WSS_URI, 141 | initialBackoff: 1000, 142 | maxBackoff: 30000, 143 | maxRetries: Infinity, 144 | debug: true, 145 | }); 146 | 147 | // Set up event handlers 148 | wsManager.on("open", () => { 149 | /** 150 | * Create a new subscription request for each program ID 151 | */ 152 | SUBSCRIBE_LP.filter((pool) => pool.enabled).forEach((pool) => { 153 | const subscriptionMessage = { 154 | jsonrpc: "2.0", 155 | id: pool.id, 156 | method: "logsSubscribe", 157 | params: [ 158 | { 159 | mentions: [pool.program], 160 | }, 161 | { 162 | commitment: "processed", // Can use finalized to be more accurate. 163 | }, 164 | ], 165 | }; 166 | wsManager.send(JSON.stringify(subscriptionMessage)); 167 | }); 168 | }); 169 | 170 | wsManager.on("message", async (data: WebSocket.Data) => { 171 | try { 172 | const jsonString = data.toString(); // Convert data to a string 173 | const parsedData = JSON.parse(jsonString); // Parse the JSON string 174 | 175 | // Handle subscription response 176 | if (parsedData.result !== undefined && !parsedData.error) { 177 | console.log("✅ Subscription confirmed"); 178 | return; 179 | } 180 | 181 | // Only log RPC errors for debugging 182 | if (parsedData.error) { 183 | console.error("🚫 RPC Error:", parsedData.error); 184 | return; 185 | } 186 | 187 | // Safely access the nested structure 188 | const logs = parsedData?.params?.result?.value?.logs; 189 | const signature = parsedData?.params?.result?.value?.signature; 190 | 191 | // Validate `logs` is an array and if we have a signtature 192 | if (!Array.isArray(logs) || !signature) return; 193 | 194 | // Verify if this is a new pool creation 195 | const liquidityPoolInstructions = SUBSCRIBE_LP.filter((pool) => pool.enabled).map((pool) => pool.instruction); 196 | const containsCreate = logs.some((log: string) => typeof log === "string" && liquidityPoolInstructions.some((instruction) => log.includes(instruction))); 197 | 198 | if (!containsCreate || typeof signature !== "string") return; 199 | 200 | // Verify if we have reached the max concurrent transactions 201 | if (activeTransactions >= MAX_CONCURRENT) { 202 | console.log("⏳ Max concurrent transactions reached, skipping..."); 203 | return; 204 | } 205 | 206 | // Add additional concurrent transaction 207 | activeTransactions++; 208 | 209 | // Process transaction asynchronously 210 | processTransaction(signature) 211 | .catch((error) => { 212 | console.error("Error processing transaction:", error); 213 | }) 214 | .finally(() => { 215 | activeTransactions--; 216 | }); 217 | } catch (error) { 218 | console.error("💥 Error processing message:", { 219 | error: error instanceof Error ? error.message : "Unknown error", 220 | timestamp: new Date().toISOString(), 221 | }); 222 | } 223 | }); 224 | 225 | wsManager.on("error", (error: Error) => { 226 | console.error("WebSocket error:", error.message); 227 | }); 228 | 229 | wsManager.on("state_change", (state: ConnectionState) => { 230 | if (state === ConnectionState.RECONNECTING) { 231 | console.log("📴 WebSocket connection lost, attempting to reconnect..."); 232 | } else if (state === ConnectionState.CONNECTED) { 233 | console.log("🔄 WebSocket reconnected successfully."); 234 | } 235 | }); 236 | 237 | // Start the connection 238 | wsManager.connect(); 239 | 240 | // Handle application shutdown 241 | process.on("SIGINT", () => { 242 | console.log("\n🛑 Shutting down..."); 243 | wsManager.disconnect(); 244 | process.exit(0); 245 | }); 246 | 247 | process.on("SIGTERM", () => { 248 | console.log("\n🛑 Shutting down..."); 249 | wsManager.disconnect(); 250 | process.exit(0); 251 | }); 252 | } 253 | 254 | // Start the application 255 | main().catch((err) => { 256 | console.error("Fatal error:", err); 257 | process.exit(1); 258 | }); 259 | --------------------------------------------------------------------------------