import { Connection, Keypair, PublicKey, VersionedTransaction, TOKEN_PROGRAM_ID, sendAndConfirmTransaction, type Commitment } from "@solana/web3.js"; import { getAssociatedTokenAddress, getAccount, TokenAccountNotFoundError, TokenInvalidAccountOwnerError } from "@solana/spl-token"; import * as fs from "fs/promises"; import bs58 from "bs58"; // Token addresses - defined at the top for easy configuration const TOKEN_ADDRESS = "5fsvCy3yaLduYJ5mkSS7ERwwraJ5JPVSDakedLfpxaF"; // Your token const USDC_ADDRESS = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC const SOL_ADDRESS = "So11111111111111111111111111111111111111112"; // Wrapped SOL // Trading configuration const BUY_AMOUNT_USDC = 0.5; // Amount of USDC to buy with each cycle const MINIMUM_USDC_BALANCE = 1.0; // Minimum USDC balance to maintain const AUTO_PURCHASE_AMOUNT = 5.0; // Amount of USDC to buy when balance is low // Configuration interface for JSON file interface Config { privateKey?: string; rpcUrl?: string; } // Jupiter API response interfaces interface JupiterQuote { inputMint: string; inAmount: string; outputMint: string; outAmount: string; otherAmountThreshold: string; swapMode: string; slippageBps: number; platformFee?: any; priceImpactPct: string; routePlan: any[]; } interface JupiterSwapResponse { swapTransaction: string; lastValidBlockHeight?: number; } // Function to generate random delay between 2 and 5 minutes (in milliseconds) const getRandomDelay = (): number => { const min = 2 * 60 * 1000; // 2 minutes const max = 5 * 60 * 1000; // 5 minutes return Math.floor(Math.random() * (max - min + 1)) + min; }; // Function to load or create config async function loadConfig(): Promise { try { const data = await fs.readFile("config.json", "utf-8"); return JSON.parse(data) as Config; } catch (error) { console.log("Config not found, generating new keypair..."); const keypair = Keypair.generate(); const config: Config = { privateKey: bs58.encode(keypair.secretKey), rpcUrl: "https://api.mainnet-beta.solana.com" // Default RPC }; await fs.writeFile("config.json", JSON.stringify(config, null, 2)); console.log("New config created. Please add your RPC URL to config.json"); return config; } } // Function to get token balance with proper error handling async function getTokenBalance( connection: Connection, wallet: PublicKey, tokenMint: string, decimals: number = 9 ): Promise { try { const tokenMintPubkey = new PublicKey(tokenMint); const associatedTokenAddress = await getAssociatedTokenAddress( tokenMintPubkey, wallet, false ); try { const tokenAccount = await getAccount(connection, associatedTokenAddress); return Number(tokenAccount.amount) / Math.pow(10, decimals); } catch (error) { if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { console.log(`Token account not found for ${tokenMint}`); return 0; } throw error; } } catch (error) { console.error("Error fetching token balance:", error); return 0; } } // Function to get SOL balance async function getSolBalance(connection: Connection, wallet: PublicKey): Promise { try { const balance = await connection.getBalance(wallet); return balance / 1e9; // Convert lamports to SOL } catch (error) { console.error("Error fetching SOL balance:", error); return 0; } } // Function to get USDC balance specifically async function getUSDCBalance(connection: Connection, wallet: PublicKey): Promise { return await getTokenBalance(connection, wallet, USDC_ADDRESS, 6); // USDC has 6 decimals } // Function to automatically purchase USDC with SOL when balance is low async function ensureUSDCBalance( connection: Connection, wallet: Keypair ): Promise { try { const usdcBalance = await getUSDCBalance(connection, wallet.publicKey); console.log(`šŸ’° Current USDC balance: ${usdcBalance.toFixed(4)} USDC`); if (usdcBalance >= MINIMUM_USDC_BALANCE) { console.log("āœ… USDC balance is sufficient"); return true; } console.log(`āš ļø USDC balance is low (${usdcBalance.toFixed(4)} < ${MINIMUM_USDC_BALANCE})`); console.log(`šŸ”„ Attempting to purchase ${AUTO_PURCHASE_AMOUNT} USDC with SOL...`); const solBalance = await getSolBalance(connection, wallet.publicKey); console.log(`šŸ’° Current SOL balance: ${solBalance.toFixed(4)} SOL`); if (solBalance < 0.1) { // Need some SOL for the transaction + fees console.error("āŒ Insufficient SOL balance to purchase USDC"); return false; } // Perform SOL to USDC swap const success = await performSwap( connection, wallet, SOL_ADDRESS, USDC_ADDRESS, AUTO_PURCHASE_AMOUNT, true, false, "SOL to USDC" ); if (success > 0) { console.log(`āœ… Successfully purchased USDC`); // Wait a moment for the transaction to settle await new Promise(resolve => setTimeout(resolve, 5000)); const newUsdcBalance = await getUSDCBalance(connection, wallet.publicKey); console.log(`šŸ’° New USDC balance: ${newUsdcBalance.toFixed(4)} USDC`); return newUsdcBalance >= MINIMUM_USDC_BALANCE; } else { console.error("āŒ Failed to purchase USDC"); return false; } } catch (error) { console.error("āŒ Error ensuring USDC balance:", error); return false; } } // Main swap function using Jupiter API async function performSwap( connection: Connection, wallet: Keypair, tokenAddress: string, quoteAddress: string, amount: number, isBuy: boolean, isFullSell: boolean = false, swapDescription: string = "" ): Promise { try { const inputMint = isBuy ? quoteAddress : tokenAddress; const outputMint = isBuy ? tokenAddress : quoteAddress; let amountInLamports: number; let decimals: number; if (isFullSell) { // Use entire token balance for full sell const balance = await getTokenBalance(connection, wallet.publicKey, tokenAddress); if (balance <= 0) { console.log("No tokens to sell in full sell."); return 0; } amountInLamports = Math.floor(balance * 1e9); // Convert to lamports console.log(`Full sell amount: ${balance} tokens`); } else { // Determine decimals based on token type if (quoteAddress === USDC_ADDRESS) { decimals = isBuy ? 6 : 9; // USDC has 6 decimals, tokens have 9 } else if (quoteAddress === SOL_ADDRESS) { decimals = 9; // SOL has 9 decimals } else { decimals = 9; // Default to 9 decimals } amountInLamports = Math.floor(amount * Math.pow(10, decimals)); } const description = swapDescription || `${isBuy ? 'Buying' : 'Selling'}`; console.log(`${description} with amount: ${amountInLamports} lamports`); // Fetch quote from Jupiter API with error handling const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amountInLamports}&slippageBps=300`; const quoteResponse = await fetch(quoteUrl); if (!quoteResponse.ok) { throw new Error(`Quote API error: ${quoteResponse.status} ${quoteResponse.statusText}`); } const quote: JupiterQuote = await quoteResponse.json(); if (!quote || !quote.outAmount) { throw new Error("Invalid quote response from Jupiter API"); } console.log(`Quote received: ${quote.inAmount} -> ${quote.outAmount}`); // Store amount bought for tracking (if buying) let outputAmount = 0; if (isBuy) { const outputDecimals = outputMint === USDC_ADDRESS ? 6 : 9; outputAmount = Number(quote.outAmount) / Math.pow(10, outputDecimals); } // Fetch swap transaction const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ quoteResponse: quote, userPublicKey: wallet.publicKey.toBase58(), wrapAndUnwrapSol: true, dynamicComputeUnitLimit: true, prioritizationFeeLamports: 1000 }), }); if (!swapResponse.ok) { throw new Error(`Swap API error: ${swapResponse.status} ${swapResponse.statusText}`); } const swapData: JupiterSwapResponse = await swapResponse.json(); if (!swapData.swapTransaction) { throw new Error("No swap transaction returned from Jupiter API"); } // Deserialize and sign transaction const transactionBuf = Buffer.from(swapData.swapTransaction, "base64"); const transaction = VersionedTransaction.deserialize(transactionBuf); transaction.sign([wallet]); // Send transaction with proper confirmation const signature = await connection.sendRawTransaction(transaction.serialize(), { skipPreflight: false, preflightCommitment: "confirmed" as Commitment, maxRetries: 3 }); console.log(`Transaction sent: ${signature}`); // Confirm transaction const latestBlockhash = await connection.getLatestBlockhash(); const confirmation = await connection.confirmTransaction({ signature, blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight }, "confirmed"); if (confirmation.value.err) { throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`); } console.log(`āœ… Swap ${description || (isBuy ? "buy" : isFullSell ? "full sell" : "sell")} completed: ${signature}`); return isBuy ? outputAmount : 0; } catch (error) { console.error(`āŒ Error during ${swapDescription || (isBuy ? "buy" : isFullSell ? "full sell" : "sell")}:`, error); return 0; } } // Main execution async function main(): Promise { const config = await loadConfig(); if (!config.rpcUrl) { console.error("āŒ RPC URL not provided in config.json"); console.log("Please add your RPC URL to config.json and restart"); process.exit(1); } if (!config.privateKey) { console.error("āŒ Private key not found in config"); process.exit(1); } const connection = new Connection(config.rpcUrl, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }); const wallet = Keypair.fromSecretKey(bs58.decode(config.privateKey)); let lastFullSellTime = 0; const FULL_SELL_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds console.log("šŸš€ Starting trading bot..."); console.log("šŸ“ Wallet public key:", wallet.publicKey.toBase58()); console.log("šŸŽÆ Trading token:", TOKEN_ADDRESS); console.log("šŸ’µ Quote token:", USDC_ADDRESS); // Check initial balances const solBalance = await getSolBalance(connection, wallet.publicKey); const usdcBalance = await getUSDCBalance(connection, wallet.publicKey); const tokenBalance = await getTokenBalance(connection, wallet.publicKey, TOKEN_ADDRESS); console.log(`šŸ’° Initial SOL balance: ${solBalance.toFixed(4)} SOL`); console.log(`šŸ’µ Initial USDC balance: ${usdcBalance.toFixed(4)} USDC`); console.log(`šŸŖ™ Initial token balance: ${tokenBalance.toFixed(4)} tokens`); if (solBalance < 0.01) { console.warn("āš ļø Low SOL balance. Make sure you have enough SOL for transaction fees."); } // Main trading loop while (true) { try { const currentTime = Date.now(); // Check if it's time for a full sell (every 1 hour) if (currentTime - lastFullSellTime >= FULL_SELL_INTERVAL) { console.log("ā° Time for full sell..."); await performSwap(connection, wallet, TOKEN_ADDRESS, USDC_ADDRESS, 0, false, true); lastFullSellTime = currentTime; console.log("āœ… Full sell completed. Next full sell in 1 hour."); // Wait before starting next cycle await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } // Ensure we have enough USDC before trading const hasEnoughUSDC = await ensureUSDCBalance(connection, wallet); if (!hasEnoughUSDC) { console.log("āŒ Unable to ensure sufficient USDC balance, waiting 2 minutes before retry..."); await new Promise((resolve) => setTimeout(resolve, 2 * 60 * 1000)); continue; } // Regular buy-sell cycle console.log("šŸ’° Starting buy phase..."); const boughtAmount = await performSwap(connection, wallet, TOKEN_ADDRESS, USDC_ADDRESS, BUY_AMOUNT_USDC, true); if (boughtAmount <= 0) { console.log("āŒ Buy failed, skipping sell phase"); await new Promise((resolve) => setTimeout(resolve, 30000)); // Wait 30 seconds before retry continue; } // Wait for random delay const delay1 = getRandomDelay(); console.log(`ā³ Waiting ${(delay1 / 1000 / 60).toFixed(1)} minutes before sell...`); await new Promise((resolve) => setTimeout(resolve, delay1)); // Sell half of the bought amount console.log("šŸ’ø Starting sell phase..."); const sellAmount = boughtAmount / 2; await performSwap(connection, wallet, TOKEN_ADDRESS, USDC_ADDRESS, sellAmount, false); // Wait for random delay before next cycle const delay2 = getRandomDelay(); console.log(`ā³ Waiting ${(delay2 / 1000 / 60).toFixed(1)} minutes before next cycle...`); await new Promise((resolve) => setTimeout(resolve, delay2)); } catch (error) { console.error("āŒ Error in main loop:", error); console.log("ā³ Waiting 30 seconds before retry..."); await new Promise((resolve) => setTimeout(resolve, 30000)); } } } // Handle graceful shutdown process.on('SIGINT', () => { console.log('\nšŸ›‘ Shutting down trading bot...'); process.exit(0); }); process.on('SIGTERM', () => { console.log('\nšŸ›‘ Shutting down trading bot...'); process.exit(0); }); // Run the script console.log("šŸ¤– Solana Trading Bot Starting..."); main().catch((error) => { console.error("šŸ’„ Fatal error:", error); process.exit(1); });