├── .gitignore ├── client ├── PoolValidator │ ├── Addresses.ts │ ├── BlackLists.ts │ ├── ValidationResult.ts │ ├── RaydiumPoolValidator.ts │ ├── RaydiumSafetyCheck.ts │ └── RaydiumPoolParser.ts ├── Trader │ ├── Addresses.ts │ ├── ExitStrategy.ts │ ├── TradesAnalyzer.ts │ ├── BuyToken.ts │ ├── Trader.ts │ ├── SellToken.ts │ ├── TradesFetcher.ts │ └── SerumMarket.ts ├── RunTurbo.ts ├── test2.json ├── formaClmmConfigs.ts ├── RaydiumConfig.ts ├── StateAggregator │ ├── StateTypes.ts │ ├── DbWriter.ts │ └── ConsoleOutput.ts ├── Config.ts ├── index.ts ├── formatClmmKeysById.ts ├── RaydiumAMM │ ├── formatAmmKeysById.ts │ └── AmmSwap.ts ├── RaydiumUtils.ts ├── test.json ├── formatClmmKeys.ts ├── CllmSwap.ts ├── tt.json ├── ManualTrader.ts ├── SwapTest.ts ├── RaydiumSwap.ts ├── Utils.ts ├── TurboBot.ts ├── ObserveOpenBooks.ts ├── SafetyCheck.ts ├── PoolMaker.ts └── Swap.ts ├── Cargo.toml ├── src └── lib.rs ├── ecosystem.config.js ├── package.json ├── .vscode └── launch.json ├── README.md └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | test-ledger 2 | target 3 | node_modules 4 | .env 5 | client/dist 6 | client/.DS_Store 7 | *.log 8 | -------------------------------------------------------------------------------- /client/PoolValidator/Addresses.ts: -------------------------------------------------------------------------------- 1 | export const BURN_ACC_ADDRESS = 'burn68h9dS2tvZwtCFMt79SyaEgvqtcZZWJphizQxgt' -------------------------------------------------------------------------------- /client/PoolValidator/BlackLists.ts: -------------------------------------------------------------------------------- 1 | export const KNOWN_SCAM_ACCOUNTS = new Set( 2 | [ 3 | '4zc1rPxyHpTj1iQVnMcgBW7bTvJVfQ7zD3fRDBYSbXM4', 4 | '3S21CNv3sUu436S9c5wV6sKp6tznHf68S9io5pZby41a', 5 | '2MyASLtd3jWRLpzVVNUBZr8ZVTWTngAbB2nEi2gfefT8' 6 | ] 7 | ) -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana_sniper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "solana_sniper" 8 | crate-type = ["cdylib", "lib"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | solana-program = "1.17.18" 14 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use solana_program::{ 2 | account_info::AccountInfo, 3 | entrypoint, 4 | entrypoint::ProgramResult, 5 | pubkey::Pubkey, 6 | msg, 7 | }; 8 | 9 | // declare and export the program's entrypoint 10 | entrypoint!(process_instruction); 11 | 12 | // program entrypoint's implementation 13 | pub fn process_instruction( 14 | program_id: &Pubkey, 15 | accounts: &[AccountInfo], 16 | instruction_data: &[u8] 17 | ) -> ProgramResult { 18 | // log a message to the blockchain 19 | msg!("Hello, world!"); 20 | 21 | // gracefully exit the program 22 | Ok(()) 23 | } -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [{ 3 | name: 'solana_sniper', // Application name 4 | script: 'client/dist/index.js', // Main script path 5 | instances: '1', // Number of instances to start (can be 'max' to use all CPUs) 6 | autorestart: false, // Auto-restart if the app crashes 7 | watch: false, // Watch mode: restarts the app on file changes 8 | max_memory_restart: '8G', // Restart the app if it reaches 1GB memory usage 9 | env: { 10 | NODE_ENV: 'development', // Environment variables for development 11 | }, 12 | env_production: { 13 | NODE_ENV: 'production', // Environment variables for production 14 | } 15 | }] 16 | }; 17 | -------------------------------------------------------------------------------- /client/Trader/Addresses.ts: -------------------------------------------------------------------------------- 1 | import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, Token, WSOL } from "@raydium-io/raydium-sdk"; 2 | import { Keypair, PublicKey } from "@solana/web3.js" 3 | import { NATIVE_MINT } from "@solana/spl-token" 4 | import { Wallet } from "@project-serum/anchor"; 5 | import base58 from "bs58"; 6 | import { config } from "../Config"; 7 | 8 | export const OWNER_ADDRESS = new PublicKey(config.walletPublic) 9 | export const [SOL_SPL_TOKEN_ADDRESS] = PublicKey.findProgramAddressSync( 10 | [OWNER_ADDRESS.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), NATIVE_MINT.toBuffer()], 11 | ASSOCIATED_TOKEN_PROGRAM_ID 12 | ) 13 | export const WSOL_TOKEN = new Token(TOKEN_PROGRAM_ID, WSOL.mint, WSOL.decimals) 14 | export const PAYER = new Wallet(Keypair.fromSecretKey(base58.decode(config.walletPrivate))) -------------------------------------------------------------------------------- /client/RunTurbo.ts: -------------------------------------------------------------------------------- 1 | import { config } from './Config' 2 | import { Connection } from '@solana/web3.js' 3 | import { TurboBot } from './TurboBot' 4 | 5 | const connection = new Connection(config.rpcHttpURL, { 6 | wsEndpoint: config.rpcWsURL 7 | }) 8 | 9 | let backupConnection: Connection | null = null 10 | if (config.backupRpcUrl && config.backupWsRpcUrl) { 11 | backupConnection = new Connection(config.backupRpcUrl, { 12 | wsEndpoint: config.backupWsRpcUrl 13 | }) 14 | } 15 | 16 | const bot = new TurboBot(connection, backupConnection) 17 | 18 | async function main() { 19 | console.log('Run Turbo Bot') 20 | 21 | await bot.start(false) 22 | // await bot.buySellQuickTest('BZivKpJWgQvrA3yYe3ubomufeGVouoYoUhosmBEdqF9y') 23 | console.log('Trading complete') 24 | } 25 | 26 | main().catch(console.error) -------------------------------------------------------------------------------- /client/test2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "accountIndex": 8, 4 | "mint": "So11111111111111111111111111111111111111112", 5 | "owner": "GThUX1Atko4tqhN2NaiTazWSeFWMuiUvfFnyJyUghFMJ", 6 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 7 | "uiTokenAmount": { 8 | "amount": "42002240000000", 9 | "decimals": 9, 10 | "uiAmount": 42002.24, 11 | "uiAmountString": "42002.24" 12 | } 13 | }, 14 | { 15 | "accountIndex": 9, 16 | "mint": "8YuKKJwRyFCGcgN2N5WNx5GcjSRyKKQ8ZkMhxjTuPFYa", 17 | "owner": "4zc1rPxyHpTj1iQVnMcgBW7bTvJVfQ7zD3fRDBYSbXM4", 18 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 19 | "uiTokenAmount": { 20 | "amount": "1000000000000000", 21 | "decimals": 6, 22 | "uiAmount": 1000000000, 23 | "uiAmountString": "1000000000" 24 | } 25 | } 26 | ] -------------------------------------------------------------------------------- /client/PoolValidator/ValidationResult.ts: -------------------------------------------------------------------------------- 1 | import { LiquidityPoolInfo } from "@raydium-io/raydium-sdk" 2 | import { PoolKeys } from "./RaydiumPoolParser" 3 | import { TrendAnalisis } from "../Trader/TradesAnalyzer" 4 | 5 | export type PoolFeatures = { 6 | swap: boolean, 7 | addLiquidity: boolean, 8 | removeLiquidity: boolean, 9 | } 10 | 11 | // export type PoolValidationResults = { 12 | // pool: PoolKeys, 13 | // poolInfo: LiquidityPoolInfo, 14 | // poolFeatures: PoolFeatures, 15 | // safetyStatus: TokenSafetyStatus, 16 | // startTimeInEpoch: number | null, 17 | // trend: TrendAnalisis | null, 18 | // reason: string 19 | // } 20 | 21 | export type TokenSafetyStatus = 22 | 'RED' // 100% scam will be rugged very fast 23 | | 'YELLOW' // 99% scam, but we probably have bewteen 1-5 minutes to get some profit 24 | | 'GREEN' // 100% SAFE, if we are early should be easy to get 100%-10000% 25 | | 'TURBO' // Attempt to quick buy and sell no matter if it's scam -------------------------------------------------------------------------------- /client/formaClmmConfigs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AmmConfigLayout, 3 | ApiClmmConfigItem 4 | } from '@raydium-io/raydium-sdk'; 5 | import { 6 | AccountInfo, 7 | PublicKey, 8 | Connection 9 | } from '@solana/web3.js'; 10 | 11 | 12 | export function formatConfigInfo(id: PublicKey, account: AccountInfo): ApiClmmConfigItem { 13 | const info = AmmConfigLayout.decode(account.data) 14 | 15 | return { 16 | id: id.toBase58(), 17 | index: info.index, 18 | protocolFeeRate: info.protocolFeeRate, 19 | tradeFeeRate: info.tradeFeeRate, 20 | tickSpacing: info.tickSpacing, 21 | fundFeeRate: info.fundFeeRate, 22 | fundOwner: info.fundOwner.toString(), 23 | description: '', 24 | } 25 | } 26 | 27 | export async function formatClmmConfigs(connection: Connection, programId: string) { 28 | const configAccountInfo = await connection.getProgramAccounts(new PublicKey(programId), { filters: [{ dataSize: AmmConfigLayout.span }] }) 29 | return configAccountInfo.map(i => formatConfigInfo(i.pubkey, i.account)).reduce((a, b) => { a[b.id] = b; return a }, {} as { [id: string]: ApiClmmConfigItem }) 30 | } -------------------------------------------------------------------------------- /client/RaydiumConfig.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ENDPOINT as _ENDPOINT, 3 | Currency, 4 | LOOKUP_TABLE_CACHE, 5 | MAINNET_PROGRAM_ID, 6 | RAYDIUM_MAINNET, 7 | Token, 8 | TOKEN_PROGRAM_ID, 9 | TxVersion, 10 | } from '@raydium-io/raydium-sdk'; 11 | import { 12 | Connection, 13 | Keypair, 14 | PublicKey, 15 | } from '@solana/web3.js'; 16 | 17 | export const PROGRAMIDS = MAINNET_PROGRAM_ID; 18 | 19 | export const ENDPOINT = _ENDPOINT; 20 | 21 | export const RAYDIUM_MAINNET_API = RAYDIUM_MAINNET; 22 | 23 | export const makeTxVersion = TxVersion.V0; // LEGACY 24 | 25 | export const addLookupTableInfo = LOOKUP_TABLE_CACHE // only mainnet. other = undefined 26 | 27 | export const DEFAULT_TOKEN = { 28 | 'SOL': new Currency(9, 'USDC', 'USDC'), 29 | 'WSOL': new Token(TOKEN_PROGRAM_ID, new PublicKey('So11111111111111111111111111111111111111112'), 9, 'WSOL', 'WSOL'), 30 | 'USDC': new Token(TOKEN_PROGRAM_ID, new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), 6, 'USDC', 'USDC'), 31 | 'RAY': new Token(TOKEN_PROGRAM_ID, new PublicKey('4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R'), 6, 'RAY', 'RAY'), 32 | 'RAY_USDC-LP': new Token(TOKEN_PROGRAM_ID, new PublicKey('FGYXP4vBkMEtKhxrmEBcWN8VNmXX8qNgEJpENKDETZ4Y'), 6, 'RAY-USDC', 'RAY-USDC'), 33 | } -------------------------------------------------------------------------------- /client/Trader/ExitStrategy.ts: -------------------------------------------------------------------------------- 1 | export interface ExitStrategy { 2 | exitTimeoutInMillis: number, 3 | targetProfit: number, 4 | profitCalcIterationDelayMillis: number, 5 | } 6 | 7 | export const SAFE_EXIT_STRATEGY: ExitStrategy = { 8 | exitTimeoutInMillis: 30 * 60 * 1000, // wait for 30 minutes 9 | targetProfit: 0.29, // 30% (1% for slippage) to target 10 | profitCalcIterationDelayMillis: 500 // 0.5 seconds 11 | } 12 | 13 | export const DANGEROUS_EXIT_STRATEGY: ExitStrategy = { 14 | exitTimeoutInMillis: 10 * 60 * 1000, // 10 minutes time when token looks good 15 | targetProfit: 0.19, // make 20% (1% for slippage) in more secure way. owner could dump all tokens 16 | profitCalcIterationDelayMillis: 500 // 0.5 seconds 17 | } 18 | 19 | export const RED_TEST_EXIT_STRATEGY: ExitStrategy = { 20 | exitTimeoutInMillis: 1000, // 1 second time when token looks good 21 | targetProfit: 0.01, // make 9% (10% for slippage) in more secure way. owner could dump all tokens 22 | profitCalcIterationDelayMillis: 500 // 0.5 seconds 23 | } 24 | 25 | export const TURBO_EXIT_STRATEGY: ExitStrategy = { 26 | exitTimeoutInMillis: 1 * 60 * 1000, // 1 minute for turbo 27 | targetProfit: 0.89, // aim to 100% with 11% slippage 28 | profitCalcIterationDelayMillis: 50 // 0.05 seconds 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana_sniper", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node client/dist/index.js", 9 | "single": "node client/dist/SwapTest.js", 10 | "fills": "node client/dist/Trader/SerumMarket.js", 11 | "turbo": "node client/dist/RunTurbo.js" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@types/bs58": "^4.0.4", 18 | "@types/express": "^4.17.21", 19 | "@types/node": "^20.11.16", 20 | "@types/sqlite3": "^3.1.11", 21 | "@types/ws": "^8.5.10", 22 | "@types/yargs": "^17.0.32", 23 | "typescript": "^5.3.3" 24 | }, 25 | "dependencies": { 26 | "@project-serum/anchor": "^0.26.0", 27 | "@raydium-io/raydium-sdk": "^1.3.1-beta.47", 28 | "@solana/spl-token": "^0.4.0", 29 | "@solana/web3.js": "^1.90.0", 30 | "bigint-buffer": "^1.1.5", 31 | "chalk": "^4.1.2", 32 | "cli-table3": "^0.6.3", 33 | "csv-parser": "^3.0.0", 34 | "dotenv": "^16.4.1", 35 | "express": "^4.18.2", 36 | "log-update-async-hook": "^2.0.7", 37 | "piscina": "^4.3.1", 38 | "rxjs": "^7.8.1", 39 | "sqlite": "^5.1.1", 40 | "sqlite3": "^5.1.7", 41 | "tweetnacl": "^1.0.3", 42 | "ws": "^8.16.0", 43 | "yargs": "^17.7.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/StateAggregator/StateTypes.ts: -------------------------------------------------------------------------------- 1 | import { TokenSafetyStatus } from "../PoolValidator/ValidationResult" 2 | 3 | export type PoolStatus = { 4 | safety: TokenSafetyStatus | null, 5 | isEnabled: boolean, 6 | reason: string | null 7 | } 8 | 9 | export type BuyTxInfo = { 10 | amountInSOL: number, 11 | txId: string | null, 12 | error: string | null, 13 | newTokenAmount: number | null 14 | } 15 | 16 | export type SellTxInfo = { 17 | soldForAmountInSOL: number | null, 18 | txId: string | null, 19 | error: string | null 20 | } 21 | 22 | //'Status', 'ID', 'Safety', 'Start Time', 'Token', 'Buy', 'Sell', 'Profit' 23 | 24 | export type StateRecord = { 25 | poolId: string, 26 | status: string, 27 | startTime: string | null, 28 | tokenId: string | null, 29 | safetyInfo: string | null, 30 | buyInfo: string | null, 31 | sellInfo: string | null, 32 | profit: string | null, 33 | maxProfit: number | null, 34 | } 35 | 36 | export type TradingWallet = { id: number, startValue: number, current: number, totalProfit: number } 37 | 38 | 39 | 40 | export function createStateRecord( 41 | requiredFields: Pick, 42 | optionalFields?: Partial> 43 | ): StateRecord { 44 | const defaultStateRecord: Omit = { 45 | startTime: null, 46 | tokenId: null, 47 | safetyInfo: null, 48 | buyInfo: null, 49 | sellInfo: null, 50 | profit: null, 51 | maxProfit: null 52 | }; 53 | 54 | return { ...defaultStateRecord, ...requiredFields, ...optionalFields }; 55 | } -------------------------------------------------------------------------------- /client/Config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import { TokenSafetyStatus } from './PoolValidator/ValidationResult' 3 | dotenv.config() 4 | 5 | const SAFE_VALOTILITY_RATE = 1.0 6 | 7 | interface Config { 8 | rpcHttpURL: string, 9 | rpcWsURL: string, 10 | simulateOnly: boolean, 11 | safePriceValotilityRate: number, 12 | safeBuysCountInFirstMinute: number, 13 | allowedTradingSafety: Set, 14 | walletPublic: string, 15 | walletPrivate: string, 16 | dumpTradingHistoryToFile: boolean, 17 | validatorsLimit: number, 18 | appPort: number, 19 | buySOLAmount: number | null, 20 | backupRpcUrl: string | null, 21 | backupWsRpcUrl: string | null 22 | } 23 | 24 | export let config: Config = { 25 | rpcHttpURL: process.env.RPC_URL!, 26 | rpcWsURL: process.env.WS_URL!, 27 | simulateOnly: process.env.SIMULATION_ONLY === 'true', 28 | safePriceValotilityRate: process.env.SAFE_PRICE_VALOTILITY_RATE ? Number(process.env.SAFE_PRICE_VALOTILITY_RATE) : SAFE_VALOTILITY_RATE, 29 | safeBuysCountInFirstMinute: process.env.SAFE_BUYS_COUNT_IN_FIRST_MINUTE ? Number(process.env.SAFE_BUYS_COUNT_IN_FIRST_MINUTE) : 40, 30 | allowedTradingSafety: new Set(['GREEN', 'YELLOW']), 31 | walletPublic: process.env.WALLET_PUBLIC_KEY!, 32 | walletPrivate: process.env.WALLET_PRIVATE_KEY!, 33 | dumpTradingHistoryToFile: process.env.DUMP_HISTORY_TRADING_RECORDS_TO_FILE === 'true', 34 | validatorsLimit: Number(process.env.VALIDATORS_LIMIT!), 35 | appPort: process.env.APP_PORT ? Number(process.env.APP_PORT) : 3000, 36 | buySOLAmount: process.env.BUY_SOL_AMOUNT ? Number(process.env.BUY_SOL_AMOUNT) : null, 37 | backupRpcUrl: process.env.BACKUP_RPC_URL ?? null, 38 | backupWsRpcUrl: process.env.BACKUP_WS_URL ?? null 39 | } -------------------------------------------------------------------------------- /client/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express' 2 | import { config } from './Config' 3 | import { Connection } from '@solana/web3.js' 4 | import { TradingBot } from './Bot' 5 | 6 | const connection = new Connection(config.rpcHttpURL, { 7 | wsEndpoint: config.rpcWsURL 8 | }) 9 | 10 | 11 | const app = express() 12 | 13 | // Internal state 14 | const bot = new TradingBot(connection) 15 | 16 | // Single endpoint that increments and displays the visit count 17 | app.get('/start', (req: Request, res: Response) => { 18 | if (bot.isStarted()) { 19 | res.send('Is already started') 20 | } else { 21 | bot.start() 22 | res.send(`Bot is started to handle new pools`); 23 | } 24 | }) 25 | 26 | app.get('/stop', (req: Request, res: Response) => { 27 | if (bot.isStarted()) { 28 | bot.stop() 29 | res.send('Bot is stopped') 30 | } else { 31 | res.send(`Bot is already stopped`); 32 | } 33 | }) 34 | 35 | app.get('/wallet', (req: Request, res: Response) => { 36 | if (bot.isStarted()) { 37 | res.send(`Bot is started. Current wallet:\n${JSON.stringify(bot.getWalletTradingInfo())}`) 38 | } else { 39 | res.send(`Bot is not started. Current wallet:\n${JSON.stringify(bot.getWalletTradingInfo())}`) 40 | } 41 | }) 42 | 43 | // app.get('/skipped', (req: Request, res: Response) => { 44 | // res.send(JSON.stringify(bot.getSkippedPools())) 45 | // }) 46 | 47 | // app.get('/trades', (req: Request, res: Response) => { 48 | // res.send(JSON.stringify(bot.getTradingResults())) 49 | // }) 50 | 51 | // app.get('/running_validations', (req: Request, res: Response) => { 52 | // const mapToObject = Object.fromEntries(bot.getRunningValidationInfo()) 53 | // res.json(mapToObject) 54 | // }) 55 | 56 | // app.get('/completed_validations', (req: Request, res: Response) => { 57 | // const mapToObject = Object.fromEntries(bot.getCompletedValidations()) 58 | // res.json(mapToObject) 59 | // }) 60 | 61 | // app.get('/validation_errors', (req: Request, res: Response) => { 62 | // res.json(bot.getValidationErrors()) 63 | // }) 64 | 65 | // Start the server 66 | app.listen(config.appPort, '0.0.0.0', () => { 67 | console.log(`Server running on http://localhost:${config.appPort}`); 68 | }) -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "preLaunchTask": "tsc: build - tsconfig.json", 12 | "program": "${workspaceFolder}/client/index.ts", 13 | "outFiles": [ 14 | "${workspaceFolder}/client/dist/**/*.js" 15 | ], 16 | "sourceMaps": true 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Launch Single Swap", 22 | "preLaunchTask": "tsc: build - tsconfig.json", 23 | "program": "${workspaceFolder}/client/SwapTest.ts", 24 | "outFiles": [ 25 | "${workspaceFolder}/client/dist/**/*.js" 26 | ], 27 | "sourceMaps": true 28 | }, 29 | { 30 | "type": "node", 31 | "request": "launch", 32 | "name": "Launch Bot Test", 33 | "preLaunchTask": "tsc: build - tsconfig.json", 34 | "program": "${workspaceFolder}/client/Bot.ts", 35 | "outFiles": [ 36 | "${workspaceFolder}/client/dist/**/*.js" 37 | ], 38 | "sourceMaps": true 39 | }, 40 | { 41 | "type": "node", 42 | "request": "launch", 43 | "name": "Save Trades", 44 | "preLaunchTask": "tsc: build - tsconfig.json", 45 | "program": "${workspaceFolder}/client/Trader/SerumMarket.ts", 46 | "outFiles": [ 47 | "${workspaceFolder}/client/dist/**/*.js" 48 | ], 49 | "sourceMaps": true 50 | }, 51 | { 52 | "type": "node", 53 | "request": "launch", 54 | "name": "Manual trades", 55 | "args": [ 56 | "--operation=buy", 57 | "--mint=CgzdCjj5YNH51uFfQftFbuJKMrwgdWheVjwqjU84MV8y", 58 | "--pair=DG9PgGwpA2RjvNrWbWrhPWR8bZLig5nDSWTz14uSHyRQ" 59 | ], 60 | "preLaunchTask": "tsc: build - tsconfig.json", 61 | "program": "${workspaceFolder}/client/ManualTrader.ts", 62 | "outFiles": [ 63 | "${workspaceFolder}/client/dist/**/*.js" 64 | ], 65 | "sourceMaps": true 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /client/formatClmmKeysById.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiClmmConfigItem, 3 | ApiClmmPoolsItem, 4 | PoolInfoLayout 5 | } from '@raydium-io/raydium-sdk'; 6 | import { 7 | PublicKey, 8 | Connection 9 | } from '@solana/web3.js'; 10 | 11 | import { formatConfigInfo } from './formaClmmConfigs'; 12 | import { getApiClmmPoolsItemStatisticsDefault } from './formatClmmKeys'; 13 | 14 | async function getMintProgram(connection: Connection, mint: PublicKey) { 15 | const account = await connection.getAccountInfo(mint) 16 | if (account === null) throw Error(`getMintProgram - get id info error, mint ${mint.toString()}`) 17 | return account.owner 18 | } 19 | async function getConfigInfo(connection: Connection, configId: PublicKey): Promise { 20 | const account = await connection.getAccountInfo(configId) 21 | if (account === null) throw Error(`getConfigInfo - get id info error, configId ${configId}`) 22 | return formatConfigInfo(configId, account) 23 | } 24 | 25 | export async function formatClmmKeysById(connection: Connection, id: string): Promise { 26 | const account = await connection.getAccountInfo(new PublicKey(id)) 27 | if (account === null) throw Error(`formatClmmKeysById - get id info error, id ${id}`) 28 | const info = PoolInfoLayout.decode(account.data) 29 | 30 | return { 31 | id, 32 | mintProgramIdA: (await getMintProgram(connection, info.mintA)).toString(), 33 | mintProgramIdB: (await getMintProgram(connection, info.mintB)).toString(), 34 | mintA: info.mintA.toString(), 35 | mintB: info.mintB.toString(), 36 | vaultA: info.vaultA.toString(), 37 | vaultB: info.vaultB.toString(), 38 | mintDecimalsA: info.mintDecimalsA, 39 | mintDecimalsB: info.mintDecimalsB, 40 | ammConfig: await getConfigInfo(connection, info.ammConfig), 41 | rewardInfos: await Promise.all( 42 | info.rewardInfos 43 | .filter((i) => !i.tokenMint.equals(PublicKey.default)) 44 | .map(async (i) => ({ 45 | mint: i.tokenMint.toString(), 46 | programId: (await getMintProgram(connection, i.tokenMint)).toString(), 47 | })) 48 | ), 49 | tvl: 0, 50 | day: getApiClmmPoolsItemStatisticsDefault(), 51 | week: getApiClmmPoolsItemStatisticsDefault(), 52 | month: getApiClmmPoolsItemStatisticsDefault(), 53 | lookupTableAccount: PublicKey.default.toBase58(), 54 | } 55 | } -------------------------------------------------------------------------------- /client/RaydiumAMM/formatAmmKeysById.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiPoolInfoV4, 3 | LIQUIDITY_STATE_LAYOUT_V4, 4 | Liquidity, 5 | MARKET_STATE_LAYOUT_V3, 6 | Market, 7 | SPL_MINT_LAYOUT 8 | } from '@raydium-io/raydium-sdk'; 9 | import { 10 | PublicKey, 11 | Connection 12 | } from '@solana/web3.js'; 13 | 14 | 15 | export async function formatAmmKeysById(connection: Connection, id: string): Promise { 16 | const account = await connection.getAccountInfo(new PublicKey(id)) 17 | if (account === null) throw Error(' get id info error ') 18 | const info = LIQUIDITY_STATE_LAYOUT_V4.decode(account.data) 19 | 20 | const marketId = info.marketId 21 | const marketAccount = await connection.getAccountInfo(marketId) 22 | if (marketAccount === null) throw Error(' get market info error') 23 | const marketInfo = MARKET_STATE_LAYOUT_V3.decode(marketAccount.data) 24 | 25 | const lpMint = info.lpMint 26 | const lpMintAccount = await connection.getAccountInfo(lpMint) 27 | if (lpMintAccount === null) throw Error(' get lp mint info error') 28 | const lpMintInfo = SPL_MINT_LAYOUT.decode(lpMintAccount.data) 29 | 30 | return { 31 | id, 32 | baseMint: info.baseMint.toString(), 33 | quoteMint: info.quoteMint.toString(), 34 | lpMint: info.lpMint.toString(), 35 | baseDecimals: info.baseDecimal.toNumber(), 36 | quoteDecimals: info.quoteDecimal.toNumber(), 37 | lpDecimals: lpMintInfo.decimals, 38 | version: 4, 39 | programId: account.owner.toString(), 40 | authority: Liquidity.getAssociatedAuthority({ programId: account.owner }).publicKey.toString(), 41 | openOrders: info.openOrders.toString(), 42 | targetOrders: info.targetOrders.toString(), 43 | baseVault: info.baseVault.toString(), 44 | quoteVault: info.quoteVault.toString(), 45 | withdrawQueue: info.withdrawQueue.toString(), 46 | lpVault: info.lpVault.toString(), 47 | marketVersion: 3, 48 | marketProgramId: info.marketProgramId.toString(), 49 | marketId: info.marketId.toString(), 50 | marketAuthority: Market.getAssociatedAuthority({ programId: info.marketProgramId, marketId: info.marketId }).publicKey.toString(), 51 | marketBaseVault: marketInfo.baseVault.toString(), 52 | marketQuoteVault: marketInfo.quoteVault.toString(), 53 | marketBids: marketInfo.bids.toString(), 54 | marketAsks: marketInfo.asks.toString(), 55 | marketEventQueue: marketInfo.eventQueue.toString(), 56 | lookupTableAccount: PublicKey.default.toString() 57 | } 58 | } -------------------------------------------------------------------------------- /client/RaydiumUtils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildSimpleTransaction, 3 | findProgramAddress, 4 | InnerSimpleV0Transaction, 5 | SPL_ACCOUNT_LAYOUT, 6 | TOKEN_PROGRAM_ID, 7 | TokenAccount, 8 | } from '@raydium-io/raydium-sdk'; 9 | import { 10 | Connection, 11 | Keypair, 12 | PublicKey, 13 | SendOptions, 14 | Signer, 15 | Transaction, 16 | VersionedTransaction, 17 | } from '@solana/web3.js'; 18 | 19 | import { 20 | addLookupTableInfo, 21 | makeTxVersion, 22 | } from './RaydiumConfig'; 23 | 24 | export async function sendTx( 25 | connection: Connection, 26 | payer: Keypair | Signer, 27 | txs: (VersionedTransaction | Transaction)[], 28 | options?: SendOptions 29 | ): Promise { 30 | const txids: string[] = []; 31 | for (const iTx of txs) { 32 | if (iTx instanceof VersionedTransaction) { 33 | iTx.sign([payer]); 34 | txids.push(await connection.sendTransaction(iTx, options)); 35 | } else { 36 | txids.push(await connection.sendTransaction(iTx, [payer], options)); 37 | } 38 | } 39 | return txids; 40 | } 41 | 42 | export async function getWalletTokenAccount(connection: Connection, wallet: PublicKey): Promise { 43 | const walletTokenAccount = await connection.getTokenAccountsByOwner(wallet, { 44 | programId: TOKEN_PROGRAM_ID, 45 | }); 46 | return walletTokenAccount.value.map((i) => ({ 47 | pubkey: i.pubkey, 48 | programId: i.account.owner, 49 | accountInfo: SPL_ACCOUNT_LAYOUT.decode(i.account.data), 50 | })); 51 | } 52 | 53 | export async function buildAndSendTx(connection: Connection, wallet: Keypair, innerSimpleV0Transaction: InnerSimpleV0Transaction[], options?: SendOptions) { 54 | const willSendTx = await buildSimpleTransaction({ 55 | connection, 56 | makeTxVersion, 57 | payer: wallet.publicKey, 58 | innerTransactions: innerSimpleV0Transaction, 59 | addLookupTableInfo: addLookupTableInfo, 60 | }) 61 | 62 | return await sendTx(connection, wallet, willSendTx, options) 63 | } 64 | 65 | export function getATAAddress(programId: PublicKey, owner: PublicKey, mint: PublicKey) { 66 | const { publicKey, nonce } = findProgramAddress( 67 | [owner.toBuffer(), programId.toBuffer(), mint.toBuffer()], 68 | new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") 69 | ); 70 | return { publicKey, nonce }; 71 | } 72 | 73 | export async function sleepTime(ms: number) { 74 | console.log((new Date()).toLocaleString(), 'sleepTime', ms) 75 | return new Promise(resolve => setTimeout(resolve, ms)) 76 | } -------------------------------------------------------------------------------- /client/Trader/TradesAnalyzer.ts: -------------------------------------------------------------------------------- 1 | import { TradeRecord } from "./TradesFetcher"; 2 | 3 | export function findDumpingRecord(data: TradeRecord[], threshold: number = 50): [TradeRecord, TradeRecord] | null { 4 | for (let i = data.length - 1; i > 1; i--) { 5 | const next = data[i] 6 | const current = data[i - 1] 7 | if (current.type !== 'SELL') { continue } 8 | const percentageChange = ((next.priceInSOL - current.priceInSOL) / current.priceInSOL) * 100; 9 | 10 | if (percentageChange <= -threshold) return [current, next]; // Dump detected 11 | } 12 | return null; // No dump detected 13 | } 14 | 15 | export type ChartTrend = 'EQUILIBRIUM' | 'PUMPING' | 'DUMPING' 16 | export type TrendAnalisis = { 17 | type: ChartTrend, 18 | averageGrowthRate: number, 19 | volatility: number, 20 | buysCount: number 21 | } 22 | 23 | export function standardDeviation(values: number[]): number { 24 | const avg = values.reduce((sum, value) => sum + value, 0) / values.length; 25 | const squareDiffs = values.map(value => (value - avg) ** 2); 26 | const avgSquareDiff = squareDiffs.reduce((sum, value) => sum + value, 0) / squareDiffs.length; 27 | return Math.sqrt(avgSquareDiff); 28 | } 29 | 30 | const FAST_PRICE_CHANGING_RATE = 0.001 31 | 32 | export function analyzeTrend( 33 | data: TradeRecord[], 34 | timeLimitSeconds: number | null = 60, 35 | onlyBuy: boolean = true, 36 | filterOutLargeBets: boolean = true 37 | ): TrendAnalisis { 38 | const lastEpochTime = timeLimitSeconds ? data[0].epochTime + timeLimitSeconds : data[data.length - 1].epochTime // Check only first minute 39 | const tooLargeBet = 2 40 | const filtered = data.filter(x => { 41 | const timeFilter = x.epochTime <= lastEpochTime 42 | const typeFilter = onlyBuy ? x.type === 'BUY' : true 43 | const priceFilter = filterOutLargeBets ? x.priceInSOL <= tooLargeBet : true 44 | return timeFilter && typeFilter && priceFilter 45 | }) 46 | let growthRates: number[] = [] 47 | // Calculate growth rates between each pair of records 48 | for (let i = 1; i < filtered.length; i++) { 49 | const prevPrice = filtered[i - 1].priceInSOL; 50 | const currentPrice = filtered[i].priceInSOL; 51 | const growthRate = (currentPrice - prevPrice) / prevPrice; 52 | growthRates.push(growthRate); 53 | } 54 | 55 | // Calculate average growth rate 56 | const averageGrowthRate = growthRates.reduce((acc, rate) => acc + rate, 0) / growthRates.length; 57 | 58 | // Calculate volatility (standard deviation of growth rates) 59 | const volatility = standardDeviation(growthRates); 60 | 61 | // Determine trend based on average growth rate and volatility 62 | 63 | let trend: ChartTrend = 'EQUILIBRIUM' 64 | if (averageGrowthRate >= FAST_PRICE_CHANGING_RATE) { 65 | trend = 'PUMPING' 66 | } else if (averageGrowthRate <= -FAST_PRICE_CHANGING_RATE) { 67 | trend = 'DUMPING' 68 | } 69 | 70 | return { type: trend, averageGrowthRate, volatility, buysCount: filtered.length }; 71 | } -------------------------------------------------------------------------------- /client/RaydiumAMM/AmmSwap.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { 4 | jsonInfo2PoolKeys, 5 | Liquidity, 6 | LiquidityPoolKeys, 7 | Percent, 8 | Token, 9 | TokenAmount, 10 | } from '@raydium-io/raydium-sdk'; 11 | import { Keypair, Connection } from '@solana/web3.js'; 12 | 13 | import { 14 | DEFAULT_TOKEN, 15 | makeTxVersion 16 | } from '../RaydiumConfig'; 17 | import { formatAmmKeysById } from './formatAmmKeysById'; 18 | import { 19 | buildAndSendTx, 20 | getWalletTokenAccount, 21 | } from '../RaydiumUtils'; 22 | 23 | type WalletTokenAccounts = Awaited> 24 | type TestTxInputInfo = { 25 | outputToken: Token 26 | targetPool: string 27 | inputTokenAmount: TokenAmount 28 | slippage: Percent 29 | walletTokenAccounts: WalletTokenAccounts 30 | wallet: Keypair 31 | } 32 | 33 | export async function swapOnlyAmm(connection: Connection, wallet: Keypair, input: TestTxInputInfo) { 34 | // -------- pre-action: get pool info -------- 35 | const targetPoolInfo = await formatAmmKeysById(connection, input.targetPool) 36 | assert(targetPoolInfo, 'cannot find the target pool') 37 | const poolKeys = jsonInfo2PoolKeys(targetPoolInfo) as LiquidityPoolKeys 38 | 39 | // -------- step 1: coumpute amount out -------- 40 | const { amountOut, minAmountOut } = Liquidity.computeAmountOut({ 41 | poolKeys: poolKeys, 42 | poolInfo: await Liquidity.fetchInfo({ connection, poolKeys }), 43 | amountIn: input.inputTokenAmount, 44 | currencyOut: input.outputToken, 45 | slippage: input.slippage, 46 | }) 47 | 48 | // -------- step 2: create instructions by SDK function -------- 49 | //makeSwapInstructionSimple 50 | const { innerTransactions } = await Liquidity.makeSwapInstructionSimple({ 51 | connection, 52 | poolKeys, 53 | userKeys: { 54 | tokenAccounts: input.walletTokenAccounts, 55 | // tokenAccountIn: quoteTokenAccount.pubkey, 56 | // tokenAccountOut: baseTokenAddess, 57 | owner: input.wallet.publicKey, 58 | }, 59 | amountIn: input.inputTokenAmount, 60 | amountOut: minAmountOut, 61 | fixedSide: 'in', 62 | makeTxVersion, 63 | }) 64 | 65 | console.log('amountOut:', amountOut.toFixed(), ' minAmountOut: ', minAmountOut.toFixed()) 66 | 67 | return { txids: await buildAndSendTx(connection, wallet, innerTransactions) } 68 | } 69 | 70 | // async function howToUse() { 71 | // const inputToken = DEFAULT_TOKEN.USDC // USDC 72 | // const outputToken = DEFAULT_TOKEN.RAY // RAY 73 | // const targetPool = 'EVzLJhqMtdC1nPmz8rNd6xGfVjDPxpLZgq7XJuNfMZ6' // USDC-RAY pool 74 | // const inputTokenAmount = new TokenAmount(inputToken, 10000) 75 | // const slippage = new Percent(1, 100) 76 | // const walletTokenAccounts = await getWalletTokenAccount(connection, wallet.publicKey) 77 | 78 | // swapOnlyAmm({ 79 | // outputToken, 80 | // targetPool, 81 | // inputTokenAmount, 82 | // slippage, 83 | // walletTokenAccounts, 84 | // wallet: wallet, 85 | // }).then(({ txids }) => { 86 | // /** continue with txids */ 87 | // console.log('txids', txids) 88 | // }) 89 | // } -------------------------------------------------------------------------------- /client/test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": 4, 4 | "instructions": [ 5 | { 6 | "parsed": { 7 | "info": { 8 | "extensionTypes": [ 9 | "immutableOwner" 10 | ], 11 | "mint": "AhTTDdWqwMJu3h8e6ugxbQTcjw1m8t3oSq9zq2FfEFoW" 12 | }, 13 | "type": "getAccountDataSize" 14 | }, 15 | "program": "spl-token", 16 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 17 | "stackHeight": 2 18 | }, 19 | { 20 | "parsed": { 21 | "info": { 22 | "lamports": 2039280, 23 | "newAccount": "BRqDFvQndhVmyE2M2ksc1ZymCPKZeQ6UPCYq9pPyAAju", 24 | "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 25 | "source": "5cq1HURq8pagTHF58iHZxmLDeuemTpUx5JXnuUbq6Tx4", 26 | "space": 165 27 | }, 28 | "type": "createAccount" 29 | }, 30 | "program": "system", 31 | "programId": "11111111111111111111111111111111", 32 | "stackHeight": 2 33 | }, 34 | { 35 | "parsed": { 36 | "info": { 37 | "account": "BRqDFvQndhVmyE2M2ksc1ZymCPKZeQ6UPCYq9pPyAAju" 38 | }, 39 | "type": "initializeImmutableOwner" 40 | }, 41 | "program": "spl-token", 42 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 43 | "stackHeight": 2 44 | }, 45 | { 46 | "parsed": { 47 | "info": { 48 | "account": "BRqDFvQndhVmyE2M2ksc1ZymCPKZeQ6UPCYq9pPyAAju", 49 | "mint": "AhTTDdWqwMJu3h8e6ugxbQTcjw1m8t3oSq9zq2FfEFoW", 50 | "owner": "5cq1HURq8pagTHF58iHZxmLDeuemTpUx5JXnuUbq6Tx4" 51 | }, 52 | "type": "initializeAccount3" 53 | }, 54 | "program": "spl-token", 55 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 56 | "stackHeight": 2 57 | } 58 | ] 59 | }, 60 | { 61 | "index": 5, 62 | "instructions": [ 63 | { 64 | "parsed": { 65 | "info": { 66 | "amount": "6000000", 67 | "authority": "5cq1HURq8pagTHF58iHZxmLDeuemTpUx5JXnuUbq6Tx4", 68 | "destination": "4CLdfszjFVtJWeUwi4NG3gLQfvGxEhawkBPwNHVv18FA", 69 | "source": "9mH7g4tScm6ebhATSDeMcYCHYDxgeHnkt9io1KeWRUdv" 70 | }, 71 | "type": "transfer" 72 | }, 73 | "program": "spl-token", 74 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 75 | "stackHeight": 2 76 | }, 77 | { 78 | "parsed": { 79 | "info": { 80 | "amount": "390928946243", 81 | "authority": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", 82 | "destination": "BRqDFvQndhVmyE2M2ksc1ZymCPKZeQ6UPCYq9pPyAAju", 83 | "source": "HKUCxwzVUovFuqc28Y4eZCxooFmYLqJgpurPFVy3zt8r" 84 | }, 85 | "type": "transfer" 86 | }, 87 | "program": "spl-token", 88 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 89 | "stackHeight": 2 90 | } 91 | ] 92 | } 93 | ] -------------------------------------------------------------------------------- /client/StateAggregator/DbWriter.ts: -------------------------------------------------------------------------------- 1 | import { Database, open } from 'sqlite'; 2 | import sqlite3 from 'sqlite3'; 3 | import { StateRecord, TradingWallet } from './StateTypes'; 4 | 5 | let db: Database 6 | export let dbIsInited = false 7 | 8 | export async function initializeDb() { 9 | db = await open({ 10 | filename: './mydb.sqlite', 11 | driver: sqlite3.Database, 12 | }); 13 | 14 | // Create the table if it doesn't exist 15 | await db.exec(`CREATE TABLE IF NOT EXISTS state_records ( 16 | id INTEGER PRIMARY KEY AUTOINCREMENT, 17 | poolId TEXT NOT NULL UNIQUE, /* Ensure there's a UNIQUE constraint for UPSERT to work */ 18 | status TEXT NOT NULL, 19 | startTime TEXT, 20 | tokenId TEXT, 21 | safetyInfo TEXT, 22 | buyInfo TEXT, 23 | sellInfo TEXT, 24 | profit TEXT, 25 | maxProfit REAL, 26 | updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP 27 | )`); 28 | 29 | await db.exec(`CREATE TABLE IF NOT EXISTS TradingWallet ( 30 | id INTEGER PRIMARY KEY AUTOINCREMENT, 31 | startValue REAL NOT NULL, 32 | current REAL NOT NULL, 33 | totalProfit REAL NOT NULL 34 | )`); 35 | 36 | await db.exec(`CREATE INDEX IF NOT EXISTS idx_poolId ON state_records (poolId)`); 37 | 38 | dbIsInited = true 39 | } 40 | 41 | export async function createNewTradingWallet(startValue: number = 1, current: number = 1, totalProfit: number = 0): Promise { 42 | await db.run(`INSERT INTO TradingWallet (startValue, current, totalProfit) VALUES (?, ?, ?)`, [startValue, current, totalProfit]); 43 | const { id } = await db.get(`SELECT last_insert_rowid() as id`); 44 | return await db.get(`SELECT * FROM TradingWallet WHERE id = ?`, [id]); 45 | } 46 | 47 | export async function updateTradingWalletRecord(record: TradingWallet) { 48 | await db.run(`UPDATE TradingWallet SET startValue = ?, current = ?, totalProfit = ? WHERE id = ?`, [record.startValue, record.current, record.totalProfit, record.id]); 49 | } 50 | 51 | export async function getStateRecordByPoolId(poolId: string): Promise { 52 | return await db.get(`SELECT * FROM state_records WHERE poolId = ?`, [poolId]); 53 | } 54 | 55 | export async function upsertRecord(record: StateRecord) { 56 | try { 57 | await db.run(`INSERT INTO state_records (poolId, status, startTime, tokenId, safetyInfo, buyInfo, sellInfo, profit, maxProfit, updatedAt) 58 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) 59 | ON CONFLICT(poolId) 60 | DO UPDATE SET 61 | status = excluded.status, 62 | startTime = excluded.startTime, 63 | tokenId = excluded.tokenId, 64 | safetyInfo = excluded.safetyInfo, 65 | buyInfo = excluded.buyInfo, 66 | sellInfo = excluded.sellInfo, 67 | profit = excluded.profit, 68 | maxProfit = excluded.maxProfit, 69 | updatedAt = CURRENT_TIMESTAMP`, 70 | record.poolId, record.status, record.startTime, record.tokenId, record.safetyInfo, record.buyInfo, record.sellInfo, record.profit, record.maxProfit); 71 | } catch (e) { 72 | console.error(`Failed to write into DB. ${e}`) 73 | } 74 | } -------------------------------------------------------------------------------- /client/formatClmmKeys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiClmmPoolsItem, 3 | ApiClmmPoolsItemStatistics, 4 | PoolInfoLayout, 5 | getMultipleAccountsInfoWithCustomFlags 6 | } from '@raydium-io/raydium-sdk'; 7 | import { 8 | AddressLookupTableAccount, 9 | PublicKey, 10 | Connection 11 | } from '@solana/web3.js'; 12 | 13 | import { formatClmmConfigs } from './formaClmmConfigs'; 14 | 15 | export function getApiClmmPoolsItemStatisticsDefault(): ApiClmmPoolsItemStatistics { 16 | return { 17 | volume: 0, 18 | volumeFee: 0, 19 | feeA: 0, 20 | feeB: 0, 21 | feeApr: 0, 22 | rewardApr: { A: 0, B: 0, C: 0 }, 23 | apr: 0, 24 | priceMin: 0, 25 | priceMax: 0, 26 | } 27 | } 28 | 29 | export async function formatClmmKeys(connection: Connection, programId: string, findLookupTableAddress: boolean = false): Promise { 30 | const filterDefKey = PublicKey.default.toString() 31 | 32 | const poolAccountInfo = await connection.getProgramAccounts(new PublicKey(programId), { filters: [{ dataSize: PoolInfoLayout.span }] }) 33 | 34 | const configIdToData = await formatClmmConfigs(connection, programId) 35 | 36 | const poolAccountFormat = poolAccountInfo.map(i => ({ id: i.pubkey, ...PoolInfoLayout.decode(i.account.data) })) 37 | 38 | const allMint = [...new Set(poolAccountFormat.map(i => [i.mintA.toString(), i.mintB.toString(), ...i.rewardInfos.map(ii => ii.tokenMint.toString())]).flat())].filter(i => i !== filterDefKey).map(i => ({ pubkey: new PublicKey(i) })) 39 | const mintAccount = await getMultipleAccountsInfoWithCustomFlags(connection, allMint) 40 | const mintInfoDict = mintAccount.filter(i => i.accountInfo !== null).reduce((a, b) => { a[b.pubkey.toString()] = { programId: b.accountInfo!.owner.toString() }; return a }, {} as { [mint: string]: { programId: string } }) 41 | 42 | 43 | const poolInfoDict = poolAccountFormat.map(i => { 44 | const mintProgramIdA = mintInfoDict[i.mintA.toString()].programId 45 | const mintProgramIdB = mintInfoDict[i.mintB.toString()].programId 46 | const rewardInfos = i.rewardInfos 47 | .filter((i) => !i.tokenMint.equals(PublicKey.default)) 48 | .map((i) => ({ 49 | mint: i.tokenMint.toString(), 50 | programId: mintInfoDict[i.tokenMint.toString()].programId, 51 | })) 52 | 53 | return { 54 | id: i.id.toString(), 55 | mintProgramIdA, 56 | mintProgramIdB, 57 | mintA: i.mintA.toString(), 58 | mintB: i.mintB.toString(), 59 | vaultA: i.vaultA.toString(), 60 | vaultB: i.vaultB.toString(), 61 | mintDecimalsA: i.mintDecimalsA, 62 | mintDecimalsB: i.mintDecimalsB, 63 | ammConfig: configIdToData[i.ammConfig.toString()], 64 | rewardInfos, 65 | tvl: 0, 66 | day: getApiClmmPoolsItemStatisticsDefault(), 67 | week: getApiClmmPoolsItemStatisticsDefault(), 68 | month: getApiClmmPoolsItemStatisticsDefault(), 69 | lookupTableAccount: PublicKey.default.toBase58(), 70 | } 71 | }).reduce((a, b) => { a[b.id] = b; return a }, {} as { [id: string]: ApiClmmPoolsItem }) 72 | 73 | if (findLookupTableAddress) { 74 | const ltas = await connection.getProgramAccounts(new PublicKey('AddressLookupTab1e1111111111111111111111111'), { 75 | filters: [{ memcmp: { offset: 22, bytes: 'RayZuc5vEK174xfgNFdD9YADqbbwbFjVjY4NM8itSF9' } }] 76 | }) 77 | for (const itemLTA of ltas) { 78 | const keyStr = itemLTA.pubkey.toString() 79 | const ltaForamt = new AddressLookupTableAccount({ key: itemLTA.pubkey, state: AddressLookupTableAccount.deserialize(itemLTA.account.data) }) 80 | for (const itemKey of ltaForamt.state.addresses) { 81 | const itemKeyStr = itemKey.toString() 82 | if (poolInfoDict[itemKeyStr] === undefined) continue 83 | poolInfoDict[itemKeyStr].lookupTableAccount = keyStr 84 | } 85 | } 86 | } 87 | 88 | return Object.values(poolInfoDict) 89 | } -------------------------------------------------------------------------------- /client/CllmSwap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiClmmPoolsItem, 3 | Clmm, 4 | fetchMultipleMintInfos, 5 | Percent, 6 | Token, 7 | TokenAmount, 8 | } from '@raydium-io/raydium-sdk'; 9 | import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; 10 | import { 11 | Keypair, 12 | PublicKey, 13 | Connection, 14 | } from '@solana/web3.js'; 15 | 16 | import { 17 | DEFAULT_TOKEN, 18 | makeTxVersion, 19 | } from './RaydiumConfig'; 20 | import { formatClmmKeysById } from './formatClmmKeysById'; 21 | import { 22 | buildAndSendTx, 23 | getWalletTokenAccount, 24 | } from './RaydiumUtils'; 25 | 26 | type WalletTokenAccounts = Awaited> 27 | type TestTxInputInfo = { 28 | outputToken: Token 29 | targetPool: string 30 | inputTokenAmount: TokenAmount 31 | slippage: Percent 32 | walletTokenAccounts: WalletTokenAccounts 33 | wallet: Keypair 34 | } 35 | 36 | 37 | export async function swapOnlyCLMM(connection: Connection, wallet: Keypair, input: TestTxInputInfo) { 38 | // -------- pre-action: fetch Clmm pools info -------- 39 | const clmmPools: ApiClmmPoolsItem[] = [await formatClmmKeysById(connection, input.targetPool)] 40 | const { [input.targetPool]: clmmPoolInfo } = await Clmm.fetchMultiplePoolInfos({ 41 | connection, 42 | poolKeys: clmmPools, 43 | chainTime: new Date().getTime() / 1000, 44 | }) 45 | 46 | // -------- step 1: fetch tick array -------- 47 | const tickCache = await Clmm.fetchMultiplePoolTickArrays({ 48 | connection, 49 | poolKeys: [clmmPoolInfo.state], 50 | batchRequest: true, 51 | }) 52 | 53 | // -------- step 2: calc amount out by SDK function -------- 54 | // Configure input/output parameters, in this example, this token amount will swap 0.0001 USDC to RAY 55 | const { minAmountOut, remainingAccounts } = Clmm.computeAmountOutFormat({ 56 | poolInfo: clmmPoolInfo.state, 57 | tickArrayCache: tickCache[input.targetPool], 58 | amountIn: input.inputTokenAmount, 59 | currencyOut: input.outputToken, 60 | slippage: input.slippage, 61 | epochInfo: await connection.getEpochInfo(), 62 | token2022Infos: await fetchMultipleMintInfos({ 63 | connection, mints: [ 64 | ...clmmPools.map(i => [{ mint: i.mintA, program: i.mintProgramIdA }, { mint: i.mintB, program: i.mintProgramIdB }]).flat().filter(i => i.program === TOKEN_2022_PROGRAM_ID.toString()).map(i => new PublicKey(i.mint)), 65 | ] 66 | }), 67 | catchLiquidityInsufficient: false, 68 | }) 69 | 70 | // -------- step 3: create instructions by SDK function -------- 71 | const { innerTransactions } = await Clmm.makeSwapBaseInInstructionSimple({ 72 | connection, 73 | poolInfo: clmmPoolInfo.state, 74 | ownerInfo: { 75 | feePayer: input.wallet.publicKey, 76 | wallet: input.wallet.publicKey, 77 | tokenAccounts: input.walletTokenAccounts, 78 | }, 79 | inputMint: input.inputTokenAmount.token.mint, 80 | amountIn: input.inputTokenAmount.raw, 81 | amountOutMin: minAmountOut.amount.raw, 82 | remainingAccounts, 83 | makeTxVersion, 84 | }) 85 | 86 | return { txids: await buildAndSendTx(connection, wallet, innerTransactions) } 87 | } 88 | 89 | // async function howToUse() { 90 | // const inputToken = DEFAULT_TOKEN.USDC // USDC 91 | // const outputToken = DEFAULT_TOKEN.RAY // RAY 92 | // const targetPool = '61R1ndXxvsWXXkWSyNkCxnzwd3zUNB8Q2ibmkiLPC8ht' // USDC-RAY pool 93 | // const inputTokenAmount = new TokenAmount(inputToken, 100) 94 | // const slippage = new Percent(1, 100) 95 | // const walletTokenAccounts = await getWalletTokenAccount(connection, wallet.publicKey) 96 | 97 | // swapOnlyCLMM({ 98 | // outputToken, 99 | // targetPool, 100 | // inputTokenAmount, 101 | // slippage, 102 | // walletTokenAccounts, 103 | // wallet: wallet, 104 | // }).then(({ txids }) => { 105 | // /** continue with txids */ 106 | // console.log('txids', txids) 107 | // }) 108 | // } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Shitcoin Sniper Bot 2 | This sniper bot operating on the Solana network excels at quickly purchasing 'shitcoins' and identifying tokens with potential for growth. 3 | 4 | 5 | ## Overview 6 | 7 | The Shitcoin Sniper Bot is a specialized tool developed for the Solana network to effectively and swiftly navigate the unpredictable domain of 'shitcoins.' These cryptocurrencies often have uncertain value, and the bot aims to identify those with promising growth potential. It assists in streamlining the purchasing process by executing rapid, precise transactions within Solana's dynamic ecosystem. 8 | 9 | Equipped with advanced algorithms and real-time data analysis, this bot acts as a diligent observer, scanning numerous tokens for signs of profitability. It is adept at recognizing market trends and patterns, allowing users to spot emerging opportunities in the volatile cryptocurrency landscape. It quickly pinpoints tokens ripe for investment, predicting their growth paths to enable users to capitalize on potential gains before these opportunities become widely recognized. 10 | 11 | ## Key Features 12 | 13 | - **Automated Purchasing:** Facilitates the automated buying of tokens on the Solana network using the Raydium and Jupiter platforms. 14 | - **Sniper Mode:** Rapid identification and purchase of newly listed tokens through scanning on Dextools and liquidity pools on Raydium/Jupiter. 15 | - **Profitable Token Scanner:** Employs sophisticated algorithms to detect tokens with high growth potential. 16 | - **Customizable Settings:** Users can modify parameters to tailor their purchasing strategies. 17 | - **Real-time Monitoring:** Delivers live updates and continuous monitoring of token prices. 18 | 19 | ## System Requirements 20 | 21 | - Compatible with Windows 7/10/11 22 | - Requires .NET Framework 4.5 23 | - Needs RPC services for each network, such as shyft.to 24 | 25 | ## Installation Instructions 26 | 27 | 1. Enter your Solana private key, ensuring it is not your primary account or seed phrase. 28 | 2. Launch the bot. 29 | 30 | ## Usage Guide 31 | 32 | - **Account and Security:** 33 | - Load your Solana account file, ensuring you do not use your primary account or seed phrase. 34 | - Opt for SOCKS5 proxies if necessary, and select a high-speed private proxy. 35 | 36 | - **Configuration:** 37 | - Set the RPCs for Solana, with optimal performance seen on private RPCs. 38 | - Choose your token scanning mode: Dextools pair lists or Raydium/Jupiter liquidity pools searches. 39 | - Specify a maximum transaction fee for sniping activities. 40 | 41 | - **Operation:** 42 | - Start the bot to commence its operations. 43 | 44 | ## Contribution Guidelines 45 | 46 | We welcome community contributions. Here’s how you can participate: 47 | 48 | - **Reporting Bugs:** Follow our set protocol to submit bug reports. 49 | - **Proposing Features:** Submit your ideas per our feature proposal guidelines. 50 | - **Code Submission:** Adhere to our code formatting standards and pull request procedures. 51 | 52 | ## Disclaimer 53 | 54 | **Risk Warning:** The Shitcoin Sniper Bot serves educational and experimental purposes exclusively. Engaging in cryptocurrency trading carries significant risk, including the potential loss of initial capital. The bot provides no guarantees of profit or specific outcomes. Users assume full responsibility for any risks incurred from using this tool. It is crucial to conduct personal research, exercise diligence, and evaluate risk tolerance before embarking on any trading activities, whether automated or manual. The bot's creators and contributors bear no liability for financial losses sustained through its use. 55 | 56 | By utilizing the Shitcoin Sniper Bot, you acknowledge and accept the inherent risks in cryptocurrency trading and agree to release its creators and contributors from liability for any related damages or losses. 57 | 58 | ## Contact Information 59 | 60 | This is initial version (Version 1). 61 | 62 | Ready Project Version 2 & 3 (bundling, etc…) for sale… 63 | 64 | For technical support and development inquiries, feel free to reach out via the following platforms: 65 | 66 | - [Telegram](https://t.me/rizz_cat/) 67 | - [Twitter](https://x.com/rez_cats/) 68 | -------------------------------------------------------------------------------- /client/tt.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockTime": 1705104741, 3 | "meta": { 4 | "computeUnitsConsumed": 8423, 5 | "err": null, 6 | "fee": 45000, 7 | "innerInstructions": [], 8 | "loadedAddresses": { 9 | "readonly": [], 10 | "writable": [] 11 | }, 12 | "logMessages": [ 13 | "Program ComputeBudget111111111111111111111111111111 invoke [1]", 14 | "Program ComputeBudget111111111111111111111111111111 success", 15 | "Program ComputeBudget111111111111111111111111111111 invoke [1]", 16 | "Program ComputeBudget111111111111111111111111111111 success", 17 | "Program F6fmDVCQfvnEq2KR8hhfZSEczfM9JK9fWbCsYJNbTGn7 invoke [1]", 18 | "Program log: Instruction: Initialize", 19 | "Program F6fmDVCQfvnEq2KR8hhfZSEczfM9JK9fWbCsYJNbTGn7 consumed 351 of 799700 compute units", 20 | "Program F6fmDVCQfvnEq2KR8hhfZSEczfM9JK9fWbCsYJNbTGn7 success", 21 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", 22 | "Program log: Instruction: Burn", 23 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4706 of 799349 compute units", 24 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", 25 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", 26 | "Program log: Instruction: CloseAccount", 27 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2916 of 794643 compute units", 28 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", 29 | "Program 11111111111111111111111111111111 invoke [1]", 30 | "Program 11111111111111111111111111111111 success" 31 | ], 32 | "postBalances": [ 33 | 457385760, 34 | 9308050288, 35 | 0, 36 | 1461600, 37 | 1, 38 | 1, 39 | 1141440, 40 | 934087680 41 | ], 42 | "postTokenBalances": [], 43 | "preBalances": [ 44 | 455430760, 45 | 9308011008, 46 | 2039280, 47 | 1461600, 48 | 1, 49 | 1, 50 | 1141440, 51 | 934087680 52 | ], 53 | "preTokenBalances": [ 54 | { 55 | "accountIndex": 2, 56 | "mint": "HS9fgyz9NKFDGAR9P5eowh955K2E72APMgvEQS9NjJxo", 57 | "owner": "6NgoyRMJ5H1hdQtRUmE7wRFVogXxXH1aoHSy2acZaEi6", 58 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 59 | "uiTokenAmount": { 60 | "amount": "135645599662505", 61 | "decimals": 9, 62 | "uiAmount": 135645.599662505, 63 | "uiAmountString": "135645.599662505" 64 | } 65 | } 66 | ], 67 | "rewards": [], 68 | "status": { 69 | "Ok": null 70 | } 71 | }, 72 | "slot": 241469716, 73 | "transaction": { 74 | "message": { 75 | "header": { 76 | "numReadonlySignedAccounts": 0, 77 | "numReadonlyUnsignedAccounts": 4, 78 | "numRequiredSignatures": 1 79 | }, 80 | "accountKeys": [ 81 | "6NgoyRMJ5H1hdQtRUmE7wRFVogXxXH1aoHSy2acZaEi6", 82 | "burn68h9dS2tvZwtCFMt79SyaEgvqtcZZWJphizQxgt", 83 | "DwhoEs94H346zDzLYtVASYvZZ8AxrGodTfqhtA5MA9dx", 84 | "HS9fgyz9NKFDGAR9P5eowh955K2E72APMgvEQS9NjJxo", 85 | "11111111111111111111111111111111", 86 | "ComputeBudget111111111111111111111111111111", 87 | "F6fmDVCQfvnEq2KR8hhfZSEczfM9JK9fWbCsYJNbTGn7", 88 | "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 89 | ], 90 | "recentBlockhash": "DJpCnmrrAcieAwVut4oGFTuyGWYJG3WEmL4DFtruuqsf", 91 | "instructions": [ 92 | { 93 | "accounts": [], 94 | "data": "3Sy41WEwNLnT", 95 | "programIdIndex": 5, 96 | "stackHeight": null 97 | }, 98 | { 99 | "accounts": [], 100 | "data": "E6NUis", 101 | "programIdIndex": 5, 102 | "stackHeight": null 103 | }, 104 | { 105 | "accounts": [], 106 | "data": "WPNHsFPyEMr", 107 | "programIdIndex": 6, 108 | "stackHeight": null 109 | }, 110 | { 111 | "accounts": [ 112 | 2, 113 | 3, 114 | 0 115 | ], 116 | "data": "7PubXfMxnGeB", 117 | "programIdIndex": 7, 118 | "stackHeight": null 119 | }, 120 | { 121 | "accounts": [ 122 | 2, 123 | 0, 124 | 0 125 | ], 126 | "data": "A", 127 | "programIdIndex": 7, 128 | "stackHeight": null 129 | }, 130 | { 131 | "accounts": [ 132 | 0, 133 | 1 134 | ], 135 | "data": "3Bxs4KgzsZ8hsiGf", 136 | "programIdIndex": 4, 137 | "stackHeight": null 138 | } 139 | ], 140 | "indexToProgramIds": {} 141 | }, 142 | "signatures": [ 143 | "28WXa4qu4ACYPPcq8RwLAPxAEq1yfUrHBRukmJhTAMA9Q7cxcaqs4WzhihyPCcUbd5CBe66ZjyrPqBquak7aGo6B" 144 | ] 145 | } 146 | } -------------------------------------------------------------------------------- /client/ManualTrader.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs/yargs'; 2 | import { hideBin } from 'yargs/helpers'; 3 | import { config } from './Config' 4 | import { Connection, PublicKey } from '@solana/web3.js'; 5 | import { getAssociatedTokenAddressSync } from '@solana/spl-token'; 6 | import { error } from 'console'; 7 | import { ApiPoolInfoV4, LIQUIDITY_STATE_LAYOUT_V4, Liquidity, LiquidityPoolKeysV4, LiquidityStateV4, MARKET_STATE_LAYOUT_V3, Market } from '@raydium-io/raydium-sdk'; 8 | 9 | const RAYDIUM_POOL_V4_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8' 10 | 11 | type operation = 'buy' | 'sell' 12 | 13 | // const argv = yargs(hideBin(process.argv)).options({ 14 | // operation: { type: 'string', default: 'sell' }, 15 | // mint: { type: 'string', demandOption: true }, 16 | // pair: { type: 'string', demandOption: true }, 17 | // amount: { type: 'number', default: 0.1 }, 18 | // }).parseSync() 19 | 20 | // async function main() { 21 | // config.simulateOnly = false 22 | // const connection = new Connection(config.rpcHttpURL, { 23 | // wsEndpoint: config.rpcWsURL 24 | // }) 25 | 26 | // const mintAddress = new PublicKey(argv.mint) 27 | // const poolInfo = await getPoolInfo(connection, new PublicKey(argv.pair)) 28 | // console.log(poolInfo) 29 | // } 30 | 31 | export async function getPoolInfo(connection: Connection, poolId: PublicKey): Promise { 32 | const info = await connection.getAccountInfo(poolId); 33 | if (!info) { 34 | throw error('No Pool Info') 35 | } 36 | 37 | let amAccountData = { id: poolId, programId: info.owner, ...LIQUIDITY_STATE_LAYOUT_V4.decode(info.data) } 38 | const marketProgramId = amAccountData.marketProgramId 39 | const allMarketInfo = await connection.getAccountInfo(marketProgramId) 40 | if (!allMarketInfo) { 41 | throw error('No Pool Info') 42 | } 43 | const itemMarketInfo = MARKET_STATE_LAYOUT_V3.decode(allMarketInfo.data) 44 | 45 | 46 | const marketInfo = { 47 | marketProgramId: allMarketInfo.owner.toString(), 48 | marketAuthority: Market.getAssociatedAuthority({ programId: allMarketInfo.owner, marketId: marketProgramId }).publicKey.toString(), 49 | marketBaseVault: itemMarketInfo.baseVault.toString(), 50 | marketQuoteVault: itemMarketInfo.quoteVault.toString(), 51 | marketBids: itemMarketInfo.bids.toString(), 52 | marketAsks: itemMarketInfo.asks.toString(), 53 | marketEventQueue: itemMarketInfo.eventQueue.toString() 54 | } 55 | 56 | const format: ApiPoolInfoV4 = { 57 | id: amAccountData.id.toString(), 58 | baseMint: amAccountData.baseMint.toString(), 59 | quoteMint: amAccountData.quoteMint.toString(), 60 | lpMint: amAccountData.lpMint.toString(), 61 | baseDecimals: amAccountData.baseDecimal.toNumber(), 62 | quoteDecimals: amAccountData.quoteDecimal.toNumber(), 63 | lpDecimals: amAccountData.baseDecimal.toNumber(), 64 | version: 4, 65 | programId: amAccountData.programId.toString(), 66 | authority: Liquidity.getAssociatedAuthority({ programId: amAccountData.programId }).publicKey.toString(), 67 | openOrders: amAccountData.openOrders.toString(), 68 | targetOrders: amAccountData.targetOrders.toString(), 69 | baseVault: amAccountData.baseVault.toString(), 70 | quoteVault: amAccountData.quoteVault.toString(), 71 | withdrawQueue: amAccountData.withdrawQueue.toString(), 72 | lpVault: amAccountData.lpVault.toString(), 73 | marketVersion: 3, 74 | marketId: amAccountData.marketId.toString(), 75 | ...marketInfo, 76 | lookupTableAccount: PublicKey.default.toString() 77 | } 78 | 79 | return format 80 | } 81 | 82 | async function buy(connection: Connection, mintAddress: PublicKey) { 83 | const allAccs = (await connection.getProgramAccounts(new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID))).values 84 | 85 | const poolAddress = getAssociatedTokenAddressSync(mintAddress, new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID)) 86 | return poolAddress.toString() 87 | } 88 | 89 | // function convertStringKeysToDataKeys(poolId: PublicKey, poolInfo: LiquidityStateV4): LiquidityPoolKeysV4 { 90 | // return { 91 | // id: poolId, 92 | // baseMint: poolInfo.baseMint, 93 | // quoteMint: poolInfo.quoteMint, 94 | // lpMint: poolInfo.lpMint, 95 | // baseDecimals: poolInfo.baseDecimal.toNumber(), 96 | // quoteDecimals: poolInfo.quoteDecimal.toNumber(), 97 | // lpDecimals: 6, 98 | // version: 4, 99 | // programId: poolInfo.marketProgramId, 100 | // authority: new , 101 | // openOrders: new PublicKey(poolInfo.openOrders), 102 | // targetOrders: new PublicKey(poolInfo.targetOrders), 103 | // baseVault: new PublicKey(poolInfo.baseVault), 104 | // quoteVault: new PublicKey(poolInfo.quoteVault), 105 | // withdrawQueue: new PublicKey(poolInfo.withdrawQueue), 106 | // lpVault: new PublicKey(poolInfo.lpVault), 107 | // marketVersion: 3, 108 | // marketProgramId: new PublicKey(poolInfo.marketProgramId), 109 | // marketId: new PublicKey(poolInfo.marketId), 110 | // marketAuthority: new PublicKey(poolInfo.marketAuthority), 111 | // marketBaseVault: new PublicKey(poolInfo.baseVault), 112 | // marketQuoteVault: new PublicKey(poolInfo.quoteVault), 113 | // marketBids: new PublicKey(poolInfo.marketBids), 114 | // marketAsks: new PublicKey(poolInfo.marketAsks), 115 | // marketEventQueue: new PublicKey(poolInfo.marketEventQueue), 116 | // } as LiquidityPoolKeysV4; 117 | // } 118 | 119 | // main().catch(console.error) -------------------------------------------------------------------------------- /client/Trader/BuyToken.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey, TokenAmount as TA } from '@solana/web3.js' 2 | import { TOKEN_PROGRAM_ID } from '@solana/spl-token' 3 | import { LiquidityPoolKeysV4, Token, TokenAmount, WSOL } from "@raydium-io/raydium-sdk" 4 | import { Wallet } from '@project-serum/anchor' 5 | import chalk from 'chalk' 6 | import { calculateAmountOut, swapTokens } from '../Swap' 7 | import { confirmTransaction, getNewTokenBalance, getTransactionConfirmation, lamportsToSOLNumber, retryAsyncFunction } from '../Utils' 8 | import { config } from '../Config' 9 | 10 | const WSOL_TOKEN = new Token(TOKEN_PROGRAM_ID, WSOL.mint, WSOL.decimals) 11 | 12 | interface Success { 13 | kind: 'SUCCESS', 14 | newTokenAmount: number 15 | } 16 | 17 | interface FailedToBuy { 18 | kind: 'NO_BUY', 19 | reason: string 20 | } 21 | 22 | interface NoConfirmation { 23 | kind: 'NO_CONFIRMATION', 24 | reason: string, 25 | txId: string 26 | } 27 | 28 | interface FailedToGetBoughtTokensAmount { 29 | kind: 'NO_TOKENS_AMOUNT', 30 | reason: string 31 | } 32 | 33 | export type BuyResult = Success | FailedToBuy | NoConfirmation | FailedToGetBoughtTokensAmount 34 | 35 | 36 | export async function buyToken( 37 | connection: Connection, 38 | payer: Wallet, 39 | amountToBuy: number, 40 | tokenToBuy: Token, 41 | tokenToBuyAccountAddress: PublicKey, 42 | poolInfo: LiquidityPoolKeysV4, 43 | mainTokenAccountAddress: PublicKey): Promise { 44 | const buyAmount = new TokenAmount(WSOL_TOKEN, amountToBuy, false) 45 | 46 | let buyError = '' 47 | let signature = '' 48 | let txLanded = false 49 | let newTokenAmount: number | null = null 50 | if (config.simulateOnly) { 51 | try { 52 | newTokenAmount = await retryAsyncFunction(calcTokemAmountOut, [connection, buyAmount, tokenToBuy, poolInfo]) 53 | } catch (e) { 54 | console.error(`Failed to simulate buying shitcoin with error ${e}. Retrying.`); 55 | buyError = `${e}` 56 | } 57 | } else { 58 | try { 59 | console.log(`Buying`) 60 | let attempt = 1 61 | while (!txLanded && attempt <= 6) { 62 | console.log(`Buy attempt ${attempt}`) 63 | signature = await swapTokens(connection, poolInfo, mainTokenAccountAddress, tokenToBuyAccountAddress, payer, buyAmount) 64 | txLanded = await confirmTransaction(connection, signature) 65 | attempt += 1 66 | } 67 | } catch (e) { 68 | console.error(`Failed to buy shitcoin with error ${e}. Retrying.`) 69 | buyError = `${e}` 70 | } 71 | 72 | if (!txLanded) { 73 | return { kind: 'NO_BUY', reason: buyError } 74 | } 75 | } 76 | 77 | 78 | // let transactionConfirmed = newTokenAmount !== null 79 | // let confirmationError = '' 80 | // if (!config.simulateOnly) { 81 | // try { 82 | // const transactionConfirmation = await retryAsyncFunction(getTransactionConfirmation, [connection, txid], 3, 500) 83 | // if (transactionConfirmation.err) { 84 | // confirmationError = `${transactionConfirmation.err}` 85 | // } else { 86 | // transactionConfirmed = true 87 | // } 88 | // } catch (e) { 89 | // confirmationError = `${e}` 90 | // } 91 | 92 | // if (!transactionConfirmed) { 93 | // return { kind: 'NO_CONFIRMATION', reason: confirmationError, txId: txid } 94 | // } 95 | // } 96 | 97 | let snipedAmount: number | null 98 | if (!config.simulateOnly) { 99 | const shitTokenBalance = await retryAsyncFunction(getNewTokenBalance, 100 | [connection, signature, tokenToBuy.mint.toString(), payer.publicKey.toString()], 10, 1000); 101 | 102 | if (shitTokenBalance !== undefined) { 103 | snipedAmount = shitTokenBalance.uiTokenAmount.uiAmount; 104 | } else { 105 | const balance = await retryAsyncFunction(getTokenAccountBalance, [connection, tokenToBuyAccountAddress]) 106 | snipedAmount = balance.uiAmount 107 | } 108 | } else { 109 | snipedAmount = newTokenAmount 110 | } 111 | 112 | if (snipedAmount) { 113 | return { kind: 'SUCCESS', newTokenAmount: snipedAmount } 114 | } else { 115 | return { kind: 'NO_TOKENS_AMOUNT', reason: 'Unknown' } 116 | } 117 | } 118 | 119 | // Don't buy, just simulate 120 | async function calcTokemAmountOut( 121 | connection: Connection, 122 | amountIn: TokenAmount, 123 | tokenOut: Token, 124 | poolKeys: LiquidityPoolKeysV4): Promise { 125 | try { 126 | const { 127 | amountOut, 128 | minAmountOut, 129 | currentPrice, 130 | executionPrice, 131 | priceImpact, 132 | fee, 133 | } = await calculateAmountOut(connection, amountIn, tokenOut, poolKeys) 134 | 135 | // console.log(chalk.yellow('Calculated buy prices')); 136 | // console.log(`${chalk.bold('current price: ')}: ${currentPrice.toFixed()}`); 137 | // if (executionPrice !== null) { 138 | // console.log(`${chalk.bold('execution price: ')}: ${executionPrice.toFixed()}`); 139 | // } 140 | // console.log(`${chalk.bold('price impact: ')}: ${priceImpact.toFixed()}`); 141 | // console.log(`${chalk.bold('amount out: ')}: ${amountOut.toFixed()}`); 142 | // console.log(`${chalk.bold('min amount out: ')}: ${minAmountOut.toFixed()}`); 143 | 144 | const amountOutNumber = lamportsToSOLNumber(amountOut.raw, tokenOut.decimals) ?? 0 145 | 146 | return amountOutNumber 147 | } catch (e) { 148 | //console.log(chalk.yellow('Faiiled to calculate amountOut')); 149 | return null; 150 | } 151 | } 152 | 153 | async function getTokenAccountBalance(connection: Connection, tokenAccountAddress: PublicKey): Promise { 154 | const balance = (await connection.getTokenAccountBalance(tokenAccountAddress)).value 155 | return balance 156 | } -------------------------------------------------------------------------------- /client/Trader/Trader.ts: -------------------------------------------------------------------------------- 1 | import { TokenSafetyStatus } from '../PoolValidator/ValidationResult' 2 | import { GeneralTokenCondition } from '../Swap' 3 | import { LiquidityPoolKeysV4, Token, TokenAmount, WSOL } from '@raydium-io/raydium-sdk' 4 | import { SOL_SPL_TOKEN_ADDRESS, PAYER, OWNER_ADDRESS } from "./Addresses" 5 | import { DANGEROUS_EXIT_STRATEGY, ExitStrategy, SAFE_EXIT_STRATEGY, TURBO_EXIT_STRATEGY } from './ExitStrategy' 6 | import { formatDate } from '../Utils' 7 | import { buyToken } from './BuyToken' 8 | import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from '@solana/spl-token' 9 | import { SellResults, sellToken } from './SellToken' 10 | import { Connection } from '@solana/web3.js' 11 | import { onBuyResults } from '../StateAggregator/ConsoleOutput' 12 | import { config } from '../Config' 13 | import chalk from 'chalk' 14 | 15 | export type TraderResults = { 16 | boughtAmountInSOL: number | null, 17 | buyingTokenCondition: GeneralTokenCondition | null, 18 | soldForAmountInSOL: number | null, 19 | pnl: number | null, 20 | error: string | null 21 | } 22 | 23 | // module.exports = async (data: PoolValidationResults) => { 24 | // const sellResults = await tryPerformTrading(data) 25 | // return sellResults 26 | // } 27 | 28 | export async function tryPerformTrading(connection: Connection, pool: LiquidityPoolKeysV4, safetyStatus: TokenSafetyStatus): Promise { 29 | if (safetyStatus === 'RED') { 30 | console.error('RED token comes to trader. Skipping') 31 | return { kind: 'FAILED', reason: 'RED coin', txId: null, boughtForSol: null, buyTime: null } 32 | } 33 | 34 | let tokenAMint = pool.baseMint.toString() === WSOL.mint ? pool.baseMint : pool.quoteMint; 35 | let tokenBMint = pool.baseMint.toString() !== WSOL.mint ? pool.baseMint : pool.quoteMint; 36 | let tokenBDecimals = pool.baseMint.toString() === tokenBMint.toString() ? pool.baseDecimals : pool.quoteDecimals; 37 | const tokenBToken = new Token(TOKEN_PROGRAM_ID, tokenBMint, tokenBDecimals) 38 | const tokenBAccountAddress = getAssociatedTokenAddressSync(tokenBMint, OWNER_ADDRESS, false); 39 | 40 | 41 | if (pool.quoteMint.toString() !== WSOL.mint && pool.baseMint.toString() !== WSOL.mint) { 42 | return { kind: 'FAILED', reason: 'No SOL in pair', txId: null, boughtForSol: null, buyTime: null } 43 | } 44 | 45 | const buyAmount = getBuyAmountInSOL(safetyStatus)! 46 | const exitStrategy = getExitStrategy(safetyStatus)! 47 | 48 | const buyResult = await buyToken(connection, PAYER, buyAmount, tokenBToken, tokenBAccountAddress, pool, SOL_SPL_TOKEN_ADDRESS) 49 | 50 | if (safetyStatus !== 'TURBO') { 51 | onBuyResults(pool.id.toString(), buyResult) 52 | } 53 | 54 | const buyDate = new Date() 55 | 56 | if (buyResult.kind !== 'SUCCESS') { 57 | //TODO: Handle errors 58 | return { kind: 'FAILED', reason: `Buy transaction failed`, txId: null, buyTime: formatDate(buyDate), boughtForSol: null } 59 | } 60 | 61 | const amountToSell = new TokenAmount(tokenBToken, buyResult.newTokenAmount, false) 62 | let sellResults = await sellToken( 63 | connection, 64 | buyAmount, 65 | amountToSell, 66 | pool, 67 | SOL_SPL_TOKEN_ADDRESS, 68 | tokenBAccountAddress, 69 | exitStrategy) 70 | sellResults.buyTime = formatDate(buyDate) 71 | return sellResults 72 | } 73 | 74 | export async function instaBuyAndSell(connection: Connection, pool: LiquidityPoolKeysV4, solAmount: number): Promise { 75 | let tokenAMint = pool.baseMint.toString() === WSOL.mint ? pool.baseMint : pool.quoteMint; 76 | let tokenBMint = pool.baseMint.toString() !== WSOL.mint ? pool.baseMint : pool.quoteMint; 77 | let tokenBDecimals = pool.baseMint.toString() === tokenBMint.toString() ? pool.baseDecimals : pool.quoteDecimals; 78 | const tokenBToken = new Token(TOKEN_PROGRAM_ID, tokenBMint, tokenBDecimals) 79 | const tokenBAccountAddress = getAssociatedTokenAddressSync(tokenBMint, OWNER_ADDRESS, false); 80 | 81 | 82 | if (pool.quoteMint.toString() !== WSOL.mint && pool.baseMint.toString() !== WSOL.mint) { 83 | return { kind: 'FAILED', reason: 'No SOL in pair', txId: null, boughtForSol: null, buyTime: null } 84 | } 85 | 86 | const buyAmount = solAmount 87 | const buyResult = await buyToken(connection, PAYER, buyAmount, tokenBToken, tokenBAccountAddress, pool, SOL_SPL_TOKEN_ADDRESS) 88 | const buyDate = new Date() 89 | 90 | if (buyResult.kind !== 'SUCCESS') { 91 | //TODO: Handle errors 92 | return { kind: 'FAILED', reason: `Buy transaction failed`, txId: null, buyTime: formatDate(buyDate), boughtForSol: null } 93 | } 94 | 95 | const amountToSell = new TokenAmount(tokenBToken, buyResult.newTokenAmount, false) 96 | const exitStrategy: ExitStrategy = { 97 | exitTimeoutInMillis: 500, 98 | targetProfit: 1, 99 | profitCalcIterationDelayMillis: 100, 100 | } 101 | let sellResults = await sellToken( 102 | connection, 103 | buyAmount, 104 | amountToSell, 105 | pool, 106 | SOL_SPL_TOKEN_ADDRESS, 107 | tokenBAccountAddress, 108 | exitStrategy) 109 | sellResults.buyTime = formatDate(buyDate) 110 | return sellResults 111 | } 112 | 113 | 114 | function getBuyAmountInSOL(tokenStatus: TokenSafetyStatus): number | null { 115 | if (config.buySOLAmount) { 116 | return config.buySOLAmount 117 | } 118 | switch (tokenStatus) { 119 | case 'RED': return null 120 | case 'YELLOW': return 0.2 121 | case 'GREEN': return 0.3 122 | case 'TURBO': return 0.1 123 | } 124 | } 125 | 126 | function getExitStrategy(tokenStatus: TokenSafetyStatus): ExitStrategy | null { 127 | switch (tokenStatus) { 128 | case 'RED': return null 129 | case 'YELLOW': return DANGEROUS_EXIT_STRATEGY 130 | case 'GREEN': return SAFE_EXIT_STRATEGY 131 | case 'TURBO': return TURBO_EXIT_STRATEGY 132 | } 133 | } 134 | 135 | -------------------------------------------------------------------------------- /client/Trader/SellToken.ts: -------------------------------------------------------------------------------- 1 | import { Connection, ParsedTransactionWithMeta, PublicKey } from "@solana/web3.js"; 2 | import { ExitStrategy } from "./ExitStrategy"; 3 | import { LiquidityPoolKeysV4, TokenAmount, WSOL } from "@raydium-io/raydium-sdk"; 4 | import { PAYER, WSOL_TOKEN } from "./Addresses"; 5 | import { swapTokens, waitForProfitOrTimeout } from "../Swap"; 6 | import { confirmTransaction, delay, formatDate, getTransactionConfirmation, retryAsyncFunction } from "../Utils"; 7 | import { config } from "../Config"; 8 | 9 | type SellSuccess = { 10 | kind: 'SUCCESS', 11 | txId: string, 12 | buyTime: string, 13 | sellTime: string, 14 | boughtForSol: number, 15 | soldForSOL: number, 16 | estimatedProfit: number, 17 | profit: number 18 | } 19 | 20 | type SellFailure = { 21 | kind: 'FAILED', 22 | buyTime: string | null, 23 | boughtForSol: number | null, 24 | txId: string | null, 25 | reason: string 26 | } 27 | export type SellResults = SellSuccess | SellFailure 28 | 29 | export async function sellToken( 30 | connection: Connection, 31 | spentAmount: number, 32 | amountToSell: TokenAmount, 33 | pool: LiquidityPoolKeysV4, 34 | mainTokenAccountAddress: PublicKey, 35 | shitcoinAccountAddress: PublicKey, 36 | exitStrategy: ExitStrategy): Promise { 37 | const estimatedProfit = await waitForProfitOrTimeout( 38 | spentAmount, 39 | exitStrategy.targetProfit, 40 | connection, 41 | amountToSell, 42 | WSOL_TOKEN, 43 | pool, 44 | exitStrategy.profitCalcIterationDelayMillis, 45 | exitStrategy.exitTimeoutInMillis) 46 | 47 | let soldForSOLAmount: number = estimatedProfit.amountOut 48 | let finalProfit: number = estimatedProfit.profit 49 | if (config.simulateOnly) { 50 | const sellDate = new Date() 51 | return { kind: 'SUCCESS', txId: 'Simulation', soldForSOL: soldForSOLAmount, estimatedProfit: estimatedProfit.profit, profit: finalProfit, boughtForSol: spentAmount, sellTime: formatDate(sellDate), buyTime: '' } 52 | } else { 53 | let { confirmedTxId, error } = await sellAndConfirm(connection, pool, mainTokenAccountAddress, shitcoinAccountAddress, amountToSell) 54 | 55 | if (confirmedTxId === null) { 56 | const retryResults = await sellAndConfirm(connection, pool, mainTokenAccountAddress, shitcoinAccountAddress, amountToSell) 57 | confirmedTxId = retryResults.confirmedTxId 58 | error = retryResults.error 59 | } 60 | 61 | if (confirmedTxId === null) { 62 | return { kind: 'FAILED', txId: confirmedTxId, reason: error ?? 'Unknown', buyTime: '', boughtForSol: spentAmount } 63 | } 64 | 65 | const sellDate = new Date() 66 | 67 | soldForSOLAmount = await getSOLAmount(connection, confirmedTxId) 68 | finalProfit = (soldForSOLAmount - spentAmount) / spentAmount 69 | return { kind: 'SUCCESS', txId: confirmedTxId, soldForSOL: soldForSOLAmount, estimatedProfit: estimatedProfit.profit, profit: finalProfit, boughtForSol: spentAmount, sellTime: formatDate(sellDate), buyTime: '' } 70 | } 71 | } 72 | 73 | 74 | async function sellAndConfirm( 75 | connection: Connection, 76 | pool: LiquidityPoolKeysV4, 77 | mainTokenAccountAddress: PublicKey, 78 | shitcoinAccountAddress: PublicKey, 79 | amountToSell: TokenAmount): Promise<{ confirmedTxId: string | null, error: string | null }> { 80 | 81 | let signature = '' 82 | let txLanded = false 83 | let sellError = '' 84 | let attempt = 1 85 | while (!txLanded && attempt <= 20) { 86 | console.log(`Sell attempt ${attempt}`) 87 | try { 88 | signature = await swapTokens(connection, 89 | pool, 90 | shitcoinAccountAddress, 91 | mainTokenAccountAddress, 92 | PAYER, 93 | amountToSell) 94 | txLanded = await confirmTransaction(connection, signature) 95 | } catch (e) { 96 | console.error(`Failed to sell shitcoin with error ${e}. Retrying.`); 97 | sellError = JSON.stringify(e) 98 | } 99 | attempt += 1 100 | } 101 | 102 | // let transactionConfirmed = false 103 | // let confirmationError = '' 104 | // try { 105 | // const transactionConfirmation = await retryAsyncFunction(getTransactionConfirmation, [connection, signature], 5, 300) 106 | // if (transactionConfirmation.err) { 107 | // confirmationError = `${transactionConfirmation.err}` 108 | // } else { 109 | // transactionConfirmed = true 110 | // } 111 | // } catch (e) { 112 | // confirmationError = `${e}` 113 | // } 114 | 115 | return { confirmedTxId: txLanded ? signature : null, error: txLanded ? null : sellError } 116 | } 117 | 118 | interface SPLTransferInfo { 119 | amount: string 120 | authority: string 121 | destination: string 122 | source: string 123 | } 124 | 125 | async function getSOLAmount(connection: Connection, sellTxId: string): Promise { 126 | const parsedTx = await getParsedTxWithMeta(connection, sellTxId) 127 | if (parsedTx === null) { 128 | return 0 129 | } 130 | 131 | const inner = parsedTx.meta?.innerInstructions 132 | if (!inner) { return 0 } 133 | 134 | const splTransferPairs = inner.map(x => x.instructions.filter((a: any) => a.program === 'spl-token' && a.parsed.type === 'transfer')) 135 | const splTransferPair = splTransferPairs.find(x => x.length >= 2) 136 | 137 | if (splTransferPair) { 138 | const outInfo: SPLTransferInfo = (splTransferPair[1] as any).parsed.info 139 | const solAmount = Number(outInfo.amount) / (10 ** WSOL.decimals) 140 | return solAmount 141 | } 142 | return 0 143 | } 144 | 145 | async function getParsedTxWithMeta(connection: Connection, txId: string): Promise { 146 | let result: ParsedTransactionWithMeta | null = null 147 | const maxAttempts = 5 148 | let attempt = 1 149 | while (attempt <= maxAttempts) { 150 | result = await connection.getParsedTransaction(txId, { maxSupportedTransactionVersion: 0 }) 151 | if (result !== null) { 152 | return result 153 | } 154 | await delay(500) 155 | attempt += 1 156 | } 157 | return result 158 | } -------------------------------------------------------------------------------- /client/Trader/TradesFetcher.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, ParsedTransactionWithMeta, Connection, ParsedAccountData } from '@solana/web3.js' 2 | import { LiquidityPoolKeysV4, WSOL } from '@raydium-io/raydium-sdk' 3 | import { PoolKeys } from '../PoolValidator/RaydiumPoolParser' 4 | import path from 'path' 5 | import fs from 'fs' 6 | import { config } from '../Config' 7 | 8 | 9 | export type TradeType = 'BUY' | 'SELL' 10 | 11 | export interface TradeRecord { 12 | signature: string 13 | time: string 14 | epochTime: number 15 | type: TradeType 16 | tokenAmount: number 17 | solAmount: number 18 | usdAmount: number 19 | priceInSOL: number 20 | priceInUSD: number 21 | } 22 | 23 | interface SPLTransferInfo { 24 | amount: string 25 | authority: string 26 | destination: string 27 | source: string 28 | } 29 | 30 | export async function fetchLatestTrades( 31 | connection: Connection, 32 | poolKeys: LiquidityPoolKeysV4, 33 | tradesLimit: number | null = null 34 | ): Promise { 35 | const isTokenBase = poolKeys.quoteMint.toString() === WSOL.mint 36 | const tokenMintAddress = isTokenBase ? poolKeys.baseMint : poolKeys.quoteMint 37 | const tokenDecimals = isTokenBase ? poolKeys.baseDecimals : poolKeys.quoteDecimals 38 | const txs = await fetchAllTransactions(connection, new PublicKey(poolKeys.id), tradesLimit) 39 | const tradeRecords = await parseTradingData(poolKeys, txs, new PublicKey(tokenMintAddress), tokenDecimals) 40 | tradeRecords.sort((a, b) => a.epochTime - b.epochTime) 41 | if (config.dumpTradingHistoryToFile) { 42 | const startDumpingToFile = new Date() 43 | saveToCSV(poolKeys.id.toString(), tradeRecords) 44 | const endDumpingToFile = new Date() 45 | console.log(`Poole ${poolKeys.id}. Dumping to file took ${endDumpingToFile.getUTCSeconds() - startDumpingToFile.getUTCSeconds()}`) 46 | } 47 | return tradeRecords 48 | } 49 | 50 | async function fetchAllTransactions(connection: Connection, address: PublicKey, maxCount: number | null): Promise { 51 | let results: ParsedTransactionWithMeta[] = [] 52 | let hasMore = true 53 | const limit = 1000 54 | let beforeTx: string | undefined = undefined 55 | while (hasMore || (maxCount ? results.length < maxCount : true)) { 56 | const fetchedIds = await connection.getConfirmedSignaturesForAddress2(address, { limit: limit, before: beforeTx }) 57 | if (fetchedIds.length === 0) { break } 58 | const filtered = fetchedIds.filter(x => !x.err) 59 | const tradesOrNull = await connection.getParsedTransactions(filtered.map(x => x.signature), { maxSupportedTransactionVersion: 0 }) 60 | const trades: ParsedTransactionWithMeta[] = tradesOrNull.filter((transaction): transaction is ParsedTransactionWithMeta => transaction !== null); 61 | results.push(...trades) 62 | hasMore = fetchedIds.length === limit 63 | beforeTx = fetchedIds[fetchedIds.length - 1].signature 64 | } 65 | return results 66 | } 67 | 68 | async function parseTradingData( 69 | poolKeys: LiquidityPoolKeysV4, 70 | transactions: (ParsedTransactionWithMeta | null)[], 71 | tokenMint: PublicKey, 72 | tokenDecimals: number): Promise { 73 | let results: TradeRecord[] = [] 74 | 75 | for (let txOrNull of transactions) { 76 | if (!txOrNull) { continue } 77 | 78 | const inner = txOrNull.meta?.innerInstructions 79 | if (!inner) { continue } 80 | 81 | const splTransferPairs = inner.map(x => x.instructions.filter((a: any) => a.program === 'spl-token')) 82 | const splTransferPair = splTransferPairs.find(x => x.length === 2) 83 | if (splTransferPair) { 84 | const inInfo: SPLTransferInfo = (splTransferPair[0] as any).parsed.info 85 | const outInfo: SPLTransferInfo = (splTransferPair[1] as any).parsed.info 86 | 87 | const quoteIsToken = poolKeys.quoteMint.toString() === tokenMint.toString() 88 | 89 | const isSelling = quoteIsToken ? inInfo.destination === poolKeys.quoteVault.toString() : inInfo.destination === poolKeys.baseVault.toString() //userOtherTokenPostBalance < userOtherTokenPreBalance 90 | const txDate = new Date(0) 91 | txDate.setUTCSeconds(txOrNull.blockTime ?? 0) 92 | 93 | const shitAmount = Number(isSelling ? inInfo.amount : outInfo.amount) / (10 ** tokenDecimals) //Math.abs(userOtherTokenPostBalance - userOtherTokenPreBalance) 94 | const solAmount = Number(isSelling ? outInfo.amount : inInfo.amount) / (10 ** WSOL.decimals) //Math.abs(userSolPostBalance - userSolPreBalance) 95 | const priceInSOL = solAmount / shitAmount 96 | results.push({ 97 | signature: txOrNull.transaction.signatures[0], 98 | time: formatTime(txDate), 99 | epochTime: txOrNull.blockTime ?? 0, 100 | type: isSelling ? 'SELL' : 'BUY', 101 | tokenAmount: shitAmount, 102 | solAmount, 103 | usdAmount: solAmount * 110, 104 | priceInSOL: priceInSOL, 105 | priceInUSD: priceInSOL * 110 106 | }) 107 | } 108 | } 109 | 110 | return results 111 | } 112 | 113 | function formatTime(date: Date): string { 114 | // Get hours, minutes, and seconds from the date 115 | const hours = String(date.getHours()).padStart(2, '0'); 116 | const minutes = String(date.getMinutes()).padStart(2, '0'); 117 | const seconds = String(date.getSeconds()).padStart(2, '0'); 118 | 119 | // Format time as 'HH:MM:SS' 120 | return `${hours}:${minutes}:${seconds}`; 121 | } 122 | 123 | function saveToCSV(fileName: string, data: TradeRecord[]) { 124 | const titleKeys = Object.keys(data[0]) 125 | const refinedData = [] 126 | refinedData.push(titleKeys) 127 | data.forEach(item => { 128 | refinedData.push(Object.values(item)) 129 | }) 130 | 131 | let csvContent = '' 132 | 133 | let filePath = path.join(__dirname, `/trading_data/${fileName}.csv`) 134 | 135 | refinedData.forEach(row => { 136 | csvContent += row.join(',') + '\n' 137 | }) 138 | 139 | if (fs.existsSync(filePath)) { 140 | fileName = path.join(__dirname, `/trading_data/${fileName}_1.csv`) 141 | } 142 | fs.writeFileSync(filePath, csvContent, {}) 143 | } -------------------------------------------------------------------------------- /client/Trader/SerumMarket.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | dotenv.config() 3 | import fs from 'fs' 4 | import path from 'path' 5 | import csv from 'csv-parser' 6 | import { TradeRecord, TradeType, fetchLatestTrades } from './TradesFetcher' 7 | import { ChartTrend, analyzeTrend, findDumpingRecord } from './TradesAnalyzer' 8 | import { PoolKeys, fetchPoolKeysForLPInitTransactionHash } from '../PoolValidator/RaydiumPoolParser' 9 | import { config } from '../Config' 10 | import { Connection, PublicKey } from '@solana/web3.js' 11 | import { AccountLayout } from '@solana/spl-token' 12 | import { convertStringKeysToDataKeys } from '../Utils' 13 | 14 | const connection = new Connection(config.rpcHttpURL, { 15 | wsEndpoint: config.rpcWsURL 16 | }) 17 | 18 | const raydiumPoolAuthority = "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1" 19 | 20 | const poolsToGetPriceChanges = [ 21 | // 'ASUUfLjhtacBbjx7KswSraRYvZMUdbWsjMNjDYngzaex', 22 | // 'DoNtsgPxYZfpq5cpBFH1PqAwhJDU9G7RCG1EPgCUiwRx', 23 | // 'A3isw2Xco9TtpvKgm9j7UcjUoGpCP4FfhrnXPGnxgKrQ', 24 | // 'Es6nqcHuFvj8VNJ1obfwkoD6zAgwvdY2WCa4EGsPrfZU', 25 | // 'AqaXfDnGCzTK14U69QjWWThDCEBD5iiwvpM5dMCxeWBj', 26 | // '3ZMzLJMozbPKex7jyhsQUwDYxecJNWutdKEhoDsBmRjk', 27 | // '2QhsDSRz9fYCmg48qNyFfSLaTp4K8XcNKuCBeTaH8ns5', 28 | // '5CrZMaLVzDaZiinmPYM9ntAAqsmAvH4JKAKDczYRbXdd', 29 | // 'CzMUQP2fYvz5AF1wWmU19YhbCwhYhhCe9JiLiZjk3a8r', 30 | // 'HjQwYyivK56MYnKkgjSMe5bvvHWtTQziZnbdnALNbjXN', 31 | // '6Mms42RMhkc1xkEaNGiqFasiXKD1mmYj9LwgHJecU6D4', 32 | '9tP6LFHbJnikaRRCxxYAtKNeRsVVtvNjDxNVNmg1FY4g' 33 | ] 34 | 35 | const poolFirstTxs = [ 36 | 'qqHR5VkyFbcJg8D9aSGPQ5Z5DEHDRPJ1pAPPWS2yF8MVvzeJaLgVkZWn4qYd1VrpNubsTDUJH2iUpgHRAdaCC9h', 37 | '2bZ89AbTRvNBK6aaroExTPrCS9E2DPj2EB5oMWe6PP9ApAYe11BGqJn9vFi1JWGBBhC1rRDoR7wFQYZDztdYZf9i', 38 | '4fWfkn3dv4fRdaJvdMJxkP42er7bFoHgCrtD3jyEWcgJsJR6dn149UbqaHoYoBzCNCDxreqEe5t3QKon3keErAeH', 39 | '28bum3cR7aHuUBEN8fbnJ5Vfp6L1m6jyFkLFUCvZTyUMrx44kfLyT51eZgCEsFHxskBeh8JdCDBUi4Cni7kQVF2m', 40 | 'mgQMbynw1s3QnckDS7JAyr382fd7nfmeBR64TAFSdNEppR7VrLjEu62tmeitmCfStKirLvHeZuuyWWhmxTUfYgx', 41 | 'WHxPpJg82SUtMNh4YAnAeKqDCpuQcLRJ8qhU1vUWQZr5bVzMJxPiBJ4r3hrqzegvuJDAJ4TbH4ifjVJvq2reX98', 42 | '5zHKMquUARQPWETqe9tnA1hyhMNE2mtmZ8gFN5xhBhQyRgJkZ1ZEf1k2J5mfq2oeniUt2hWoUPxANALMxnrKRGCq', 43 | 'Cd4kA68ofmeRYqYA2XcCg1xhyZZyz9FHCRTpCUXzBZhrsEBFqsQTQpJT75CiXNG8oXn65CUG1T6En496Ptp2ub8', 44 | '2VX5P1mRNF7w5mMtMtNBNg7icZ5CVokPkeFzUQthtJmkemcMvMXUGevNkcB9aKC5W5KQKQgSqBLD8Rrpu5D6Ms5j', 45 | '4NP2XjcdtaGTvQTsf88isCh4yQPw4pHgfmEycaJ7EkVnLSrveSTJi6EjcHH2ZpQgqSmfZAh17fKtnsogHSBwDukw', 46 | '4k4GFMnhM3PXBeykRRLPTnqp74VHnPKKR7UBsqEcEz84vyg9ryvHk5EHbiVH5BxqAboCdUUiHNRvpoXCPe5q6qHg', 47 | '4NYWxJsyXzRWo4pu3u1V6AEoNbB4n8wqaQzSB95QCfyiTKCvzH69hL4thZbuNQKSJXPHjLECiq4SnotYK9e9nWUh' 48 | ] 49 | 50 | async function testLPTOkenAccInfo() { 51 | const tokenAccAddress = new PublicKey('GwNzvvq8LwRXBuoZqAKqCAwpS6Y4RgKxsMDAas6z722U') 52 | const accInfo = await connection.getAccountInfo(tokenAccAddress) 53 | if (accInfo) { 54 | const parsed = AccountLayout.decode(accInfo.data) 55 | console.log(`${parsed}`) 56 | } 57 | } 58 | 59 | async function test() { 60 | let i = 11 61 | const parsed: { poolId: string, trades: TradeRecord[] }[] = [] 62 | while (i < poolFirstTxs.length) { 63 | const { poolKeys } = await fetchPoolKeysForLPInitTransactionHash(connection, poolFirstTxs[i]) 64 | const fetched = await getTradeRecords(poolKeys) 65 | parsed.push(fetched) 66 | i++ 67 | } 68 | 69 | for (let { poolId, trades } of parsed) { 70 | saveToCSV(poolId, trades.sort((a, b) => a.epochTime - b.epochTime)) 71 | } 72 | } 73 | 74 | async function loadSaved() { 75 | for (let poolId of poolsToGetPriceChanges) { 76 | const records = await parseCSV(poolId) 77 | const dumpRes = findDumpingRecord(records) 78 | const trend = analyzeTrend(records) 79 | console.log(`${poolId} - ${trendToColor(trend.type, trend.buysCount, trend.volatility)}, rate=${trend.averageGrowthRate}, volatility=${trend.volatility}`) 80 | } 81 | } 82 | 83 | function trendToColor(trend: ChartTrend, buysCount: number, volatility: number): string { 84 | if ((volatility > config.safePriceValotilityRate) || buysCount < config.safeBuysCountInFirstMinute) { return '🔴 Bad' } 85 | switch (trend) { 86 | case 'EQUILIBRIUM': return '🟢 Good' //'🟡 So So' 87 | case 'PUMPING': return '🟢 Good' 88 | case 'DUMPING': return '🔴 Bad' 89 | default: return '⚪ Unknown' 90 | } 91 | } 92 | 93 | // loadSaved() 94 | // test() 95 | testLPTOkenAccInfo() 96 | 97 | async function parseCSV(poolId: string): Promise { 98 | return new Promise((resolve, _) => { 99 | const filePath = path.join(__dirname, `/trading_data/${poolId}.csv`) 100 | const records: TradeRecord[] = [] 101 | 102 | const mapValues = (args: { header: string, index: number, value: any }) => { 103 | if (args.index <= 1) { 104 | return args.value 105 | } 106 | if (args.index === 3) { 107 | return args.value as TradeType 108 | } 109 | return Number(args.value) 110 | } 111 | 112 | const csvOptions = { 113 | headers: ['signature', 'time', 'epochTime', 'type', 'tokenAmount', 'solAmount', 'usdAmount', 'priceInSOL', 'priceInUSD'], 114 | mapValues: mapValues, 115 | skipLines: 1 116 | } 117 | fs.createReadStream(filePath) 118 | .pipe(csv(csvOptions)) 119 | .on("data", function (row: TradeRecord) { 120 | records.push(row) 121 | }) 122 | .on('end', () => { 123 | records.sort((a, b) => a.epochTime - b.epochTime) 124 | resolve(records) 125 | }) 126 | }) 127 | } 128 | 129 | async function getTradeRecords(poolKeys: PoolKeys): Promise<{ poolId: string, trades: TradeRecord[] }> { 130 | const tradeRecords = await fetchLatestTrades(connection, convertStringKeysToDataKeys(poolKeys)) 131 | return { poolId: poolKeys.id, trades: tradeRecords } 132 | } 133 | 134 | function saveToCSV(fileName: string, data: TradeRecord[]) { 135 | const titleKeys = Object.keys(data[0]) 136 | const refinedData = [] 137 | refinedData.push(titleKeys) 138 | data.forEach(item => { 139 | refinedData.push(Object.values(item)) 140 | }) 141 | 142 | let csvContent = '' 143 | 144 | const filePath = path.join(__dirname, `/trading_data/${fileName}.csv`) 145 | 146 | refinedData.forEach(row => { 147 | csvContent += row.join(',') + '\n' 148 | }) 149 | fs.writeFileSync(filePath, csvContent, {}) 150 | } 151 | -------------------------------------------------------------------------------- /client/SwapTest.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); 3 | import { Connection, PublicKey, Keypair } from '@solana/web3.js' 4 | import { confirmTransaction, findTokenAccountAddress, getTokenAccounts, getNewTokenBalance } from './Utils'; 5 | import { TokenAmount, Token, Percent, jsonInfo2PoolKeys, LiquidityPoolKeys, WSOL, ASSOCIATED_TOKEN_PROGRAM_ID } from "@raydium-io/raydium-sdk"; 6 | import { NATIVE_MINT, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token'; 7 | import { Wallet } from '@project-serum/anchor' 8 | import base58 from 'bs58' 9 | import { swapOnlyCLMM } from "./CllmSwap" 10 | import { DEFAULT_TOKEN } from './RaydiumConfig'; 11 | import { getWalletTokenAccount } from './RaydiumUtils'; 12 | import { swapOnlyAmm } from './RaydiumAMM/AmmSwap' 13 | import chalk from 'chalk'; 14 | import { formatAmmKeysById } from './RaydiumAMM/formatAmmKeysById'; 15 | import { swapTokens, sellTokens } from './Swap'; 16 | import RaydiumSwap from './RaydiumSwap'; 17 | 18 | const wallet = new Wallet(Keypair.fromSecretKey(base58.decode(process.env.WALLET_PRIVATE_KEY!))); 19 | const SHIT = '8w63or5Dfjb24TYmzXnY3VHPsczA1pUT7SzDW3JFe6Uz'; 20 | const SHIT_POOL_ID = '4doQnCB4ppx2oPfewcYrp63i7fvRN5bWb9x1XZwt6j3S'; 21 | const BUY_AMOUNT_IN_SOL = 0.01 // e.g. 0.01 SOL -> B_TOKEN 22 | 23 | 24 | const connection = new Connection(process.env.RPC_URL!, { 25 | wsEndpoint: process.env.WS_URL!, commitment: 'confirmed' 26 | }); 27 | 28 | async function runSwapTest() { 29 | const [wsolAccountAddress] = PublicKey.findProgramAddressSync( 30 | [wallet.publicKey.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), NATIVE_MINT.toBuffer()], 31 | ASSOCIATED_TOKEN_PROGRAM_ID 32 | ); 33 | 34 | console.log('Startttt') 35 | const targetPoolInfo = await formatAmmKeysById(connection, SHIT_POOL_ID); 36 | const poolKeys = jsonInfo2PoolKeys(targetPoolInfo) as LiquidityPoolKeys 37 | 38 | const tokenToSnipe = new Token(TOKEN_PROGRAM_ID, new PublicKey(SHIT), 9) 39 | const quoteToken = DEFAULT_TOKEN.WSOL; 40 | const quoteTokenAmount = new TokenAmount(quoteToken, 0.01, false) 41 | const allWalletTokenAccounts = await getWalletTokenAccount(connection, wallet.publicKey) 42 | 43 | const shitCoinAccount = allWalletTokenAccounts.find( 44 | (acc) => acc.accountInfo.mint.toString() === SHIT, 45 | )?.pubkey ?? getAssociatedTokenAddressSync(new PublicKey(SHIT), wallet.publicKey, false); 46 | 47 | const startWsolBalance = await connection.getTokenAccountBalance(wsolAccountAddress) 48 | 49 | const buyTxid = await swapTokens( 50 | connection, 51 | poolKeys, 52 | wsolAccountAddress, 53 | shitCoinAccount, 54 | wallet, 55 | quoteTokenAmount 56 | ); 57 | 58 | // const [buyTxid] = (await swapOnlyAmm(connection, wallet.payer, { 59 | // outputToken: tokenToSnipe, 60 | // targetPool: SHIT_POOL_ID, 61 | // inputTokenAmount: quoteTokenAmount, 62 | // slippage: buySlippage, 63 | // walletTokenAccounts: allWalletTokenAccounts, 64 | // wallet: wallet.payer, 65 | // })).txids; 66 | 67 | console.log(`BUY tx https://solscan.io/tx/${buyTxid}`); 68 | 69 | console.log(`${chalk.yellow('Confirming...')}`); 70 | // const _ = await connection.getLatestBlockhash({ 71 | // commitment: 'confirmed', 72 | // }); 73 | const transactionConfirmed = await confirmTransaction(connection, buyTxid); 74 | if (transactionConfirmed) { 75 | console.log(`${chalk.green('Confirmed')}`); 76 | } else { 77 | console.log(`${chalk.red('Failed :(')}`); 78 | return; 79 | } 80 | 81 | // const shitCoinAcc = await findTokenAccountAddress(connection, tokenToSnipe.mint, wallet.publicKey); 82 | // if (shitCoinAcc === null) { 83 | // console.error("Failed to fetch token balance after transaction"); 84 | // return; 85 | // } 86 | 87 | const shitTokenBalance = await getNewTokenBalance(connection, buyTxid, SHIT, wallet.publicKey.toString()); 88 | let snipedUIAmount: number 89 | if (shitTokenBalance !== undefined) { 90 | snipedUIAmount = shitTokenBalance.uiTokenAmount.uiAmount ?? 0; 91 | } else { 92 | console.log(`${chalk.red("Couldn't fetch new balance. Trying to fetch account with balance")}`) 93 | const balance = (await connection.getTokenAccountBalance(shitCoinAccount)).value 94 | snipedUIAmount = balance.uiAmount ?? 0 95 | } 96 | if (snipedUIAmount <= 0) { 97 | console.log(`${chalk.red("Couldn't get token balance, try to sell ot manually.")}`) 98 | console.log(`${chalk.yellow("BUY tx:")} ${buyTxid}`); 99 | return; 100 | } 101 | 102 | console.log(`${chalk.yellow(`Got ${snipedUIAmount} tokens`)}`); 103 | console.log('Selling') 104 | 105 | const sellTokenAmount = new TokenAmount(tokenToSnipe, snipedUIAmount, false) 106 | 107 | // const sellTxid = await sellTokens(connection, poolKeys, wsolAccount, shitCoinAcc, wallet, sellTokenAmount); 108 | 109 | let sellTxid = await swapTokens( 110 | connection, 111 | poolKeys, 112 | shitCoinAccount, 113 | wsolAccountAddress, 114 | wallet, 115 | sellTokenAmount 116 | ); 117 | 118 | // let [sellTxid] = (await swapOnlyAmm(connection, wallet.payer, { 119 | // outputToken: quoteToken, 120 | // targetPool: SHIT_POOL_ID, 121 | // inputTokenAmount: sellTokenAmount, 122 | // slippage: sellSlippage, 123 | // walletTokenAccounts: updatedWalletTokenAccounts, 124 | // wallet: wallet.payer, 125 | // })).txids; 126 | 127 | console.log(`SELL tx https://solscan.io/tx/${sellTxid}`); 128 | 129 | console.log(`${chalk.yellow('Confirming...')}`); 130 | const sellConfirmed = await confirmTransaction(connection, sellTxid); 131 | if (sellConfirmed) { 132 | console.log(`${chalk.green('Confirmed')}`); 133 | } else { 134 | console.log(`${chalk.red('Failed :( retry sell')}`); 135 | const sellTokenAmount = new TokenAmount(tokenToSnipe, snipedUIAmount, false) 136 | 137 | // const sellTxid = await sellTokens(connection, poolKeys, wsolAccount, shitCoinAcc, wallet, sellTokenAmount); 138 | 139 | sellTxid = await swapTokens( 140 | connection, 141 | poolKeys, 142 | shitCoinAccount, 143 | wsolAccountAddress, 144 | wallet, 145 | sellTokenAmount 146 | ); 147 | 148 | console.log(`${chalk.yellow('Confirming...')}`); 149 | const sellRetryConfirmed = await confirmTransaction(connection, sellTxid); 150 | if (sellRetryConfirmed) { 151 | console.log(`${chalk.green('Confirmed')}`); 152 | } else { 153 | console.log(`${chalk.red('Failed :( retry sell')}`); 154 | } 155 | } 156 | 157 | 158 | 159 | const finalWsolBalance = await getNewTokenBalance(connection, sellTxid, WSOL.mint, wallet.publicKey.toString()); 160 | const startNumber = startWsolBalance.value.uiAmount ?? 0; 161 | const endNumber = finalWsolBalance?.uiTokenAmount.uiAmount ?? 0; 162 | const shotProfit = (endNumber - startNumber) / 0.01; 163 | const profitInPercent = shotProfit * 100; 164 | const formatted = Number(profitInPercent.toPrecision(3)); 165 | const finalProfitString = formatted.toFixed(1) + '%'; 166 | 167 | console.log(`Start WSOL: ${startNumber}`); 168 | console.log(`End WSOL: ${endNumber}`); 169 | console.log(`Shot profit: ${shotProfit > 0 ? chalk.green(finalProfitString) : chalk.red(finalProfitString)}`); 170 | } 171 | 172 | runSwapTest(); -------------------------------------------------------------------------------- /client/RaydiumSwap.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey, Keypair, Transaction, VersionedTransaction, TransactionMessage, TokenBalance } from '@solana/web3.js' 2 | import { 3 | Liquidity, 4 | LiquidityPoolKeys, 5 | LiquidityAssociatedPoolKeys, 6 | jsonInfo2PoolKeys, 7 | LiquidityPoolJsonInfo, 8 | TokenAccount, 9 | Token, 10 | TokenAmount, 11 | TOKEN_PROGRAM_ID, 12 | Percent, 13 | SPL_ACCOUNT_LAYOUT, 14 | TradeV2 15 | } from '@raydium-io/raydium-sdk' 16 | import { Wallet } from '@project-serum/anchor' 17 | import base58 from 'bs58' 18 | import { printTime } from './Utils'; 19 | 20 | const OWNER_ADDRESS = new PublicKey(process.env.WALLET_PUBLIC_KEY!); 21 | 22 | class RaydiumSwap { 23 | allPoolKeysJson: LiquidityPoolJsonInfo[] = [] 24 | connection: Connection 25 | wallet: Wallet 26 | 27 | constructor(connection: Connection, privateKey: string) { 28 | this.connection = connection 29 | this.wallet = new Wallet(Keypair.fromSecretKey(base58.decode(privateKey))) 30 | } 31 | 32 | async loadPoolKeys() { 33 | const liquidityJsonResp = await fetch('https://api.raydium.io/v2/sdk/liquidity/mainnet.json') 34 | if (!liquidityJsonResp.ok) return [] 35 | const liquidityJson = (await liquidityJsonResp.json()) as { official: any; unOfficial: any } 36 | const allPoolKeysJson = [...(liquidityJson?.official ?? []), ...(liquidityJson?.unOfficial ?? [])] 37 | 38 | this.allPoolKeysJson = allPoolKeysJson 39 | } 40 | 41 | async getNewTokenBalance(hash: string, tokenAddress: PublicKey): Promise { 42 | const tr = await this.connection.getTransaction(hash); 43 | const postTokenBalances = tr?.meta?.postTokenBalances; 44 | if (postTokenBalances === null || postTokenBalances === undefined) { 45 | return undefined; 46 | } 47 | 48 | const addressStr = tokenAddress.toString(); 49 | 50 | const tokenBalance = postTokenBalances.find((x) => x.mint === addressStr && x.owner === OWNER_ADDRESS.toString()); 51 | return tokenBalance; 52 | } 53 | 54 | findPoolInfoForTokens(mintA: string, mintB: string): LiquidityPoolKeys | null { 55 | const poolData = this.allPoolKeysJson.find( 56 | (i) => (i.baseMint === mintA && i.quoteMint === mintB) || (i.baseMint === mintB && i.quoteMint === mintA) 57 | ) 58 | 59 | if (!poolData) return null 60 | 61 | return jsonInfo2PoolKeys(poolData) as LiquidityPoolKeys 62 | } 63 | 64 | async getOwnerTokenAccounts() { 65 | const walletTokenAccount = await this.connection.getTokenAccountsByOwner(this.wallet.publicKey, { 66 | programId: TOKEN_PROGRAM_ID, 67 | }) 68 | 69 | return walletTokenAccount.value.map((i) => ({ 70 | pubkey: i.pubkey, 71 | programId: i.account.owner, 72 | accountInfo: SPL_ACCOUNT_LAYOUT.decode(i.account.data), 73 | })) 74 | } 75 | 76 | async getSwapTransaction( 77 | toToken: PublicKey, 78 | // fromToken: string, 79 | amount: number, 80 | poolKeys: LiquidityPoolKeys, 81 | maxLamports: number = 100000, 82 | useVersionedTransaction = true, 83 | fixedSide: 'in' | 'out' = 'in' 84 | ): Promise { 85 | const directionIn = poolKeys.quoteMint.toString() == toToken.toString() 86 | const { minAmountOut, amountIn } = await this.calcAmountOut(poolKeys, amount, directionIn) 87 | 88 | const userTokenAccounts = await this.getOwnerTokenAccounts() 89 | const swapTransaction = await Liquidity.makeSwapInstructionSimple({ 90 | connection: this.connection, 91 | makeTxVersion: useVersionedTransaction ? 0 : 1, 92 | poolKeys: { 93 | ...poolKeys, 94 | }, 95 | userKeys: { 96 | tokenAccounts: userTokenAccounts, 97 | owner: this.wallet.publicKey, 98 | }, 99 | amountIn: amountIn, 100 | amountOut: minAmountOut, 101 | fixedSide: fixedSide, 102 | config: { 103 | bypassAssociatedCheck: false, 104 | }, 105 | computeBudgetConfig: { 106 | microLamports: maxLamports, 107 | }, 108 | }) 109 | 110 | const recentBlockhashForSwap = await this.connection.getLatestBlockhash() 111 | const instructions = swapTransaction.innerTransactions[0].instructions.filter(Boolean) 112 | 113 | if (useVersionedTransaction) { 114 | const versionedTransaction = new VersionedTransaction( 115 | new TransactionMessage({ 116 | payerKey: this.wallet.publicKey, 117 | recentBlockhash: recentBlockhashForSwap.blockhash, 118 | instructions: instructions, 119 | }).compileToV0Message() 120 | ) 121 | 122 | versionedTransaction.sign([this.wallet.payer]) 123 | 124 | return versionedTransaction 125 | } 126 | 127 | const legacyTransaction = new Transaction({ 128 | blockhash: recentBlockhashForSwap.blockhash, 129 | lastValidBlockHeight: recentBlockhashForSwap.lastValidBlockHeight, 130 | feePayer: this.wallet.publicKey, 131 | }) 132 | 133 | legacyTransaction.add(...instructions) 134 | 135 | return legacyTransaction 136 | } 137 | 138 | async sendLegacyTransaction(tx: Transaction) { 139 | const txid = await this.connection.sendTransaction(tx, [this.wallet.payer], { 140 | skipPreflight: true, 141 | maxRetries: 2, 142 | }) 143 | 144 | return txid 145 | } 146 | 147 | async sendVersionedTransaction(tx: VersionedTransaction) { 148 | const txid = await this.connection.sendTransaction(tx, { 149 | skipPreflight: true, 150 | maxRetries: 3, 151 | }) 152 | return txid 153 | } 154 | 155 | async simulateLegacyTransaction(tx: Transaction) { 156 | const txid = await this.connection.simulateTransaction(tx, [this.wallet.payer]) 157 | 158 | return txid 159 | } 160 | 161 | async simulateVersionedTransaction(tx: VersionedTransaction) { 162 | const txid = await this.connection.simulateTransaction(tx) 163 | 164 | return txid 165 | } 166 | 167 | getTokenAccountByOwnerAndMint(mint: PublicKey) { 168 | return { 169 | programId: TOKEN_PROGRAM_ID, 170 | pubkey: PublicKey.default, 171 | accountInfo: { 172 | mint: mint, 173 | amount: 0, 174 | }, 175 | } as unknown as TokenAccount 176 | } 177 | 178 | async calcAmountOut(poolKeys: LiquidityPoolKeys, rawAmountIn: number, swapInDirection: boolean) { 179 | const poolInfo = await Liquidity.fetchInfo({ connection: this.connection, poolKeys }) 180 | 181 | let currencyInMint = poolKeys.baseMint 182 | let currencyInDecimals = poolInfo.baseDecimals 183 | let currencyOutMint = poolKeys.quoteMint 184 | let currencyOutDecimals = poolInfo.quoteDecimals 185 | 186 | if (!swapInDirection) { 187 | currencyInMint = poolKeys.quoteMint 188 | currencyInDecimals = poolInfo.quoteDecimals 189 | currencyOutMint = poolKeys.baseMint 190 | currencyOutDecimals = poolInfo.baseDecimals 191 | } 192 | 193 | const currencyIn = new Token(TOKEN_PROGRAM_ID, currencyInMint, currencyInDecimals) 194 | const amountIn = new TokenAmount(currencyIn, rawAmountIn, false) 195 | const currencyOut = new Token(TOKEN_PROGRAM_ID, currencyOutMint, currencyOutDecimals) 196 | const slippage = swapInDirection ? (new Percent(20, 100)) : (new Percent(5, 100)); // 10% slippage 197 | 198 | const { amountOut, minAmountOut, currentPrice, executionPrice, priceImpact, fee } = Liquidity.computeAmountOut({ 199 | poolKeys, 200 | poolInfo, 201 | amountIn, 202 | currencyOut, 203 | slippage, 204 | }) 205 | 206 | return { 207 | amountIn, 208 | amountOut, 209 | minAmountOut, 210 | currentPrice, 211 | executionPrice, 212 | priceImpact, 213 | fee, 214 | } 215 | } 216 | } 217 | 218 | export default RaydiumSwap -------------------------------------------------------------------------------- /client/Utils.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN_PROGRAM_ID, TokenAccount, SPL_ACCOUNT_LAYOUT, LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk'; 2 | import { Connection, PublicKey, SignatureResult, Commitment, TokenBalance } from '@solana/web3.js' 3 | import chalk from 'chalk'; 4 | import { BN } from '@project-serum/anchor'; 5 | import { PoolKeys } from './PoolValidator/RaydiumPoolParser'; 6 | 7 | export function printTime(date: Date) { 8 | const formatted = formatDate(date); 9 | console.log(formatted); 10 | } 11 | 12 | export function formatDate(date: Date): string { 13 | const hours = date.getHours().toString().padStart(2, '0'); 14 | const minutes = date.getMinutes().toString().padStart(2, '0'); 15 | const seconds = date.getSeconds().toString().padStart(2, '0'); 16 | const milliseconds = date.getMilliseconds().toString().padStart(3, '0'); 17 | const formattedTime = `${hours}:${minutes}:${seconds}.${milliseconds}`; 18 | return formattedTime; 19 | } 20 | 21 | export function timeout(ms: number, cancellationToken: { cancelled: boolean } | null = null): Promise { 22 | return new Promise((_, reject) => { 23 | setTimeout(() => { 24 | if (cancellationToken != null) { 25 | cancellationToken.cancelled = true; 26 | } 27 | reject(new Error(`Timed out after ${ms}ms`)); 28 | }, ms); 29 | }); 30 | } 31 | 32 | export function delay(ms: number): Promise { 33 | return new Promise(resolve => setTimeout(resolve, ms)); 34 | } 35 | 36 | export async function findTokenAccountAddress(connection: Connection, tokenMintAddress: PublicKey, owner: PublicKey): Promise { 37 | const tokenAccountsByOwner = await connection.getParsedTokenAccountsByOwner( 38 | owner, 39 | { programId: TOKEN_PROGRAM_ID } 40 | ); 41 | 42 | const myTokenAccount = tokenAccountsByOwner.value.find(account => account.account.data.parsed.info.mint === tokenMintAddress.toString()); 43 | if (myTokenAccount) { 44 | console.log('Your token account address:', myTokenAccount.pubkey.toString()); 45 | return myTokenAccount.pubkey; 46 | } else { 47 | console.log('Token account not found for this mint address and wallet.'); 48 | return null; 49 | } 50 | } 51 | 52 | export async function getTransactionConfirmation(connection: Connection, txid: string): Promise { 53 | console.log(`Confirming...`) 54 | const confirmResult = await connection.confirmTransaction({ signature: txid, ...(await connection.getLatestBlockhash()) }, 'confirmed'); 55 | console.log(`Confirming... Get results`) 56 | return confirmResult.value; 57 | } 58 | 59 | export async function confirmTransaction(connection: Connection, txid: string): Promise { 60 | try { 61 | const confirmResult = await Promise.race([ 62 | getTransactionConfirmation(connection, txid), 63 | timeout(20 * 1000) 64 | ]) 65 | const transactionFailed = confirmResult.err !== null; 66 | if (transactionFailed) { 67 | console.log(`Buying transaction ${chalk.bold(txid)} ${chalk.red('FAILED')}. Error: ${chalk.redBright(JSON.stringify(confirmResult.err))}`); 68 | return false; 69 | } 70 | return true; 71 | } catch (e) { 72 | console.log(`Buying transaction ${chalk.bold(txid)} ${chalk.red('FAILED')}. Error: ${chalk.redBright(e)}`); 73 | return false; 74 | } 75 | } 76 | 77 | export async function getTokenAccounts( 78 | connection: Connection, 79 | owner: PublicKey, 80 | commitment?: Commitment, 81 | ) { 82 | 83 | const tokenResp = await connection.getTokenAccountsByOwner( 84 | owner, 85 | { 86 | programId: TOKEN_PROGRAM_ID, 87 | }, 88 | commitment, 89 | ); 90 | 91 | const accounts: TokenAccount[] = []; 92 | for (const { pubkey, account } of tokenResp.value) { 93 | accounts.push({ 94 | pubkey, 95 | programId: account.owner, 96 | accountInfo: SPL_ACCOUNT_LAYOUT.decode(account.data), 97 | }); 98 | } 99 | 100 | return accounts; 101 | } 102 | 103 | export async function getNewTokenBalance(connection: Connection, hash: string, tokenAddress: string, ownerAddress: string): Promise { 104 | let tr = await connection.getTransaction(hash, { maxSupportedTransactionVersion: 0, commitment: 'confirmed' }); 105 | if (tr === null) { 106 | const _ = await connection.getLatestBlockhash('confirmed'); 107 | tr = await connection.getTransaction(hash, { maxSupportedTransactionVersion: 0, commitment: 'confirmed' }); 108 | } 109 | const postTokenBalances = tr?.meta?.postTokenBalances; 110 | if (postTokenBalances === null || postTokenBalances === undefined) { 111 | return undefined; 112 | } 113 | const tokenBalance = postTokenBalances.find((x) => x.mint === tokenAddress && x.owner === ownerAddress); 114 | return tokenBalance; 115 | } 116 | 117 | export async function makeTokenAccount() { 118 | //splToken.createAssociatedTokenAccountIdempotent 119 | } 120 | 121 | export function lamportsToSOLNumber(lamportsBN: BN, decimals: number = 9): number | undefined { 122 | //const SOL_DECIMALS = 9; // SOL has 9 decimal places 123 | const divisor = new BN(10).pow(new BN(decimals)); 124 | 125 | // Convert lamports to SOL as a BN to maintain precision 126 | const solBN = lamportsBN.div(divisor); 127 | 128 | // Additionally, handle fractional part if necessary 129 | const fractionalBN = lamportsBN.mod(divisor); 130 | 131 | // Convert integer part to number 132 | if (solBN.lte(new BN(Number.MAX_SAFE_INTEGER))) { 133 | const integerPart = solBN.toNumber(); 134 | const fractionalPart = fractionalBN.toNumber() / Math.pow(10, decimals); 135 | 136 | // Combine integer and fractional parts 137 | const total = integerPart + fractionalPart; 138 | 139 | return total; 140 | } else { 141 | console.warn('The amount of SOL exceeds the safe integer limit for JavaScript numbers.'); 142 | return undefined; // or handle as appropriate 143 | } 144 | } 145 | 146 | export async function retryAsyncFunction( 147 | fn: (...args: Args) => Promise, // Async function to retry 148 | args: Args, // Arguments of the async function 149 | retries: number = 5, // Number of retries 150 | delayMs: number = 300 // Delay between retries in milliseconds 151 | ): Promise { 152 | let lastError: Error | undefined; 153 | 154 | for (let attempt = 0; attempt < retries; attempt++) { 155 | try { 156 | return await fn(...args); // Attempt to execute the function 157 | } catch (error) { 158 | lastError = error as Error; 159 | if (attempt < retries - 1) { 160 | await delay(delayMs) // Wait for the delay before retrying 161 | } 162 | } 163 | } 164 | 165 | // If all retries failed, throw the last error 166 | throw lastError; 167 | } 168 | 169 | export async function retryAsyncFunctionOrDefault( 170 | fn: (...args: Args) => Promise, // Async function to retry 171 | args: Args, // Arguments of the async function 172 | defaultValue: T, // Default value if all attempts failed 173 | retries: number = 5, // Number of retries 174 | delay: number = 300 // Delay between retries in milliseconds 175 | ): Promise { 176 | try { 177 | return retryAsyncFunction(fn, args, retries, delay) 178 | } catch { 179 | return defaultValue 180 | } 181 | } 182 | 183 | export function convertStringKeysToDataKeys(poolInfo: PoolKeys): LiquidityPoolKeysV4 { 184 | return { 185 | id: new PublicKey(poolInfo.id), 186 | baseMint: new PublicKey(poolInfo.baseMint), 187 | quoteMint: new PublicKey(poolInfo.quoteMint), 188 | lpMint: new PublicKey(poolInfo.lpMint), 189 | baseDecimals: poolInfo.baseDecimals, 190 | quoteDecimals: poolInfo.quoteDecimals, 191 | lpDecimals: poolInfo.lpDecimals, 192 | version: 4, 193 | programId: new PublicKey(poolInfo.programId), 194 | authority: new PublicKey(poolInfo.authority), 195 | openOrders: new PublicKey(poolInfo.openOrders), 196 | targetOrders: new PublicKey(poolInfo.targetOrders), 197 | baseVault: new PublicKey(poolInfo.baseVault), 198 | quoteVault: new PublicKey(poolInfo.quoteVault), 199 | withdrawQueue: new PublicKey(poolInfo.withdrawQueue), 200 | lpVault: new PublicKey(poolInfo.lpVault), 201 | marketVersion: 3, 202 | marketProgramId: new PublicKey(poolInfo.marketProgramId), 203 | marketId: new PublicKey(poolInfo.marketId), 204 | marketAuthority: new PublicKey(poolInfo.marketAuthority), 205 | marketBaseVault: new PublicKey(poolInfo.baseVault), 206 | marketQuoteVault: new PublicKey(poolInfo.quoteVault), 207 | marketBids: new PublicKey(poolInfo.marketBids), 208 | marketAsks: new PublicKey(poolInfo.marketAsks), 209 | marketEventQueue: new PublicKey(poolInfo.marketEventQueue), 210 | } as LiquidityPoolKeysV4; 211 | } -------------------------------------------------------------------------------- /client/TurboBot.ts: -------------------------------------------------------------------------------- 1 | import { Connection, Logs, PublicKey } from '@solana/web3.js'; 2 | import { findLogEntry } from './PoolValidator/RaydiumPoolParser'; 3 | import chalk from 'chalk'; 4 | import { parsePoolCreationTx, ParsedPoolCreationTx, checkIfPoolPostponed, checkIfSwapEnabled } from './PoolValidator/RaydiumPoolValidator'; 5 | import { config } from './Config'; 6 | import { SellResults } from './Trader/SellToken'; 7 | import { OWNER_ADDRESS, SOL_SPL_TOKEN_ADDRESS } from './Trader/Addresses'; 8 | import { instaBuyAndSell, tryPerformTrading } from './Trader/Trader'; 9 | import { checkToken } from './PoolValidator/RaydiumSafetyCheck'; 10 | import { TradingWallet } from './StateAggregator/StateTypes'; 11 | import WebSocket from 'ws'; 12 | import { getPoolInfo } from './ManualTrader'; 13 | import { convertStringKeysToDataKeys } from './Utils'; 14 | 15 | 16 | const RAYDIUM_PUBLIC_KEY = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'; 17 | 18 | const LOW_LP_IN_USD = 500; 19 | const HIGH_LP_IN_USD = 100000000; 20 | 21 | export class TurboBot { 22 | private seenTxs = new Set() 23 | private onLogsSubscriptionId: number | null = null 24 | private connection: Connection 25 | private backupConnection: Connection | null 26 | private tradingWallet: TradingWallet = { 27 | id: 0, 28 | startValue: 1, 29 | current: 1, 30 | totalProfit: 0 31 | } 32 | 33 | constructor(connection: Connection, backupConnection: Connection | null = null) { 34 | this.connection = connection 35 | this.backupConnection = backupConnection 36 | } 37 | 38 | isStarted() { 39 | return this.onLogsSubscriptionId !== null 40 | } 41 | 42 | private updateWSOLBalance(tradeResults: SellResults) { 43 | if (tradeResults.boughtForSol) { 44 | const soldForSol = tradeResults.kind === 'SUCCESS' ? tradeResults.soldForSOL : 0 45 | const profitAbsolute = soldForSol - tradeResults.boughtForSol 46 | const newWalletBalance = this.tradingWallet.current + profitAbsolute 47 | const totalProfit = (newWalletBalance - this.tradingWallet.startValue) / this.tradingWallet.startValue 48 | this.tradingWallet = { ...this.tradingWallet, current: newWalletBalance, totalProfit } 49 | 50 | console.log(`Wallet:\n${JSON.stringify(this.tradingWallet, null, 2)}`) 51 | //updateTradingWalletRecord(this.tradingWallet) 52 | } 53 | } 54 | 55 | private async updateRealWsolBalance() { 56 | const newWalletBalance = (await this.connection.getTokenAccountBalance(SOL_SPL_TOKEN_ADDRESS)).value.uiAmount ?? 0 57 | const totalProfit = (newWalletBalance - this.tradingWallet.startValue) / this.tradingWallet.startValue 58 | this.tradingWallet = { ...this.tradingWallet, current: newWalletBalance, totalProfit } 59 | console.log(`Wallet:\n${JSON.stringify(this.tradingWallet, null, 2)}`) 60 | } 61 | 62 | private async fetchInitialWalletSOLBalance() { 63 | if (config.simulateOnly) { return } 64 | console.log(`Fetching wallet balance`) 65 | const balance = (await this.connection.getTokenAccountBalance(SOL_SPL_TOKEN_ADDRESS)).value.uiAmount ?? 0 66 | console.log(`Balance is ${balance}`) 67 | this.tradingWallet.current = balance 68 | this.tradingWallet.startValue = balance 69 | } 70 | 71 | async buySellQuickTest(poolCreationTx: string) { 72 | console.log(`Start Solana bot. Simulation=${config.simulateOnly}`) 73 | await this.fetchInitialWalletSOLBalance() 74 | console.log(`Wallet:\n${JSON.stringify(this.tradingWallet, null, 2)}`) 75 | 76 | 77 | const poolInfo = await getPoolInfo(this.connection, new PublicKey(poolCreationTx)) 78 | 79 | console.log(`Pool parsed, buying and selling`) 80 | const tradeResults = await instaBuyAndSell(this.connection, convertStringKeysToDataKeys(poolInfo), 0.01) 81 | console.log(chalk.yellow('Got trading results')) 82 | console.log(`BUY at ${tradeResults.buyTime ?? 'null'}`) 83 | if (tradeResults.kind === 'SUCCESS') { 84 | console.log(`SELL at ${tradeResults.sellTime}`) 85 | } else { 86 | console.log(`Couldn't sell`) 87 | } 88 | this.updateWSOLBalance(tradeResults) 89 | } 90 | 91 | async start(singleTrade: boolean = false) { 92 | return new Promise(async (resolve, reject) => { 93 | console.log(`Start Solana bot. Simulation=${config.simulateOnly}`) 94 | await this.fetchInitialWalletSOLBalance() 95 | console.log(`Wallet:\n${JSON.stringify(this.tradingWallet, null, 2)}`) 96 | 97 | const raydium = new PublicKey(RAYDIUM_PUBLIC_KEY); 98 | 99 | let isCheckingPool = false 100 | this.onLogsSubscriptionId = this.connection.onLogs(raydium, async (txLogs) => { 101 | //console.log(`Log received. ${txLogs.signature}`) 102 | if (isCheckingPool || this.seenTxs.has(txLogs.signature)) { return } 103 | isCheckingPool = true 104 | this.seenTxs.add(txLogs.signature) 105 | const parsedInfo = await this.parseTx(txLogs) 106 | if (!parsedInfo) { 107 | isCheckingPool = false 108 | return 109 | } 110 | const check = await checkToken(this.connection, parsedInfo, true) 111 | if (check.kind === 'CreatorIsScammer') { 112 | console.log(`Pool ${parsedInfo.poolKeys.id} - creator is known scammer`) 113 | isCheckingPool = false 114 | return 115 | } 116 | 117 | if (check.kind !== 'Complete') { 118 | console.log(`Pool ${parsedInfo.poolKeys.id} discarded`) 119 | isCheckingPool = false 120 | return 121 | } 122 | 123 | if (check.data.totalLiquidity.amountInUSD < LOW_LP_IN_USD || check.data.totalLiquidity.amountInUSD > HIGH_LP_IN_USD) { 124 | console.log(`Pool ${parsedInfo.poolKeys.id} - Liquidity is too low or too high. ${check.data.totalLiquidity.amount} ${check.data.totalLiquidity.symbol}`) 125 | isCheckingPool = false 126 | return 127 | } 128 | 129 | if (check.data.ownershipInfo.isMintable) { 130 | console.log(`Pool ${parsedInfo.poolKeys.id} - token is mintable`) 131 | isCheckingPool = false 132 | return 133 | } 134 | 135 | console.log(`Pool looks good, buying.`) 136 | const tradeResults = await tryPerformTrading(this.connection, check.data.pool, 'TURBO') 137 | console.log(chalk.yellow('Got trading results')) 138 | console.log(`BUY at ${tradeResults.buyTime ?? 'null'}`) 139 | if (tradeResults.kind === 'SUCCESS') { 140 | console.log(`SELL at ${tradeResults.sellTime}`) 141 | } else { 142 | console.log(`Couldn't sell`) 143 | } 144 | if (config.simulateOnly) { 145 | this.updateWSOLBalance(tradeResults) 146 | } else { 147 | await this.updateRealWsolBalance() 148 | } 149 | 150 | if (singleTrade) { 151 | this.connection.removeOnLogsListener(this.onLogsSubscriptionId ?? 0) 152 | this.onLogsSubscriptionId = null 153 | resolve() 154 | } 155 | isCheckingPool = false 156 | }) 157 | 158 | // const ws = new WebSocket(config.rpcWsURL) 159 | // ws.onopen = () => { 160 | // ws.send( 161 | // JSON.stringify({ 162 | // "jsonrpc": "2.0", 163 | // "id": 1, 164 | // "method": "logsSubscribe", 165 | // "params": ["all"] 166 | // } 167 | // ) 168 | // ) 169 | 170 | // ws.onmessage = (evt) => { 171 | // try { 172 | // console.log(`New logs from WS: ${evt.data.toString()}`) 173 | // } catch (e) { 174 | // console.log(e) 175 | // } 176 | // } 177 | // } 178 | // ws.onerror = (e) => { 179 | // console.log(`WS error1: ${e.error.errors[0]}`) 180 | // console.log(`WS error2: ${e.error.errors[1]}`) 181 | // } 182 | }) 183 | 184 | 185 | } 186 | 187 | private async parseTx(txLogs: Logs): Promise { 188 | const logEntry = findLogEntry('init_pc_amount', txLogs.logs) 189 | if (!logEntry) { return null } 190 | return this.getPoolCreationTx(txLogs.signature) 191 | } 192 | 193 | private async getPoolCreationTx(txSignature: string): Promise { 194 | try { 195 | const info = await parsePoolCreationTx(this.connection, txSignature) 196 | const postponeInfo = checkIfPoolPostponed(info) 197 | if (postponeInfo.startTime) { 198 | console.log(`Pool ${info.poolKeys.id} is postponed`) 199 | return null 200 | } 201 | const isEnabled = checkIfSwapEnabled(info).isEnabled 202 | if (!isEnabled) { 203 | console.log(`Pool ${info.poolKeys.id} is disabled`) 204 | return null 205 | } 206 | return info 207 | } catch (e) { 208 | console.error(`Failed to parse tx. ${e}`) 209 | return null 210 | } 211 | } 212 | 213 | } -------------------------------------------------------------------------------- /client/ObserveOpenBooks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, PublicKey, 3 | ParsedInstruction, 4 | PartiallyDecodedInstruction, 5 | Commitment, KeyedAccountInfo 6 | } from "@solana/web3.js"; 7 | import { printTime } from "./Utils"; 8 | import { 9 | GetStructureSchema, 10 | LIQUIDITY_STATE_LAYOUT_V4, 11 | LiquidityStateV4, 12 | Liquidity, LiquidityPoolKeys, 13 | MAINNET_PROGRAM_ID, MARKET_STATE_LAYOUT_V2, 14 | Market, struct, publicKey, Token, MARKET_STATE_LAYOUT_V3, MarketStateV3, LiquidityPoolKeysV4 15 | } from "@raydium-io/raydium-sdk"; 16 | export const RAYDIUM_LIQUIDITY_PROGRAM_ID_V4 = MAINNET_PROGRAM_ID.AmmV4; 17 | export const OPENBOOK_PROGRAM_ID = MAINNET_PROGRAM_ID.OPENBOOK_MARKET; 18 | import chalk from 'chalk'; 19 | import bs58 from "bs58"; 20 | import { getAssociatedTokenAddressSync } from "@solana/spl-token"; 21 | 22 | const MINIMAL_MARKET_STATE_LAYOUT_V3 = struct([ 23 | publicKey('eventQueue'), 24 | publicKey('bids'), 25 | publicKey('asks'), 26 | ]); 27 | 28 | export type MinimalMarketStateLayoutV3 = typeof MINIMAL_MARKET_STATE_LAYOUT_V3; 29 | export type MinimalMarketLayoutV3 = 30 | GetStructureSchema; 31 | 32 | export type MinimalTokenAccountData = { 33 | mint: PublicKey; 34 | address: PublicKey; 35 | poolKeys?: LiquidityPoolKeys; 36 | market?: MinimalMarketLayoutV3; 37 | }; 38 | 39 | const OWNER_ADDRESS = new PublicKey(process.env.WALLET_PUBLIC_KEY!); 40 | 41 | const solanaConnection = new Connection(process.env.RPC_URL!, { 42 | wsEndpoint: process.env.WS_URL! 43 | }); 44 | const commitment: Commitment = 'confirmed'; 45 | const quoteToken = Token.WSOL; 46 | let existingLiquidityPools: Set = new Set(); 47 | let existingOpenBookMarkets: Set = new Set(); 48 | let existingTokenAccounts: Map = new Map< 49 | string, 50 | MinimalTokenAccountData 51 | >(); 52 | 53 | function shouldBuy(key: string): boolean { 54 | return true; 55 | //return USE_SNIPE_LIST ? snipeList.includes(key) : true; 56 | } 57 | 58 | function createPoolKeys( 59 | id: PublicKey, 60 | accountData: LiquidityStateV4, 61 | minimalMarketLayoutV3: MinimalMarketLayoutV3, 62 | ): LiquidityPoolKeys { 63 | return { 64 | id, 65 | baseMint: accountData.baseMint, 66 | quoteMint: accountData.quoteMint, 67 | lpMint: accountData.lpMint, 68 | baseDecimals: accountData.baseDecimal.toNumber(), 69 | quoteDecimals: accountData.quoteDecimal.toNumber(), 70 | lpDecimals: 5, 71 | version: 4, 72 | programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, 73 | authority: Liquidity.getAssociatedAuthority({ 74 | programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, 75 | }).publicKey, 76 | openOrders: accountData.openOrders, 77 | targetOrders: accountData.targetOrders, 78 | baseVault: accountData.baseVault, 79 | quoteVault: accountData.quoteVault, 80 | marketVersion: 3, 81 | marketProgramId: accountData.marketProgramId, 82 | marketId: accountData.marketId, 83 | marketAuthority: Market.getAssociatedAuthority({ 84 | programId: accountData.marketProgramId, 85 | marketId: accountData.marketId, 86 | }).publicKey, 87 | marketBaseVault: accountData.baseVault, 88 | marketQuoteVault: accountData.quoteVault, 89 | marketBids: minimalMarketLayoutV3.bids, 90 | marketAsks: minimalMarketLayoutV3.asks, 91 | marketEventQueue: minimalMarketLayoutV3.eventQueue, 92 | withdrawQueue: accountData.withdrawQueue, 93 | lpVault: accountData.lpVault, 94 | lookupTableAccount: PublicKey.default, 95 | }; 96 | } 97 | 98 | function makePool( 99 | accountId: PublicKey, 100 | accountData: LiquidityStateV4, 101 | ): LiquidityPoolKeys | null { 102 | const tokenAccount = existingTokenAccounts.get( 103 | accountData.baseMint.toString(), 104 | ); 105 | 106 | if (!tokenAccount) { 107 | return null; 108 | } 109 | 110 | return createPoolKeys( 111 | accountId, 112 | accountData, 113 | tokenAccount.market!, 114 | ); 115 | 116 | // tokenAccount.poolKeys = createPoolKeys( 117 | // accountId, 118 | // accountData, 119 | // tokenAccount.market!, 120 | // ); 121 | // const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction( 122 | // { 123 | // poolKeys: tokenAccount.poolKeys, 124 | // userKeys: { 125 | // tokenAccountIn: quoteTokenAssociatedAddress, 126 | // tokenAccountOut: tokenAccount.address, 127 | // owner: wallet.publicKey, 128 | // }, 129 | // amountIn: quoteAmount.raw, 130 | // minAmountOut: 0, 131 | // }, 132 | // tokenAccount.poolKeys.version, 133 | // ); 134 | 135 | // const latestBlockhash = await solanaConnection.getLatestBlockhash({ 136 | // commitment: commitment, 137 | // }); 138 | // const messageV0 = new TransactionMessage({ 139 | // payerKey: wallet.publicKey, 140 | // recentBlockhash: latestBlockhash.blockhash, 141 | // instructions: [ 142 | // ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }), 143 | // ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30000 }), 144 | // createAssociatedTokenAccountIdempotentInstruction( 145 | // wallet.publicKey, 146 | // tokenAccount.address, 147 | // wallet.publicKey, 148 | // accountData.baseMint, 149 | // ), 150 | // ...innerTransaction.instructions, 151 | // ], 152 | // }).compileToV0Message(); 153 | // const transaction = new VersionedTransaction(messageV0); 154 | // transaction.sign([wallet, ...innerTransaction.signers]); 155 | // const signature = await solanaConnection.sendRawTransaction( 156 | // transaction.serialize(), 157 | // { 158 | // maxRetries: 20, 159 | // preflightCommitment: commitment, 160 | // }, 161 | // ); 162 | // logger.info( 163 | // { 164 | // mint: accountData.baseMint, 165 | // url: `https://solscan.io/tx/${signature}?cluster=${network}`, 166 | // }, 167 | // 'Buy', 168 | // ); 169 | } 170 | 171 | function processRaydiumPool(updatedAccountInfo: KeyedAccountInfo): LiquidityPoolKeys | null { 172 | let accountData: LiquidityStateV4 | undefined; 173 | try { 174 | accountData = LIQUIDITY_STATE_LAYOUT_V4.decode( 175 | updatedAccountInfo.accountInfo.data, 176 | ); 177 | 178 | if (!shouldBuy(accountData.baseMint.toString())) { 179 | return null; 180 | } 181 | 182 | return makePool(updatedAccountInfo.accountId, accountData); 183 | } catch (e) { 184 | console.error({ ...accountData, error: e }, `Failed to process pool`); 185 | return null; 186 | } 187 | } 188 | 189 | function processOpenBookMarket( 190 | updatedAccountInfo: KeyedAccountInfo, 191 | ) { 192 | let accountData: MarketStateV3 | undefined; 193 | try { 194 | accountData = MARKET_STATE_LAYOUT_V3.decode( 195 | updatedAccountInfo.accountInfo.data, 196 | ); 197 | 198 | // to be competitive, we collect market data before buying the token... 199 | if (existingTokenAccounts.has(accountData.baseMint.toString())) { 200 | return; 201 | } 202 | 203 | const ata = getAssociatedTokenAddressSync( 204 | accountData.baseMint, 205 | OWNER_ADDRESS, 206 | ); 207 | existingTokenAccounts.set(accountData.baseMint.toString(), < 208 | MinimalTokenAccountData 209 | >{ 210 | address: ata, 211 | mint: accountData.baseMint, 212 | market: { 213 | bids: accountData.bids, 214 | asks: accountData.asks, 215 | eventQueue: accountData.eventQueue, 216 | }, 217 | }); 218 | } catch (e) { 219 | console.error({ ...accountData, error: e }, `Failed to process market`); 220 | } 221 | } 222 | 223 | export function startObserving(onNewPool: (pool: LiquidityPoolKeys) => void) { 224 | const raydiumSubscriptionId = solanaConnection.onProgramAccountChange( 225 | RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, 226 | async (updatedAccountInfo) => { 227 | const key = updatedAccountInfo.accountId.toString(); 228 | const existing = existingLiquidityPools.has(key); 229 | if (!existing) { 230 | existingLiquidityPools.add(key); 231 | const pool = processRaydiumPool(updatedAccountInfo); 232 | let x: LiquidityPoolKeysV4 233 | 234 | if (pool !== null) { 235 | onNewPool(pool); 236 | } 237 | } 238 | }, 239 | commitment, 240 | [ 241 | { dataSize: LIQUIDITY_STATE_LAYOUT_V4.span }, 242 | { 243 | memcmp: { 244 | offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'), 245 | bytes: quoteToken.mint.toBase58(), 246 | }, 247 | }, 248 | { 249 | memcmp: { 250 | offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'), 251 | bytes: OPENBOOK_PROGRAM_ID.toBase58(), 252 | }, 253 | }, 254 | { 255 | memcmp: { 256 | offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'), 257 | bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]), 258 | }, 259 | }, 260 | ], 261 | ); 262 | 263 | const openBookSubscriptionId = solanaConnection.onProgramAccountChange( 264 | OPENBOOK_PROGRAM_ID, 265 | async (updatedAccountInfo) => { 266 | console.log('On OPENBOOK_PROGRAM_ID'); 267 | const key = updatedAccountInfo.accountId.toString(); 268 | const existing = existingOpenBookMarkets.has(key); 269 | if (!existing) { 270 | existingOpenBookMarkets.add(key); 271 | const _ = processOpenBookMarket(updatedAccountInfo); 272 | } 273 | }, 274 | commitment, 275 | [ 276 | { dataSize: MARKET_STATE_LAYOUT_V2.span }, 277 | { 278 | memcmp: { 279 | offset: MARKET_STATE_LAYOUT_V2.offsetOf('quoteMint'), 280 | bytes: quoteToken.mint.toBase58(), 281 | }, 282 | }, 283 | ], 284 | ); 285 | 286 | } -------------------------------------------------------------------------------- /client/StateAggregator/ConsoleOutput.ts: -------------------------------------------------------------------------------- 1 | import Table from 'cli-table3' 2 | import { StateRecord, createStateRecord } from './StateTypes' 3 | import { WSOL } from '@raydium-io/raydium-sdk' 4 | import { formatDate } from '../Utils'; 5 | import { ParsedPoolCreationTx } from '../PoolValidator/RaydiumPoolValidator'; 6 | import { PoolSafetyData, SafetyCheckResult } from '../PoolValidator/RaydiumSafetyCheck'; 7 | import { TokenSafetyStatus } from '../PoolValidator/ValidationResult'; 8 | import { SellResults } from '../Trader/SellToken'; 9 | import { BuyResult } from '../Trader/BuyToken'; 10 | import { dbIsInited, getStateRecordByPoolId, upsertRecord } from './DbWriter' 11 | import chalk from 'chalk'; 12 | 13 | 14 | ///Color(+Reason) ID(First mint TX/PoolId) StartTime TokenId Liquidity Percent in Pool Is Mintable Buy Sell Profit 15 | 16 | //let allRecordsByFirstMintTx = new Map() 17 | 18 | async function getOrMakeRecordByTxId(poolId: string): Promise { 19 | const dbRecord = await getStateRecordByPoolId(poolId) 20 | return dbRecord ?? createStateRecord({ poolId, status: 'Just created' }) 21 | } 22 | 23 | 24 | const table = new Table({ 25 | head: ['Status', 'ID', 'Safety', 'Start Time', 'Token', 'Buy', 'Sell', 'Profit', 'Max Profit'] 26 | , //colWidths: [100, 200] 27 | }) 28 | 29 | // export function renderCurrentState() { 30 | // table.length = 0 31 | 32 | // for (const record of allRecordsByFirstMintTx.values()) { 33 | // const converted = recordToConsoleTable(record) 34 | // table.push(converted) 35 | // } 36 | 37 | // logUpdate(table.toString()) 38 | // } 39 | 40 | function recordToConsoleTable(record: StateRecord): string[] { 41 | // let safetyStatus = '⚪\nUnknown' 42 | // case 'EQUILIBRIUM': return '🟢 Good' //'🟡 So So' 43 | // case 'PUMPING': return '🟢 Good' 44 | // case 'DUMPING': return '🔴 Bad' 45 | // default: return '⚪ Unknown' 46 | 47 | let idStr = `Pool: https://solscan.io/address/${record.poolId}` 48 | 49 | let tokenStr = '' 50 | if (record.tokenId) { 51 | tokenStr = `https://solscan.io/token/${record.tokenId}` 52 | } 53 | 54 | return [ 55 | record.status, 56 | idStr, 57 | record.safetyInfo ?? '⚪', 58 | record.startTime ?? '', 59 | tokenStr, 60 | record.buyInfo ?? '', 61 | record.safetyInfo ?? '', 62 | record.profit ?? '', 63 | `${record.maxProfit?.toFixed(2) ?? ''}` 64 | ] 65 | } 66 | 67 | //**** Pool creation + validation ****// 68 | 69 | export async function onPoolDataParsed(parsed: ParsedPoolCreationTx, startTime: number | null, isEnabled: boolean) { 70 | const dbRecord = await getStateRecordByPoolId(parsed.poolKeys.id) 71 | if (dbRecord) { return } 72 | 73 | const tokenId = parsed.poolKeys.baseMint !== WSOL.mint ? parsed.poolKeys.baseMint : parsed.poolKeys.quoteMint; 74 | 75 | let status = '' 76 | let startDate: string | null = null 77 | if (startTime) { 78 | startDate = formatDate(new Date(startTime)) 79 | status = `🟠\nPostponed to ${startDate}` 80 | } else if (!isEnabled) { 81 | status = `⚫\nSwap is Disabled` 82 | } else { 83 | status = `⚪\nParsed data and start valiidating` 84 | } 85 | 86 | const record = createStateRecord({ poolId: parsed.poolKeys.id, status: status }) 87 | 88 | //allRecordsByFirstMintTx.set(parsed.poolKeys.id, { ...record, tokenId, startTime: startDate, status }) 89 | await upsertRecord({ ...record, tokenId, startTime: startDate, status }) 90 | //renderCurrentState() 91 | } 92 | 93 | 94 | export async function onPoolValidationChanged(results: SafetyCheckResult) { 95 | let poolId = '' 96 | let status = '' 97 | switch (results.kind) { 98 | case 'CreatorIsScammer': { 99 | poolId = results.pool.poolKeys.id 100 | status = `🔴\nSkipped because creator ${results.pool.creator.toString()} is in blacklist.` 101 | break 102 | } 103 | case 'WaitLPBurningTooLong': { 104 | poolId = results.data.poolKeys.id 105 | status = `🔴\n Skipped because it's postponed for too long.` 106 | break 107 | } 108 | case 'WaitLPBurning': { 109 | poolId = results.data.pool.id.toString() 110 | status = `🟠\n Waiting LP tokens to burn. LP mint ${results.lpTokenMint.toString()}` 111 | break 112 | } 113 | case 'Complete': { 114 | poolId = results.data.pool.id.toString() 115 | status = `🟠\n Received validation results, evaluating...` 116 | break 117 | } 118 | } 119 | 120 | const record = await getOrMakeRecordByTxId(poolId) 121 | const updated = { 122 | ...record, 123 | status, 124 | } 125 | // /allRecordsByFirstMintTx.set(poolId, updated) 126 | upsertRecord(updated) 127 | //renderCurrentState() 128 | } 129 | 130 | export async function onPoolValidationEvaluated(data: { status: TokenSafetyStatus, data: PoolSafetyData, reason: string }) { 131 | const record = await getOrMakeRecordByTxId(data.data.pool.id.toString()) 132 | let tokenSafetyIndicator = '' 133 | switch (data.status) { 134 | case 'RED': { 135 | tokenSafetyIndicator = '🔴\n' 136 | break 137 | } 138 | case 'YELLOW': { 139 | tokenSafetyIndicator = '🟡\n' 140 | break 141 | } 142 | case 'GREEN': { 143 | tokenSafetyIndicator = '🟢\n' 144 | break 145 | } 146 | } 147 | tokenSafetyIndicator += data.reason 148 | 149 | const updated = { 150 | ...record, 151 | status: tokenSafetyIndicator, 152 | } 153 | //allRecordsByFirstMintTx.set(data.data.pool.id.toString(), updated) 154 | //renderCurrentState() 155 | upsertRecord(updated) 156 | } 157 | export async function onStartGettingTrades(data: { status: TokenSafetyStatus, data: PoolSafetyData, reason: string }) { 158 | const record = await getOrMakeRecordByTxId(data.data.pool.id.toString()) 159 | const updated = { 160 | ...record, 161 | status: `${record.status}\nGetting trades txs`, 162 | } 163 | // allRecordsByFirstMintTx.set(data.data.pool.id.toString(), updated) 164 | // renderCurrentState() 165 | upsertRecord(updated) 166 | } 167 | 168 | export async function onTradesEvaluated(data: PoolSafetyData, status: string) { 169 | const record = await getOrMakeRecordByTxId(data.pool.id.toString()) 170 | const updated = { 171 | ...record, 172 | status: `${record.status}\n${status}`, 173 | } 174 | // allRecordsByFirstMintTx.set(data.pool.id.toString(), updated) 175 | // renderCurrentState() 176 | upsertRecord(updated) 177 | } 178 | 179 | export async function onStartTrading(data: PoolSafetyData) { 180 | const record = await getOrMakeRecordByTxId(data.pool.id.toString()) 181 | const updated = { 182 | ...record, 183 | buyInfo: 'started', 184 | } 185 | // allRecordsByFirstMintTx.set(data.pool.id.toString(), updated) 186 | // renderCurrentState() 187 | upsertRecord(updated) 188 | } 189 | 190 | export async function onFinishTrading(data: PoolSafetyData, results: SellResults) { 191 | const record = await getOrMakeRecordByTxId(data.pool.id.toString()) 192 | let sellInfo = '' 193 | let buyInfo = '' 194 | let estimatedProfit = '' 195 | let finalProfit = '' 196 | 197 | switch (results.kind) { 198 | case 'FAILED': { 199 | if (results.boughtForSol) { 200 | sellInfo = `Lost ${results.boughtForSol}\n${results.reason}` 201 | } else { 202 | sellInfo = `Couldn't make trades\n${results.reason}` 203 | } 204 | break 205 | } 206 | case 'SUCCESS': { 207 | buyInfo = `at ${results.buyTime}` 208 | sellInfo = `Sell at ${results.sellTime}\nfor ${results.soldForSOL} SOL` 209 | estimatedProfit = 'Estimated PNL: ' + (results.estimatedProfit < 0 ? '-' : '+') + `${results.estimatedProfit.toFixed(2)}` 210 | finalProfit = 'Final PNL: ' + (results.profit < 0 ? '-' : '+') + `${results.profit.toFixed(2)}` 211 | break 212 | } 213 | } 214 | 215 | const updated = { 216 | ...record, 217 | buyInfo: `${record.buyInfo}\n${buyInfo}`, 218 | sellInfo: sellInfo, 219 | profit: `${estimatedProfit}\n${finalProfit}` 220 | } 221 | // allRecordsByFirstMintTx.set(data.pool.id.toString(), updated) 222 | // renderCurrentState() 223 | upsertRecord(updated) 224 | } 225 | 226 | export async function onBuyResults(poolId: string, buyResults: BuyResult) { 227 | let buyInfo = '' 228 | switch (buyResults.kind) { 229 | case 'NO_BUY': { 230 | buyInfo = `Failed to buy.\n ${buyResults.reason}` 231 | break 232 | } 233 | case 'NO_CONFIRMATION': { 234 | buyInfo = `Couldn't confirm buy tx.\n ${buyResults.reason}` 235 | break 236 | } 237 | case 'NO_TOKENS_AMOUNT': { 238 | buyInfo = `No tokens amount.\n ${buyResults.reason}` 239 | break 240 | } 241 | case 'SUCCESS': { 242 | buyInfo = `Success.\nBought ${buyResults.newTokenAmount} tokens` 243 | break 244 | } 245 | } 246 | 247 | if (!dbIsInited) { 248 | const isSuccess = buyResults.kind === 'SUCCESS' 249 | console.log(isSuccess ? chalk.green(buyInfo) : chalk.red(buyInfo)) 250 | return 251 | } 252 | 253 | const record = await getOrMakeRecordByTxId(poolId) 254 | const updated = { 255 | ...record, 256 | buyInfo: buyInfo 257 | } 258 | // allRecordsByFirstMintTx.set(poolId, updated) 259 | // renderCurrentState() 260 | upsertRecord(updated) 261 | } 262 | 263 | 264 | export async function onTradingPNLChanged(poolId: string, newPNL: number) { 265 | if (!dbIsInited) { 266 | const date = new Date() 267 | const isNegative = newPNL < 0 268 | const logTxt = `[${formatDate(date)}]: PNL - ${(newPNL * 100).toFixed(2)}%` 269 | console.log(isNegative ? chalk.red(logTxt) : chalk.green(logTxt)) 270 | return 271 | } 272 | const record = await getOrMakeRecordByTxId(poolId) 273 | const currentMax = record.maxProfit 274 | let updatedMaxPNL = 0 275 | if (currentMax) { 276 | updatedMaxPNL = newPNL > currentMax ? newPNL : currentMax 277 | } else { 278 | updatedMaxPNL = newPNL 279 | } 280 | const updated = { 281 | ...record, 282 | maxProfit: updatedMaxPNL 283 | } 284 | // allRecordsByFirstMintTx.set(poolId, updated) 285 | // renderCurrentState() 286 | upsertRecord(updated) 287 | } -------------------------------------------------------------------------------- /client/PoolValidator/RaydiumPoolValidator.ts: -------------------------------------------------------------------------------- 1 | import { PoolFeatures, TokenSafetyStatus } from './ValidationResult' 2 | import { PoolKeys, fetchPoolKeysForLPInitTransactionHash } from './RaydiumPoolParser' 3 | import { Liquidity, LiquidityPoolInfo, LiquidityPoolKeysV4, LiquidityPoolStatus } from '@raydium-io/raydium-sdk' 4 | import { PoolSafetyData, SafetyCheckResult } from './RaydiumSafetyCheck' 5 | import { convertStringKeysToDataKeys, delay } from '../Utils' 6 | import { Connection, PublicKey, TokenBalance } from '@solana/web3.js' 7 | import { TradeRecord, fetchLatestTrades } from '../Trader/TradesFetcher' 8 | import { TrendAnalisis, analyzeTrend, findDumpingRecord } from '../Trader/TradesAnalyzer' 9 | 10 | export type ValidatePoolData = { 11 | mintTxId: string, 12 | date: Date, 13 | } 14 | 15 | // module.exports = async (data: ValidatePoolData) => { 16 | // console.log(`Receive message in validation worker. TxId: ${data.mintTxId}.`) 17 | // const validationResults = await validateNewPool(data.mintTxId) 18 | // console.log(`Finished validation in validation worker. TxId: ${data.mintTxId}.`) 19 | // return validationResults 20 | // } 21 | 22 | export type PoolPostponed = { 23 | kind: 'Postponed', 24 | parsed: ParsedPoolCreationTx, 25 | startTime: number // in epoch millis 26 | } 27 | 28 | export type PoolDisabled = { 29 | kind: 'Disabled', 30 | poolKeys: PoolKeys 31 | } 32 | 33 | export type PoolSafetyAssesed = { 34 | kind: 'Validated', 35 | poolKeys: PoolKeys, 36 | results: SafetyCheckResult 37 | } 38 | 39 | export type PoolValidationResults = PoolPostponed | PoolDisabled | PoolSafetyAssesed 40 | 41 | export type ParsedPoolCreationTx = { 42 | binaryKeys: LiquidityPoolKeysV4, 43 | info: LiquidityPoolInfo, 44 | poolKeys: PoolKeys, 45 | creator: PublicKey, 46 | lpTokenMint: PublicKey 47 | } 48 | 49 | export type TradingInfo = { 50 | trades: TradeRecord[], 51 | dump: [TradeRecord, TradeRecord] | null, 52 | analysis: TrendAnalisis | null 53 | } 54 | 55 | export async function parsePoolCreationTx(connection: Connection, mintTxId: string) 56 | : Promise { 57 | const { poolKeys, mintTransaction } = await fetchPoolKeysForLPInitTransactionHash(connection, mintTxId) // With poolKeys you can do a swap 58 | const binaryPoolKeys = convertStringKeysToDataKeys(poolKeys) 59 | const info = await tryParseLiquidityPoolInfo(connection, binaryPoolKeys) 60 | if (info === null) { 61 | throw Error(`Couldn't get LP info, perhaps RPC issues`) 62 | } 63 | 64 | if (!mintTransaction.meta || !mintTransaction.meta.innerInstructions || !mintTransaction.meta.preTokenBalances || !mintTransaction.meta.postTokenBalances) { 65 | throw Error(`Couldn't get creator address from initial pool tx ${mintTxId}`) 66 | } 67 | 68 | const firstInnerInstructionsSet = mintTransaction.meta.innerInstructions[0].instructions as any[] 69 | const creatorAddress = new PublicKey(firstInnerInstructionsSet[0].parsed.info.source) 70 | 71 | /// Find LP-token minted by providing liquidity to the pool 72 | /// Serves as a permission to remove liquidity 73 | const preBalanceTokens = reduceBalancesToTokensSet(mintTransaction.meta.preTokenBalances) 74 | const postBalanceTokens = reduceBalancesToTokensSet(mintTransaction.meta.postTokenBalances) 75 | let lpTokenMint: string | null = null 76 | for (let x of postBalanceTokens) { 77 | if (!preBalanceTokens.has(x)) { 78 | lpTokenMint = x 79 | break 80 | } 81 | } 82 | 83 | if (lpTokenMint === null) { 84 | /// NO LP tokens 85 | throw Error(`No LP tokens`) 86 | } 87 | 88 | return { binaryKeys: binaryPoolKeys, info, poolKeys, creator: creatorAddress, lpTokenMint: new PublicKey(lpTokenMint) } 89 | } 90 | 91 | export function checkIfPoolPostponed(parsed: ParsedPoolCreationTx): { parsed: ParsedPoolCreationTx, startTime: number | null } { 92 | const status = parsed.info.status.toNumber() 93 | if (status === LiquidityPoolStatus.WaitingForStart) { 94 | //if (Date.now() / 1000 < startTime.toNumber()) 95 | const startTime = parsed.info.startTime.toNumber() 96 | return { parsed, startTime } 97 | } 98 | return { parsed, startTime: null } 99 | } 100 | 101 | export function checkIfSwapEnabled(parsed: ParsedPoolCreationTx): { parsed: ParsedPoolCreationTx, isEnabled: boolean } { 102 | const features: PoolFeatures = Liquidity.getEnabledFeatures(parsed.info) 103 | return { parsed, isEnabled: features.swap } 104 | } 105 | 106 | export function evaluateSafetyState(data: PoolSafetyData, isLiquidityLocked: boolean): { status: TokenSafetyStatus, data: PoolSafetyData, reason: string } { 107 | const MIN_PERCENT_NEW_TOKEN_INPOOL = 0.1 108 | const LOW_IN_USD = 500; 109 | const HIGH_IN_USD = 100000000; 110 | 111 | /// Check is liquidiity amount is too low r otoo high (both are suspicous) 112 | if (data.totalLiquidity.amountInUSD < LOW_IN_USD || data.totalLiquidity.amountInUSD > HIGH_IN_USD) { 113 | return { 114 | data, 115 | status: 'RED', 116 | reason: `Liquidity is too low or too high. ${data.totalLiquidity.amount} ${data.totalLiquidity.symbol}` 117 | } 118 | } 119 | 120 | if (!isLiquidityLocked) { 121 | /// If locked percent of liquidity is less then SAFE_LOCKED_LIQUIDITY_PERCENT 122 | /// most likely it will be rugged at any time, better to stay away 123 | return { 124 | data, 125 | status: 'RED', 126 | reason: `Liquidity is not locked`, 127 | } 128 | } 129 | 130 | if (data.newTokenPoolBalancePercent >= 0.99) { 131 | /// When almost all tokens in pool 132 | if (data.ownershipInfo.isMintable) { 133 | /// When token is still mintable 134 | /// We can try to get some money out of it 135 | return { 136 | data, 137 | status: 'YELLOW', 138 | reason: `Most of the tokens are in pool, but token is still mintable` 139 | } 140 | } else { 141 | /// When token is not mintable 142 | return { 143 | data, 144 | status: 'GREEN', 145 | reason: `Liquidity is locked. Token is not mintable. Green light`, 146 | } 147 | } 148 | } else if (data.newTokenPoolBalancePercent >= MIN_PERCENT_NEW_TOKEN_INPOOL) { 149 | /// When at least MIN_PERCENT_NEW_TOKEN_INPOOL tokens in pool 150 | if (!data.ownershipInfo.isMintable) { 151 | /// If token is not mintable 152 | return { 153 | data, 154 | status: 'YELLOW', 155 | reason: `At least 80% of the tokens are in pool, and token is not mintable`, 156 | } 157 | } if (data.newTokenPoolBalancePercent >= 0.95) { 158 | /// If token is mintable, but should not be dumped fast (from my experience) 159 | return { 160 | data, 161 | status: 'YELLOW', 162 | reason: `>95% of tokens are in pool, but token is still mintable`, 163 | } 164 | } else { 165 | /// Many tokens are not in pool and token is mintable. Could be dumped very fast. 166 | return { 167 | data, 168 | status: 'RED', 169 | reason: `Many tokens are not in pool and token is mintable`, 170 | } 171 | } 172 | } else { 173 | /// Too much new tokens is not in pool. Could be dumped very fast. 174 | return { 175 | data, 176 | status: 'RED', 177 | reason: `Less then ${MIN_PERCENT_NEW_TOKEN_INPOOL * 100}% of tokens are in pool.`, 178 | } 179 | } 180 | } 181 | 182 | export async function checkLatestTrades(connection: Connection, poolKeys: LiquidityPoolKeysV4): Promise { 183 | try { 184 | const latestTrades = await fetchLatestTrades(connection, poolKeys) 185 | const dumpRes = findDumpingRecord(latestTrades) 186 | if (dumpRes) { 187 | return { trades: latestTrades, dump: dumpRes, analysis: null } 188 | } 189 | const analysis = analyzeTrend(latestTrades) 190 | return { trades: latestTrades, dump: null, analysis: analysis } 191 | } catch (e) { 192 | console.error(`Pool ${poolKeys.id.toString()} error get trades:\n${e}`) 193 | return { trades: [], dump: null, analysis: null } 194 | } 195 | } 196 | 197 | export async function makeSwapPoolKeysAndGetInfo(connection: Connection, poolKeys: PoolKeys): 198 | Promise<{ poolKeys: LiquidityPoolKeysV4, poolInfo: LiquidityPoolInfo }> { 199 | const binaryPoolKeys = convertStringKeysToDataKeys(poolKeys) 200 | const info = await tryParseLiquidityPoolInfo(connection, binaryPoolKeys) 201 | return { poolKeys: binaryPoolKeys, poolInfo: info! } 202 | } 203 | 204 | async function tryParseLiquidityPoolInfo(connection: Connection, poolKeys: LiquidityPoolKeysV4, attempt: number = 1, maxAttempts: number = 5): Promise { 205 | try { 206 | console.log(`Getting LP info attempt ${attempt}.`) 207 | const info = await Liquidity.fetchInfo({ connection: connection, poolKeys: poolKeys }) 208 | if (info !== null) { 209 | console.log(`Successfully fetched LP info from attempt ${attempt}`) 210 | return info; // Return the transaction if it's not null 211 | } else if (attempt < maxAttempts) { 212 | console.log(`Fetching LP info attempt ${attempt} failed, retrying...`) 213 | await delay(200) // Wait for the specified delay 214 | return tryParseLiquidityPoolInfo(connection, poolKeys, attempt + 1, maxAttempts) 215 | } else { 216 | console.log('Max attempts of fetching LP info reached, returning null') 217 | return null; // Return null if max attempts are reached 218 | } 219 | } catch (error) { 220 | console.error(`Fetching LP info attempt ${attempt} failed with error: ${error}, retrying...`) 221 | if (attempt < maxAttempts) { 222 | await delay(200) // Wait for the specified delay // Wait for the specified delay before retrying 223 | return tryParseLiquidityPoolInfo(connection, poolKeys, attempt + 1, maxAttempts) 224 | } else { 225 | console.log('Max attempts of fetching LP info reached, returning null') 226 | return null; // Return null if max attempts are reached 227 | } 228 | } 229 | } 230 | 231 | function reduceBalancesToTokensSet(balances: TokenBalance[]): Set { 232 | const result = new Set() 233 | return balances.reduce((set, b) => { 234 | set.add(b.mint) 235 | return set 236 | }, result) 237 | } -------------------------------------------------------------------------------- /client/SafetyCheck.ts: -------------------------------------------------------------------------------- 1 | import { ParsedTransactionWithMeta, PublicKey, Connection, ParsedAccountData, TokenBalance } from '@solana/web3.js' 2 | import { Currency, CurrencyAmount, LiquidityPoolKeysV4, Token, TokenAmount, WSOL } from '@raydium-io/raydium-sdk' 3 | import { getAssociatedTokenAddressSync, MINT_SIZE, MintLayout } from '@solana/spl-token' 4 | import { BURN_ACC_ADDRESS } from './PoolValidator/Addresses' 5 | import { KNOWN_SCAM_ACCOUNTS } from './PoolValidator/BlackLists' 6 | import { BN } from "@project-serum/anchor"; 7 | import chalk from 'chalk' 8 | import { delay } from './Utils' 9 | 10 | const BURN_INSTRUCTION_LOG = "Program log: Instruction: Burn" 11 | const RAYDIUM_OWNER_AUTHORITY = '5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1'; 12 | 13 | type OwnershipInfo = { 14 | mintAuthority: string | null, 15 | freezeAuthority: string | null, 16 | isMintable: boolean, 17 | authorityBalancePercent: number 18 | } 19 | 20 | type LiquidityValue = { 21 | amount: number, 22 | amountInUSD: number, 23 | symbol: string 24 | } 25 | 26 | export type SafetyCheckResult = { 27 | creator: PublicKey, 28 | totalLiquidity: LiquidityValue, 29 | lockedPercentOfLiqiodity: number, 30 | newTokenPoolBalancePercent: number, 31 | ownershipInfo: OwnershipInfo, 32 | } 33 | 34 | export async function checkToken(connection: Connection, tx: ParsedTransactionWithMeta, pool: LiquidityPoolKeysV4): Promise { 35 | if (!tx.meta || !tx.meta.innerInstructions || !tx.meta.preTokenBalances || !tx.meta.postTokenBalances) { 36 | console.log(`meta is null ${tx.meta === null}`) 37 | console.log(`innerInstructions is null ${tx.meta?.innerInstructions === null || tx.meta?.innerInstructions === undefined}`) 38 | console.log(`post balances is null ${tx.meta?.postTokenBalances === null || tx.meta?.postTokenBalances === undefined}`) 39 | return null 40 | } 41 | 42 | const firstInnerInstructionsSet = tx.meta.innerInstructions[0].instructions as any[] 43 | const creatorAddress = new PublicKey(firstInnerInstructionsSet[0].parsed.info.source) 44 | 45 | /// Check blacklist first 46 | if (KNOWN_SCAM_ACCOUNTS.has(creatorAddress.toString())) { 47 | console.log(`Creater blacklisted ${creatorAddress.toString()}`) 48 | return null 49 | } 50 | 51 | const baseIsWSOL = pool.baseMint.toString() === WSOL.mint 52 | const otherTokenMint = baseIsWSOL ? pool.quoteMint : pool.baseMint 53 | 54 | /// Check mint and freeze authorities 55 | /// Ideally `not set` 56 | /// Not to bad if address is not cretor's 57 | /// Red-flag if addresses is the same as creator's 58 | const otherTokenInfo = await connection.getAccountInfo(otherTokenMint) 59 | const mintInfo = MintLayout.decode(otherTokenInfo!.data!.subarray(0, MINT_SIZE)) 60 | const totalSupply = Number(mintInfo.supply) / (10 ** mintInfo.decimals) 61 | const mintAuthority = mintInfo.mintAuthorityOption > 0 ? mintInfo.mintAuthority : null 62 | const freezeAuthority = mintInfo.freezeAuthorityOption > 0 ? mintInfo.freezeAuthority : null 63 | 64 | /// Check creators and authorities balances 65 | const calcOwnershipPercent = async (address: PublicKey) => { 66 | const tokenAcc = getAssociatedTokenAddressSync(otherTokenMint, address) 67 | const value = (await connection.getTokenAccountBalance(tokenAcc)).value.uiAmount ?? 0 68 | return value / totalSupply 69 | } 70 | 71 | const creatorsPercentage = await calcOwnershipPercent(creatorAddress) 72 | let authorityPercentage: number = 0 73 | if (mintAuthority) { 74 | authorityPercentage = await calcOwnershipPercent(mintAuthority) 75 | } else if (freezeAuthority) { 76 | authorityPercentage = await calcOwnershipPercent(freezeAuthority) 77 | } 78 | 79 | /// Find LP-token minted by providing liquidity to the pool 80 | /// Serves as a permission to remove liquidity 81 | const preBalanceTokens = reduceBalancesToTokensSet(tx.meta.preTokenBalances) 82 | const postBalanceTokens = reduceBalancesToTokensSet(tx.meta.postTokenBalances) 83 | let lpTokenMint: string | null = null 84 | for (let x of postBalanceTokens) { 85 | if (!preBalanceTokens.has(x)) { 86 | lpTokenMint = x 87 | break 88 | } 89 | } 90 | 91 | if (lpTokenMint === null) { 92 | /// NO LP tokens 93 | console.log(`No LP tokens`) 94 | return null 95 | } 96 | 97 | /// LP tokens balance right after first mint transaction 98 | const mintTxLPTokenBalance = tx.meta.postTokenBalances.find(x => x.mint === lpTokenMint)! 99 | 100 | const lpTokenAccount = getAssociatedTokenAddressSync(new PublicKey(lpTokenMint), creatorAddress) 101 | 102 | let percentLockedLP: number 103 | 104 | if ((mintTxLPTokenBalance.uiTokenAmount.uiAmount ?? 0) <= 1) { 105 | percentLockedLP = 1 106 | } else { 107 | percentLockedLP = await getPercentOfBurnedTokensWithRetry( 108 | 4, /// 4 attempts 109 | 30 * 1000, /// wait for 30 seconds before next retry 110 | connection, 111 | lpTokenAccount, 112 | lpTokenMint, 113 | mintTxLPTokenBalance 114 | ) 115 | } 116 | 117 | /// Get real liquiidity value 118 | const realCurrencyLPBalance = await connection.getTokenAccountBalance(baseIsWSOL ? pool.baseVault : pool.quoteVault); 119 | //const lpVaultBalance = await connection.getTokenAccountBalance(poolKeys.lpVault); 120 | const SOL_EXCHANGE_RATE = 110 /// With EXTRA as of 08.02.2024 121 | const liquitity = realCurrencyLPBalance.value.uiAmount ?? 0; 122 | const isSOL = realCurrencyLPBalance.value.decimals === WSOL.decimals; 123 | const symbol = isSOL ? 'SOL' : 'USD'; 124 | const amountInUSD = isSOL ? liquitity * SOL_EXCHANGE_RATE : liquitity 125 | 126 | console.log(chalk.bgBlue(`Real Liquidity ${liquitity} ${symbol}`)); 127 | 128 | 129 | ///Check largest holders 130 | /// Should Raydiium LP 131 | 132 | const largestAccounts = await connection.getTokenLargestAccounts(otherTokenMint); 133 | const raydiumTokenAccount = await connection.getParsedTokenAccountsByOwner(new PublicKey(RAYDIUM_OWNER_AUTHORITY), { mint: otherTokenMint }); 134 | 135 | let newTokenPoolBalancePercent = 0 136 | if (largestAccounts.value.length > 0 && raydiumTokenAccount.value.length > 0) { 137 | const poolAcc = raydiumTokenAccount.value[0].pubkey.toString() 138 | const poolBalance = largestAccounts.value.find(x => x.address.toString() === poolAcc) 139 | if (poolBalance) { 140 | newTokenPoolBalancePercent = (poolBalance.uiAmount ?? 0) / totalSupply 141 | } 142 | } 143 | 144 | return { 145 | creator: creatorAddress, 146 | lockedPercentOfLiqiodity: percentLockedLP, 147 | totalLiquidity: { 148 | amount: liquitity, 149 | amountInUSD, 150 | symbol 151 | }, 152 | newTokenPoolBalancePercent, 153 | ownershipInfo: { 154 | mintAuthority: mintAuthority?.toString() ?? null, 155 | freezeAuthority: freezeAuthority?.toString() ?? null, 156 | isMintable: mintAuthority !== null, 157 | authorityBalancePercent: authorityPercentage 158 | } 159 | } 160 | } 161 | 162 | function reduceBalancesToTokensSet(balances: TokenBalance[]): Set { 163 | const result = new Set() 164 | return balances.reduce((set, b) => { 165 | set.add(b.mint) 166 | return set 167 | }, result) 168 | } 169 | 170 | async function getPercentOfBurnedTokensWithRetry( 171 | attempts: number, 172 | waitBeforeAttempt: number, 173 | connection: Connection, 174 | lpTokenAccount: PublicKey, 175 | lpTokenMint: string, 176 | mintTxLPTokenBalance: TokenBalance, 177 | ): Promise { 178 | let attempt = 1 179 | while (attempt <= attempts) { 180 | try { 181 | const burnedPercent = await getPercentOfBurnedTokens(connection, lpTokenAccount, lpTokenMint, mintTxLPTokenBalance) 182 | if (burnedPercent >= 1) { 183 | return burnedPercent 184 | } 185 | attempt += 1 186 | if (attempt > attempts) { return 0 } 187 | await delay(200 + waitBeforeAttempt) 188 | } catch (e) { 189 | console.log(chalk.red(`Failed to get amount of burned LP tokens: ${e}.`)) 190 | attempt += 1 191 | if (attempt > attempts) { return 0 } 192 | await delay(200 + waitBeforeAttempt) 193 | } 194 | } 195 | return 0 196 | } 197 | 198 | const BURN_INSTRCUTIONS = new Set(['burnChecked', 'burn']) 199 | 200 | async function getPercentOfBurnedTokens( 201 | connection: Connection, 202 | lpTokenAccount: PublicKey, 203 | lpTokenMint: string, 204 | mintTxLPTokenBalance: TokenBalance, 205 | ): Promise { 206 | const lpTokenAccountTxIds = await connection.getConfirmedSignaturesForAddress2(lpTokenAccount) 207 | const lpTokenAccountTxs = await connection 208 | .getParsedTransactions( 209 | lpTokenAccountTxIds.map(x => x.signature), 210 | { maxSupportedTransactionVersion: 0 }) 211 | 212 | const filteredLPTokenAccountTxs = lpTokenAccountTxs.filter(x => x && x.meta && !x.meta.err) 213 | 214 | /// Check for either BURN instruction 215 | /// Transferring to burn address 216 | let totalLPTokensBurned: number = 0 217 | for (let i = 0; i < filteredLPTokenAccountTxs.length; i++) { 218 | const parsedWithMeta = filteredLPTokenAccountTxs[i] 219 | if (!parsedWithMeta || !parsedWithMeta.meta) { continue } 220 | const preLPTokenBalance = parsedWithMeta.meta.preTokenBalances?.find(x => x.mint === lpTokenMint) 221 | const postLPTokenBalance = parsedWithMeta.meta.postTokenBalances?.find(x => x.mint === lpTokenMint) 222 | const burnInstructions = parsedWithMeta.transaction 223 | .message.instructions 224 | .map((x: any) => x.parsed) 225 | .filter(x => x && BURN_INSTRCUTIONS.has(x.type) && x.info.mint === lpTokenMint) 226 | 227 | if (burnInstructions && burnInstructions.length > 0) { 228 | const amountBurned: BN = burnInstructions.reduce((acc: BN, x) => { 229 | const amount = x.info?.amount ?? x.info?.tokenAmount?.amount 230 | if (amount) { 231 | const currencyAmount = new BN(amount) 232 | return acc.add(currencyAmount) 233 | } 234 | }, new BN('0')) 235 | const burnedInHuman = amountBurned.toNumber() / (10 ** mintTxLPTokenBalance.uiTokenAmount.decimals) 236 | totalLPTokensBurned += burnedInHuman 237 | } else { 238 | const burnAddressInMessage = parsedWithMeta.transaction.message.accountKeys.find(x => x.pubkey.toString() === BURN_ACC_ADDRESS) 239 | if (burnAddressInMessage) { 240 | totalLPTokensBurned += (preLPTokenBalance?.uiTokenAmount.uiAmount ?? 0) - (postLPTokenBalance?.uiTokenAmount.uiAmount ?? 0) 241 | } 242 | } 243 | } 244 | 245 | return totalLPTokensBurned / (mintTxLPTokenBalance.uiTokenAmount.uiAmount ?? 1) 246 | } -------------------------------------------------------------------------------- /client/PoolMaker.ts: -------------------------------------------------------------------------------- 1 | import { LiquidityPoolKeysV4, Market, MARKET_STATE_LAYOUT_V3, TOKEN_PROGRAM_ID } from "@raydium-io/raydium-sdk"; 2 | import { 3 | Connection, 4 | PublicKey, 5 | ParsedTransactionWithMeta, 6 | PartiallyDecodedInstruction, 7 | ParsedInnerInstruction, 8 | ParsedInstruction 9 | } from "@solana/web3.js"; 10 | import chalk from "chalk"; 11 | import { delay } from "./Utils"; 12 | 13 | const RAYDIUM_POOL_V4_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'; 14 | const SERUM_OPENBOOK_PROGRAM_ID = 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX'; 15 | const SOL_MINT = 'So11111111111111111111111111111111111111112'; 16 | const SOL_DECIMALS = 9; 17 | 18 | export function findLogEntry(needle: string, logEntries: Array): string | null { 19 | for (let i = 0; i < logEntries.length; ++i) { 20 | if (logEntries[i].includes(needle)) { 21 | return logEntries[i]; 22 | } 23 | } 24 | 25 | return null; 26 | } 27 | 28 | export async function fetchPoolKeysForLPInitTransactionHash(txSignature: string, connection: Connection) 29 | : Promise<{ poolKeys: LiquidityPoolKeysV4, mintTransaction: ParsedTransactionWithMeta }> { 30 | console.log(chalk.yellow(`Fetching TX inf ${txSignature}`)); 31 | const tx = await retryGetParsedTransaction(connection, txSignature, 5) 32 | if (!tx) { 33 | throw new Error('Failed to fetch transaction with signature ' + txSignature); 34 | } 35 | const poolInfo = parsePoolInfoFromLpTransaction(tx); 36 | const marketInfo = await fetchMarketInfo(poolInfo.marketId, connection); 37 | 38 | const keys = { 39 | id: poolInfo.id, 40 | baseMint: poolInfo.baseMint, 41 | quoteMint: poolInfo.quoteMint, 42 | lpMint: poolInfo.lpMint, 43 | baseDecimals: poolInfo.baseDecimals, 44 | quoteDecimals: poolInfo.quoteDecimals, 45 | lpDecimals: poolInfo.lpDecimals, 46 | version: 4, 47 | programId: poolInfo.programId, 48 | authority: poolInfo.authority, 49 | openOrders: poolInfo.openOrders, 50 | targetOrders: poolInfo.targetOrders, 51 | baseVault: poolInfo.baseVault, 52 | quoteVault: poolInfo.quoteVault, 53 | withdrawQueue: poolInfo.withdrawQueue, 54 | lpVault: poolInfo.lpVault, 55 | marketVersion: 3, 56 | marketProgramId: poolInfo.marketProgramId, 57 | marketId: poolInfo.marketId, 58 | marketAuthority: Market.getAssociatedAuthority({ programId: poolInfo.marketProgramId, marketId: poolInfo.marketId }).publicKey, 59 | marketBaseVault: marketInfo.baseVault, 60 | marketQuoteVault: marketInfo.quoteVault, 61 | marketBids: marketInfo.bids, 62 | marketAsks: marketInfo.asks, 63 | marketEventQueue: marketInfo.eventQueue, 64 | } as LiquidityPoolKeysV4; 65 | return { mintTransaction: tx, poolKeys: keys } 66 | } 67 | 68 | async function retryGetParsedTransaction( 69 | connection: Connection, 70 | txSignature: string, 71 | maxAttempts: number, 72 | delayMs: number = 200, 73 | attempt: number = 1 74 | ): Promise { 75 | try { 76 | const tx = await connection.getParsedTransaction(txSignature, { maxSupportedTransactionVersion: 0 }); 77 | if (tx !== null) { 78 | return tx; // Return the transaction if it's not null 79 | } else if (attempt < maxAttempts) { 80 | console.log(`Attempt ${attempt} failed, retrying...`); 81 | await delay(delayMs) // Wait for the specified delay 82 | return retryGetParsedTransaction(connection, txSignature, maxAttempts, delayMs, attempt + 1); 83 | } else { 84 | console.log('Max attempts reached, returning null'); 85 | return null; // Return null if max attempts are reached 86 | } 87 | } catch (error) { 88 | console.error(`Attempt ${attempt} failed with error: ${error}, retrying...`); 89 | if (attempt < maxAttempts) { 90 | await delay(delayMs) // Wait for the specified delay // Wait for the specified delay before retrying 91 | return retryGetParsedTransaction(connection, txSignature, maxAttempts, delayMs, attempt + 1); 92 | } else { 93 | console.log('Max attempts reached, returning null'); 94 | return null; // Return null if max attempts are reached 95 | } 96 | } 97 | } 98 | 99 | async function fetchMarketInfo(marketId: PublicKey, connection: Connection) { 100 | const marketAccountInfo = await connection.getAccountInfo(marketId); 101 | if (!marketAccountInfo) { 102 | throw new Error('Failed to fetch market info for market id ' + marketId.toBase58()); 103 | } 104 | 105 | return MARKET_STATE_LAYOUT_V3.decode(marketAccountInfo.data); 106 | } 107 | 108 | 109 | function parsePoolInfoFromLpTransaction(txData: ParsedTransactionWithMeta) { 110 | const initInstruction = findInstructionByProgramId(txData.transaction.message.instructions, new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID)) as PartiallyDecodedInstruction | null; 111 | if (!initInstruction) { 112 | throw new Error('Failed to find lp init instruction in lp init tx'); 113 | } 114 | const baseMint = initInstruction.accounts[8]; 115 | const baseVault = initInstruction.accounts[10]; 116 | const quoteMint = initInstruction.accounts[9]; 117 | const quoteVault = initInstruction.accounts[11]; 118 | const lpMint = initInstruction.accounts[7]; 119 | const baseAndQuoteSwapped = baseMint.toBase58() === SOL_MINT; 120 | const lpMintInitInstruction = findInitializeMintInInnerInstructionsByMintAddress(txData.meta?.innerInstructions ?? [], lpMint); 121 | if (!lpMintInitInstruction) { 122 | throw new Error('Failed to find lp mint init instruction in lp init tx'); 123 | } 124 | const lpMintInstruction = findMintToInInnerInstructionsByMintAddress(txData.meta?.innerInstructions ?? [], lpMint); 125 | if (!lpMintInstruction) { 126 | throw new Error('Failed to find lp mint to instruction in lp init tx'); 127 | } 128 | const baseTransferInstruction = findTransferInstructionInInnerInstructionsByDestination(txData.meta?.innerInstructions ?? [], baseVault, TOKEN_PROGRAM_ID); 129 | if (!baseTransferInstruction) { 130 | throw new Error('Failed to find base transfer instruction in lp init tx'); 131 | } 132 | const quoteTransferInstruction = findTransferInstructionInInnerInstructionsByDestination(txData.meta?.innerInstructions ?? [], quoteVault, TOKEN_PROGRAM_ID); 133 | if (!quoteTransferInstruction) { 134 | throw new Error('Failed to find quote transfer instruction in lp init tx'); 135 | } 136 | const lpDecimals = lpMintInitInstruction.parsed.info.decimals; 137 | const lpInitializationLogEntryInfo = extractLPInitializationLogEntryInfoFromLogEntry(findLogEntry('init_pc_amount', txData.meta?.logMessages ?? []) ?? ''); 138 | const basePreBalance = (txData.meta?.preTokenBalances ?? []).find(balance => balance.mint === baseMint.toBase58()); 139 | if (!basePreBalance) { 140 | throw new Error('Failed to find base tokens preTokenBalance entry to parse the base tokens decimals'); 141 | } 142 | const baseDecimals = basePreBalance.uiTokenAmount.decimals; 143 | 144 | return { 145 | id: initInstruction.accounts[4], 146 | baseMint, 147 | quoteMint, 148 | lpMint, 149 | baseDecimals: baseAndQuoteSwapped ? SOL_DECIMALS : baseDecimals, 150 | quoteDecimals: baseAndQuoteSwapped ? baseDecimals : SOL_DECIMALS, 151 | lpDecimals, 152 | version: 4, 153 | programId: new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID), 154 | authority: initInstruction.accounts[5], 155 | openOrders: initInstruction.accounts[6], 156 | targetOrders: initInstruction.accounts[13], 157 | baseVault, 158 | quoteVault, 159 | withdrawQueue: new PublicKey("11111111111111111111111111111111"), 160 | lpVault: new PublicKey(lpMintInstruction.parsed.info.account), 161 | marketVersion: 3, 162 | marketProgramId: initInstruction.accounts[15], 163 | marketId: initInstruction.accounts[16], 164 | baseReserve: parseInt(baseTransferInstruction.parsed.info.amount), 165 | quoteReserve: parseInt(quoteTransferInstruction.parsed.info.amount), 166 | lpReserve: parseInt(lpMintInstruction.parsed.info.amount), 167 | openTime: lpInitializationLogEntryInfo.open_time, 168 | } 169 | } 170 | 171 | function findTransferInstructionInInnerInstructionsByDestination(innerInstructions: Array, destinationAccount: PublicKey, programId?: PublicKey): ParsedInstruction | null { 172 | for (let i = 0; i < innerInstructions.length; i++) { 173 | for (let y = 0; y < innerInstructions[i].instructions.length; y++) { 174 | const instruction = innerInstructions[i].instructions[y] as ParsedInstruction; 175 | if (!instruction.parsed) { continue }; 176 | if (instruction.parsed.type === 'transfer' && instruction.parsed.info.destination === destinationAccount.toBase58() && (!programId || instruction.programId.equals(programId))) { 177 | return instruction; 178 | } 179 | } 180 | } 181 | 182 | return null; 183 | } 184 | 185 | function findInitializeMintInInnerInstructionsByMintAddress(innerInstructions: Array, mintAddress: PublicKey): ParsedInstruction | null { 186 | for (let i = 0; i < innerInstructions.length; i++) { 187 | for (let y = 0; y < innerInstructions[i].instructions.length; y++) { 188 | const instruction = innerInstructions[i].instructions[y] as ParsedInstruction; 189 | if (!instruction.parsed) { continue }; 190 | if (instruction.parsed.type === 'initializeMint' && instruction.parsed.info.mint === mintAddress.toBase58()) { 191 | return instruction; 192 | } 193 | } 194 | } 195 | 196 | return null; 197 | } 198 | 199 | function findMintToInInnerInstructionsByMintAddress(innerInstructions: Array, mintAddress: PublicKey): ParsedInstruction | null { 200 | for (let i = 0; i < innerInstructions.length; i++) { 201 | for (let y = 0; y < innerInstructions[i].instructions.length; y++) { 202 | const instruction = innerInstructions[i].instructions[y] as ParsedInstruction; 203 | if (!instruction.parsed) { continue }; 204 | if (instruction.parsed.type === 'mintTo' && instruction.parsed.info.mint === mintAddress.toBase58()) { 205 | return instruction; 206 | } 207 | } 208 | } 209 | 210 | return null; 211 | } 212 | 213 | function findInstructionByProgramId(instructions: Array, programId: PublicKey): ParsedInstruction | PartiallyDecodedInstruction | null { 214 | for (let i = 0; i < instructions.length; i++) { 215 | if (instructions[i].programId.equals(programId)) { 216 | return instructions[i]; 217 | } 218 | } 219 | 220 | return null; 221 | } 222 | 223 | function extractLPInitializationLogEntryInfoFromLogEntry(lpLogEntry: string): { nonce: number, open_time: number, init_pc_amount: number, init_coin_amount: number } { 224 | const lpInitializationLogEntryInfoStart = lpLogEntry.indexOf('{'); 225 | 226 | return JSON.parse(fixRelaxedJsonInLpLogEntry(lpLogEntry.substring(lpInitializationLogEntryInfoStart))); 227 | } 228 | 229 | function fixRelaxedJsonInLpLogEntry(relaxedJson: string): string { 230 | return relaxedJson.replace(/([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, "$1\"$2\":"); 231 | } -------------------------------------------------------------------------------- /client/PoolValidator/RaydiumSafetyCheck.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Connection, TokenBalance } from '@solana/web3.js' 2 | import { LiquidityPoolKeysV4, WSOL } from '@raydium-io/raydium-sdk' 3 | import { MINT_SIZE, MintLayout } from '@solana/spl-token' 4 | import { BURN_ACC_ADDRESS } from './Addresses' 5 | import { KNOWN_SCAM_ACCOUNTS } from './BlackLists' 6 | import { BN } from "@project-serum/anchor"; 7 | import chalk from 'chalk' 8 | import { delay, timeout } from '../Utils' 9 | import { error } from 'console' 10 | import { ParsedPoolCreationTx } from './RaydiumPoolValidator' 11 | 12 | const RAYDIUM_OWNER_AUTHORITY = '5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1'; 13 | 14 | type OwnershipInfo = { 15 | mintAuthority: string | null, 16 | freezeAuthority: string | null, 17 | isMintable: boolean, 18 | totalSupply: number 19 | } 20 | 21 | type LiquidityValue = { 22 | amount: number, 23 | amountInUSD: number, 24 | symbol: string 25 | } 26 | 27 | export type PoolSafetyData = { 28 | creator: PublicKey, 29 | totalLiquidity: LiquidityValue, 30 | newTokenPoolBalancePercent: number, 31 | ownershipInfo: OwnershipInfo, 32 | pool: LiquidityPoolKeysV4, 33 | tokenMint: PublicKey 34 | } 35 | 36 | export type WaitLPBurning = { 37 | kind: 'WaitLPBurning', 38 | data: PoolSafetyData, 39 | lpTokenMint: PublicKey 40 | } 41 | 42 | export type WaitLPBurningComplete = { 43 | kind: 'WaitLPBurningComplete', 44 | data: PoolSafetyData, 45 | isliquidityLocked: boolean, 46 | } 47 | 48 | export type WaitLPBurningTooLong = { 49 | kind: 'WaitLPBurningTooLong', 50 | data: ParsedPoolCreationTx, 51 | } 52 | 53 | type CreatorIsInBlackList = { 54 | kind: 'CreatorIsScammer', 55 | pool: ParsedPoolCreationTx, 56 | } 57 | 58 | export type SafetyCheckComplete = { 59 | kind: 'Complete', 60 | isliquidityLocked: boolean, 61 | data: PoolSafetyData 62 | } 63 | 64 | export type SafetyCheckResult = WaitLPBurning | WaitLPBurningTooLong | WaitLPBurningComplete | CreatorIsInBlackList | SafetyCheckComplete 65 | 66 | export async function checkToken(connection: Connection, data: ParsedPoolCreationTx, isLightCheck: boolean = false): Promise { 67 | /// Check blacklist first 68 | if (KNOWN_SCAM_ACCOUNTS.has(data.creator.toString())) { 69 | return { kind: 'CreatorIsScammer', pool: data } 70 | } 71 | 72 | const pool = data.poolKeys 73 | 74 | const baseIsWSOL = pool.baseMint.toString() === WSOL.mint 75 | const otherTokenMint = new PublicKey(baseIsWSOL ? pool.quoteMint : pool.baseMint) 76 | 77 | /// Check mint and freeze authorities 78 | /// Ideally `not set` 79 | /// Not to bad if address is not cretor's 80 | /// Red-flag if addresses is the same as creator's 81 | const ownershipInfo = await getTokenOwnershipInfo(connection, otherTokenMint) 82 | 83 | // /// Check creators and authorities balances 84 | // const calcOwnershipPercent = async (address: PublicKey) => { 85 | // const tokenAcc = getAssociatedTokenAddressSync(otherTokenMint, address) 86 | // const value = (await connection.getTokenAccountBalance(tokenAcc)).value.uiAmount ?? 0 87 | // return value / totalSupply 88 | // } 89 | 90 | // const creatorsPercentage = await calcOwnershipPercent(creatorAddress) 91 | // let authorityPercentage: number = 0 92 | // if (mintAuthority) { 93 | // authorityPercentage = await calcOwnershipPercent(mintAuthority) 94 | // } else if (freezeAuthority) { 95 | // authorityPercentage = await calcOwnershipPercent(freezeAuthority) 96 | // } 97 | 98 | ///Check largest holders 99 | /// Should Raydiium LP 100 | let newTokenPoolBalancePercent = 0 101 | if (!isLightCheck) { 102 | const largestAccounts = await connection.getTokenLargestAccounts(otherTokenMint); 103 | const raydiumTokenAccount = await connection.getParsedTokenAccountsByOwner(new PublicKey(RAYDIUM_OWNER_AUTHORITY), { mint: otherTokenMint }); 104 | 105 | if (largestAccounts.value.length > 0 && raydiumTokenAccount.value.length > 0) { 106 | const poolAcc = raydiumTokenAccount.value[0].pubkey.toString() 107 | const poolBalance = largestAccounts.value.find(x => x.address.toString() === poolAcc) 108 | if (poolBalance) { 109 | newTokenPoolBalancePercent = (poolBalance.uiAmount ?? 0) / ownershipInfo.totalSupply 110 | } 111 | } 112 | } 113 | 114 | /// Get real liquiidity value 115 | const realCurrencyLPBalance = await connection.getTokenAccountBalance(new PublicKey(baseIsWSOL ? pool.baseVault : pool.quoteVault)); 116 | //const lpVaultBalance = await connection.getTokenAccountBalance(poolKeys.lpVault); 117 | const SOL_EXCHANGE_RATE = 150 /// With EXTRA as of 08.03.2024 118 | const liquitity = realCurrencyLPBalance.value.uiAmount ?? 0; 119 | const isSOL = realCurrencyLPBalance.value.decimals === WSOL.decimals; 120 | const symbol = isSOL ? 'SOL' : 'USD'; 121 | const amountInUSD = isSOL ? liquitity * SOL_EXCHANGE_RATE : liquitity 122 | 123 | const resultData: PoolSafetyData = { 124 | creator: data.creator, 125 | totalLiquidity: { 126 | amount: liquitity, 127 | amountInUSD, 128 | symbol 129 | }, 130 | newTokenPoolBalancePercent, 131 | ownershipInfo, 132 | pool: data.binaryKeys, 133 | tokenMint: otherTokenMint 134 | } 135 | 136 | let isLiquidityLocked = false 137 | 138 | if (isLightCheck) { 139 | return { kind: 'Complete', data: resultData, isliquidityLocked: isLiquidityLocked } 140 | } 141 | 142 | isLiquidityLocked = await checkIfLPTokenBurnedWithRetry(connection, 3, 200, data.lpTokenMint) 143 | // Liqidity is not locked, but more than half of supply is in pool 144 | // Possible that LP token wiill be burned later. Wait for a few hours 145 | if (!isLiquidityLocked && newTokenPoolBalancePercent >= 0.5) { 146 | console.log(chalk.cyan(`Pools ${pool.id.toString()}. All tokens are in pool, but LP tokens aren't burned yet. Start verifying it`)) 147 | return { kind: 'WaitLPBurning', data: resultData, lpTokenMint: data.lpTokenMint } 148 | } 149 | 150 | return { kind: 'Complete', data: resultData, isliquidityLocked: isLiquidityLocked } 151 | } 152 | 153 | export async function getTokenOwnershipInfo(connection: Connection, tokenMint: PublicKey): Promise { 154 | const otherTokenInfo = await connection.getAccountInfo(tokenMint) 155 | const mintInfo = MintLayout.decode(otherTokenInfo!.data!.subarray(0, MINT_SIZE)) 156 | const totalSupply = Number(mintInfo.supply) / (10 ** mintInfo.decimals) 157 | let mintAuthority = mintInfo.mintAuthorityOption > 0 ? mintInfo.mintAuthority : null 158 | const freezeAuthority = mintInfo.freezeAuthorityOption > 0 ? mintInfo.freezeAuthority : null 159 | return { 160 | mintAuthority: mintAuthority?.toString() ?? null, 161 | freezeAuthority: freezeAuthority?.toString() ?? null, 162 | isMintable: mintAuthority !== null, 163 | totalSupply 164 | } 165 | } 166 | 167 | export async function checkLPTokenBurnedOrTimeout( 168 | connection: Connection, 169 | lpTokenMint: PublicKey, 170 | timeoutInMillis: number, 171 | ): Promise { 172 | let isBurned = false 173 | try { 174 | isBurned = await Promise.race([ 175 | listenToLPTokenSupplyChanges(connection, lpTokenMint), 176 | timeout(timeoutInMillis) 177 | ]) 178 | 179 | return isBurned 180 | } catch (e) { 181 | console.log(`Timeout happened during refreshing burned LP tokens percent`) 182 | return isBurned 183 | } 184 | } 185 | 186 | async function listenToLPTokenSupplyChanges( 187 | connection: Connection, 188 | lpTokenMint: PublicKey, 189 | ): Promise { 190 | console.log(`Subscribing to LP mint changes. Waiting to burn. Mint: ${lpTokenMint.toString()}`) 191 | return new Promise((resolve, reject) => { 192 | connection.onAccountChange(lpTokenMint, (accInfoBuffer, _) => { 193 | const lpTokenMintInfo = MintLayout.decode(accInfoBuffer.data.subarray(0, MINT_SIZE)) 194 | const lastSupply = Number(lpTokenMintInfo.supply) / (10 ** lpTokenMintInfo.decimals) 195 | console.log(`LP token mint ${lpTokenMint.toString()} changed. Current supply: ${lastSupply}`) 196 | const isBurned = lastSupply <= 100 197 | if (isBurned) { 198 | console.log(`LP token ${lpTokenMint.toString()} is Burned`) 199 | resolve(isBurned) 200 | } 201 | }) 202 | }) 203 | } 204 | 205 | async function checkIfLPTokenBurnedWithRetry( 206 | connection: Connection, 207 | attempts: number, 208 | waitBeforeAttempt: number, 209 | lpTokenMint: PublicKey, 210 | ): Promise { 211 | let attempt = 1 212 | while (attempt <= attempts) { 213 | try { 214 | const supply = await getTokenSupply(connection, lpTokenMint) 215 | if (supply <= 100) { 216 | return true 217 | } 218 | attempt += 1 219 | if (attempt > attempts) { return false } 220 | await delay(200 + waitBeforeAttempt) 221 | } catch (e) { 222 | console.log(chalk.red(`Failed to get LP token supply: ${e}.`)) 223 | attempt += 1 224 | if (attempt > attempts) { return false } 225 | await delay(200 + waitBeforeAttempt) 226 | } 227 | } 228 | return false 229 | } 230 | 231 | async function getTokenSupply( 232 | connection: Connection, 233 | tokenMint: PublicKey 234 | ): Promise { 235 | const accountInfo = await connection.getAccountInfo(tokenMint) 236 | if (!accountInfo) { 237 | throw error('Couldnt get token mint info') 238 | } 239 | const lpTokenMintInfo = MintLayout.decode(accountInfo.data.subarray(0, MINT_SIZE)) 240 | const lastSupply = Number(lpTokenMintInfo.supply) / (10 ** lpTokenMintInfo.decimals) 241 | return lastSupply 242 | } 243 | 244 | const BURN_INSTRCUTIONS = new Set(['burnChecked', 'burn']) 245 | 246 | async function getPercentOfBurnedTokens( 247 | connection: Connection, 248 | lpTokenAccount: PublicKey, 249 | lpTokenMint: string, 250 | mintTxLPTokenBalance: TokenBalance, 251 | ): Promise { 252 | const lpTokenAccountTxIds = await connection.getConfirmedSignaturesForAddress2(lpTokenAccount) 253 | const lpTokenAccountTxs = await connection 254 | .getParsedTransactions( 255 | lpTokenAccountTxIds.map(x => x.signature), 256 | { maxSupportedTransactionVersion: 0 }) 257 | 258 | const filteredLPTokenAccountTxs = lpTokenAccountTxs.filter(x => x && x.meta && !x.meta.err) 259 | 260 | /// Check for either BURN instruction 261 | /// Transferring to burn address 262 | let totalLPTokensBurned: number = 0 263 | for (let i = 0; i < filteredLPTokenAccountTxs.length; i++) { 264 | const parsedWithMeta = filteredLPTokenAccountTxs[i] 265 | if (!parsedWithMeta || !parsedWithMeta.meta) { continue } 266 | const preLPTokenBalance = parsedWithMeta.meta.preTokenBalances?.find(x => x.mint === lpTokenMint) 267 | const postLPTokenBalance = parsedWithMeta.meta.postTokenBalances?.find(x => x.mint === lpTokenMint) 268 | const burnInstructions = parsedWithMeta.transaction 269 | .message.instructions 270 | .map((x: any) => x.parsed) 271 | .filter(x => x && BURN_INSTRCUTIONS.has(x.type) && x.info.mint === lpTokenMint) 272 | 273 | if (burnInstructions && burnInstructions.length > 0) { 274 | const amountBurned: BN = burnInstructions.reduce((acc: BN, x) => { 275 | const amount = x.info?.amount ?? x.info?.tokenAmount?.amount 276 | if (amount) { 277 | const currencyAmount = new BN(amount) 278 | return acc.add(currencyAmount) 279 | } 280 | }, new BN('0')) 281 | const burnedInHuman = amountBurned.toNumber() / (10 ** mintTxLPTokenBalance.uiTokenAmount.decimals) 282 | totalLPTokensBurned += burnedInHuman 283 | } else { 284 | const burnAddressInMessage = parsedWithMeta.transaction.message.accountKeys.find(x => x.pubkey.toString() === BURN_ACC_ADDRESS) 285 | if (burnAddressInMessage) { 286 | totalLPTokensBurned += (preLPTokenBalance?.uiTokenAmount.uiAmount ?? 0) - (postLPTokenBalance?.uiTokenAmount.uiAmount ?? 0) 287 | } 288 | } 289 | } 290 | 291 | return totalLPTokensBurned / (mintTxLPTokenBalance.uiTokenAmount.uiAmount ?? 1) 292 | } -------------------------------------------------------------------------------- /client/PoolValidator/RaydiumPoolParser.ts: -------------------------------------------------------------------------------- 1 | import { LiquidityPoolKeysV4, Market, MARKET_STATE_LAYOUT_V3, TOKEN_PROGRAM_ID } from "@raydium-io/raydium-sdk"; 2 | import { 3 | Connection, 4 | PublicKey, 5 | ParsedTransactionWithMeta, 6 | PartiallyDecodedInstruction, 7 | ParsedInnerInstruction, 8 | ParsedInstruction 9 | } from "@solana/web3.js"; 10 | import chalk from "chalk"; 11 | import { delay } from "../Utils"; 12 | 13 | const RAYDIUM_POOL_V4_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'; 14 | const SERUM_OPENBOOK_PROGRAM_ID = 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX'; 15 | const SOL_MINT = 'So11111111111111111111111111111111111111112'; 16 | const SOL_DECIMALS = 9; 17 | 18 | export interface PoolKeys { 19 | id: string, 20 | baseMint: string, 21 | quoteMint: string, 22 | lpMint: string, 23 | baseDecimals: number, 24 | quoteDecimals: number, 25 | lpDecimals: number, 26 | version: number, 27 | programId: string, 28 | authority: string, 29 | openOrders: string, 30 | targetOrders: string, 31 | baseVault: string, 32 | quoteVault: string, 33 | withdrawQueue: string, 34 | lpVault: string, 35 | marketVersion: number, 36 | marketProgramId: string, 37 | marketId: string, 38 | marketAuthority: string, 39 | marketBaseVault: string, 40 | marketQuoteVault: string, 41 | marketBids: string, 42 | marketAsks: string, 43 | marketEventQueue: string, 44 | } 45 | 46 | export function findLogEntry(needle: string, logEntries: Array): string | null { 47 | for (let i = 0; i < logEntries.length; ++i) { 48 | if (logEntries[i].includes(needle)) { 49 | return logEntries[i]; 50 | } 51 | } 52 | 53 | return null; 54 | } 55 | 56 | export async function fetchPoolKeysForLPInitTransactionHash(connection: Connection, txSignature: string) 57 | : Promise<{ poolKeys: PoolKeys, mintTransaction: ParsedTransactionWithMeta }> { 58 | console.log(chalk.yellow(`Fetching TX inf ${txSignature}`)); 59 | const tx = await retryGetParsedTransaction(connection, txSignature, 5) 60 | if (!tx) { 61 | throw new Error('Failed to fetch transaction with signature ' + txSignature); 62 | } 63 | const poolInfo = parsePoolInfoFromLpTransaction(tx); 64 | const marketInfo = await fetchMarketInfo(connection, poolInfo.marketId); 65 | 66 | const keys = { 67 | id: poolInfo.id.toString(), 68 | baseMint: poolInfo.baseMint.toString(), 69 | quoteMint: poolInfo.quoteMint.toString(), 70 | lpMint: poolInfo.lpMint.toString(), 71 | baseDecimals: poolInfo.baseDecimals, 72 | quoteDecimals: poolInfo.quoteDecimals, 73 | lpDecimals: poolInfo.lpDecimals, 74 | version: 4, 75 | programId: poolInfo.programId.toString(), 76 | authority: poolInfo.authority.toString(), 77 | openOrders: poolInfo.openOrders.toString(), 78 | targetOrders: poolInfo.targetOrders.toString(), 79 | baseVault: poolInfo.baseVault.toString(), 80 | quoteVault: poolInfo.quoteVault.toString(), 81 | withdrawQueue: poolInfo.withdrawQueue.toString(), 82 | lpVault: poolInfo.lpVault.toString(), 83 | marketVersion: 3, 84 | marketProgramId: poolInfo.marketProgramId.toString(), 85 | marketId: poolInfo.marketId.toString(), 86 | marketAuthority: Market.getAssociatedAuthority({ programId: poolInfo.marketProgramId, marketId: poolInfo.marketId }).publicKey.toString(), 87 | marketBaseVault: marketInfo.baseVault.toString(), 88 | marketQuoteVault: marketInfo.quoteVault.toString(), 89 | marketBids: marketInfo.bids.toString(), 90 | marketAsks: marketInfo.asks.toString(), 91 | marketEventQueue: marketInfo.eventQueue.toString(), 92 | } 93 | return { mintTransaction: tx, poolKeys: keys } 94 | } 95 | 96 | async function retryGetParsedTransaction( 97 | connection: Connection, 98 | txSignature: string, 99 | maxAttempts: number, 100 | delayMs: number = 200, 101 | attempt: number = 1 102 | ): Promise { 103 | try { 104 | console.log(`Attempt ${attempt} to get https://solscan.io/tx/${txSignature} info`) 105 | const tx = await connection.getParsedTransaction(txSignature, { maxSupportedTransactionVersion: 0 }); 106 | if (tx !== null) { 107 | console.log(`Successfully fetched https://solscan.io/tx/${txSignature} info from attempt ${attempt}`) 108 | return tx; // Return the transaction if it's not null 109 | } else if (attempt < maxAttempts) { 110 | console.log(`Attempt ${attempt} failed, retrying...`) 111 | await delay(delayMs) // Wait for the specified delay 112 | return retryGetParsedTransaction(connection, txSignature, maxAttempts, delayMs, attempt + 1); 113 | } else { 114 | console.log('Max attempts reached, returning null'); 115 | return null; // Return null if max attempts are reached 116 | } 117 | } catch (error) { 118 | console.error(`Attempt ${attempt} failed with error: ${error}, retrying...`); 119 | if (attempt < maxAttempts) { 120 | await delay(delayMs) // Wait for the specified delay // Wait for the specified delay before retrying 121 | return retryGetParsedTransaction(connection, txSignature, maxAttempts, delayMs, attempt + 1); 122 | } else { 123 | console.log('Max attempts reached, returning null'); 124 | return null; // Return null if max attempts are reached 125 | } 126 | } 127 | } 128 | 129 | async function fetchMarketInfo(connection: Connection, marketId: PublicKey) { 130 | const marketAccountInfo = await connection.getAccountInfo(marketId); 131 | if (!marketAccountInfo) { 132 | throw new Error('Failed to fetch market info for market id ' + marketId.toBase58()); 133 | } 134 | 135 | return MARKET_STATE_LAYOUT_V3.decode(marketAccountInfo.data); 136 | } 137 | 138 | 139 | function parsePoolInfoFromLpTransaction(txData: ParsedTransactionWithMeta) { 140 | const initInstruction = findInstructionByProgramId(txData.transaction.message.instructions, new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID)) as PartiallyDecodedInstruction | null; 141 | if (!initInstruction) { 142 | throw new Error('Failed to find lp init instruction in lp init tx'); 143 | } 144 | const baseMint = initInstruction.accounts[8]; 145 | const baseVault = initInstruction.accounts[10]; 146 | const quoteMint = initInstruction.accounts[9]; 147 | const quoteVault = initInstruction.accounts[11]; 148 | const lpMint = initInstruction.accounts[7]; 149 | const baseAndQuoteSwapped = baseMint.toBase58() === SOL_MINT; 150 | const lpMintInitInstruction = findInitializeMintInInnerInstructionsByMintAddress(txData.meta?.innerInstructions ?? [], lpMint); 151 | if (!lpMintInitInstruction) { 152 | throw new Error('Failed to find lp mint init instruction in lp init tx'); 153 | } 154 | const lpMintInstruction = findMintToInInnerInstructionsByMintAddress(txData.meta?.innerInstructions ?? [], lpMint); 155 | if (!lpMintInstruction) { 156 | throw new Error('Failed to find lp mint to instruction in lp init tx'); 157 | } 158 | const baseTransferInstruction = findTransferInstructionInInnerInstructionsByDestination(txData.meta?.innerInstructions ?? [], baseVault, TOKEN_PROGRAM_ID); 159 | if (!baseTransferInstruction) { 160 | throw new Error('Failed to find base transfer instruction in lp init tx'); 161 | } 162 | const quoteTransferInstruction = findTransferInstructionInInnerInstructionsByDestination(txData.meta?.innerInstructions ?? [], quoteVault, TOKEN_PROGRAM_ID); 163 | if (!quoteTransferInstruction) { 164 | throw new Error('Failed to find quote transfer instruction in lp init tx'); 165 | } 166 | const lpDecimals = lpMintInitInstruction.parsed.info.decimals; 167 | const lpInitializationLogEntryInfo = extractLPInitializationLogEntryInfoFromLogEntry(findLogEntry('init_pc_amount', txData.meta?.logMessages ?? []) ?? ''); 168 | const basePreBalance = (txData.meta?.preTokenBalances ?? []).find(balance => balance.mint === baseMint.toBase58()); 169 | if (!basePreBalance) { 170 | throw new Error('Failed to find base tokens preTokenBalance entry to parse the base tokens decimals'); 171 | } 172 | const baseDecimals = basePreBalance.uiTokenAmount.decimals; 173 | 174 | return { 175 | id: initInstruction.accounts[4], 176 | baseMint, 177 | quoteMint, 178 | lpMint, 179 | baseDecimals: baseAndQuoteSwapped ? SOL_DECIMALS : baseDecimals, 180 | quoteDecimals: baseAndQuoteSwapped ? baseDecimals : SOL_DECIMALS, 181 | lpDecimals, 182 | version: 4, 183 | programId: new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID), 184 | authority: initInstruction.accounts[5], 185 | openOrders: initInstruction.accounts[6], 186 | targetOrders: initInstruction.accounts[13], 187 | baseVault, 188 | quoteVault, 189 | withdrawQueue: new PublicKey("11111111111111111111111111111111"), 190 | lpVault: new PublicKey(lpMintInstruction.parsed.info.account), 191 | marketVersion: 3, 192 | marketProgramId: initInstruction.accounts[15], 193 | marketId: initInstruction.accounts[16], 194 | baseReserve: parseInt(baseTransferInstruction.parsed.info.amount), 195 | quoteReserve: parseInt(quoteTransferInstruction.parsed.info.amount), 196 | lpReserve: parseInt(lpMintInstruction.parsed.info.amount), 197 | openTime: lpInitializationLogEntryInfo.open_time, 198 | } 199 | } 200 | 201 | function findTransferInstructionInInnerInstructionsByDestination(innerInstructions: Array, destinationAccount: PublicKey, programId?: PublicKey): ParsedInstruction | null { 202 | for (let i = 0; i < innerInstructions.length; i++) { 203 | for (let y = 0; y < innerInstructions[i].instructions.length; y++) { 204 | const instruction = innerInstructions[i].instructions[y] as ParsedInstruction; 205 | if (!instruction.parsed) { continue }; 206 | if (instruction.parsed.type === 'transfer' && instruction.parsed.info.destination === destinationAccount.toBase58() && (!programId || instruction.programId.equals(programId))) { 207 | return instruction; 208 | } 209 | } 210 | } 211 | 212 | return null; 213 | } 214 | 215 | function findInitializeMintInInnerInstructionsByMintAddress(innerInstructions: Array, mintAddress: PublicKey): ParsedInstruction | null { 216 | for (let i = 0; i < innerInstructions.length; i++) { 217 | for (let y = 0; y < innerInstructions[i].instructions.length; y++) { 218 | const instruction = innerInstructions[i].instructions[y] as ParsedInstruction; 219 | if (!instruction.parsed) { continue }; 220 | if (instruction.parsed.type === 'initializeMint' && instruction.parsed.info.mint === mintAddress.toBase58()) { 221 | return instruction; 222 | } 223 | } 224 | } 225 | 226 | return null; 227 | } 228 | 229 | function findMintToInInnerInstructionsByMintAddress(innerInstructions: Array, mintAddress: PublicKey): ParsedInstruction | null { 230 | for (let i = 0; i < innerInstructions.length; i++) { 231 | for (let y = 0; y < innerInstructions[i].instructions.length; y++) { 232 | const instruction = innerInstructions[i].instructions[y] as ParsedInstruction; 233 | if (!instruction.parsed) { continue }; 234 | if (instruction.parsed.type === 'mintTo' && instruction.parsed.info.mint === mintAddress.toBase58()) { 235 | return instruction; 236 | } 237 | } 238 | } 239 | 240 | return null; 241 | } 242 | 243 | function findInstructionByProgramId(instructions: Array, programId: PublicKey): ParsedInstruction | PartiallyDecodedInstruction | null { 244 | for (let i = 0; i < instructions.length; i++) { 245 | if (instructions[i].programId.equals(programId)) { 246 | return instructions[i]; 247 | } 248 | } 249 | 250 | return null; 251 | } 252 | 253 | function extractLPInitializationLogEntryInfoFromLogEntry(lpLogEntry: string): { nonce: number, open_time: number, init_pc_amount: number, init_coin_amount: number } { 254 | const lpInitializationLogEntryInfoStart = lpLogEntry.indexOf('{'); 255 | 256 | return JSON.parse(fixRelaxedJsonInLpLogEntry(lpLogEntry.substring(lpInitializationLogEntryInfoStart))); 257 | } 258 | 259 | function fixRelaxedJsonInLpLogEntry(relaxedJson: string): string { 260 | return relaxedJson.replace(/([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, "$1\"$2\":"); 261 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 24 | /* Modules */ 25 | "module": "commonjs", /* Specify what module code is generated. */ 26 | "rootDir": "./client", /* Specify the root folder within your source files. */ 27 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 32 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 35 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 36 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 37 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 38 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 39 | // "resolveJsonModule": true, /* Enable importing .json files. */ 40 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 41 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 42 | /* JavaScript Support */ 43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 53 | "outDir": "./client/dist", /* Specify an output folder for all emitted files. */ 54 | // "removeComments": true, /* Disable emitting comments. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | /* Interop Constraints */ 71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 72 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | "sourceMap": true, 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | /* Completeness */ 99 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 100 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 101 | }, 102 | "include": [ 103 | "client/**/*" 104 | ], 105 | "exclude": [ 106 | "node_modules", 107 | "**/*.spec.ts" 108 | ] 109 | } -------------------------------------------------------------------------------- /client/Swap.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyAmount, Fraction, Liquidity, LiquidityPoolKeysV4, Percent, Price, SOL, Token, TokenAccount, TokenAmount, WSOL } from "@raydium-io/raydium-sdk"; 2 | import { Connection, PublicKey, Commitment, TransactionMessage, ComputeBudgetProgram, VersionedTransaction, Transaction, TransactionInstruction } from "@solana/web3.js"; 3 | import { 4 | createAssociatedTokenAccountIdempotentInstruction 5 | } from '@solana/spl-token'; 6 | import { Wallet } from '@project-serum/anchor' 7 | import chalk from "chalk"; 8 | import { delay, lamportsToSOLNumber, timeout } from "./Utils"; 9 | import { onTradingPNLChanged } from "./StateAggregator/ConsoleOutput"; 10 | import * as nacl from "tweetnacl"; 11 | 12 | export async function swapTokens( 13 | connection: Connection, 14 | poolKeys: LiquidityPoolKeysV4, 15 | tokenAccountIn: PublicKey, 16 | tokenAccountOut: PublicKey, 17 | signer: Wallet, 18 | amountIn: TokenAmount, 19 | commitment: Commitment = 'confirmed' 20 | ): Promise { 21 | const otherTokenMint = (poolKeys.baseMint.toString() === WSOL.mint) ? poolKeys.quoteMint : poolKeys.baseMint; 22 | const associatedTokenAcc = (amountIn.token.mint.toString() === WSOL.mint) ? tokenAccountOut : tokenAccountIn; 23 | const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction( 24 | { 25 | poolKeys: poolKeys, 26 | userKeys: { 27 | tokenAccountIn: tokenAccountIn, 28 | tokenAccountOut: tokenAccountOut, 29 | owner: signer.publicKey, 30 | }, 31 | amountIn: amountIn.raw, 32 | minAmountOut: 0, 33 | }, 34 | poolKeys.version, 35 | ); 36 | 37 | console.log(`Getting last block`) 38 | const blockhashResponse = await connection.getLatestBlockhashAndContext(); 39 | const lastValidBlockHeight = blockhashResponse.context.slot + 150; 40 | const recentFee = await connection.getRecentPrioritizationFees({ lockedWritableAccounts: [new PublicKey('675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8')] }) 41 | const avgFee = (recentFee.reduce((sum, x) => sum + x.prioritizationFee, 0)) / recentFee.length 42 | console.log(`Avg fee: ${avgFee}`) 43 | const effectiveFee = Math.round(avgFee * 1.5) 44 | console.log(`Effective Fee: ${effectiveFee}`) 45 | // const latestBlockhash = await connection.getLatestBlockhash({ 46 | // commitment: 'finalized', 47 | // }); 48 | console.log(`Building tx`) 49 | 50 | const messageV0 = new TransactionMessage({ 51 | payerKey: signer.publicKey, 52 | recentBlockhash: blockhashResponse.value.blockhash, 53 | instructions: [ 54 | ComputeBudgetProgram.setComputeUnitLimit({ units: 300000 }), 55 | ComputeBudgetProgram.setComputeUnitPrice({ microLamports: effectiveFee }), 56 | createAssociatedTokenAccountIdempotentInstruction( 57 | signer.publicKey, 58 | associatedTokenAcc, 59 | signer.publicKey, 60 | otherTokenMint, 61 | ), 62 | ...innerTransaction.instructions, 63 | ], 64 | }).compileToV0Message(); 65 | 66 | 67 | const transaction = new VersionedTransaction(messageV0); 68 | transaction.sign([signer.payer]); 69 | console.log(`Sending tx`) 70 | const signature = await connection.sendRawTransaction( 71 | transaction.serialize(), 72 | { 73 | skipPreflight: true, 74 | maxRetries: 20 75 | }, 76 | ); 77 | 78 | console.log(`Tx sent https://solscan.io/tx/${signature}`) 79 | return signature; 80 | } 81 | 82 | export async function sellTokens( 83 | connection: Connection, 84 | poolKeys: LiquidityPoolKeysV4, 85 | quoteTokenAccount: TokenAccount, 86 | baseTokenAccount: TokenAccount, 87 | signer: Wallet, 88 | amountToSell: TokenAmount, 89 | commitment: Commitment = 'confirmed' 90 | ): Promise { 91 | const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction( 92 | { 93 | poolKeys: poolKeys, 94 | userKeys: { 95 | tokenAccountIn: baseTokenAccount.pubkey, 96 | tokenAccountOut: quoteTokenAccount.pubkey, 97 | owner: signer.publicKey, 98 | }, 99 | amountIn: amountToSell.raw, 100 | minAmountOut: 0, 101 | }, 102 | poolKeys.version, 103 | ); 104 | 105 | const latestBlockhash = await connection.getLatestBlockhash({ 106 | commitment: 'finalized', 107 | }); 108 | const messageV0 = new TransactionMessage({ 109 | payerKey: signer.publicKey, 110 | recentBlockhash: latestBlockhash.blockhash, 111 | instructions: [ 112 | ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }), 113 | ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30000 }), 114 | createAssociatedTokenAccountIdempotentInstruction( 115 | signer.publicKey, 116 | baseTokenAccount.pubkey, 117 | signer.publicKey, 118 | baseTokenAccount.accountInfo.mint, 119 | ), 120 | ...innerTransaction.instructions, 121 | ], 122 | }).compileToV0Message(); 123 | const transaction = new VersionedTransaction(messageV0); 124 | transaction.sign([signer.payer, ...innerTransaction.signers]); 125 | const signature = await connection.sendRawTransaction( 126 | transaction.serialize(), 127 | { 128 | maxRetries: 20, 129 | preflightCommitment: commitment, 130 | }, 131 | ); 132 | return signature; 133 | } 134 | 135 | export async function calculateAmountOut(connection: Connection, amountIn: TokenAmount, tokenOut: Token, poolKeys: LiquidityPoolKeysV4) { 136 | const poolInfo = await Liquidity.fetchInfo({ connection: connection, poolKeys }) 137 | const slippage = new Percent(1000, 100); // 1000% slippage 138 | 139 | const { amountOut, minAmountOut, currentPrice, executionPrice, priceImpact, fee } = Liquidity.computeAmountOut({ 140 | poolKeys, 141 | poolInfo, 142 | amountIn, 143 | currencyOut: tokenOut, 144 | slippage, 145 | }) 146 | 147 | return { 148 | amountIn, 149 | amountOut, 150 | minAmountOut, 151 | currentPrice, 152 | executionPrice, 153 | priceImpact, 154 | fee, 155 | } 156 | } 157 | 158 | export async function calcProfit( 159 | spent: number, 160 | connection: Connection, 161 | amountIn: TokenAmount, 162 | tokenOut: Token, 163 | poolKeys: LiquidityPoolKeysV4): Promise<{ currentAmountOut: number, profit: number } | null> { 164 | try { 165 | const { 166 | amountOut, 167 | minAmountOut, 168 | currentPrice, 169 | executionPrice, 170 | priceImpact, 171 | fee, 172 | } = await calculateAmountOut(connection, amountIn, tokenOut, poolKeys) 173 | // console.log(chalk.yellow('Calculated sell prices')); 174 | // console.log(`${chalk.bold('current price: ')}: ${currentPrice.toFixed()}`); 175 | // if (executionPrice !== null) { 176 | // console.log(`${chalk.bold('execution price: ')}: ${executionPrice.toFixed()}`); 177 | // } 178 | // console.log(`${chalk.bold('price impact: ')}: ${priceImpact.toFixed()}`); 179 | // console.log(`${chalk.bold('amount out: ')}: ${amountOut.toFixed()}`); 180 | // console.log(`${chalk.bold('min amount out: ')}: ${minAmountOut.toFixed()}`); 181 | 182 | const amountOutInSOL = lamportsToSOLNumber(amountOut.raw) ?? 0 183 | const potentialProfit = (amountOutInSOL - spent) / spent; 184 | 185 | return { currentAmountOut: amountOutInSOL, profit: potentialProfit }; 186 | } catch (e) { 187 | console.error('Faiiled to calculate amountOut and profit.'); 188 | return null; 189 | } 190 | } 191 | 192 | async function loopAndWaitForProfit( 193 | spentAmount: number, 194 | targetProfitPercentage: number, 195 | connection: Connection, 196 | amountIn: TokenAmount, 197 | tokenOut: Token, 198 | poolKeys: LiquidityPoolKeysV4, 199 | amountOutCalculationDelayMs: number, 200 | profitObject: { amountOut: number, profit: number }, 201 | cancellationToken: { cancelled: boolean } 202 | ) { 203 | console.log(`Target profit: ${targetProfitPercentage}`) 204 | const STOP_LOSS_PERCENT = -0.5 205 | 206 | let profitToTakeOrLose: number = 0; 207 | let prevAmountOut: number = 0; 208 | let priceDownCounter = 5; 209 | //priceDownCounter > 0 && 210 | do { 211 | if (cancellationToken.cancelled) { 212 | break; 213 | } 214 | try { 215 | const calculationResult = await calcProfit(spentAmount, connection, amountIn, tokenOut, poolKeys); 216 | if (calculationResult !== null) { 217 | const { currentAmountOut, profit } = calculationResult; 218 | const profitChanges = Math.abs(profit - profitToTakeOrLose) 219 | if (profitChanges >= 0.01) { 220 | onTradingPNLChanged(poolKeys.id.toString(), profit) 221 | } 222 | profitToTakeOrLose = profit; 223 | profitObject.profit = profit 224 | profitObject.amountOut = currentAmountOut 225 | 226 | if (currentAmountOut < prevAmountOut) { 227 | priceDownCounter -= 1; 228 | } else { 229 | if (priceDownCounter < 5) { priceDownCounter += 1; } 230 | } 231 | 232 | prevAmountOut = currentAmountOut; 233 | } 234 | await delay(amountOutCalculationDelayMs); 235 | } catch (e) { 236 | await delay(amountOutCalculationDelayMs); 237 | } 238 | } while (profitToTakeOrLose < targetProfitPercentage && profitToTakeOrLose > STOP_LOSS_PERCENT) 239 | 240 | return { amountOut: prevAmountOut, profit: profitToTakeOrLose }; 241 | } 242 | 243 | export async function waitForProfitOrTimeout( 244 | spentAmount: number, 245 | targetProfitPercentage: number, 246 | connection: Connection, 247 | amountIn: TokenAmount, 248 | tokenOut: Token, 249 | poolKeys: LiquidityPoolKeysV4, 250 | profitCalculationIterationDelayMs: number, 251 | timeutInMillis: number 252 | ): Promise<{ amountOut: number, profit: number }> { 253 | const cancellationToken = { cancelled: false } 254 | let profitObject: { amountOut: number, profit: number } = { amountOut: 0, profit: 0 } 255 | try { 256 | await Promise.race([ 257 | loopAndWaitForProfit(spentAmount, targetProfitPercentage, connection, amountIn, tokenOut, poolKeys, profitCalculationIterationDelayMs, profitObject, cancellationToken), 258 | timeout(timeutInMillis, cancellationToken) 259 | ]) 260 | } catch (e) { 261 | const profitInPercent = (profitObject.profit * 100).toFixed(2) + '%' 262 | console.error(`Timeout happened ${chalk.bold('Profit to take: ')} ${profitObject.profit < 0 ? chalk.red(profitInPercent) : chalk.green(profitInPercent)}`); 263 | } 264 | return { amountOut: profitObject.amountOut, profit: profitObject.profit } 265 | } 266 | 267 | export async function validateTradingTrendOrTimeout( 268 | connection: Connection, 269 | amountIn: TokenAmount, 270 | tokenOut: Token, 271 | poolKeys: LiquidityPoolKeysV4): Promise { 272 | let trendingCondition: GeneralTokenCondition | null; 273 | const cancellationToken = { cancelled: false } 274 | try { 275 | trendingCondition = await Promise.race([ 276 | loopAndCheckPriceTrend(connection, amountIn, tokenOut, poolKeys, cancellationToken), 277 | timeout(30 * 1000, cancellationToken) // 30 seconds 278 | ]); 279 | } catch (e) { 280 | console.error(chalk.red(`Timeout happend. Can't identify trend.`)); 281 | return null; 282 | } 283 | return trendingCondition; 284 | } 285 | 286 | type PriceTrend = 'UP' | 'DOWN' | 'UNSTABLE'; 287 | export type GeneralTokenCondition = 288 | 'PUMPING' | 'DUMPING' | 'NOT_PUMPING_BUT_GROWING' | 'NOT_DUMPING_BUT_DIPPING' | 'ALREADY_DUMPED' 289 | 290 | async function loopAndCheckPriceTrend( 291 | connection: Connection, 292 | amountIn: TokenAmount, 293 | tokenOut: Token, 294 | poolKeys: LiquidityPoolKeysV4, 295 | cancellationToken: { cancelled: boolean } 296 | ): Promise { 297 | if (cancellationToken.cancelled) { 298 | return null; 299 | } 300 | const amountOfChecks = 5; 301 | let failedAttempts = 0; 302 | let prevPriceTrend: PriceTrend = 'DOWN'; 303 | let allChecks: { amountOut: TokenAmount | CurrencyAmount, trend: PriceTrend }[] = new Array(); 304 | 305 | for (let i = 1; i <= amountOfChecks; i++) { 306 | const priceTrend = await checkPriceTrend(connection, amountIn, tokenOut, poolKeys); 307 | if (cancellationToken.cancelled) { 308 | return null; 309 | } 310 | if (priceTrend === null) { 311 | failedAttempts += 1; 312 | } else { 313 | allChecks.push(priceTrend); 314 | } 315 | } 316 | const lastItemsIndex = amountOfChecks - failedAttempts - 1; 317 | 318 | if (cancellationToken.cancelled) { 319 | return null; 320 | } 321 | 322 | if (failedAttempts > 2) { 323 | console.error(chalk.red(`Too many fails. Can't see the trend`)); 324 | return null; 325 | } 326 | 327 | if (allChecks.find((x) => x.trend === 'UNSTABLE')) { 328 | /// Price is too valotile, most likely it's already dumped 329 | return 'ALREADY_DUMPED' 330 | } 331 | 332 | const upsCount = allChecks.filter((x) => x.trend === 'UP').length; 333 | const downsCount = allChecks.filter((x) => x.trend === 'DOWN').length; 334 | 335 | if (allChecks[0].amountOut.lt(allChecks[lastItemsIndex].amountOut)) { // overall growing trend 336 | return (upsCount - downsCount) > 2 ? 'PUMPING' : 'NOT_PUMPING_BUT_GROWING'; 337 | } else { // overall dipping trend 338 | return (downsCount - upsCount) > 2 ? 'DUMPING' : 'NOT_DUMPING_BUT_DIPPING'; 339 | } 340 | } 341 | 342 | async function checkPriceTrend( 343 | connection: Connection, 344 | amountIn: TokenAmount, 345 | tokenOut: Token, 346 | poolKeys: LiquidityPoolKeysV4): Promise<{ amountOut: TokenAmount | CurrencyAmount, trend: PriceTrend } | null> { 347 | try { 348 | const firstAttempt = await calculateAmountOut(connection, amountIn, tokenOut, poolKeys) 349 | await delay(200); 350 | const secodAttempt = await calculateAmountOut(connection, amountIn, tokenOut, poolKeys) 351 | 352 | const dumpedPriceImpactPercent = 29 /// 30% 353 | const secondPriceImpact = Number(secodAttempt.priceImpact) 354 | const firstPriceImpact = Number(firstAttempt.priceImpact) 355 | if (secondPriceImpact > dumpedPriceImpactPercent || firstPriceImpact > dumpedPriceImpactPercent) { 356 | return { amountOut: secodAttempt.amountOut, trend: 'UNSTABLE' } 357 | } 358 | 359 | const trend = firstAttempt.amountOut.gt(secodAttempt.amountOut) ? 'DOWN' : 'UP'; 360 | return { amountOut: secodAttempt.amountOut, trend }; 361 | } catch (e) { 362 | console.error(chalk.yellow('Failed to get amountOut and identify price trend.')); 363 | return null; 364 | } 365 | } --------------------------------------------------------------------------------