import { BigNumberish, Liquidity, LIQUIDITY_STATE_LAYOUT_V4, LiquidityPoolKeys, LiquidityStateV4, MARKET_STATE_LAYOUT_V3, MarketStateV3, Token, TokenAmount, } from '@raydium-io/raydium-sdk'; import { AccountLayout, createAssociatedTokenAccountIdempotentInstruction, createCloseAccountInstruction, getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID, } from '@solana/spl-token'; import { Keypair, Connection, PublicKey, ComputeBudgetProgram, KeyedAccountInfo, TransactionMessage, VersionedTransaction, } from '@solana/web3.js'; import { getTokenAccounts, RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, OPENBOOK_PROGRAM_ID, createPoolKeys } from './liquidity'; import { logger } from './utils'; import { getMinimalMarketV3, MinimalMarketLayoutV3 } from './market'; import { MintLayout } from './types'; import bs58 from 'bs58'; import * as fs from 'fs'; import * as path from 'path'; import { AUTO_SELL, AUTO_SELL_DELAY, CHECK_IF_MINT_IS_RENOUNCED, COMMITMENT_LEVEL, LOG_LEVEL, MAX_SELL_RETRIES, NETWORK, PRIVATE_KEY, QUOTE_AMOUNT, QUOTE_MINT, RPC_ENDPOINT, RPC_WEBSOCKET_ENDPOINT, SNIPE_LIST_REFRESH_INTERVAL, USE_SNIPE_LIST, MIN_POOL_SIZE, } from './constants'; const solanaConnection = new Connection(RPC_ENDPOINT, { wsEndpoint: RPC_WEBSOCKET_ENDPOINT, }); export interface MinimalTokenAccountData { mint: PublicKey; address: PublicKey; poolKeys?: LiquidityPoolKeys; market?: MinimalMarketLayoutV3; } const existingLiquidityPools: Set = new Set(); const existingOpenBookMarkets: Set = new Set(); const existingTokenAccounts: Map = new Map(); let wallet: Keypair; let quoteToken: Token; let quoteTokenAssociatedAddress: PublicKey; let quoteAmount: TokenAmount; let quoteMinPoolSizeAmount: TokenAmount; let snipeList: string[] = []; async function init(): Promise { logger.level = LOG_LEVEL; // get wallet wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY)); logger.info(`Wallet Address: ${wallet.publicKey}`); // get quote mint and amount switch (QUOTE_MINT) { case 'WSOL': { quoteToken = Token.WSOL; quoteAmount = new TokenAmount(Token.WSOL, QUOTE_AMOUNT, false); quoteMinPoolSizeAmount = new TokenAmount(quoteToken, MIN_POOL_SIZE, false); break; } case 'USDC': { quoteToken = new Token( TOKEN_PROGRAM_ID, new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), 6, 'USDC', 'USDC', ); quoteAmount = new TokenAmount(quoteToken, QUOTE_AMOUNT, false); quoteMinPoolSizeAmount = new TokenAmount(quoteToken, MIN_POOL_SIZE, false); break; } default: { throw new Error(`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`); } } logger.info(`Snipe list: ${USE_SNIPE_LIST}`); logger.info(`Check mint renounced: ${CHECK_IF_MINT_IS_RENOUNCED}`); logger.info( `Min pool size: ${quoteMinPoolSizeAmount.isZero() ? 'false' : quoteMinPoolSizeAmount.toFixed()} ${quoteToken.symbol}`, ); logger.info(`Buy amount: ${quoteAmount.toFixed()} ${quoteToken.symbol}`); logger.info(`Auto sell: ${AUTO_SELL}`); logger.info(`Sell delay: ${AUTO_SELL_DELAY === 0 ? 'false' : AUTO_SELL_DELAY}`); // check existing wallet for associated token account of quote mint const tokenAccounts = await getTokenAccounts(solanaConnection, wallet.publicKey, COMMITMENT_LEVEL); for (const ta of tokenAccounts) { existingTokenAccounts.set(ta.accountInfo.mint.toString(), { mint: ta.accountInfo.mint, address: ta.pubkey, }); } const tokenAccount = tokenAccounts.find((acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString())!; if (!tokenAccount) { throw new Error(`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`); } quoteTokenAssociatedAddress = tokenAccount.pubkey; // load tokens to snipe loadSnipeList(); } function saveTokenAccount(mint: PublicKey, accountData: MinimalMarketLayoutV3) { const ata = getAssociatedTokenAddressSync(mint, wallet.publicKey); const tokenAccount = { address: ata, mint: mint, market: { bids: accountData.bids, asks: accountData.asks, eventQueue: accountData.eventQueue, }, }; existingTokenAccounts.set(mint.toString(), tokenAccount); return tokenAccount; } export async function processRaydiumPool(id: PublicKey, poolState: LiquidityStateV4) { if (!shouldBuy(poolState.baseMint.toString())) { return; } if (!quoteMinPoolSizeAmount.isZero()) { const poolSize = new TokenAmount(quoteToken, poolState.swapQuoteInAmount, true); logger.info(`Processing pool: ${id.toString()} with ${poolSize.toFixed()} ${quoteToken.symbol} in liquidity`); if (poolSize.lt(quoteMinPoolSizeAmount)) { logger.warn( { mint: poolState.baseMint, pooled: `${poolSize.toFixed()} ${quoteToken.symbol}`, }, `Skipping pool, smaller than ${quoteMinPoolSizeAmount.toFixed()} ${quoteToken.symbol}`, `Swap quote in amount: ${poolSize.toFixed()}`, ); return; } } if (CHECK_IF_MINT_IS_RENOUNCED) { const mintOption = await checkMintable(poolState.baseMint); if (mintOption !== true) { logger.warn({ mint: poolState.baseMint }, 'Skipping, owner can mint tokens!'); return; } } await buy(id, poolState); } export async function checkMintable(vault: PublicKey): Promise { try { let { data } = (await solanaConnection.getAccountInfo(vault)) || {}; if (!data) { return; } const deserialize = MintLayout.decode(data); return deserialize.mintAuthorityOption === 0; } catch (e) { logger.debug(e); logger.error({ mint: vault }, `Failed to check if mint is renounced`); } } export async function processOpenBookMarket(updatedAccountInfo: KeyedAccountInfo) { let accountData: MarketStateV3 | undefined; try { accountData = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data); // to be competitive, we collect market data before buying the token... if (existingTokenAccounts.has(accountData.baseMint.toString())) { return; } saveTokenAccount(accountData.baseMint, accountData); } catch (e) { logger.debug(e); logger.error({ mint: accountData?.baseMint }, `Failed to process market`); } } async function buy(accountId: PublicKey, accountData: LiquidityStateV4): Promise { try { let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString()); if (!tokenAccount) { // it's possible that we didn't have time to fetch open book data const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, COMMITMENT_LEVEL); tokenAccount = saveTokenAccount(accountData.baseMint, market); } tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!); const { innerTransaction } = Liquidity.makeSwapFixedInInstruction( { poolKeys: tokenAccount.poolKeys, userKeys: { tokenAccountIn: quoteTokenAssociatedAddress, tokenAccountOut: tokenAccount.address, owner: wallet.publicKey, }, amountIn: quoteAmount.raw, minAmountOut: 0, }, tokenAccount.poolKeys.version, ); const latestBlockhash = await solanaConnection.getLatestBlockhash({ commitment: COMMITMENT_LEVEL, }); const messageV0 = new TransactionMessage({ payerKey: wallet.publicKey, recentBlockhash: latestBlockhash.blockhash, instructions: [ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 421197 }), ComputeBudgetProgram.setComputeUnitLimit({ units: 101337 }), createAssociatedTokenAccountIdempotentInstruction( wallet.publicKey, tokenAccount.address, wallet.publicKey, accountData.baseMint, ), ...innerTransaction.instructions, ], }).compileToV0Message(); const transaction = new VersionedTransaction(messageV0); transaction.sign([wallet, ...innerTransaction.signers]); const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), { preflightCommitment: COMMITMENT_LEVEL, }); logger.info({ mint: accountData.baseMint, signature }, `Sent buy tx`); const confirmation = await solanaConnection.confirmTransaction( { signature, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, blockhash: latestBlockhash.blockhash, }, COMMITMENT_LEVEL, ); if (!confirmation.value.err) { logger.info( { mint: accountData.baseMint, signature, url: `https://solscan.io/tx/${signature}?cluster=${NETWORK}`, }, `Confirmed buy tx`, ); } else { logger.debug(confirmation.value.err); logger.info({ mint: accountData.baseMint, signature }, `Error confirming buy tx`); } } catch (e) { logger.debug(e); logger.error({ mint: accountData.baseMint }, `Failed to buy token`); } } async function sell(accountId: PublicKey, mint: PublicKey, amount: BigNumberish): Promise { let sold = false; let retries = 0; if (AUTO_SELL_DELAY > 0) { await new Promise((resolve) => setTimeout(resolve, AUTO_SELL_DELAY)); } do { try { const tokenAccount = existingTokenAccounts.get(mint.toString()); if (!tokenAccount) { return; } if (!tokenAccount.poolKeys) { logger.warn({ mint }, 'No pool keys found'); return; } if (amount === 0) { logger.info( { mint: tokenAccount.mint, }, `Empty balance, can't sell`, ); return; } const { innerTransaction } = Liquidity.makeSwapFixedInInstruction( { poolKeys: tokenAccount.poolKeys!, userKeys: { tokenAccountOut: quoteTokenAssociatedAddress, tokenAccountIn: tokenAccount.address, owner: wallet.publicKey, }, amountIn: amount, minAmountOut: 0, }, tokenAccount.poolKeys!.version, ); const latestBlockhash = await solanaConnection.getLatestBlockhash({ commitment: COMMITMENT_LEVEL, }); const messageV0 = new TransactionMessage({ payerKey: wallet.publicKey, recentBlockhash: latestBlockhash.blockhash, instructions: [ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 421197 }), ComputeBudgetProgram.setComputeUnitLimit({ units: 101337 }), ...innerTransaction.instructions, createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey), ], }).compileToV0Message(); const transaction = new VersionedTransaction(messageV0); transaction.sign([wallet, ...innerTransaction.signers]); const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), { preflightCommitment: COMMITMENT_LEVEL, }); logger.info({ mint, signature }, `Sent sell tx`); const confirmation = await solanaConnection.confirmTransaction( { signature, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, blockhash: latestBlockhash.blockhash, }, COMMITMENT_LEVEL, ); if (confirmation.value.err) { logger.debug(confirmation.value.err); logger.info({ mint, signature }, `Error confirming sell tx`); continue; } logger.info( { dex: `https://dexscreener.com/solana/${mint}?maker=${wallet.publicKey}`, mint, signature, url: `https://solscan.io/tx/${signature}?cluster=${NETWORK}`, }, `Confirmed sell tx`, ); sold = true; } catch (e: any) { // wait for a bit before retrying await new Promise((resolve) => setTimeout(resolve, 100)); retries++; logger.debug(e); logger.error({ mint }, `Failed to sell token, retry: ${retries}/${MAX_SELL_RETRIES}`); } } while (!sold && retries < MAX_SELL_RETRIES); } function loadSnipeList() { if (!USE_SNIPE_LIST) { return; } const count = snipeList.length; const data = fs.readFileSync(path.join(__dirname, 'snipe-list.txt'), 'utf-8'); snipeList = data .split('\n') .map((a) => a.trim()) .filter((a) => a); if (snipeList.length != count) { logger.info(`Loaded snipe list: ${snipeList.length}`); } } function shouldBuy(key: string): boolean { return USE_SNIPE_LIST ? snipeList.includes(key) : true; } const runListener = async () => { await init(); const runTimestamp = Math.floor(new Date().getTime() / 1000); const raydiumSubscriptionId = solanaConnection.onProgramAccountChange( RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, async (updatedAccountInfo) => { const key = updatedAccountInfo.accountId.toString(); const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data); const poolOpenTime = parseInt(poolState.poolOpenTime.toString()); const existing = existingLiquidityPools.has(key); if (poolOpenTime > runTimestamp && !existing) { existingLiquidityPools.add(key); const _ = processRaydiumPool(updatedAccountInfo.accountId, poolState); } }, COMMITMENT_LEVEL, [ { dataSize: LIQUIDITY_STATE_LAYOUT_V4.span }, { memcmp: { offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'), bytes: quoteToken.mint.toBase58(), }, }, { memcmp: { offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'), bytes: OPENBOOK_PROGRAM_ID.toBase58(), }, }, { memcmp: { offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'), bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]), }, }, ], ); const openBookSubscriptionId = solanaConnection.onProgramAccountChange( OPENBOOK_PROGRAM_ID, async (updatedAccountInfo) => { const key = updatedAccountInfo.accountId.toString(); const existing = existingOpenBookMarkets.has(key); if (!existing) { existingOpenBookMarkets.add(key); const _ = processOpenBookMarket(updatedAccountInfo); } }, COMMITMENT_LEVEL, [ { dataSize: MARKET_STATE_LAYOUT_V3.span }, { memcmp: { offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'), bytes: quoteToken.mint.toBase58(), }, }, ], ); if (AUTO_SELL) { const walletSubscriptionId = solanaConnection.onProgramAccountChange( TOKEN_PROGRAM_ID, async (updatedAccountInfo) => { const accountData = AccountLayout.decode(updatedAccountInfo.accountInfo!.data); if (updatedAccountInfo.accountId.equals(quoteTokenAssociatedAddress)) { return; } const _ = sell(updatedAccountInfo.accountId, accountData.mint, accountData.amount); }, COMMITMENT_LEVEL, [ { dataSize: 165, }, { memcmp: { offset: 32, bytes: wallet.publicKey.toBase58(), }, }, ], ); logger.info(`Listening for wallet changes: ${walletSubscriptionId}`); } logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`); logger.info(`Listening for open book changes: ${openBookSubscriptionId}`); if (USE_SNIPE_LIST) { setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL); } }; runListener();