├── src ├── telegram-bot-api.d.ts ├── index.ts ├── router │ ├── index.ts │ └── router.start.ts ├── solana.ts ├── bot.ts ├── Raydium │ ├── swapConfig.ts │ ├── raydium.ts │ ├── RaydiumSwap.ts │ └── raydium_dex_swap.ts ├── config.ts ├── tokenadd.ts ├── util │ ├── types.ts │ ├── db.ts │ └── helper.ts └── startTrade.ts ├── .gitignore ├── trading.db ├── assets ├── Monitor.png ├── beauty.jpg ├── Monitor1.png ├── Stratergy.png ├── Solana_Tank_Bot.png └── Trading_Process.png ├── .env_example ├── tsconfig.json ├── package.json └── README.md /src/telegram-bot-api.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'telegram-bot-api'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | //node_modules 2 | node_modules 3 | 4 | //.env 5 | .env -------------------------------------------------------------------------------- /trading.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/montedev0516/Solana_Sniper-BOT_Tank/HEAD/trading.db -------------------------------------------------------------------------------- /assets/Monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/montedev0516/Solana_Sniper-BOT_Tank/HEAD/assets/Monitor.png -------------------------------------------------------------------------------- /assets/beauty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/montedev0516/Solana_Sniper-BOT_Tank/HEAD/assets/beauty.jpg -------------------------------------------------------------------------------- /assets/Monitor1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/montedev0516/Solana_Sniper-BOT_Tank/HEAD/assets/Monitor1.png -------------------------------------------------------------------------------- /assets/Stratergy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/montedev0516/Solana_Sniper-BOT_Tank/HEAD/assets/Stratergy.png -------------------------------------------------------------------------------- /assets/Solana_Tank_Bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/montedev0516/Solana_Sniper-BOT_Tank/HEAD/assets/Solana_Tank_Bot.png -------------------------------------------------------------------------------- /assets/Trading_Process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/montedev0516/Solana_Sniper-BOT_Tank/HEAD/assets/Trading_Process.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const TelegramBot = require("node-telegram-bot-api"); 2 | 3 | import bot from "./bot" 4 | import router from './router'; 5 | 6 | ( () => { 7 | router(bot); 8 | })(); 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.env_example: -------------------------------------------------------------------------------- 1 | TELEGRAM_TOKEN='...' 2 | 3 | RPC_URL='https://mainnet.helius-rpc.com/?api-key=...' 4 | WEBSOCKET_URL = "wss://mainnet.helius-rpc.com/?api-key=..." 5 | 6 | FALCONHIT_API_KEY="..." 7 | MORALIS_API_KEY="..." 8 | BITQUERY_V2_TOKEN="..." 9 | BITQUERY_V1_TOKEN="..." -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import startRouter from "./router.start"; 3 | 4 | 5 | const router = (bot: TelegramBot) => { 6 | startRouter(bot); 7 | 8 | bot.on('polling_error', (e) => { 9 | console.error(e); 10 | }); 11 | } 12 | 13 | export default router; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "types": ["telegram"], 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "include": ["./src/**/*", "bot1"] 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/solana.ts: -------------------------------------------------------------------------------- 1 | import { Connection, clusterApiUrl } from '@solana/web3.js'; 2 | 3 | const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); 4 | 5 | // Implement Solana-related functions here 6 | 7 | 8 | // Respond to the callback query with an alert and update the bot's message 9 | bot.answerCallbackQuery(callbackQuery.id, { 10 | text: `You pressed ${category}`, 11 | show_alert: true, 12 | }); -------------------------------------------------------------------------------- /src/bot.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import dotenv from "dotenv"; 3 | import { MoralisStart } from "./util/helper"; 4 | dotenv.config(); 5 | 6 | const token = process.env.TELEGRAM_TOKEN; 7 | if (!token) { 8 | console.error("Bot token is not set in .env"); 9 | process.exit(1); 10 | } 11 | console.log("Bot token:", token); 12 | // Create a new Telegram bot using polling to fetch new updates 13 | const bot = new TelegramBot(token, { polling: true }); 14 | 15 | MoralisStart().then(() => { 16 | console.log("Moralis started!"); 17 | }); 18 | 19 | export default bot; -------------------------------------------------------------------------------- /src/Raydium/swapConfig.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | executeSwap: false, // Send tx when true, simulate tx when false 3 | useVersionedTransaction: true, 4 | tokenAAmount: 0.0001, // Swap 0.01 SOL for USDT in this example 5 | solTokenAddress: "So11111111111111111111111111111111111111112", // Token to swap for the other, SOL in this case 6 | tokenBAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC address 7 | maxLamports: 4500000, // Micro lamports for priority fee 8 | direction: "in" as "in" | "out", // Swap direction: 'in' or 'out' 9 | liquidityFile: "https://api.raydium.io/v2/sdk/liquidity/mainnet.json", 10 | maxRetries: 5, 11 | }; -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from "@solana/web3.js"; 2 | import dotenv from "dotenv"; 3 | dotenv.config(); 4 | 5 | export const solBuyAmountRange: number[] = [0.00001, 0.00005]; 6 | export const msgCatchInternalDuration = 20000; 7 | export const sellInternalDuration = 10000; 8 | export const priceFactor: number[] = [0.01, 2, 10]; 9 | 10 | const RPC_URL: string = process.env.RPC_URL as string; // ENTER YOUR RPC 11 | const WEBSOCKET_URL: string = process.env.WEBSOCKET_URL as string; 12 | 13 | 14 | export const connection = new Connection(RPC_URL, { wsEndpoint: WEBSOCKET_URL, confirmTransactionInitialTimeout: 30000, commitment: 'confirmed' }) 15 | export const solanaWallets: string[] = [""]; 16 | 17 | -------------------------------------------------------------------------------- /src/tokenadd.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { getMint } from '@solana/spl-token'; 3 | 4 | const TOKEN_MINT_ADDRESS = "7NgbAAMf3ozg4NG3Ynt2de5TA2afMZZkfkGpEpC2mXYu" 5 | 6 | async function getTokenDetails(mintAddress: string) { 7 | const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); 8 | const mintPublicKey = new PublicKey(mintAddress); 9 | 10 | // Get mint information using the getMint function 11 | const mintInfo = await getMint(connection, mintPublicKey); 12 | 13 | console.log("Token Details:", mintInfo); 14 | } 15 | 16 | 17 | // Replace 'TOKEN_MINT_ADDRESS' with the actual mint address of the token you're interested in 18 | getTokenDetails(TOKEN_MINT_ADDRESS); 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-trading-bot", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "types": "tsc", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "nodemon ./src/index.ts", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@coral-xyz/anchor": "^0.30.0", 16 | "@project-serum/serum": "^0.13.65", 17 | "@raydium-io/raydium-sdk": "^1.3.1-beta.52", 18 | "@solana/spl-token": "^0.4.6", 19 | "@solana/web3.js": "^1.92.3", 20 | "bs58": "^5.0.0", 21 | "dotenv": "^16.4.5", 22 | "input": "^1.0.1", 23 | "moralis": "^2.26.5", 24 | "node-fetch": "^3.3.2", 25 | "node-telegram-bot-api": "^0.66.0", 26 | "nodemon": "^3.1.3", 27 | "sqlite3": "^5.1.7", 28 | "telegram": "^2.22.2", 29 | "telegram-bot-api": "^2.0.1", 30 | "telegram-scraper": "^1.0.1" 31 | }, 32 | "devDependencies": { 33 | "@types/big.js": "^6.2.2", 34 | "@types/bn.js": "^5.1.5", 35 | "@types/node": "^20.14.2", 36 | "@types/node-telegram-bot-api": "^0.64.6", 37 | "ts-node": "^10.9.2", 38 | "typescript": "^5.4.5" 39 | }, 40 | "description": "" 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Telegram Signal Trading Bot 2 | 3 | ## Personal Trading Bot, not public 4 | 5 | ## Trading Stratergy 6 | 7 | ![Stratergy](/assets/Stratergy.png) 8 | 9 | ## Realtime-Monitor Channel 10 | 11 | * **Channel Link** = https://t.me/Maestrosdegen 12 | 13 | * **Maestrosdegen Telegram Signal Channel for realtime monitoring** 14 | 15 | ![Monitorchannel](/assets/Monitor.png) 16 | 17 | * we need to distinct solana token address and ethereum token address, etc. 18 | 19 | ## Buy and Sell Criteria 20 | 21 | **I want an automated trading bot on solana, raydium with some criteria:** 22 | 23 | * (-) input data: (to buy) 24 | * (+) solana contract from a telegram channel or group ID 25 | * (+) solana contract from data.txt file 26 | * (-) result: 27 | * (+) Instantly buy tokens 28 | * (+) Sell: setup take profit 1, take profit 2, loss 29 | * (-) options: 30 | * (+) Jito fee 31 | * (+) min and max LQ 32 | * (+) Slippage 33 | 34 | # Telegram Tokens, RPC_node, Required APIs 35 | 36 | * **TELEGRAM_TOKEN**='...' 37 | 38 | * **RPC_URL**='https://mainnet.helius-rpc.com/?api-key=12e48098-...' 39 | * **WEBSOCKET_URL** = "wss://mainnet.helius-rpc.com/?api-key=12e48098-..." 40 | 41 | * **FALCONHIT_API_KEY**="" 42 | * **MORALIS_API_KEY**="" 43 | * **BITQUERY_V2_TOKEN**="" 44 | * **BITQUERY_V1_TOKEN**="" 45 | 46 | ## Install and Working 47 | 48 | 1. **yarn install** 49 | 2. **yarn start** 50 | 3. Find **@tank_...** (in Telegram) 51 | 4. **/start** (Telegram) 52 | 5. selection buttons ... 53 | 54 | ## working process 55 | 56 | ![working process](/assets/Trading_Process.png) 57 | -------------------------------------------------------------------------------- /src/util/types.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js" 2 | export enum addressType { 3 | "SOLANA", 4 | "EVM", 5 | "INVALID" 6 | } 7 | 8 | export type signal = { 9 | id: number, 10 | contractAddress: string, 11 | action: "buy" | "sell", 12 | amount: string, 13 | priceFactor?: number, 14 | platform: "raydium" 15 | chain: "solana" 16 | timestamp: string 17 | } 18 | 19 | export type buyActionType = { 20 | signalNumber: number, 21 | contractAdress: string, 22 | price: number, 23 | platform: string, 24 | chain: string 25 | } 26 | 27 | export type sellActionType = { 28 | id: number, 29 | contractAddress: string, 30 | priceFactor: number | undefined 31 | } 32 | 33 | export type signalMap = { 34 | [key: number]: signal | null 35 | } 36 | 37 | export type poolInfoDataType = { 38 | id: PublicKey 39 | baseMint: PublicKey 40 | quoteMint: PublicKey 41 | lpMint: PublicKey 42 | baseDecimals: number 43 | quoteDecimals: number 44 | lpDecimals: number 45 | version: number 46 | programId: PublicKey 47 | authority: PublicKey 48 | openOrders: PublicKey 49 | targetOrders: PublicKey 50 | baseVault: PublicKey 51 | quoteVault: PublicKey 52 | withdrawQueue: PublicKey 53 | lpVault: PublicKey 54 | marketVersion: PublicKey 55 | marketProgramId: PublicKey 56 | marketId: PublicKey 57 | marketAuthority: PublicKey 58 | marketBaseVault: PublicKey 59 | marketQuoteVault: PublicKey 60 | marketBids: PublicKey 61 | marketAsks: PublicKey 62 | marketEventQueue: PublicKey 63 | lookupTableAccount: string 64 | } 65 | -------------------------------------------------------------------------------- /src/router/router.start.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { 3 | createSignal, 4 | tokenBuy, 5 | scrapeMessages, 6 | tokenSell 7 | } from "../startTrade"; 8 | import { msgCatchInternalDuration, sellInternalDuration } from "../config"; 9 | 10 | const startRouter = (bot: TelegramBot) => { 11 | // Session state for each chat 12 | const sessions: any = {}; 13 | let globalChatId: any; 14 | 15 | // Define the inline keyboard layout for interaction 16 | const options = { 17 | reply_markup: { 18 | inline_keyboard: [ 19 | [{ text: "🛒 Buy", callback_data: "buy" }, { text: "📈 Sell", callback_data: "sell" }], 20 | [{ text: "💼 Help", callback_data: "help" }, { text: "📬 Channel", url: "https://t.me/Maestrosdegen" }] 21 | ], 22 | }, 23 | }; 24 | 25 | const selectedBuyOptions = { 26 | reply_markup: { 27 | inline_keyboard: [ 28 | [{ text: "🛒 Manual Buy", callback_data: "manual_buy" }], 29 | [{ text: "🚀 Auto Buy", callback_data: "auto_buy" }] 30 | ], 31 | }, 32 | }; 33 | 34 | const stopOptions = { 35 | reply_markup: { 36 | inline_keyboard: [ 37 | [{ text: "🛒 Stop Trading", callback_data: "stop_buy" }], 38 | ], 39 | }, 40 | }; 41 | 42 | bot.onText(/\/start/, (msg: any) => { 43 | const chatId = msg.chat.id; 44 | console.log("chatId", chatId); 45 | const welcomeMessage = "🍄 Welcome to my soltank_bot!\n\n`AAEuA3DeoblV-LZQwoexDgWJoM2Tg0-E2Ns `\n\n`https://t.me/mysol_tankbot`\n\n 🥞 Please choose a category below:"; 46 | bot.sendMessage(chatId, welcomeMessage, options); 47 | }); 48 | 49 | bot.on("callback_query", (callbackQuery: any) => { 50 | 51 | const message = callbackQuery.message; 52 | const category = callbackQuery.data; 53 | const chatId = message.chat.id; 54 | globalChatId = chatId; 55 | 56 | let tokenBuyInterval; 57 | let tokenSellInterval; 58 | 59 | if (!sessions[chatId]) { 60 | sessions[chatId] = { waitingForAmount: false, waitingForTokenAddress: false }; 61 | } 62 | 63 | if (category === "buy") { 64 | bot.sendMessage(chatId, "🏆 Choose your buy method: ", selectedBuyOptions); 65 | } else if (category === "manual_buy") { 66 | sessions[chatId].waitingForAmount = true; 67 | bot.sendMessage(chatId, "✍ Input the amount you want to buy ... (sol) \n⚱️ For example: 1.25 "); 68 | } else if (category === "auto_buy") { 69 | bot.sendMessage(chatId, "✍ Starting auto buy right now"); 70 | // Catch signal from signal channel 71 | clearInterval(tokenBuyInterval); 72 | tokenBuyInterval = setInterval(scrapeMessages, msgCatchInternalDuration); 73 | clearInterval(tokenSellInterval); 74 | tokenSellInterval = setInterval(tokenSell, sellInternalDuration); 75 | 76 | } else if (category === "stop_buy") { 77 | clearInterval(tokenSellInterval); 78 | bot.sendMessage(chatId, "🏆 Choose your buy method: ", selectedBuyOptions); 79 | } 80 | }); 81 | 82 | bot.on("message", async (msg: any) => { 83 | const chatId = msg.chat.id; 84 | const session = sessions[chatId]; 85 | 86 | if (!session) return; // Ignore messages if session isn't initialized 87 | 88 | if (session.waitingForTokenAddress) { 89 | const tokenAddress = msg.text.trim(); 90 | if (tokenAddress) { 91 | console.log("Token address:", tokenAddress); 92 | session.tokenAddress = tokenAddress; 93 | session.waitingForTokenAddress = false; 94 | await bot.sendMessage(chatId, `👌 Success! Ready for swap ... \n\n💰 Amount: ${session.amount.toFixed(6)} SOL \n🤝 Token Address: ${tokenAddress}`); 95 | // console.log("----***--SwapConfig---***---", swapConfig(tokenAddress, session.amount)); 96 | await bot.sendMessage(chatId, `Token: ${tokenAddress}, Amount: ${session.amount} SOL`); 97 | if (createSignal(tokenAddress, session.amount)){ 98 | await tokenBuy(); 99 | } 100 | await bot.sendMessage(chatId, "🏆 Choose your buy method: ", selectedBuyOptions); 101 | await bot.sendMessage(chatId, "Buy Success! \nIf you want to stop manual token buy, please click Stop button...", stopOptions); 102 | delete sessions[chatId]; // Clear session after completion 103 | } 104 | } else if (session.waitingForAmount) { 105 | const amount = parseFloat(msg.text); 106 | if (!isNaN(amount)) { 107 | session.amount = amount; 108 | session.waitingForAmount = false; 109 | session.waitingForTokenAddress = true; 110 | bot.sendMessage(chatId, "🧧 Input the token address you want to buy ... (sol) \n\n⚱️ For example: CXeaSFtgwDJ6HKrGNNxtDEwydUcbZySx8rhJmoJBkEy3 "); 111 | } else { 112 | bot.sendMessage(chatId, "Invalid amount. Please enter a valid number."); 113 | } 114 | } 115 | }); 116 | 117 | return { 118 | sellEnd: () => { 119 | bot.sendMessage(globalChatId, "Buy Success! \nIf you want to stop token auto sell, please click Stop button...", stopOptions); 120 | } 121 | } 122 | } 123 | 124 | 125 | export default startRouter; -------------------------------------------------------------------------------- /src/util/db.ts: -------------------------------------------------------------------------------- 1 | // public modules 2 | import sqlite3 from "sqlite3"; 3 | 4 | // needed types 5 | import { 6 | buyActionType, 7 | sellActionType, 8 | } from "./types"; 9 | export const buyActions: buyActionType[] = []; 10 | export const sellActions: sellActionType[] = []; 11 | 12 | const sqlite3Verbose = sqlite3.verbose(); 13 | 14 | 15 | // Open a database connection 16 | const db = new sqlite3Verbose.Database("./trading.db", (err) => { 17 | if (err) { 18 | return console.error(err); 19 | } 20 | console.log("--------------- Connected to the SQlite database --------------"); 21 | }); // In-memory database for demonstration, you can specify a file path for persistent storage 22 | 23 | // Create a table 24 | db.serialize(() => { 25 | db.run( 26 | `CREATE TABLE IF NOT EXISTS buys (id INTEGER PRIMARY KEY, contractAddress TEXT, purchasedPrice FLOAT, priceFactor INTEGER, platform TEXT, chain TEXT, date TEXT);`, 27 | (err: any, row: any) => { 28 | if (err) { 29 | console.error(err.message); 30 | } 31 | // console.log(row.id + "\t" + row.contractAddress); 32 | } 33 | ); 34 | db.run( 35 | `CREATE TABLE IF NOT EXISTS lastsignal (id INTEGER PRIMARY KEY, signalId INTEGER, date TEXT);`, 36 | (err: any, row: any) => { 37 | if (err) { 38 | console.error(err.message); 39 | } 40 | // console.log(row.id + "\t" + row.contractAddress); 41 | } 42 | ); 43 | db.run( 44 | `CREATE TABLE IF NOT EXISTS lookuptables (id INTEGER PRIMARY KEY, lutAddress TEXT);`, 45 | (err: any, row: any) => { 46 | if (err) { 47 | console.error(err.message); 48 | } 49 | } 50 | ) 51 | }); 52 | 53 | // Create 54 | export const addBuy = async () => { 55 | return new Promise((resolve, reject) => { 56 | const purchasedTime = new Date().toISOString(); 57 | 58 | const data = buyActions.map((buyAction) => [ 59 | buyAction.contractAdress, 60 | buyAction.price, 61 | 0, 62 | buyAction.platform, 63 | buyAction.chain, 64 | purchasedTime 65 | ]); 66 | 67 | 68 | // Flatten the data array to prepare for bulk insertion 69 | const flatData = data.flat(); 70 | // console.log(flatData); 71 | 72 | // Contruct placeholders for SQL statement 73 | const placeholders = buyActions.map(() => "(?, ?, ?, ?, ?, ?)").join(', '); 74 | 75 | const sql = `INSERT INTO buys (contractAddress, purchasedPrice, priceFactor, platform, chain, date) VALUES ${placeholders}`; 76 | 77 | //Insert all recored to database at once 78 | db.run(sql, flatData, function(err) { 79 | if (err) { 80 | console.error(err); 81 | reject(err); 82 | } 83 | else { 84 | console.log("bulk insert successful!"); 85 | resolve(this.lastID); 86 | } 87 | }); 88 | }); 89 | } 90 | 91 | const getSolanaTokenAddresses = async () => { 92 | return new Promise((resolve, reject) => { 93 | db.all("SELECT contractAddress from buys WHERE chain = 'solana'", (err, rows) => { 94 | if (err) { 95 | reject(err); 96 | } else { 97 | resolve(rows); 98 | } 99 | }); 100 | }) 101 | } 102 | 103 | // Read 104 | export const getSolanaBuys = async () => { 105 | return new Promise((resolve, reject) => { 106 | db.all("SELECT * FROM buys WHERE chain = 'solana'", (err, rows) => { 107 | if (err) { 108 | reject(err); 109 | } else { 110 | resolve(rows); 111 | } 112 | }); 113 | }); 114 | } 115 | 116 | // Update 117 | const updateBuy = async (id: number, priceFactor: number) => { 118 | return new Promise((resolve, reject) => { 119 | db.run( 120 | "UPDATE buys SET priceFactor = ? WHERE id = ?", 121 | [priceFactor, id], 122 | function (err) { 123 | if (err) { 124 | reject(err); 125 | } else { 126 | resolve(this.changes); // Returns the number of rows affected 127 | } 128 | } 129 | ); 130 | }); 131 | } 132 | 133 | 134 | // Delete 135 | const deleteBuy = async (id: number) => { 136 | return new Promise((resolve, reject) => { 137 | db.run("DELETE FROM buys WHERE id = ?", [id], function (err) { 138 | if (err) { 139 | reject(err); 140 | } else { 141 | resolve(this.changes); // Returns the number of rows affected 142 | } 143 | }); 144 | }); 145 | } 146 | 147 | export const updateSells = async () => { 148 | return new Promise((resolve, reject) => { 149 | const updateData = []; 150 | const deleteData = []; 151 | for (const sellAction of sellActions) { 152 | if (Number(sellAction.priceFactor) >= 2) { 153 | deleteData.push( 154 | sellAction.id 155 | ); 156 | } 157 | else { 158 | updateData.push([ 159 | sellAction.priceFactor || 0 + 1, 160 | sellAction.id, 161 | ]); 162 | } 163 | } 164 | const flatUpdateData = updateData.flat(); 165 | const flatDeleteData = deleteData.flat(); 166 | console.log("flagUpdateData => ", flatUpdateData); 167 | console.log("flatDeleteData => ", flatDeleteData); 168 | try { 169 | if (flatUpdateData.length > 0) { 170 | const updatePlaceholders = updateData.map(() => "(?)").join(', '); 171 | const updateSql = `UPDATE buys SET priceFactor = ${updatePlaceholders} where id = ${updatePlaceholders}`; 172 | db.run(updateSql, flatUpdateData, function(err) { 173 | if (err) { 174 | reject(err); 175 | } 176 | }); 177 | } 178 | if (flatDeleteData.length > 0) { 179 | const deletePlaceholders = deleteData.map(() => "(?)").join(', '); 180 | const deleteSql = `DELETE FROM buys where id = ${deletePlaceholders}`; 181 | db.run(deleteSql, flatDeleteData, function(err) { 182 | if (err) { 183 | reject(err); 184 | } 185 | }); 186 | } 187 | resolve("success update sell!"); 188 | } catch (err) { 189 | reject(err); 190 | } 191 | }) 192 | } 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /src/startTrade.ts: -------------------------------------------------------------------------------- 1 | // public module 2 | import { telegram_scraper } from 'telegram-scraper'; 3 | 4 | // private module 5 | import raydiumToken from "./Raydium/raydium" 6 | 7 | import { 8 | verifyAddress, 9 | getRandomArbitrary 10 | } from "./util/helper"; 11 | 12 | import { 13 | solBuyAmountRange 14 | } from "./config"; 15 | 16 | import { 17 | buyActions, 18 | sellActions, 19 | addBuy, 20 | getSolanaBuys, 21 | updateSells, 22 | } from './util/db'; 23 | 24 | import { 25 | convertAsSignal 26 | } from "./util/helper" 27 | 28 | import startRouter from './router/router.start'; 29 | 30 | // needed types 31 | import { 32 | signal, 33 | addressType, 34 | signalMap 35 | } from "./util/types" 36 | 37 | import bot from './bot'; 38 | 39 | 40 | let telegram_signals: signalMap = {}; 41 | let telegram_signals_list : number[] = []; 42 | let totalCnt: number = 0; 43 | 44 | 45 | export const scrapeMessages = async () => { 46 | let telegram_channel_username = 'Maestrosdegen'; 47 | let result = JSON.parse(await telegram_scraper(telegram_channel_username)); 48 | let recentMessage = result[result.length-1]["message_text"]; 49 | let spaceNumber = recentMessage.split(" ").length - 1; 50 | let spacePosition = 0; 51 | let slashNumber = 0; 52 | let slashPosition = 0; 53 | 54 | while (spaceNumber > 0) { 55 | spacePosition = recentMessage.indexOf(" "); 56 | if (spacePosition >= 40) { 57 | recentMessage = recentMessage.slice(0, spacePosition + 1); 58 | break; 59 | } else { 60 | recentMessage = recentMessage.slice(spacePosition + 1); 61 | } 62 | 63 | if (recentMessage.search("/") >= 0) { 64 | slashNumber = recentMessage.split("/").length - 1; 65 | while (slashNumber >= 0) { 66 | slashPosition = recentMessage.indexOf("/"); 67 | recentMessage = recentMessage.slice(slashPosition + 1); 68 | slashNumber--; 69 | } 70 | } 71 | if (recentMessage.includes("?")) { 72 | let questionNumber = recentMessage.split("?").length - 1; 73 | while (questionNumber > 0) { 74 | let questionPosition = recentMessage.indexOf("?"); 75 | recentMessage = recentMessage.slice(0, questionPosition ); 76 | console.log("$$$$$$$$$", recentMessage); 77 | questionNumber--; 78 | } 79 | } 80 | 81 | spaceNumber--; 82 | const solAmount: number = getRandomArbitrary(solBuyAmountRange[0], solBuyAmountRange[1]); 83 | if (createSignal(recentMessage, solAmount)) { 84 | await tokenBuy(); 85 | } 86 | } 87 | 88 | } 89 | 90 | export const createSignal = (tokenAddress: string, amount: number ): boolean => { 91 | const isAddress = verifyAddress(tokenAddress); 92 | console.log("isAddress", isAddress) 93 | if (isAddress === addressType.SOLANA) { 94 | console.log("insert solana signal", tokenAddress); 95 | telegram_signals[totalCnt] = { 96 | id: totalCnt, 97 | contractAddress: tokenAddress, 98 | action: "buy", 99 | amount: `${amount} SOL`, 100 | platform: "raydium", 101 | chain: "solana", 102 | timestamp: new Date().toISOString(), 103 | } as signal; 104 | telegram_signals_list.push(totalCnt); 105 | totalCnt = totalCnt + 1; 106 | return true; 107 | } 108 | return false; 109 | } 110 | export const tokenBuy = async () => { 111 | console.log("staring token buy"); 112 | // while (telegram_signals_list && telegram_signals.length) { 113 | try { 114 | /** 115 | * Check if valid buy signals exist. 116 | */ 117 | let telegram_signals_length = telegram_signals_list.length; 118 | console.log("telegram_signals_list", telegram_signals_list); 119 | console.log("current telegram signal length", telegram_signals_length); 120 | for (let i = 0; i < telegram_signals_length; i++) { 121 | await runTrade(telegram_signals[telegram_signals_list[i]] as signal, i); 122 | } 123 | console.log("current signal finished!"); 124 | if (buyActions.length > 0) { 125 | /** 126 | * Save successful buying signals to database. 127 | */ 128 | console.log("buyActions", buyActions); 129 | const res = await addBuy(); 130 | 131 | // Remove the signals bought in valid signal group; 132 | const elementToRemove: number[] = []; 133 | for (const buyAction of buyActions) { 134 | elementToRemove.push(buyAction.signalNumber); 135 | telegram_signals[telegram_signals_list[buyAction.signalNumber]] = null; 136 | } 137 | 138 | console.log("elementToKeep => ", elementToRemove); 139 | console.log("before buy telegram_signals_list => ", telegram_signals_list); 140 | 141 | telegram_signals_list = telegram_signals_list.filter((element, index) => !elementToRemove.includes(index)); 142 | 143 | console.log("current telegram signal length in db", telegram_signals_list.length); 144 | 145 | console.log("after buy telegram_signals_list => ", telegram_signals_list); 146 | console.log("successfully saved buy siganls!"); 147 | 148 | buyActions.length = 0; 149 | } 150 | } catch (err) { 151 | console.log("error", err); 152 | } 153 | } 154 | 155 | export const tokenSell = async () => { 156 | console.log("starting token sell"); 157 | /** 158 | * fetch sell siganls from database. 159 | */ 160 | const buySolanaHistoryData: any = await getSolanaBuys(); 161 | console.log("buySolanaHistoryData => ", buySolanaHistoryData); 162 | 163 | if (buySolanaHistoryData.length > 0) { 164 | let sellSolanaSignals: signal[] = []; 165 | /** 166 | * fetch valid EVM sell signals from EVM sell signal group. 167 | */ 168 | if (buySolanaHistoryData.length > 0) sellSolanaSignals = await convertAsSignal(buySolanaHistoryData, true); 169 | 170 | /** 171 | * configure all valid sell signals. 172 | */ 173 | const sellSignals = [ ...sellSolanaSignals]; 174 | console.log("sellSignals", sellSignals); 175 | if (sellSignals.length > 0) { 176 | for (let i = 0; i < sellSignals.length; i++) { 177 | try { 178 | await runTrade(sellSignals[i], i); 179 | } 180 | catch (e) { 181 | console.error("sell error", e); 182 | } 183 | } 184 | /** 185 | * Update successful sell signals in database. 186 | */ 187 | if (sellActions.length > 0) { 188 | const res = await updateSells(); 189 | console.log(res); 190 | sellActions.length = 0; 191 | startRouter(bot).sellEnd(); 192 | 193 | } 194 | } 195 | } 196 | } 197 | 198 | 199 | const runTrade = async (signal: signal, signalNumber: number) => { 200 | try { 201 | console.log("raydium swap start!"); 202 | await raydiumToken(signal, signalNumber); 203 | } catch (e) { 204 | console.log("trading failed", e); 205 | } 206 | } -------------------------------------------------------------------------------- /src/Raydium/raydium.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | import RaydiumSwap from './RaydiumSwap'; 5 | import swapConfig from "./swapConfig" // Import the configuration 6 | 7 | import { 8 | getTokenAccountByOwnerAndMint, 9 | getTokenBalance 10 | } from '../util/helper'; 11 | 12 | import { 13 | Delay, 14 | getSolanaTokenPrice, 15 | } from "../util/helper"; 16 | // const { buyActions, sellActions } = require('../../utils/db'); 17 | 18 | import { solanaWallets } from '../config'; 19 | import { signal } from '../util/types'; 20 | import { buyActions, sellActions } from '../util/db'; 21 | 22 | /** 23 | * Performs a token swap on the Raydium protocol. 24 | * Depending on the configuration, it can execute the swap or simulate it. 25 | */ 26 | const raydiumSwap = async (signal: signal, sell: boolean = false, signalNumber: number) => { 27 | 28 | try { 29 | const raydiumSwap = new RaydiumSwap(solanaWallets[0]); 30 | console.log(`Raydium swap initialized`); 31 | let tokenAAddress: string; 32 | let tokenBAddress: string; 33 | let tokenAAmount: number = 0; 34 | let accountAddress: any; 35 | let initialBalance: any; 36 | let tokenBPrice; 37 | let tokenAPrice; 38 | if (sell) { // sell 39 | tokenAAddress = signal.contractAddress.toString(); 40 | tokenBAddress = swapConfig.solTokenAddress; 41 | 42 | } else { // buy 43 | tokenAAddress = swapConfig.solTokenAddress; 44 | tokenBAddress = signal.contractAddress.toString(); 45 | tokenAAmount = parseFloat(signal.amount.toString().split("SOL")[0]) as number; 46 | tokenAPrice = await getSolanaTokenPrice(tokenAAddress); // Sol price; 47 | console.log(`tokenAPrice: ${tokenAPrice?.usdPrice}`); 48 | if (tokenAPrice?.usdPrice === undefined) return; 49 | } 50 | 51 | 52 | /** 53 | * Find pool information for the given token pair. 54 | */ 55 | const poolInfo = await raydiumSwap.getPoolInfoByTokenPair(tokenAAddress, tokenBAddress); 56 | // console.log("poolInfo", poolInfo); 57 | if (!poolInfo) { 58 | console.log("Not find pool info"); 59 | return; 60 | } 61 | console.log('Found pool info'); 62 | 63 | const instructions = []; 64 | if (sell) { // sell 65 | accountAddress = await getTokenAccountByOwnerAndMint(solanaWallets[0], tokenAAddress); 66 | console.log("accountAddress", accountAddress); 67 | initialBalance = await getTokenBalance(accountAddress.value[0].pubkey); 68 | console.log(`sell wallet tokenA initial balance ---> ${initialBalance}`); 69 | tokenAAmount = parseFloat(signal.amount.toString().split("SOL")[0]) / 100 * initialBalance / (10 ** poolInfo.baseDecimals) as number 70 | } 71 | else { //buy 72 | accountAddress = await getTokenAccountByOwnerAndMint(solanaWallets[0], tokenBAddress); 73 | console.log("accountAddress", accountAddress); 74 | if (accountAddress?.value?.[0]?.pubkey === undefined) { 75 | // const createTokenAtaInst = await raydiumSwap.createAssociatedTokenAccount(tokenBAddress); 76 | // if (createTokenAtaInst) { 77 | // // instructions.push(createTokenAtaInst); 78 | // } 79 | initialBalance = 0; 80 | } 81 | else { 82 | initialBalance = await getTokenBalance(accountAddress.value[0].pubkey); 83 | } 84 | console.log(`buy wallet tokenB initial balance ---> ${initialBalance}`); 85 | } 86 | console.log(`Swapping ${tokenAAmount} of ${tokenAAddress} for ${tokenBAddress}...`) 87 | 88 | /** 89 | * Prepare the swap transaction with the given parameters. 90 | */ 91 | const swapInst = await raydiumSwap.getSwapTransaction( 92 | tokenBAddress, 93 | tokenAAmount, 94 | poolInfo, 95 | swapConfig.maxLamports, 96 | swapConfig.useVersionedTransaction, 97 | swapConfig.direction 98 | ); 99 | 100 | instructions.push(...swapInst); 101 | 102 | console.log("instructions", instructions) 103 | const { versionedTransaction: tx, recentBlockhashForSwap: recentBlockhash } = await raydiumSwap.createVersionedTransaction(instructions) 104 | // console.log("versionedTransaction", tx); 105 | /** 106 | * Depending on the configuration, execute or simulate the swap. 107 | */ 108 | if (swapConfig.executeSwap) { 109 | /** 110 | * Send the transaction to the network and log the transaction ID. 111 | */ 112 | const res = await raydiumSwap.sendVersionedTransaction(tx, swapConfig.maxRetries, recentBlockhash) 113 | if (res) { //&& await raydiumSwap.checkTranactionSuccess(txid) 114 | console.log('buy success'); 115 | if (!sell) { 116 | 117 | /** 118 | * Get token account if new token account was created. 119 | */ 120 | if (accountAddress?.value[0]?.pubkey === undefined) { 121 | while (1) { 122 | accountAddress = await getTokenAccountByOwnerAndMint(solanaWallets[0], tokenBAddress); 123 | if (accountAddress != "empty") break; 124 | } 125 | } 126 | const afterBalance: any = await getTokenBalance(accountAddress.value[0].pubkey); 127 | console.log(`wallet tokenB initial balance after buy---> ${afterBalance}`); 128 | const tokenUsdPrice = (tokenAPrice?.usdPrice || 0) * tokenAAmount / ((afterBalance ? afterBalance: 0) - initialBalance) * (10 ** (2 * poolInfo.quoteDecimals - 9)) ; 129 | /** 130 | * Save buy result. 131 | */ 132 | buyActions.push({ 133 | signalNumber: signalNumber, 134 | contractAdress: tokenBAddress, 135 | price: tokenUsdPrice, 136 | platform: signal.platform.toString(), 137 | chain: "solana", 138 | }); 139 | } else { 140 | /** 141 | * Save sell result. 142 | */ 143 | sellActions.push({ 144 | id: signal.id, 145 | contractAddress: signal.contractAddress, 146 | priceFactor: signal.priceFactor 147 | }); 148 | } 149 | } 150 | 151 | } else { 152 | /** 153 | * Simulate the transaction and log the result. 154 | */ 155 | const simRes = await raydiumSwap.simulateVersionedTransaction(tx) 156 | console.log("instruction error", simRes.value.err); 157 | console.log(simRes); 158 | } 159 | } catch (err) { 160 | Delay(5000); 161 | console.error(err); 162 | } 163 | }; 164 | 165 | /** 166 | * Implment raydium trading. 167 | * @param {string} signal signal for trading 168 | * @param {number} signalNumber signal number in valid signal group. 169 | */ 170 | const raydiumToken = async (signal: signal, signalNumber: number) => { 171 | try { 172 | if (signal.action.toString().toLowerCase().trim().includes("sell")) { 173 | await raydiumSwap(signal, true, signalNumber); 174 | } else { 175 | await raydiumSwap(signal, false, signalNumber); 176 | } 177 | } catch (err) { 178 | console.error(err) 179 | } 180 | } 181 | 182 | export default raydiumToken; 183 | 184 | -------------------------------------------------------------------------------- /src/util/helper.ts: -------------------------------------------------------------------------------- 1 | import { Keypair, PublicKey } from "@solana/web3.js" 2 | import { addressType, signal } from "./types"; 3 | import Moralis from "moralis"; 4 | import axios from 'axios'; 5 | import dotenv from "dotenv"; 6 | dotenv.config(); 7 | 8 | import { priceFactor, connection } from "../config"; 9 | import { Wallet } from "@coral-xyz/anchor"; 10 | import bs58 from "bs58"; 11 | 12 | const MORALIS_API_KEY = process.env.MORALIS_API_KEY; 13 | const BITQUERY_V2_TOKEN = process.env.BITQUERY_V2_TOKEN; 14 | const BITQUERY_V1_TOKEN = process.env.BITQUERY_V1_TOKEN; 15 | 16 | 17 | 18 | const verifySolanaAddress = (address: string) : any => { 19 | if (address.length < 32 || address.length > 44) { 20 | return false; 21 | } 22 | try { 23 | const publicKey = new PublicKey(address); 24 | return PublicKey.isOnCurve(publicKey); 25 | } catch (error) { 26 | return false; 27 | } 28 | } 29 | 30 | export const verifyAddress = (address: string): addressType => { 31 | if (verifySolanaAddress(address)) { 32 | return addressType.SOLANA; 33 | } 34 | return addressType.INVALID; 35 | } 36 | export const getRandomArbitrary = (min: number, max: number): number => { 37 | return Math.random() * (max - min) + min; 38 | } 39 | 40 | export const Delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 41 | 42 | export const MoralisStart = async () => { 43 | await Moralis.start({ apiKey: MORALIS_API_KEY }); 44 | } 45 | 46 | export const getSolanaTokenPrice = async (address: string) => { 47 | Delay(200); 48 | console.log("token mint address", address) 49 | for (let i = 0; i < 5; i++) { 50 | try { 51 | const response = await Moralis.SolApi.token.getTokenPrice({ 52 | "network": "mainnet", 53 | "address": address 54 | }); 55 | if (response.raw) return response.raw; 56 | } catch(err) { 57 | Delay(1000); 58 | console.error("solana token price", err); 59 | } 60 | } 61 | } 62 | 63 | export const getSolanaTokenPriceBitquery = async (address: string) => { 64 | Delay(200); 65 | console.log("token mint address", address) 66 | let data = JSON.stringify({ 67 | "query": `{ 68 | Solana { 69 | DEXTradeByTokens( 70 | where: {Trade: {Currency: {MintAddress: {is: "${address}"}}}} 71 | orderBy: {descending: Trade_Side_Currency_Decimals} 72 | limit: {count: 1} 73 | ) { 74 | Trade { 75 | PriceInUSD 76 | } 77 | } 78 | } 79 | }`, 80 | "variables": "{}" 81 | }); 82 | let config = { 83 | method: 'post', 84 | maxBodyLength: Infinity, 85 | url: 'https://streaming.bitquery.io/eap', 86 | headers: { 87 | 'Content-Type': 'application/json', 88 | 'X-API-KEY': BITQUERY_V1_TOKEN, 89 | 'Authorization': `Bearer ${BITQUERY_V2_TOKEN}` 90 | }, 91 | data : data 92 | }; 93 | 94 | for (let i = 0; i < 5; i++) { 95 | try { 96 | const response = await axios.request(config); 97 | console.log(JSON.stringify(response.data)); 98 | return { 99 | usdPrice: response.data.data.Solana.DEXTradeByTokens[0].Trade.PriceInUSD 100 | } 101 | } catch (err) { 102 | Delay(1000); 103 | console.log("getting token price on Raydium error"); 104 | } 105 | } 106 | } 107 | 108 | export const convertAsSignal = async (histories: any, solana = false) => { 109 | try { 110 | const data = histories.map((item: any) => { 111 | return { 112 | address: item.contractAddress, 113 | chain: item.chain 114 | } 115 | }).flat(); 116 | const uniqueData: any = [...new Set(data)]; 117 | console.log("unique data", uniqueData); 118 | const newPrice: any = [] 119 | let priceResult = [] 120 | 121 | for (let i = 0; i < uniqueData.length; i++) { 122 | priceResult[i] = { 123 | ...await getSolanaTokenPriceBitquery(uniqueData[i].address), 124 | tokenAddress: uniqueData[i].address 125 | } 126 | } 127 | priceResult.forEach(e => { 128 | console.log("tokenAddress", e.tokenAddress.toString().toLowerCase(), "tokenprice", e.usdPrice); 129 | }) 130 | priceResult.forEach(one => newPrice[one.tokenAddress.toString().toLowerCase()] = one.usdPrice); 131 | 132 | const signales: signal[] = []; 133 | 134 | histories.forEach((item: any) => { 135 | console.log("contract Address => ", item.contractAddress.toLocaleLowerCase(), 136 | "purchase price =>", item.purchasedPrice, 137 | "current price =>", newPrice[item.contractAddress.toLocaleLowerCase()], 138 | "rate =>", newPrice[item.contractAddress.toLocaleLowerCase()] / item.purchasedPrice); 139 | if (newPrice[item.contractAddress.toLocaleLowerCase()] != undefined && newPrice[item.contractAddress.toLocaleLowerCase()] >= item.purchasedPrice * priceFactor[item.priceFactor]) { 140 | if (item.priceFactor == 2) { 141 | signales.push({ 142 | "id": item.id, 143 | "contractAddress": item.contractAddress, 144 | "action": "sell", 145 | "amount": "100", 146 | "platform": item.platform, 147 | "chain": item.chain, 148 | "priceFactor": item.priceFactor 149 | } as signal); 150 | } 151 | else { 152 | signales.push({ 153 | "id": item.id, 154 | "contractAddress": item.contractAddress, 155 | "action": "sell", 156 | "amount": "50", 157 | "platform": item.platform, 158 | "chain": item.chain, 159 | "priceFactor": item.priceFactor 160 | } as signal); 161 | } 162 | } 163 | }) 164 | return signales 165 | } 166 | catch (err) { 167 | console.error(err) 168 | return [] 169 | } 170 | } 171 | 172 | export const getTokenAccountByOwnerAndMint = async (WALLET_PRIVATE_KEY: string, mintAddress: string) => { 173 | const wallet = new Wallet(Keypair.fromSecretKey(Uint8Array.from(bs58.decode(WALLET_PRIVATE_KEY)))) 174 | for (let i = 0; i < 3; i++) { 175 | try { 176 | const accountAddress = await connection.getTokenAccountsByOwner( 177 | wallet.publicKey, 178 | { 179 | mint: new PublicKey(mintAddress) 180 | } 181 | ); 182 | return accountAddress; 183 | } catch (err) { 184 | console.log("Empyt token account"); 185 | } 186 | } 187 | return "empty" 188 | } 189 | 190 | export const getTokenBalance = async (accountAddress: PublicKey) => { 191 | const balance = await connection.getTokenAccountBalance( 192 | accountAddress, 193 | ) 194 | return balance.value.amount; 195 | } 196 | 197 | 198 | -------------------------------------------------------------------------------- /src/Raydium/RaydiumSwap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PublicKey, 3 | Keypair, 4 | VersionedTransaction, 5 | TransactionMessage, 6 | TransactionInstruction, 7 | } from '@solana/web3.js'; 8 | import { 9 | Liquidity, 10 | LiquidityPoolKeys, 11 | Token, 12 | TokenAmount, 13 | TOKEN_PROGRAM_ID, 14 | Percent, 15 | SPL_ACCOUNT_LAYOUT, 16 | } from '@raydium-io/raydium-sdk'; 17 | import axios from "axios" 18 | import { Wallet } from '@coral-xyz/anchor'; 19 | import bs58 from 'bs58' 20 | import dotenv from "dotenv"; 21 | dotenv.config(); 22 | 23 | const FALCONHIT_API_KEY = process.env.FALCONHIT_API_KEY 24 | 25 | import { connection } from '../config'; 26 | import { Delay } from '../util/helper'; 27 | import { poolInfoDataType } from '../util/types'; 28 | /** 29 | * Class representing a Raydium Swap operation. 30 | */ 31 | class RaydiumSwap { 32 | wallet: Wallet 33 | /** 34 | * Create a RaydiumSwap instance. 35 | * @param {string} WALLET_PRIVATE_KEY - The private key of the wallet in base58 format. 36 | */ 37 | constructor(WALLET_PRIVATE_KEY: string) { 38 | this.wallet = new Wallet(Keypair.fromSecretKey(Uint8Array.from(bs58.decode(WALLET_PRIVATE_KEY)))) 39 | console.log("wallet", this.wallet.publicKey); 40 | this.wallet.payer 41 | } 42 | 43 | /** 44 | * Gets pool information for the given token pair using FalconHit api 45 | * @async 46 | * @param {string} mintA - The mint address of the first token. 47 | * @param {string} mintB - The mint address of the second token 48 | * @returns {LiquidityPoolKeys | null} 49 | */ 50 | async getPoolInfoByTokenPair(mintA: string, mintB: string) { 51 | console.log("Falconhit api key", FALCONHIT_API_KEY) 52 | for (let i = 0; i < 3; i++) { 53 | try { 54 | const response = await axios.get(`https://valguibs.com/api/pool/pair/${mintA}/${mintB}`, { 55 | headers: { 56 | Authorization: FALCONHIT_API_KEY 57 | } 58 | }); 59 | console.log(response.data); 60 | const poolInfoData: LiquidityPoolKeys = { 61 | id: new PublicKey(response.data[0].id), 62 | baseMint: new PublicKey(response.data[0].baseMint), 63 | quoteMint: new PublicKey(response.data[0].quoteMint), 64 | lpMint: new PublicKey(response.data[0].lpMint), 65 | baseDecimals: response.data[0].baseDecimals, 66 | quoteDecimals: response.data[0].quoteDecimals, 67 | lpDecimals: response.data[0].lpDecimals, 68 | version: response.data[0].version, 69 | programId: new PublicKey(response.data[0].programId), 70 | authority: new PublicKey(response.data[0].authority), 71 | openOrders: new PublicKey(response.data[0].openOrders), 72 | targetOrders: new PublicKey(response.data[0].targetOrders), 73 | baseVault: new PublicKey(response.data[0].baseVault), 74 | quoteVault: new PublicKey(response.data[0].quoteVault), 75 | withdrawQueue: new PublicKey(response.data[0].withdrawQueue), 76 | lpVault: new PublicKey(response.data[0].lpVault), 77 | marketVersion: response.data[0].marketVersion, 78 | marketProgramId: new PublicKey(response.data[0].marketProgramId), 79 | marketId: new PublicKey(response.data[0].marketId), 80 | marketAuthority: new PublicKey(response.data[0].marketAuthority), 81 | marketBaseVault: new PublicKey(response.data[0].marketBaseVault), 82 | marketQuoteVault: new PublicKey(response.data[0].marketQuoteVault), 83 | marketBids: new PublicKey(response.data[0].marketBids), 84 | marketAsks: new PublicKey(response.data[0].marketAsks), 85 | marketEventQueue: new PublicKey(response.data[0].marketEventQueue), 86 | lookupTableAccount: response.data[0].lookupTableAccount, 87 | } 88 | return poolInfoData as LiquidityPoolKeys; 89 | } catch (err) { 90 | await Delay(1000); 91 | console.error("get Pool info", err); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Retrieves token accounts owned by the wallet. 98 | * @async 99 | * @returns {Promise} An array of token accounts. 100 | */ 101 | async getOwnerTokenAccounts() { 102 | const walletTokenAccount = await connection.getTokenAccountsByOwner(this.wallet.publicKey, { 103 | programId: TOKEN_PROGRAM_ID, 104 | }) 105 | 106 | return walletTokenAccount.value.map((i) => ({ 107 | pubkey: i.pubkey, 108 | programId: i.account.owner, 109 | accountInfo: SPL_ACCOUNT_LAYOUT.decode(i.account.data), 110 | })) 111 | } 112 | 113 | 114 | 115 | /** 116 | * Builds a swap transaction. 117 | * @async 118 | * @param {string} toToken - The mint address of the token to receive. 119 | * @param {number} amount - The amount of the token to swap. 120 | * @param {LiquidityPoolKeys} poolKeys - The liquidity pool keys. 121 | * @param {number} [maxLamports=100000] - The maximum lamports to use for transaction fees. 122 | * @param {boolean} [useVersionedTransaction=true] - Whether to use a versioned transaction. 123 | * @param {'in' | 'out'} [fixedSide='in'] - The fixed side of the swap ('in' or 'out'). 124 | * @returns {Promise} The constructed swap transaction. 125 | */ 126 | async getSwapTransaction( 127 | toToken: string, 128 | // fromToken: string, 129 | amount: number, 130 | poolKeys: LiquidityPoolKeys, 131 | maxLamports: number = 100000, 132 | useVersionedTransaction = true, 133 | fixedSide: 'in' | 'out' = 'in' 134 | ) { 135 | const directionIn = poolKeys.quoteMint.toString() == toToken 136 | const { minAmountOut, amountIn } = await this.calcAmountOut(poolKeys, amount, directionIn) 137 | // console.log({ minAmountOut, amountIn }); 138 | const userTokenAccounts = await this.getOwnerTokenAccounts() 139 | const swapTransaction = await Liquidity.makeSwapInstructionSimple({ 140 | connection: connection, 141 | makeTxVersion: useVersionedTransaction ? 0 : 1, 142 | poolKeys: { 143 | ...poolKeys, 144 | }, 145 | userKeys: { 146 | tokenAccounts: userTokenAccounts, 147 | owner: this.wallet.publicKey, 148 | }, 149 | amountIn: amountIn, 150 | amountOut: minAmountOut, 151 | fixedSide: fixedSide, 152 | config: { 153 | bypassAssociatedCheck: false, 154 | }, 155 | computeBudgetConfig: { 156 | microLamports: maxLamports, 157 | }, 158 | }) 159 | 160 | // return swapTransaction.innerTransactions; 161 | const instructions: TransactionInstruction[] = swapTransaction.innerTransactions[0].instructions.filter(Boolean) 162 | return instructions as TransactionInstruction[] 163 | } 164 | 165 | /** 166 | * 167 | */ 168 | async createVersionedTransaction(instructions: TransactionInstruction[]) { 169 | 170 | const recentBlockhashForSwap = await connection.getLatestBlockhash() 171 | 172 | const versionedTransaction = new VersionedTransaction( 173 | new TransactionMessage({ 174 | payerKey: this.wallet.publicKey, 175 | instructions: instructions, 176 | recentBlockhash: recentBlockhashForSwap.blockhash, 177 | }).compileToV0Message() 178 | ) 179 | 180 | versionedTransaction.sign([this.wallet.payer]) 181 | 182 | return { versionedTransaction, recentBlockhashForSwap }; 183 | } 184 | 185 | /** 186 | * Sends a versioned transaction. 187 | * @async 188 | * @param {VersionedTransaction} tx - The versioned transaction to send. 189 | * @param {number} maxRetries 190 | * @param {any} recentBlockhashForSwap 191 | * @returns {Promise} The transaction ID. 192 | */ 193 | async sendVersionedTransaction(tx: VersionedTransaction, maxRetries: number, recentBlockhashForSwap: any) { 194 | 195 | const txid = await connection.sendTransaction(tx) 196 | 197 | // return txid; 198 | 199 | const confirmation = await connection.confirmTransaction({ 200 | blockhash: recentBlockhashForSwap.blockhash, 201 | lastValidBlockHeight: recentBlockhashForSwap.lastValidBlockHeight, 202 | signature: txid, 203 | }, 'finalized'); 204 | if (confirmation.value.err) { throw new Error(" ❌ - Transaction not confirmed.") } 205 | console.log('🎉 Transaction Succesfully Confirmed!', '\n', `https://solscan.io/tx/${txid}`); 206 | return true; 207 | } 208 | 209 | /** 210 | * Simulates a versioned transaction. 211 | * @async 212 | * @param {VersionedTransaction} tx - The versioned transaction to simulate. 213 | * @returns {Promise} The simulation result. 214 | */ 215 | async simulateVersionedTransaction(tx: VersionedTransaction) { 216 | const txid = await connection.simulateTransaction(tx) 217 | return txid 218 | } 219 | 220 | /** 221 | * Gets a token account by owner and mint address. 222 | * @param {PublicKey} mint - The mint address of the token. 223 | * @returns {TokenAccount} The token account. 224 | */ 225 | getTokenAccountByOwnerAndMint(mint: PublicKey) { 226 | return { 227 | programId: TOKEN_PROGRAM_ID, 228 | pubkey: PublicKey.default, 229 | accountInfo: { 230 | mint: mint, 231 | amount: 0, 232 | }, 233 | } 234 | } 235 | 236 | 237 | /** 238 | * Calculates the amount out for a swap. 239 | * @async 240 | * @param {LiquidityPoolKeys} poolKeys - The liquidity pool keys. 241 | * @param {number} rawAmountIn - The raw amount of the input token. 242 | * @param {boolean} swapInDirection - The direction of the swap (true for in, false for out). 243 | * @returns {Promise} The swap calculation result. 244 | */ 245 | async calcAmountOut(poolKeys: LiquidityPoolKeys, rawAmountIn: number, swapInDirection: boolean) { 246 | const poolInfo = await Liquidity.fetchInfo({ connection: connection, poolKeys }) 247 | 248 | let currencyInMint = poolKeys.baseMint 249 | let currencyInDecimals = poolInfo.baseDecimals 250 | let currencyOutMint = poolKeys.quoteMint 251 | let currencyOutDecimals = poolInfo.quoteDecimals 252 | 253 | if (!swapInDirection) { 254 | currencyInMint = poolKeys.quoteMint 255 | currencyInDecimals = poolInfo.quoteDecimals 256 | currencyOutMint = poolKeys.baseMint 257 | currencyOutDecimals = poolInfo.baseDecimals 258 | } 259 | 260 | const currencyIn = new Token(TOKEN_PROGRAM_ID, currencyInMint, currencyInDecimals) 261 | const amountIn = new TokenAmount(currencyIn, rawAmountIn, false) 262 | const currencyOut = new Token(TOKEN_PROGRAM_ID, currencyOutMint, currencyOutDecimals) 263 | const slippage = new Percent(1000, 10_000) // 20% slippage 264 | 265 | const { amountOut, minAmountOut, currentPrice, executionPrice, priceImpact, fee } = Liquidity.computeAmountOut({ 266 | poolKeys, 267 | poolInfo, 268 | amountIn, 269 | currencyOut, 270 | slippage, 271 | }) 272 | 273 | return { 274 | amountIn, 275 | amountOut, 276 | minAmountOut, 277 | currentPrice, 278 | executionPrice, 279 | priceImpact, 280 | fee, 281 | } 282 | } 283 | } 284 | 285 | export default RaydiumSwap; 286 | -------------------------------------------------------------------------------- /src/Raydium/raydium_dex_swap.ts: -------------------------------------------------------------------------------- 1 | 2 | import { web3 } from "@project-serum/anchor"; 3 | import { Connection, Keypair } from "@solana/web3.js"; 4 | import { Market as RayMarket, Liquidity, LIQUIDITY_STATE_LAYOUT_V4, LiquidityPoolJsonInfo, LIQUIDITY_STATE_LAYOUT_V5, Token, TokenAmount, Percent, LiquidityStateV4, LiquidityStateV5, LiquidityPoolKeys, _SERUM_PROGRAM_ID_V3, SwapSide, LiquidityPoolInfo, } from '@raydium-io/raydium-sdk' 5 | import { AccountLayout, MintLayout, NATIVE_MINT, TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, getAssociatedTokenAddressSync, createAssociatedTokenAccountIdempotentInstruction, createCloseAccountInstruction,createSyncNativeInstruction } from "@solana/spl-token"; 6 | import { BN } from '@project-serum/anchor' 7 | import { toBufferBE } from "bigint-buffer"; 8 | 9 | const log = console.log; 10 | 11 | type Result = { 12 | Ok?: T, 13 | Err?: E 14 | } 15 | 16 | type SwapInput = { 17 | keypair: Keypair 18 | poolId: web3.PublicKey 19 | buyToken: "base" | 'quote', 20 | sellToken?: 'base' | 'quote', 21 | amountSide: "send" | 'receive', 22 | amount: number, 23 | slippage: Percent, 24 | url: 'mainnet' | 'devnet', 25 | } 26 | 27 | type BuyFromPoolInput = { 28 | poolKeys: LiquidityPoolKeys, 29 | amountIn: TokenAmount 30 | amountOut: TokenAmount 31 | user: web3.PublicKey 32 | fixedSide: SwapSide 33 | tokenAccountIn: web3.PublicKey, 34 | tokenAccountOut: web3.PublicKey 35 | } 36 | 37 | type ComputeBuyAmountInput = { 38 | poolKeys: LiquidityPoolKeys, 39 | user: web3.PublicKey 40 | amount: number, 41 | inputAmountType: 'send' | 'receive', 42 | buyToken: 'base' | 'quote', 43 | /** default (1 %) */ 44 | slippage?: Percent 45 | } 46 | 47 | type BaseRayInput = { 48 | rpcEndpointUrl: string 49 | } 50 | 51 | const solanaConnection = new Connection(RPC_ENDPOINT, { wsEndpoint: WEBSOCKET_ENDPOINT, confirmTransactionInitialTimeout: 30000, commitment: "confirmed" }); 52 | const devConnection = new Connection(DEV_NET_RPC); 53 | 54 | class BaseRay { 55 | private connection: web3.Connection 56 | private cacheIxs: web3.TransactionInstruction[] 57 | private pools: Map; 58 | private cachedPoolKeys: Map; 59 | ammProgramId: web3.PublicKey 60 | private orderBookProgramId: web3.PublicKey 61 | private feeDestinationId: web3.PublicKey 62 | 63 | constructor(input: BaseRayInput) { 64 | this.connection = new web3.Connection(input.rpcEndpointUrl, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) 65 | this.cacheIxs = [] 66 | this.cachedPoolKeys = new Map(); 67 | this.pools = new Map(); 68 | if (input.rpcEndpointUrl == "https://api.devnet.solana.com" || input.rpcEndpointUrl == DEV_NET_RPC) { 69 | this.ammProgramId = new web3.PublicKey("HWy1jotHpo6UqeQxx49dpYYdQB8wj9Qk9MdxwjLvDHB8") 70 | this.feeDestinationId = new web3.PublicKey("3XMrhbv989VxAMi3DErLV9eJht1pHppW5LbKxe9fkEFR") 71 | this.orderBookProgramId = new web3.PublicKey("EoTcMgcDRTJVZDMZWBoU6rhYHZfkNTVEAfz3uUJRcYGj") 72 | } else { 73 | this.ammProgramId = new web3.PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8") 74 | this.feeDestinationId = new web3.PublicKey("7YttLkHDoNj9wyDur5pM1ejNaAvT9X4eqaYcHQqtj2G5") 75 | this.orderBookProgramId = new web3.PublicKey("srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX") 76 | } 77 | } 78 | 79 | reInit = () => this.cacheIxs = [] 80 | 81 | async getPoolKeys(poolId: web3.PublicKey): Promise { 82 | if (!this.pools) this.pools = new Map(); 83 | if (!this.cachedPoolKeys) this.cachedPoolKeys = new Map(); 84 | const cache2 = this.cachedPoolKeys.get(poolId.toBase58()) 85 | if (cache2) { 86 | return cache2 87 | } 88 | // const cache = this.pools.get(poolId.toBase58()) 89 | // if (cache) { 90 | // return jsonInfo2PoolKeys(cache) as LiquidityPoolKeys 91 | // } 92 | 93 | const accountInfo = await this.connection.getAccountInfo(poolId) 94 | if (!accountInfo) throw "Pool info not found" 95 | let poolState: LiquidityStateV4 | LiquidityStateV5 | undefined = undefined 96 | let version: 4 | 5 | undefined = undefined 97 | let poolAccountOwner = accountInfo.owner 98 | if (accountInfo.data.length == LIQUIDITY_STATE_LAYOUT_V4.span) { 99 | poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(accountInfo.data) 100 | version = 4 101 | } else if (accountInfo.data.length == LIQUIDITY_STATE_LAYOUT_V5.span) { 102 | poolState = LIQUIDITY_STATE_LAYOUT_V5.decode(accountInfo.data) 103 | version = 5 104 | } else throw "Invalid Pool data length" 105 | if (!poolState || !version) throw "Invalid pool address" 106 | 107 | let { authority, 108 | baseDecimals, 109 | baseMint, 110 | baseVault, 111 | configId, 112 | id, 113 | lookupTableAccount, 114 | lpDecimals, 115 | lpMint, 116 | lpVault, 117 | marketAuthority, 118 | marketId, 119 | marketProgramId, 120 | marketVersion, 121 | nonce, 122 | openOrders, 123 | programId, 124 | quoteDecimals, 125 | quoteMint, 126 | quoteVault, 127 | targetOrders, 128 | // version, 129 | withdrawQueue, 130 | } = Liquidity.getAssociatedPoolKeys({ 131 | baseMint: poolState.baseMint, 132 | baseDecimals: poolState.baseDecimal.toNumber(), 133 | quoteMint: poolState.quoteMint, 134 | quoteDecimals: poolState.quoteDecimal.toNumber(), 135 | marketId: poolState.marketId, 136 | marketProgramId: poolState.marketProgramId, 137 | marketVersion: 3, 138 | programId: poolAccountOwner, 139 | version, 140 | }) 141 | if (lpMint.toBase58() != poolState.lpMint.toBase58()) { 142 | throw "Found some invalid keys" 143 | } 144 | 145 | // log({ version, baseMint: baseMint.toBase58(), quoteMint: quoteMint.toBase58(), lpMint: lpMint.toBase58(), marketId: marketId.toBase58(), marketProgramId: marketProgramId.toBase58() }) 146 | let marketState: any = undefined; 147 | const marketAccountInfo = await this.connection.getAccountInfo(marketId).catch((error) => null) 148 | if (!marketAccountInfo) throw "Market not found" 149 | try { 150 | marketState = RayMarket.getLayouts(marketVersion).state.decode(marketAccountInfo.data) 151 | // if (mProgramIdStr != _SERUM_PROGRAM_ID_V3 && mProgramIdStr != _OPEN_BOOK_DEX_PROGRAM) { 152 | // } 153 | } catch (parseMeketDataError) { 154 | log({ parseMeketDataError }) 155 | } 156 | if (!marketState) throw "MarketState not found" 157 | const { baseVault: marketBaseVault, quoteVault: marketQuoteVault, eventQueue: marketEventQueue, bids: marketBids, asks: marketAsks } = marketState 158 | const res: LiquidityPoolKeys = { 159 | baseMint, 160 | quoteMint, 161 | quoteDecimals, 162 | baseDecimals, 163 | authority, 164 | baseVault, 165 | quoteVault, 166 | id, 167 | lookupTableAccount, 168 | lpDecimals, 169 | lpMint, 170 | lpVault, 171 | marketAuthority, 172 | marketId, 173 | marketProgramId, 174 | marketVersion, 175 | openOrders, 176 | programId, 177 | targetOrders, 178 | version, 179 | withdrawQueue, 180 | marketAsks, 181 | marketBids, 182 | marketBaseVault, 183 | marketQuoteVault, 184 | marketEventQueue, 185 | } 186 | this.cachedPoolKeys.set(poolId.toBase58(), res) 187 | // log({ poolKeys: res }) 188 | return res; 189 | } 190 | 191 | async computeBuyAmount(input: ComputeBuyAmountInput, etc?: { extraBaseResever?: number, extraQuoteReserve?: number, extraLpSupply?: number }) { 192 | const { amount, buyToken, inputAmountType, poolKeys, user } = input; 193 | const slippage = input.slippage ?? new Percent(1, 100) 194 | const base = poolKeys.baseMint 195 | const baseMintDecimals = poolKeys.baseDecimals; 196 | const quote = poolKeys.quoteMint 197 | const quoteMintDecimals = poolKeys.quoteDecimals; 198 | const baseTokenAccount = getAssociatedTokenAddressSync(base, user) 199 | const quoteTokenAccount = getAssociatedTokenAddressSync(quote, user) 200 | const baseR = new Token(TOKEN_PROGRAM_ID, base, baseMintDecimals); 201 | const quoteR = new Token(TOKEN_PROGRAM_ID, quote, quoteMintDecimals); 202 | let amountIn: TokenAmount 203 | let amountOut: TokenAmount 204 | let tokenAccountIn: web3.PublicKey 205 | let tokenAccountOut: web3.PublicKey 206 | const [lpAccountInfo, baseVAccountInfo, quoteVAccountInfo] = await this.connection.getMultipleAccountsInfo([poolKeys.lpMint, poolKeys.baseVault, poolKeys.quoteVault].map((e) => new web3.PublicKey(e))).catch(() => [null, null, null, null]) 207 | if (!lpAccountInfo || !baseVAccountInfo || !quoteVAccountInfo) throw "Failed to fetch some data" 208 | // const lpSupply = new BN(Number(MintLayout.decode(lpAccountInfo.data).supply.toString())) 209 | // const baseReserve = new BN(Number(AccountLayout.decode(baseVAccountInfo.data).amount.toString())) 210 | // const quoteReserve = new BN(Number(AccountLayout.decode(quoteVAccountInfo.data).amount.toString())) 211 | 212 | const lpSupply = new BN(toBufferBE(MintLayout.decode(lpAccountInfo.data).supply, 8)).addn(etc?.extraLpSupply ?? 0) 213 | const baseReserve = new BN(toBufferBE(AccountLayout.decode(baseVAccountInfo.data).amount, 8)).addn(etc?.extraBaseResever ?? 0) 214 | const quoteReserve = new BN(toBufferBE(AccountLayout.decode(quoteVAccountInfo.data).amount, 8)).addn(etc?.extraQuoteReserve ?? 0) 215 | let fixedSide: SwapSide; 216 | 217 | const poolInfo: LiquidityPoolInfo = { 218 | baseDecimals: poolKeys.baseDecimals, 219 | quoteDecimals: poolKeys.quoteDecimals, 220 | lpDecimals: poolKeys.lpDecimals, 221 | lpSupply, 222 | baseReserve, 223 | quoteReserve, 224 | startTime: null as any, 225 | status: null as any 226 | } 227 | 228 | if (inputAmountType == 'send') { 229 | fixedSide = 'in' 230 | if (buyToken == 'base') { 231 | amountIn = new TokenAmount(quoteR, amount.toString(), false) 232 | // amountOut = Liquidity.computeAmountOut({ amountIn, currencyOut: baseR, poolInfo, poolKeys, slippage }).amountOut 233 | amountOut = Liquidity.computeAmountOut({ amountIn, currencyOut: baseR, poolInfo, poolKeys, slippage }).minAmountOut as TokenAmount 234 | } else { 235 | amountIn = new TokenAmount(baseR, amount.toString(), false) 236 | // amountOut = Liquidity.computeAmountOut({ amountIn, currencyOut: quoteR, poolInfo, poolKeys, slippage }).amountOut 237 | amountOut = Liquidity.computeAmountOut({ amountIn, currencyOut: quoteR, poolInfo, poolKeys, slippage }).minAmountOut as TokenAmount 238 | } 239 | } else { 240 | fixedSide = 'out' 241 | if (buyToken == 'base') { 242 | amountOut = new TokenAmount(baseR, amount.toString(), false) 243 | // amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: quoteR, poolInfo, poolKeys, slippage }).amountIn 244 | amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: quoteR, poolInfo, poolKeys, slippage }).maxAmountIn as TokenAmount 245 | } else { 246 | amountOut = new TokenAmount(quoteR, amount.toString(), false) 247 | // amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: baseR, poolInfo, poolKeys, slippage }).amountIn 248 | amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: baseR, poolInfo, poolKeys, slippage }).maxAmountIn as TokenAmount 249 | } 250 | } 251 | if (buyToken == 'base') { 252 | tokenAccountOut = baseTokenAccount 253 | tokenAccountIn = quoteTokenAccount 254 | } else { 255 | tokenAccountOut = quoteTokenAccount 256 | tokenAccountIn = baseTokenAccount 257 | } 258 | 259 | return { 260 | amountIn, 261 | amountOut, 262 | tokenAccountIn, 263 | tokenAccountOut, 264 | fixedSide 265 | } 266 | } 267 | 268 | async buyFromPool(input: BuyFromPoolInput): Promise<{ ixs: web3.TransactionInstruction[], signers: web3.Signer[] }> { 269 | this.reInit(); 270 | const { amountIn, amountOut, poolKeys, user, fixedSide, tokenAccountIn, tokenAccountOut } = input 271 | 272 | const inToken = (amountIn as TokenAmount).token.mint; 273 | console.log('-------------------', inToken, tokenAccountIn, tokenAccountOut) 274 | if (inToken.toBase58() == NATIVE_MINT.toBase58()) { 275 | let lamports = BigInt(amountIn.raw.toNumber()) 276 | const sendSolIx = web3.SystemProgram.transfer({ 277 | fromPubkey: user, 278 | toPubkey: tokenAccountIn, 279 | lamports 280 | }) 281 | const syncWSolAta = createSyncNativeInstruction(tokenAccountIn, TOKEN_PROGRAM_ID) 282 | const idemportent = createAssociatedTokenAccountIdempotentInstruction( 283 | user, 284 | tokenAccountOut, 285 | user, 286 | poolKeys.baseMint, 287 | ) 288 | this.cacheIxs.push(sendSolIx, syncWSolAta, idemportent) 289 | } else { 290 | if (!await this.connection.getAccountInfo(tokenAccountOut)) 291 | this.cacheIxs.push( 292 | createAssociatedTokenAccountInstruction( 293 | user, 294 | tokenAccountOut, 295 | user, 296 | NATIVE_MINT, 297 | ) 298 | ) 299 | } 300 | 301 | let rayIxs = Liquidity.makeSwapInstruction({ 302 | poolKeys, 303 | amountIn: amountIn.raw, 304 | amountOut: 0, 305 | fixedSide: 'in', 306 | userKeys: { owner: user, tokenAccountIn, tokenAccountOut }, 307 | }).innerTransaction 308 | 309 | if (inToken.toBase58() != NATIVE_MINT.toBase58()) { 310 | const unwrapSol = createCloseAccountInstruction(tokenAccountOut, user, user) 311 | rayIxs.instructions.push(unwrapSol) 312 | } 313 | const recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash 314 | const message = new web3.TransactionMessage({ 315 | instructions: [...this.cacheIxs, ...rayIxs.instructions], 316 | payerKey: user, 317 | recentBlockhash 318 | }).compileToV0Message() 319 | const mainTx = new web3.VersionedTransaction(message) 320 | const buysimRes = (await this.connection.simulateTransaction(mainTx)) 321 | console.log('inner buy', buysimRes) 322 | if (rayIxs.signers) mainTx.signatures.push(...rayIxs.signers) 323 | return { 324 | ixs: [...this.cacheIxs, ...rayIxs.instructions], 325 | signers: [...rayIxs.signers] 326 | } 327 | } 328 | } 329 | 330 | export function sleep(ms: number) { 331 | return new Promise(resolve => setTimeout(resolve, ms)); 332 | } 333 | export function getPubkeyFromStr(str?: string) { 334 | try { 335 | return new web3.PublicKey((str ?? "").trim()) 336 | } catch (error) { 337 | return null 338 | } 339 | } 340 | 341 | export async function sendAndConfirmTransaction(tx: web3.VersionedTransaction | web3.Transaction, connection: web3.Connection) { 342 | const rawTx = tx.serialize() 343 | const txSignature = (await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed', maxRetries: 4 }) 344 | .catch(async () => { 345 | await sleep(500) 346 | return await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) 347 | .catch((txError) => { 348 | log({ txError }) 349 | return null 350 | }) 351 | })) 352 | return txSignature 353 | } 354 | 355 | export async function swap(input: SwapInput): Promise> { 356 | if (input.sellToken) { 357 | if (input.sellToken == 'base') { 358 | input.buyToken = "quote" 359 | } else { 360 | input.buyToken = "base" 361 | } 362 | } 363 | const user = input.keypair.publicKey 364 | const connection = new web3.Connection(input.url == 'mainnet' ? solanaConnection.rpcEndpoint : devConnection.rpcEndpoint, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) 365 | const baseRay = new BaseRay({ rpcEndpointUrl: connection.rpcEndpoint }) 366 | const slippage = input.slippage 367 | const poolKeys = await baseRay.getPoolKeys(input.poolId).catch(getPoolKeysError => { log({ getPoolKeysError }); return null }) 368 | if (!poolKeys) { return { Err: "Pool info not found" } } 369 | log({ 370 | baseToken: poolKeys.baseMint.toBase58(), 371 | quoteToken: poolKeys.quoteMint.toBase58(), 372 | }) 373 | const { amount, amountSide, buyToken, } = input 374 | const swapAmountInfo = await baseRay.computeBuyAmount({ 375 | amount, buyToken, inputAmountType: amountSide, poolKeys, user, slippage 376 | }).catch((computeBuyAmountError => log({ computeBuyAmountError }))) 377 | 378 | if (!swapAmountInfo) return { Err: "failed to calculate the amount" } 379 | 380 | const { amountIn, amountOut, fixedSide, tokenAccountIn, tokenAccountOut, } = swapAmountInfo 381 | console.log('swapAmountInfo', { amountIn, amountOut, fixedSide, tokenAccountIn, tokenAccountOut, }) 382 | 383 | const txInfo = await baseRay.buyFromPool({ amountIn, amountOut, fixedSide, poolKeys, tokenAccountIn, tokenAccountOut, user }).catch(buyFromPoolError => { log({ buyFromPoolError }); return null }) 384 | if (!txInfo) return { Err: "failed to prepare swap transaction" } 385 | const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; 386 | const txMsg = new web3.TransactionMessage({ 387 | instructions: txInfo.ixs, 388 | payerKey: user, 389 | recentBlockhash, 390 | }).compileToV0Message() 391 | const tx = new web3.VersionedTransaction(txMsg) 392 | tx.sign([input.keypair, ...txInfo.signers]) 393 | const buysimRes = (await connection.simulateTransaction(tx)) 394 | console.log('tx handler buy sim res', buysimRes) 395 | const txSignature = await sendAndConfirmTransaction(tx, connection).catch((sendAndConfirmTransactionError) => { 396 | log({ sendAndConfirmTransactionError }) 397 | return null 398 | }) 399 | // const txSignature = await connection.sendTransaction(tx).catch((error) => { log({ createPoolTxError: error }); return null }); 400 | if (!txSignature) { 401 | return { Err: "Failed to send transaction" } 402 | } 403 | return { 404 | Ok: { 405 | txSignature, 406 | } 407 | } 408 | } --------------------------------------------------------------------------------