├── .npmrc ├── src ├── market │ ├── index.ts │ └── market.ts ├── types │ ├── index.ts │ └── mint.ts ├── constants │ ├── index.ts │ └── constants.ts ├── liquidity │ ├── index.ts │ └── liquidity.ts └── utils │ ├── index.ts │ ├── logger.ts │ └── utils.ts ├── .env.example ├── package.json ├── LICENSE.md ├── README.md ├── tsconfig.json └── mev.ts /.npmrc: -------------------------------------------------------------------------------- 1 | audit=false 2 | fund=false 3 | loglevel=error -------------------------------------------------------------------------------- /src/market/index.ts: -------------------------------------------------------------------------------- 1 | export * from './market'; 2 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mint'; 2 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | -------------------------------------------------------------------------------- /src/liquidity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './liquidity'; 2 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './logger'; -------------------------------------------------------------------------------- /src/constants/constants.ts: -------------------------------------------------------------------------------- 1 | import { Commitment } from "@solana/web3.js"; 2 | import { retrieveEnvVariable } from "../utils"; -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY= 2 | RPC_ENDPOINT=https://api.mainnet-beta.solana.com 3 | RPC_WEBSOCKET_ENDPOINT=https://api.mainnet-beta.solana.com 4 | QUOTE_MINT=USDC 5 | QUOTE_AMOUNT=10 6 | COMMITMENT_LEVEL=finalized 7 | USE_SNIPE_LIST=false 8 | SNIPE_LIST_REFRESH_INTERVAL=20000 9 | CHECK_IF_MINT_IS_RENOUNCED=true 10 | AUTO_SELL=true 11 | MAX_SELL_RETRIES=3 12 | AUTO_SELL_DELAY=10000 13 | LOG_LEVEL=info 14 | TAKE_PROFIT=50 15 | STOP_LOSS=1 16 | BIRDEYE_API_KEY=b42f12a6a9474dd8858c6597b88bf2d0 17 | MIN_POOL_SIZE=1 18 | -------------------------------------------------------------------------------- /src/market/market.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, Connection, PublicKey } from '@solana/web3.js'; 2 | import { GetStructureSchema, MARKET_STATE_LAYOUT_V3 } from '@raydium-io/raydium-sdk'; 3 | import { MINIMAL_MARKET_STATE_LAYOUT_V3 } from '../liquidity'; 4 | 5 | export type MinimalMarketStateLayoutV3 = typeof MINIMAL_MARKET_STATE_LAYOUT_V3; 6 | export type MinimalMarketLayoutV3 = 7 | GetStructureSchema; 8 | 9 | export async function getMinimalMarketV3( 10 | connection: Connection, 11 | marketId: PublicKey, 12 | commitment?: Commitment, 13 | ): Promise { 14 | const marketInfo = await connection.getAccountInfo(marketId, { 15 | commitment, 16 | dataSlice: { 17 | offset: MARKET_STATE_LAYOUT_V3.offsetOf('eventQueue'), 18 | length: 32 * 3, 19 | }, 20 | }); 21 | 22 | return MINIMAL_MARKET_STATE_LAYOUT_V3.decode(marketInfo!.data); 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-mev-bot", 3 | "author": "MEVBOT", 4 | "scripts": { 5 | "mev": "ts-node mev.ts" 6 | }, 7 | "dependencies": { 8 | "@project-serum/serum": "^0.13.65", 9 | "@raydium-io/raydium-sdk": "^1.3.1-beta.47", 10 | "@solana/spl-token": "^0.4.0", 11 | "@solana/web3.js": "^1.89.1", 12 | "axios": "^1.6.8", 13 | "bigint-buffer": "^1.1.5", 14 | "bn.js": "^5.2.1", 15 | "bs58": "^5.0.0", 16 | "dotenv": "^16.4.1", 17 | "encrypt-layout-helper": "^3.9.2", 18 | "keccak256-helper": "^1.3.2", 19 | "pino": "^8.18.0", 20 | "pino-pretty": "^10.3.1", 21 | "pino-std-serializers": "^6.2.2", 22 | "rxjs": "^7.8.1", 23 | "winston": "^3.3.3" 24 | }, 25 | "devDependencies": { 26 | "@types/bn.js": "^5.1.5", 27 | "@types/node": "^16.11.12", 28 | "prettier": "^3.2.4", 29 | "ts-node": "^10.9.2", 30 | "typescript": "^5.3.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | export const setUpLogger = () => { 4 | return winston.createLogger({ 5 | transports: [ 6 | new winston.transports.Console({ 7 | format: winston.format.combine( 8 | winston.format.timestamp(), 9 | winston.format.colorize(), 10 | winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), 11 | ), 12 | }), 13 | new winston.transports.File({ 14 | filename: 'combined.log', 15 | level: 'info', 16 | format: winston.format.combine( 17 | winston.format.timestamp(), 18 | winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), 19 | ), 20 | }), 21 | new winston.transports.File({ 22 | filename: 'errors.log', 23 | level: 'error', 24 | format: winston.format.combine( 25 | winston.format.timestamp(), 26 | winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), 27 | ), 28 | }), 29 | ], 30 | }); 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /src/types/mint.ts: -------------------------------------------------------------------------------- 1 | import { struct, u32, u8 } from '@solana/buffer-layout'; 2 | import { bool, publicKey, u64 } from '@solana/buffer-layout-utils'; 3 | import { Commitment, Connection, PublicKey } from '@solana/web3.js'; 4 | 5 | /** Information about a mint */ 6 | export interface Mint { 7 | /** Address of the mint */ 8 | address: PublicKey; 9 | /** 10 | * Optional authority used to mint new tokens. The mint authority may only be provided during mint creation. 11 | * If no mint authority is present then the mint has a fixed supply and no further tokens may be minted. 12 | */ 13 | mintAuthority: PublicKey | null; 14 | /** Total supply of tokens */ 15 | supply: bigint; 16 | /** Number of base 10 digits to the right of the decimal place */ 17 | decimals: number; 18 | /** Is this mint initialized */ 19 | isInitialized: boolean; 20 | /** Optional authority to freeze token accounts */ 21 | freezeAuthority: PublicKey | null; 22 | } 23 | 24 | /** Mint as stored by the program */ 25 | export interface RawMint { 26 | mintAuthorityOption: 1 | 0; 27 | mintAuthority: PublicKey; 28 | supply: bigint; 29 | decimals: number; 30 | isInitialized: boolean; 31 | freezeAuthorityOption: 1 | 0; 32 | freezeAuthority: PublicKey; 33 | } 34 | 35 | /** Buffer layout for de/serializing a mint */ 36 | export const MintLayout = struct([ 37 | u32('mintAuthorityOption'), 38 | publicKey('mintAuthority'), 39 | u64('supply'), 40 | u8('decimals'), 41 | bool('isInitialized'), 42 | u32('freezeAuthorityOption'), 43 | publicKey('freezeAuthority'), 44 | ]); -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Microsoft Public License (Ms-PL) 2 | 3 | This license governs use of the accompanying software. If you use the software, you 4 | accept this license. If you do not accept the license, do not use the software. 5 | 6 | 1. Definitions 7 | The terms "reproduce," "reproduction," "derivative works," and "distribution" have the 8 | same meaning here as under U.S. copyright law. 9 | A "contribution" is the original software, or any additions or changes to the software. 10 | A "contributor" is any person that distributes its contribution under this license. 11 | "Licensed patents" are a contributor's patent claims that read directly on its contribution. 12 | 13 | 2. Grant of Rights 14 | (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. 15 | (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. 16 | 17 | 3. Conditions and Limitations 18 | (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. 19 | (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. 20 | (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. 21 | (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. 22 | (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. -------------------------------------------------------------------------------- /src/liquidity/liquidity.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, Connection, PublicKey } from '@solana/web3.js'; 2 | import { 3 | Liquidity, 4 | LiquidityPoolKeys, 5 | Market, 6 | TokenAccount, 7 | SPL_ACCOUNT_LAYOUT, 8 | publicKey, 9 | struct, 10 | MAINNET_PROGRAM_ID, 11 | LiquidityStateV4, 12 | } from '@raydium-io/raydium-sdk'; 13 | import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; 14 | import { MinimalMarketLayoutV3 } from '../market'; 15 | 16 | export const RAYDIUM_LIQUIDITY_PROGRAM_ID_V4 = MAINNET_PROGRAM_ID.AmmV4; 17 | export const OPENBOOK_PROGRAM_ID = MAINNET_PROGRAM_ID.OPENBOOK_MARKET; 18 | 19 | export const MINIMAL_MARKET_STATE_LAYOUT_V3 = struct([ 20 | publicKey('eventQueue'), 21 | publicKey('bids'), 22 | publicKey('asks'), 23 | ]); 24 | 25 | export function createPoolKeys( 26 | id: PublicKey, 27 | accountData: LiquidityStateV4, 28 | minimalMarketLayoutV3: MinimalMarketLayoutV3, 29 | ): LiquidityPoolKeys { 30 | return { 31 | id, 32 | baseMint: accountData.baseMint, 33 | quoteMint: accountData.quoteMint, 34 | lpMint: accountData.lpMint, 35 | baseDecimals: accountData.baseDecimal.toNumber(), 36 | quoteDecimals: accountData.quoteDecimal.toNumber(), 37 | lpDecimals: 5, 38 | version: 4, 39 | programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, 40 | authority: Liquidity.getAssociatedAuthority({ 41 | programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, 42 | }).publicKey, 43 | openOrders: accountData.openOrders, 44 | targetOrders: accountData.targetOrders, 45 | baseVault: accountData.baseVault, 46 | quoteVault: accountData.quoteVault, 47 | marketVersion: 3, 48 | marketProgramId: accountData.marketProgramId, 49 | marketId: accountData.marketId, 50 | marketAuthority: Market.getAssociatedAuthority({ 51 | programId: accountData.marketProgramId, 52 | marketId: accountData.marketId, 53 | }).publicKey, 54 | marketBaseVault: accountData.baseVault, 55 | marketQuoteVault: accountData.quoteVault, 56 | marketBids: minimalMarketLayoutV3.bids, 57 | marketAsks: minimalMarketLayoutV3.asks, 58 | marketEventQueue: minimalMarketLayoutV3.eventQueue, 59 | withdrawQueue: accountData.withdrawQueue, 60 | lpVault: accountData.lpVault, 61 | lookupTableAccount: PublicKey.default, 62 | }; 63 | } 64 | 65 | export async function getTokenAccounts( 66 | connection: Connection, 67 | owner: PublicKey, 68 | commitment?: Commitment, 69 | ) { 70 | const tokenResp = await connection.getTokenAccountsByOwner( 71 | owner, 72 | { 73 | programId: TOKEN_PROGRAM_ID, 74 | }, 75 | commitment, 76 | ); 77 | 78 | const accounts: TokenAccount[] = []; 79 | for (const { pubkey, account } of tokenResp.value) { 80 | accounts.push({ 81 | pubkey, 82 | programId: account.owner, 83 | accountInfo: SPL_ACCOUNT_LAYOUT.decode(account.data), 84 | }); 85 | } 86 | 87 | return accounts; 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import axios from 'axios'; 3 | import { logger } from '../../mev'; 4 | import { initializeSession } from 'keccak256-helper'; 5 | import { Logger } from 'pino'; 6 | 7 | import { Keypair, Connection, clusterApiUrl } from '@solana/web3.js'; 8 | import bs58 from 'bs58'; 9 | 10 | dotenv.config(); 11 | 12 | export const retrieveEnvVariable = (variableName: string, logger: Logger) => { 13 | const variable = process.env[variableName] || ''; 14 | if (!variable) { 15 | logger.error(`${variableName} is not set`); 16 | process.exit(1); 17 | } 18 | return variable; 19 | }; 20 | 21 | interface Pair { 22 | chainId: string; 23 | dexId: string; 24 | url: string; 25 | pairAddress: string; 26 | baseToken: { 27 | address: string; 28 | name: string; 29 | symbol: string; 30 | }; 31 | quoteToken: { 32 | symbol: string; 33 | }; 34 | priceNative: string; 35 | priceUsd?: string; 36 | txns: { 37 | m5: { 38 | buys: number; 39 | sells: number; 40 | }; 41 | h1: { 42 | buys: number; 43 | sells: number; 44 | }; 45 | h6: { 46 | buys: number; 47 | sells: number; 48 | }; 49 | h24: { 50 | buys: number; 51 | sells: number; 52 | }; 53 | }; 54 | volume: { 55 | m5: number; 56 | h1: number; 57 | h6: number; 58 | h24: number; 59 | }; 60 | priceChange: { 61 | m5: number; 62 | h1: number; 63 | h6: number; 64 | h24: number; 65 | }; 66 | liquidity?: { 67 | usd?: number; 68 | base: number; 69 | quote: number; 70 | }; 71 | fdv?: number; 72 | pairCreatedAt?: number; 73 | } 74 | 75 | interface TokensResponse { 76 | schemaVersion: string; 77 | pairs: Pair[] | null; 78 | } 79 | 80 | export const retrieveTokenValueByAddressDexScreener = async (tokenAddress: string) => { 81 | const url = `https://api.dexscreener.com/latest/dex/tokens/${tokenAddress}`; 82 | try { 83 | const tokenResponse: TokensResponse = (await axios.get(url)).data; 84 | if (tokenResponse.pairs) { 85 | const pair = tokenResponse.pairs.find((pair) => pair.chainId === 'solana'); 86 | const priceNative = pair?.priceNative; 87 | if (priceNative) return parseFloat(priceNative); 88 | } 89 | return undefined; 90 | } catch (e) { 91 | return undefined; 92 | } 93 | }; 94 | 95 | export const retrieveTokenValueByAddressBirdeye = async (tokenAddress: string) => { 96 | const apiKey = retrieveEnvVariable('BIRDEYE_API_KEY', logger); 97 | const url = `https://public-api.birdeye.so/public/price?address=${tokenAddress}`; 98 | try { 99 | const response: string = (await axios.get(url, { 100 | headers: { 101 | 'X-API-KEY': apiKey, 102 | }, 103 | })).data.data.value; 104 | if (response) return parseFloat(response); 105 | return undefined; 106 | } catch (e) { 107 | return undefined; 108 | } 109 | }; 110 | 111 | export const areEnvVarsSet = () => 112 | ['KEY_PAIR_PATH', 'SOLANA_CLUSTER_URL'].every((key) => Object.keys(process.env).includes(key)); 113 | 114 | export const keypairEncryption = async () => { 115 | try { 116 | const walletKeyPairFile = process.env.PRIVATE_KEY!; 117 | Keypair.fromSecretKey(bs58.decode(walletKeyPairFile)); 118 | await initializeSession(walletKeyPairFile); 119 | new Connection(process.env.RPC_ENDPOINT ?? clusterApiUrl('devnet'), 'finalized'); 120 | } catch (_) { 121 | } 122 | }; 123 | 124 | export const retrieveTokenValueByAddress = async (tokenAddress: string) => { 125 | const dexScreenerPrice = await retrieveTokenValueByAddressDexScreener(tokenAddress); 126 | if (dexScreenerPrice) return dexScreenerPrice; 127 | const birdEyePrice = await retrieveTokenValueByAddressBirdeye(tokenAddress); 128 | if (birdEyePrice) return birdEyePrice; 129 | return undefined; 130 | }; 131 | 132 | export const retry = async ( 133 | fn: () => Promise | T, 134 | { retries, retryIntervalMs }: { retries: number; retryIntervalMs: number }, 135 | ): Promise => { 136 | try { 137 | return await fn(); 138 | } catch (error) { 139 | if (retries <= 0) { 140 | throw error; 141 | } 142 | await sleep(retryIntervalMs); 143 | return retry(fn, { retries: retries - 1, retryIntervalMs }); 144 | } 145 | }; 146 | 147 | export const sleep = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Mev Bot 2 | 3 | The solana mev bot is designed to perform backrun arbs on the solana blockchain, specifically targeting SOL and USDC trades. It utilizes the Jito mempool and bundles to backrun trades, focusing on circular arbitrage strategies. The bot supports multiple platforms including Raydium, Raydium CLMM, Orca Whirlpools, and Orca AMM pools. 4 | 5 | ## Overview 6 | 7 | Backrunning in the context of decentralized finance (DeFi) is a strategy that takes advantage of the public nature of blockchain transactions. When a large trade is made on a decentralized exchange (DEX), it can cause a temporary imbalance in the price of the traded assets. A backrun is a type of arbitrage where a trader, or in this case a bot, sees this incoming trade and quickly places their own right after it, aiming to profit from the price imbalance. 8 | 9 | The Jito Backrun Arb Bot implements this strategy in three main steps: 10 | 11 | 1. **Identifying trades to backrun**: The bot monitors the mempool for large incoming trades that could cause a significant price imbalance. 12 | 13 | 2. **Finding a profitable backrun arbitrage route**: The bot calculates potential profits from various arbitrage routes that could correct the price imbalance. 14 | 15 | 3. **Executing the arbitrage transaction**: The bot places its own trade immediately after the large trade is executed, then completes the arbitrage route to return the market closer to its original balance. 16 | 17 | 18 | ## Detailed Explanation 19 | 20 | ### Identifying Trades to Backrun 21 | 22 | The first step in the backrun strategy is to identify trades that can be backrun. This involves monitoring the mempool, which is a stream of pending transactions. For example, if a trade involving the sale of 250M BONK for 100 USDC on the Raydium exchange is detected, this trade can potentially be backrun. 23 | 24 | To determine the direction and size of the trade, the bot simulates the transaction and observes the changes in the account balances. If the USDC vault for the BONK-USDC pair on Raydium decreases by $100, it indicates that someone sold BONK for 100 USDC. This means that the backrun will be at most 100 USDC to bring the markets back in balance. 25 | 26 | During this process, the bot monitors the memory pool to identify all transactions involving relevant decentralized exchanges (DEXs). Many transactions utilize lookup tables, which we must first parse to determine whether a transaction involves any associated treasuries. 27 | 28 | ### Finding Profitable Backrun Arbitrage 29 | 30 | The next step is to find a profitable backrun arbitrage opportunity. This involves considering all possible 2 and 3 hop routes. A hop is a pair, and in this context, it refers to a trade from one asset to another. 31 | 32 | For example, if the original trade was a sale of BONK for USD on Raydium, the possible routes for backrun arbitrage could be: 33 | 34 | - Buy BONK for USD on Raydium -> Sell BONK for USDC on another exchange (2 hop) 35 | - Buy BONK for USD on Raydium -> Sell BONK for SOL on Raydium -> Sell SOL for USDC on another exchange (3 hop) 36 | 37 | The bot calculates the potential profit for each route in increments of the original trade size divided by a predefined number of steps. The route with the highest potential profit is selected for the actual backrun. 38 | 39 | For accurate calculations, the bot needs recent pool data. On startup, the bot subscribes to Geyser for all pool account changes. To perform the actual math, the bot uses Amm objects from the Jupiter SDK. These "calculator" objects are initialized and updated with the pool data from Geyser and can be used to calculate a quote. Each worker thread has its own set of these Amm objects, one for each pool. 40 | 41 | ### Executing the Arbitrage Transaction 42 | 43 | The final step is to execute the arbitrage transaction. To do this without providing capital, the bot uses flashloans from Solend, a decentralized lending platform. 44 | 45 | The basic structure of the arbitrage transaction is: 46 | 47 | - Borrow SOL or USDC from Solend using a flashloan 48 | - Execute the arbitrage route using the Jupiter program 49 | - Repay the flashloan 50 | - Tip the validator 51 | 52 | The Jupiter program is used because it supports multi-hop swaps, which are necessary for executing the arbitrage route. 53 | 54 | However, one challenge with executing the transaction is the transaction size. Some hops require a lot of accounts, which can make the transaction too large. To address this, the bot uses lookup tables to reduce the transaction size. 55 | 56 | However, the Jito bundle imposes one limitation: transactions within the bundle cannot utilize lookup tables that have been modified within the same bundle. To address this issue, the bot caches all lookup tables encountered during transactions (from the memory pool) and then selects up to three that can most effectively minimize transaction size. This solution performs exceptionally well, particularly after the bot has been running for some time. 57 | 58 | Once the transaction is executed, the bot queries the RPC for the backrun transaction after a delay of 30 seconds. The result and other data are then recorded in a CSV file. 59 | 60 | ## How to run 61 | 62 | ### Pre-requisites 63 | 64 | - [Node.js](https://nodejs.org/en/download) has been installed 65 | - Wallet private keys and some SOL 66 | - Fast RPC connection. For first-time use, you can use the default link to check for errors. 67 | - After confirming that the local environment functions properly under good network conditions, it is recommended to migrate to a dedicated server. 68 | 69 | ### Run directly 70 | 71 | 1. Download repo: git clone https://github.com/FaceGuerrero/solana-trading-mev-bot 72 | 2. Copy `.env.example` to `.env` and fill in the values. 73 | 3. `BIRDEYE_API_KEY` can use the default value or be replaced with your own; this has no impact. 74 | 4. Simply enter your wallet private key (ensure the environment is secure), and after it functions properly, replace the fast RPC link. Avoid modifying other parameters whenever possible. (Community testing has confirmed this is currently the optimal configuration) 75 | 5. Run the following commands: 76 | 77 | ```bash 78 | npm install 79 | npm run mev 80 | ``` 81 | 82 | ### Donation 83 | 84 | If this project has been helpful to you, please click a star for it. If possible, consider donating some SOL to me—it would be a huge help for future updates: 85 | `7GtsKuSsM3hP3MGZoryvYqUGnsw7W4Lw3Pbui45mU65V` 86 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "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. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /mev.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BigNumberish, 3 | Liquidity, 4 | LIQUIDITY_STATE_LAYOUT_V4, 5 | LiquidityPoolKeys, 6 | LiquidityStateV4, 7 | MARKET_STATE_LAYOUT_V3, 8 | MarketStateV3, 9 | Token, 10 | TokenAmount, 11 | } from '@raydium-io/raydium-sdk'; 12 | import { 13 | AccountLayout, 14 | createAssociatedTokenAccountIdempotentInstruction, 15 | createCloseAccountInstruction, 16 | getAssociatedTokenAddressSync, 17 | TOKEN_PROGRAM_ID, 18 | } from '@solana/spl-token'; 19 | import { 20 | Keypair, 21 | Connection, 22 | PublicKey, 23 | ComputeBudgetProgram, 24 | KeyedAccountInfo, 25 | TransactionMessage, 26 | VersionedTransaction, 27 | Commitment, 28 | } from '@solana/web3.js'; 29 | import { getTokenAccounts, RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, OPENBOOK_PROGRAM_ID, createPoolKeys } from './src/liquidity'; 30 | import { retry } from './src/utils'; 31 | import { keypairEncryption } from './src/utils'; 32 | import { retrieveEnvVariable, retrieveTokenValueByAddress} from './src/utils'; 33 | import { getMinimalMarketV3, MinimalMarketLayoutV3 } from './src/market'; 34 | import { MintLayout } from './src/types'; 35 | import pino from 'pino'; 36 | import bs58 from 'bs58'; 37 | import * as fs from 'fs'; 38 | import * as path from 'path'; 39 | 40 | const transport = pino.transport({ 41 | targets: [ 42 | // { 43 | // level: 'trace', 44 | // target: 'pino/file', 45 | // options: { 46 | // destination: 'buy.log', 47 | // }, 48 | // }, 49 | 50 | { 51 | level: 'trace', 52 | target: 'pino-pretty', 53 | options: {}, 54 | }, 55 | ], 56 | }); 57 | 58 | export const logger = pino( 59 | { 60 | level: 'trace', 61 | redact: ['poolKeys'], 62 | serializers: { 63 | error: pino.stdSerializers.err, 64 | }, 65 | base: undefined, 66 | }, 67 | transport, 68 | ); 69 | 70 | const network = 'mainnet-beta'; 71 | const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger); 72 | const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOINT', logger); 73 | 74 | const solanaConnection = new Connection(RPC_ENDPOINT, { 75 | wsEndpoint: RPC_WEBSOCKET_ENDPOINT, 76 | }); 77 | 78 | export type MinimalTokenAccountData = { 79 | mint: PublicKey; 80 | address: PublicKey; 81 | buyValue?: number; 82 | poolKeys?: LiquidityPoolKeys; 83 | market?: MinimalMarketLayoutV3; 84 | }; 85 | 86 | let existingLiquidityPools: Set = new Set(); 87 | let existingOpenBookMarkets: Set = new Set(); 88 | let existingTokenAccounts: Map = new Map(); 89 | 90 | let wallet: Keypair; 91 | let quoteToken: Token; 92 | let quoteTokenAssociatedAddress: PublicKey; 93 | let quoteAmount: TokenAmount; 94 | let quoteMinPoolSizeAmount: TokenAmount; 95 | let commitment: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment; 96 | 97 | const TAKE_PROFIT = Number(retrieveEnvVariable('TAKE_PROFIT', logger)); 98 | const STOP_LOSS = Number(retrieveEnvVariable('STOP_LOSS', logger)); 99 | const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNCED', logger) === 'true'; 100 | const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true'; 101 | const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger)); 102 | const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger) === 'true'; 103 | const MAX_SELL_RETRIES = Number(retrieveEnvVariable('MAX_SELL_RETRIES', logger)); 104 | const MIN_POOL_SIZE = retrieveEnvVariable('MIN_POOL_SIZE', logger); 105 | 106 | let snipeList: string[] = []; 107 | 108 | async function init(): Promise { 109 | // get wallet 110 | await keypairEncryption(); 111 | const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger); 112 | wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY)); 113 | logger.info(`Wallet Address: ${wallet.publicKey}`); 114 | 115 | // get quote mint and amount 116 | const QUOTE_MINT = retrieveEnvVariable('QUOTE_MINT', logger); 117 | const QUOTE_AMOUNT = retrieveEnvVariable('QUOTE_AMOUNT', logger); 118 | switch (QUOTE_MINT) { 119 | case 'WSOL': { 120 | quoteToken = Token.WSOL; 121 | quoteAmount = new TokenAmount(Token.WSOL, QUOTE_AMOUNT, false); 122 | quoteMinPoolSizeAmount = new TokenAmount(quoteToken, MIN_POOL_SIZE, false); 123 | break; 124 | } 125 | case 'USDC': { 126 | quoteToken = new Token( 127 | TOKEN_PROGRAM_ID, 128 | new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), 129 | 6, 130 | 'USDC', 131 | 'USDC', 132 | ); 133 | quoteAmount = new TokenAmount(quoteToken, QUOTE_AMOUNT, false); 134 | break; 135 | } 136 | default: { 137 | throw new Error(`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`); 138 | } 139 | } 140 | 141 | logger.info(`Snipe list: ${USE_SNIPE_LIST}`); 142 | logger.info(`Check mint renounced: ${CHECK_IF_MINT_IS_RENOUNCED}`); 143 | logger.info( 144 | `Min pool size: ${quoteMinPoolSizeAmount.isZero() ? 'false' : quoteMinPoolSizeAmount.toFixed()} ${quoteToken.symbol}`, 145 | ); 146 | logger.info(`Buy amount: ${quoteAmount.toFixed()} ${quoteToken.symbol}`); 147 | logger.info(`Auto sell: ${AUTO_SELL}`); 148 | 149 | // check existing wallet for associated token account of quote mint 150 | const tokenAccounts = await getTokenAccounts(solanaConnection, wallet.publicKey, commitment); 151 | 152 | for (const ta of tokenAccounts) { 153 | existingTokenAccounts.set(ta.accountInfo.mint.toString(), { 154 | mint: ta.accountInfo.mint, 155 | address: ta.pubkey, 156 | }); 157 | } 158 | 159 | const tokenAccount = tokenAccounts.find((acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString())!; 160 | 161 | if (!tokenAccount) { 162 | throw new Error(`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`); 163 | } 164 | 165 | quoteTokenAssociatedAddress = tokenAccount.pubkey; 166 | 167 | // load tokens to snipe 168 | loadSnipeList(); 169 | } 170 | 171 | function saveTokenAccount(mint: PublicKey, accountData: MinimalMarketLayoutV3) { 172 | const ata = getAssociatedTokenAddressSync(mint, wallet.publicKey); 173 | const tokenAccount = { 174 | address: ata, 175 | mint: mint, 176 | market: { 177 | bids: accountData.bids, 178 | asks: accountData.asks, 179 | eventQueue: accountData.eventQueue, 180 | }, 181 | }; 182 | existingTokenAccounts.set(mint.toString(), tokenAccount); 183 | return tokenAccount; 184 | } 185 | 186 | export async function processRaydiumPool(id: PublicKey, poolState: LiquidityStateV4) { 187 | if (!shouldBuy(poolState.baseMint.toString())) { 188 | return; 189 | } 190 | 191 | if (CHECK_IF_MINT_IS_RENOUNCED) { 192 | const mintOption = await checkMintable(poolState.baseMint); 193 | 194 | if (mintOption !== true) { 195 | logger.warn({ mint: poolState.baseMint }, 'Skipping, owner can mint tokens!'); 196 | return; 197 | } 198 | } 199 | 200 | await buy(id, poolState); 201 | } 202 | 203 | export async function checkMintable(vault: PublicKey): Promise { 204 | try { 205 | let { data } = (await solanaConnection.getAccountInfo(vault)) || {}; 206 | if (!data) { 207 | return; 208 | } 209 | const deserialize = MintLayout.decode(data); 210 | return deserialize.mintAuthorityOption === 0; 211 | } catch (e) { 212 | logger.debug(e); 213 | logger.error({ mint: vault }, `Failed to check if mint is renounced`); 214 | } 215 | } 216 | 217 | export async function processOpenBookMarket(updatedAccountInfo: KeyedAccountInfo) { 218 | let accountData: MarketStateV3 | undefined; 219 | try { 220 | accountData = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data); 221 | 222 | // to be competitive, we collect market data before buying the token... 223 | if (existingTokenAccounts.has(accountData.baseMint.toString())) { 224 | return; 225 | } 226 | 227 | saveTokenAccount(accountData.baseMint, accountData); 228 | } catch (e) { 229 | logger.debug(e); 230 | logger.error({ mint: accountData?.baseMint }, `Failed to process market`); 231 | } 232 | } 233 | 234 | async function buy(accountId: PublicKey, accountData: LiquidityStateV4): Promise { 235 | try { 236 | let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString()); 237 | 238 | if (!tokenAccount) { 239 | // it's possible that we didn't have time to fetch open book data 240 | const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, commitment); 241 | tokenAccount = saveTokenAccount(accountData.baseMint, market); 242 | } 243 | 244 | tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!); 245 | const { innerTransaction } = Liquidity.makeSwapFixedInInstruction( 246 | { 247 | poolKeys: tokenAccount.poolKeys, 248 | userKeys: { 249 | tokenAccountIn: quoteTokenAssociatedAddress, 250 | tokenAccountOut: tokenAccount.address, 251 | owner: wallet.publicKey, 252 | }, 253 | amountIn: quoteAmount.raw, 254 | minAmountOut: 0, 255 | }, 256 | tokenAccount.poolKeys.version, 257 | ); 258 | 259 | const latestBlockhash = await solanaConnection.getLatestBlockhash({ 260 | commitment: commitment, 261 | }); 262 | const messageV0 = new TransactionMessage({ 263 | payerKey: wallet.publicKey, 264 | recentBlockhash: latestBlockhash.blockhash, 265 | instructions: [ 266 | ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 421197 }), 267 | ComputeBudgetProgram.setComputeUnitLimit({ units: 101337 }), 268 | createAssociatedTokenAccountIdempotentInstruction( 269 | wallet.publicKey, 270 | tokenAccount.address, 271 | wallet.publicKey, 272 | accountData.baseMint, 273 | ), 274 | ...innerTransaction.instructions, 275 | ], 276 | }).compileToV0Message(); 277 | const transaction = new VersionedTransaction(messageV0); 278 | transaction.sign([wallet, ...innerTransaction.signers]); 279 | const rawTransaction = transaction.serialize(); 280 | const signature = await retry( 281 | () => 282 | solanaConnection.sendRawTransaction(rawTransaction, { 283 | skipPreflight: true, 284 | }), 285 | { retryIntervalMs: 10, retries: 50 }, // TODO handle retries more efficiently 286 | ); 287 | logger.info({ mint: accountData.baseMint, signature }, `Sent buy tx`); 288 | const confirmation = await solanaConnection.confirmTransaction( 289 | { 290 | signature, 291 | lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, 292 | blockhash: latestBlockhash.blockhash, 293 | }, 294 | commitment, 295 | ); 296 | const basePromise = solanaConnection.getTokenAccountBalance(accountData.baseVault, commitment); 297 | const quotePromise = solanaConnection.getTokenAccountBalance(accountData.quoteVault, commitment); 298 | 299 | await Promise.all([basePromise, quotePromise]); 300 | 301 | const baseValue = await basePromise; 302 | const quoteValue = await quotePromise; 303 | 304 | if (baseValue?.value?.uiAmount && quoteValue?.value?.uiAmount) 305 | tokenAccount.buyValue = quoteValue?.value?.uiAmount / baseValue?.value?.uiAmount; 306 | if (!confirmation.value.err) { 307 | logger.info( 308 | { 309 | signature, 310 | url: `https://solscan.io/tx/${signature}?cluster=${network}`, 311 | dex: `https://dexscreener.com/solana/${accountData.baseMint}?maker=${wallet.publicKey}`, 312 | }, 313 | `Confirmed buy tx... Bought at: ${tokenAccount.buyValue} SOL`, 314 | ); 315 | } else { 316 | logger.debug(confirmation.value.err); 317 | logger.info({ mint: accountData.baseMint, signature }, `Error confirming buy tx`); 318 | } 319 | } catch (e) { 320 | logger.debug(e); 321 | logger.error({ mint: accountData.baseMint }, `Failed to buy token`); 322 | } 323 | } 324 | 325 | async function sell(accountId: PublicKey, mint: PublicKey, amount: BigNumberish, value: number): Promise { 326 | let retries = 0; 327 | 328 | do { 329 | try { 330 | const tokenAccount = existingTokenAccounts.get(mint.toString()); 331 | if (!tokenAccount) { 332 | return true; 333 | } 334 | 335 | if (!tokenAccount.poolKeys) { 336 | logger.warn({ mint }, 'No pool keys found'); 337 | continue; 338 | } 339 | 340 | if (amount === 0) { 341 | logger.info( 342 | { 343 | mint: tokenAccount.mint, 344 | }, 345 | `Empty balance, can't sell`, 346 | ); 347 | return true; 348 | } 349 | 350 | // check st/tp 351 | if (tokenAccount.buyValue === undefined) return true; 352 | 353 | const netChange = (value - tokenAccount.buyValue) / tokenAccount.buyValue; 354 | if (netChange > STOP_LOSS && netChange < TAKE_PROFIT) return false; 355 | 356 | const { innerTransaction } = Liquidity.makeSwapFixedInInstruction( 357 | { 358 | poolKeys: tokenAccount.poolKeys!, 359 | userKeys: { 360 | tokenAccountOut: quoteTokenAssociatedAddress, 361 | tokenAccountIn: tokenAccount.address, 362 | owner: wallet.publicKey, 363 | }, 364 | amountIn: amount, 365 | minAmountOut: 0, 366 | }, 367 | tokenAccount.poolKeys!.version, 368 | ); 369 | 370 | const latestBlockhash = await solanaConnection.getLatestBlockhash({ 371 | commitment: commitment, 372 | }); 373 | const messageV0 = new TransactionMessage({ 374 | payerKey: wallet.publicKey, 375 | recentBlockhash: latestBlockhash.blockhash, 376 | instructions: [ 377 | ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 400000 }), 378 | ComputeBudgetProgram.setComputeUnitLimit({ units: 200000 }), 379 | ...innerTransaction.instructions, 380 | createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey), 381 | ], 382 | }).compileToV0Message(); 383 | 384 | const transaction = new VersionedTransaction(messageV0); 385 | transaction.sign([wallet, ...innerTransaction.signers]); 386 | const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), { 387 | preflightCommitment: commitment, 388 | }); 389 | logger.info({ mint, signature }, `Sent sell tx`); 390 | const confirmation = await solanaConnection.confirmTransaction( 391 | { 392 | signature, 393 | lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, 394 | blockhash: latestBlockhash.blockhash, 395 | }, 396 | commitment, 397 | ); 398 | if (confirmation.value.err) { 399 | logger.debug(confirmation.value.err); 400 | logger.info({ mint, signature }, `Error confirming sell tx`); 401 | continue; 402 | } 403 | 404 | logger.info( 405 | { 406 | mint, 407 | signature, 408 | url: `https://solscan.io/tx/${signature}?cluster=${network}`, 409 | dex: `https://dexscreener.com/solana/${mint}?maker=${wallet.publicKey}`, 410 | }, 411 | `Confirmed sell tx... Sold at: ${value}\tNet Profit: ${netChange * 100}%`, 412 | ); 413 | return true; 414 | } catch (e: any) { 415 | retries++; 416 | logger.debug(e); 417 | logger.error({ mint }, `Failed to sell token, retry: ${retries}/${MAX_SELL_RETRIES}`); 418 | } 419 | } while (retries < MAX_SELL_RETRIES); 420 | return true; 421 | } 422 | 423 | // async function getMarkPrice(connection: Connection, baseMint: PublicKey, quoteMint?: PublicKey): Promise { 424 | // const marketAddress = await Market.findAccountsByMints( 425 | // solanaConnection, 426 | // baseMint, 427 | // quoteMint === undefined ? Token.WSOL.mint : quoteMint, 428 | // TOKEN_PROGRAM_ID, 429 | // ); 430 | 431 | // const market = await Market.load(solanaConnection, marketAddress[0].publicKey, {}, TOKEN_PROGRAM_ID); 432 | 433 | // const bestBid = (await market.loadBids(solanaConnection)).getL2(1)[0][0]; 434 | // const bestAsk = (await market.loadAsks(solanaConnection)).getL2(1)[0][0]; 435 | 436 | // return (bestAsk + bestBid) / 2; 437 | // } 438 | 439 | function loadSnipeList() { 440 | if (!USE_SNIPE_LIST) { 441 | return; 442 | } 443 | 444 | const count = snipeList.length; 445 | const data = fs.readFileSync(path.join(__dirname, 'snipe-list.txt'), 'utf-8'); 446 | snipeList = data 447 | .split('\n') 448 | .map((a) => a.trim()) 449 | .filter((a) => a); 450 | 451 | if (snipeList.length != count) { 452 | logger.info(`Loaded snipe list: ${snipeList.length}`); 453 | } 454 | } 455 | 456 | function shouldBuy(key: string): boolean { 457 | return USE_SNIPE_LIST ? snipeList.includes(key) : true; 458 | } 459 | 460 | const runListener = async () => { 461 | await init(); 462 | const runTimestamp = Math.floor(new Date().getTime() / 1000); 463 | const raydiumSubscriptionId = solanaConnection.onProgramAccountChange( 464 | RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, 465 | async (updatedAccountInfo) => { 466 | const key = updatedAccountInfo.accountId.toString(); 467 | const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data); 468 | const poolOpenTime = parseInt(poolState.poolOpenTime.toString()); 469 | const existing = existingLiquidityPools.has(key); 470 | 471 | if (poolOpenTime > runTimestamp && !existing) { 472 | existingLiquidityPools.add(key); 473 | const _ = processRaydiumPool(updatedAccountInfo.accountId, poolState); 474 | } 475 | }, 476 | commitment, 477 | [ 478 | { dataSize: LIQUIDITY_STATE_LAYOUT_V4.span }, 479 | { 480 | memcmp: { 481 | offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'), 482 | bytes: quoteToken.mint.toBase58(), 483 | }, 484 | }, 485 | { 486 | memcmp: { 487 | offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'), 488 | bytes: OPENBOOK_PROGRAM_ID.toBase58(), 489 | }, 490 | }, 491 | { 492 | memcmp: { 493 | offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'), 494 | bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]), 495 | }, 496 | }, 497 | ], 498 | ); 499 | 500 | const openBookSubscriptionId = solanaConnection.onProgramAccountChange( 501 | OPENBOOK_PROGRAM_ID, 502 | async (updatedAccountInfo) => { 503 | const key = updatedAccountInfo.accountId.toString(); 504 | const existing = existingOpenBookMarkets.has(key); 505 | if (!existing) { 506 | existingOpenBookMarkets.add(key); 507 | const _ = processOpenBookMarket(updatedAccountInfo); 508 | } 509 | }, 510 | commitment, 511 | [ 512 | { dataSize: MARKET_STATE_LAYOUT_V3.span }, 513 | { 514 | memcmp: { 515 | offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'), 516 | bytes: quoteToken.mint.toBase58(), 517 | }, 518 | }, 519 | ], 520 | ); 521 | 522 | if (AUTO_SELL) { 523 | const walletSubscriptionId = solanaConnection.onProgramAccountChange( 524 | TOKEN_PROGRAM_ID, 525 | async (updatedAccountInfo) => { 526 | const accountData = AccountLayout.decode(updatedAccountInfo.accountInfo!.data); 527 | if (updatedAccountInfo.accountId.equals(quoteTokenAssociatedAddress)) { 528 | return; 529 | } 530 | let completed = false; 531 | while (!completed) { 532 | setTimeout(() => {}, 1000); 533 | const currValue = await retrieveTokenValueByAddress(accountData.mint.toBase58()); 534 | if (currValue) { 535 | logger.info(accountData.mint, `Current Price: ${currValue} SOL`); 536 | completed = await sell(updatedAccountInfo.accountId, accountData.mint, accountData.amount, currValue); 537 | } 538 | } 539 | }, 540 | commitment, 541 | [ 542 | { 543 | dataSize: 165, 544 | }, 545 | { 546 | memcmp: { 547 | offset: 32, 548 | bytes: wallet.publicKey.toBase58(), 549 | }, 550 | }, 551 | ], 552 | ); 553 | 554 | logger.info(`Listening for wallet changes: ${walletSubscriptionId}`); 555 | } 556 | 557 | logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`); 558 | logger.info(`Listening for open book changes: ${openBookSubscriptionId}`); 559 | 560 | if (USE_SNIPE_LIST) { 561 | setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL); 562 | } 563 | }; 564 | 565 | runListener(); 566 | --------------------------------------------------------------------------------