Filip Dunder 1 жил өмнө
parent
commit
643002ae8a

+ 34 - 10
.env.copy

@@ -1,16 +1,40 @@
+# Wallet
 PRIVATE_KEY=
+
+# Connection
 RPC_ENDPOINT=https://api.mainnet-beta.solana.com
 RPC_WEBSOCKET_ENDPOINT=wss://api.mainnet-beta.solana.com
+COMMITMENT_LEVEL=confirmed
+
+# Bot
+LOG_LEVEL=debug
+ONE_TOKEN_AT_A_TIME=true
+COMPUTE_UNIT_LIMIT=421197
+COMPUTE_UNIT_PRICE=101337
+PRE_LOAD_EXISTING_MARKETS=false
+CACHE_NEW_MARKETS=false
+
+# Buy
 QUOTE_MINT=WSOL
-QUOTE_AMOUNT=0.01
-COMMITMENT_LEVEL=finalized
+QUOTE_AMOUNT=0.001
+AUTO_BUY_DELAY=0
+MAX_BUY_RETRIES=10
+BUY_SLIPPAGE=5
+
+# Sell
+AUTO_SELL=true
+MAX_SELL_RETRIES=10
+AUTO_SELL_DELAY=0
+PRICE_CHECK_INTERVAL=2000
+PRICE_CHECK_DURATION=60000
+TAKE_PROFIT=25
+STOP_LOSS=15
+SELL_SLIPPAGE=5
+
+# Filters
 USE_SNIPE_LIST=false
 SNIPE_LIST_REFRESH_INTERVAL=30000
-CHECK_IF_MINT_IS_RENOUNCED=false
-AUTO_SELL=true
-MAX_SELL_RETRIES=5
-AUTO_SELL_DELAY=1000
-LOG_LEVEL=info
-MIN_POOL_SIZE=10
-MAX_POOL_SIZE=50
-ONE_TOKEN_AT_A_TIME=true
+CHECK_IF_MINT_IS_RENOUNCED=true
+CHECK_IF_BURNED=false
+MIN_POOL_SIZE=5
+MAX_POOL_SIZE=50

+ 393 - 0
bot.ts

@@ -0,0 +1,393 @@
+import {
+  ComputeBudgetProgram,
+  Connection,
+  Keypair,
+  PublicKey,
+  TransactionMessage,
+  VersionedTransaction,
+} from '@solana/web3.js';
+import {
+  createAssociatedTokenAccountIdempotentInstruction,
+  createCloseAccountInstruction,
+  getAccount,
+  getAssociatedTokenAddress,
+  RawAccount,
+  TOKEN_PROGRAM_ID,
+} from '@solana/spl-token';
+import {
+  Liquidity,
+  LiquidityPoolKeysV4,
+  LiquidityStateV4,
+  Percent,
+  Token,
+  TokenAmount,
+} from '@raydium-io/raydium-sdk';
+import { MarketCache, PoolCache, SnipeListCache } from './cache';
+import { PoolFilters } from './filters';
+import { TransactionExecutor } from './transactions';
+import { createPoolKeys, logger, NETWORK, sleep } from './helpers';
+import { Mutex } from 'async-mutex';
+import BN from 'bn.js';
+
+export interface BotConfig {
+  wallet: Keypair;
+  checkRenounced: boolean;
+  checkBurned: boolean;
+  minPoolSize: TokenAmount;
+  maxPoolSize: TokenAmount;
+  quoteToken: Token;
+  quoteAmount: TokenAmount;
+  quoteAta: PublicKey;
+  oneTokenAtATime: boolean;
+  useSnipeList: boolean;
+  autoSell: boolean;
+  autoBuyDelay: number;
+  autoSellDelay: number;
+  maxBuyRetries: number;
+  maxSellRetries: number;
+  unitLimit: number;
+  unitPrice: number;
+  takeProfit: number;
+  stopLoss: number;
+  buySlippage: number;
+  sellSlippage: number;
+  priceCheckInterval: number;
+  priceCheckDuration: number;
+}
+
+export class Bot {
+  private readonly poolFilters: PoolFilters;
+
+  // snipe list
+  private readonly snipeListCache?: SnipeListCache;
+
+  // one token at the time
+  private readonly mutex: Mutex;
+  private sellExecutionCount = 0;
+
+  constructor(
+    private readonly connection: Connection,
+    private readonly marketStorage: MarketCache,
+    private readonly poolStorage: PoolCache,
+    private readonly txExecutor: TransactionExecutor,
+    private readonly config: BotConfig,
+  ) {
+    this.mutex = new Mutex();
+    this.poolFilters = new PoolFilters(connection, {
+      quoteToken: this.config.quoteToken,
+      minPoolSize: this.config.minPoolSize,
+      maxPoolSize: this.config.maxPoolSize,
+    });
+
+    if (this.config.useSnipeList) {
+      this.snipeListCache = new SnipeListCache();
+      this.snipeListCache.init();
+    }
+  }
+
+  async validate() {
+    try {
+      await getAccount(this.connection, this.config.quoteAta, this.connection.commitment);
+    } catch (error) {
+      logger.error(
+        `${this.config.quoteToken.symbol} token account not found in wallet: ${this.config.wallet.publicKey.toString()}`,
+      );
+      return false;
+    }
+
+    return true;
+  }
+
+  public async buy(accountId: PublicKey, poolState: LiquidityStateV4) {
+    logger.trace({ mint: poolState.baseMint }, `Processing buy...`);
+
+    if (this.config.useSnipeList && !this.snipeListCache?.isInList(poolState.baseMint.toString())) {
+      logger.debug({ mint: poolState.baseMint.toString() }, `Skipping buy because token is not in a snipe list`);
+      return;
+    }
+
+    if (this.config.autoBuyDelay > 0) {
+      logger.debug({ mint: poolState.baseMint }, `Waiting for ${this.config.autoBuyDelay} ms before buy`);
+      await sleep(this.config.autoBuyDelay);
+    }
+
+    if (this.config.oneTokenAtATime) {
+      if (this.mutex.isLocked() || this.sellExecutionCount > 0) {
+        logger.debug(
+          { mint: poolState.baseMint.toString() },
+          `Skipping buy because one token at a time is turned on and token is already being processed`,
+        );
+        return;
+      }
+
+      await this.mutex.acquire();
+    }
+
+    try {
+      const shouldBuy = await this.poolFilters.execute(poolState);
+
+      if (!shouldBuy) {
+        logger.debug({ mint: poolState.baseMint.toString() }, `Skipping buy because pool doesn't match filters`);
+        return;
+      }
+
+      for (let i = 0; i < this.config.maxBuyRetries; i++) {
+        try {
+          const [market, mintAta] = await Promise.all([
+            this.marketStorage.get(poolState.marketId.toString()),
+            getAssociatedTokenAddress(poolState.baseMint, this.config.wallet.publicKey),
+          ]);
+          const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(accountId, poolState, market);
+
+          logger.info(
+            { mint: poolState.baseMint.toString() },
+            `Send buy transaction attempt: ${i + 1}/${this.config.maxBuyRetries}`,
+          );
+          const tokenOut = new Token(TOKEN_PROGRAM_ID, poolKeys.baseMint, poolKeys.baseDecimals);
+          const result = await this.swap(
+            poolKeys,
+            this.config.quoteAta,
+            mintAta,
+            this.config.quoteToken,
+            tokenOut,
+            this.config.quoteAmount,
+            this.config.buySlippage,
+            this.config.wallet,
+            'buy',
+          );
+
+          if (result.confirmed) {
+            logger.info(
+              {
+                mint: poolState.baseMint.toString(),
+                signature: result.signature,
+                url: `https://solscan.io/tx/${result.signature}?cluster=${NETWORK}`,
+              },
+              `Confirmed buy tx`,
+            );
+
+            break;
+          }
+
+          logger.debug(
+            {
+              mint: poolState.baseMint.toString(),
+              signature: result.signature,
+            },
+            `Error confirming buy tx`,
+          );
+        } catch (error) {
+          logger.debug({ mint: poolState.baseMint.toString(), error }, `Error confirming buy transaction`);
+        }
+      }
+    } catch (error) {
+      logger.error({ mint: poolState.baseMint.toString(), error }, `Failed to buy token`);
+    } finally {
+      if (this.config.oneTokenAtATime) {
+        this.mutex.release();
+      }
+    }
+  }
+
+  public async sell(accountId: PublicKey, rawAccount: RawAccount) {
+    if (this.config.oneTokenAtATime) {
+      this.sellExecutionCount++;
+    }
+
+    try {
+      logger.trace({ mint: rawAccount.mint }, `Processing sell...`);
+
+      const poolData = await this.poolStorage.get(rawAccount.mint.toString());
+
+      if (!poolData) {
+        logger.trace({ mint: rawAccount.mint.toString() }, `Token pool data is not found, can't sell`);
+        return;
+      }
+
+      const tokenIn = new Token(TOKEN_PROGRAM_ID, poolData.state.baseMint, poolData.state.baseDecimal.toNumber());
+      const tokenAmountIn = new TokenAmount(tokenIn, rawAccount.amount, true);
+
+      if (tokenAmountIn.isZero()) {
+        logger.info({ mint: rawAccount.mint.toString() }, `Empty balance, can't sell`);
+        return;
+      }
+
+      if (this.config.autoSellDelay > 0) {
+        logger.debug({ mint: rawAccount.mint }, `Waiting for ${this.config.autoSellDelay} ms before sell`);
+        await sleep(this.config.autoSellDelay);
+      }
+
+      for (let i = 0; i < this.config.maxSellRetries; i++) {
+        try {
+          const market = await this.marketStorage.get(poolData.state.marketId.toString());
+          const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(new PublicKey(poolData.id), poolData.state, market);
+
+          await this.priceMatch(tokenAmountIn, poolKeys);
+
+          logger.info(
+            { mint: rawAccount.mint },
+            `Send sell transaction attempt: ${i + 1}/${this.config.maxSellRetries}`,
+          );
+
+          const result = await this.swap(
+            poolKeys,
+            accountId,
+            this.config.quoteAta,
+            tokenIn,
+            this.config.quoteToken,
+            tokenAmountIn,
+            this.config.sellSlippage,
+            this.config.wallet,
+            'sell',
+          );
+
+          if (result.confirmed) {
+            logger.info(
+              {
+                dex: `https://dexscreener.com/solana/${rawAccount.mint.toString()}?maker=${this.config.wallet.publicKey}`,
+                mint: rawAccount.mint.toString(),
+                signature: result.signature,
+                url: `https://solscan.io/tx/${result.signature}?cluster=${NETWORK}`,
+              },
+              `Confirmed sell tx`,
+            );
+            break;
+          }
+
+          logger.info(
+            {
+              mint: rawAccount.mint.toString(),
+              signature: result.signature,
+            },
+            `Error confirming sell tx`,
+          );
+        } catch (error) {
+          logger.debug({ mint: rawAccount.mint.toString(), error }, `Error confirming sell transaction`);
+        }
+      }
+    } catch (error) {
+      logger.debug({ mint: rawAccount.mint.toString(), error }, `Failed to sell token`);
+    } finally {
+      if (this.config.oneTokenAtATime) {
+        this.sellExecutionCount--;
+      }
+    }
+  }
+
+  private async swap(
+    poolKeys: LiquidityPoolKeysV4,
+    ataIn: PublicKey,
+    ataOut: PublicKey,
+    tokenIn: Token,
+    tokenOut: Token,
+    amountIn: TokenAmount,
+    slippage: number,
+    wallet: Keypair,
+    direction: 'buy' | 'sell',
+  ) {
+    const slippagePercent = new Percent(slippage, 100);
+    const poolInfo = await Liquidity.fetchInfo({
+      connection: this.connection,
+      poolKeys,
+    });
+
+    const computedAmountOut = Liquidity.computeAmountOut({
+      poolKeys,
+      poolInfo,
+      amountIn,
+      currencyOut: tokenOut,
+      slippage: slippagePercent,
+    });
+
+    const latestBlockhash = await this.connection.getLatestBlockhash();
+    const { innerTransaction } = Liquidity.makeSwapFixedInInstruction(
+      {
+        poolKeys: poolKeys,
+        userKeys: {
+          tokenAccountIn: ataIn,
+          tokenAccountOut: ataOut,
+          owner: wallet.publicKey,
+        },
+        amountIn: amountIn.raw,
+        minAmountOut: computedAmountOut.minAmountOut.raw,
+      },
+      poolKeys.version,
+    );
+
+    const messageV0 = new TransactionMessage({
+      payerKey: wallet.publicKey,
+      recentBlockhash: latestBlockhash.blockhash,
+      instructions: [
+        ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }),
+        ComputeBudgetProgram.setComputeUnitLimit({ units: this.config.unitLimit }),
+        ...(direction === 'buy'
+          ? [
+              createAssociatedTokenAccountIdempotentInstruction(
+                wallet.publicKey,
+                ataOut,
+                wallet.publicKey,
+                tokenOut.mint,
+              ),
+            ]
+          : []),
+        ...innerTransaction.instructions,
+        ...(direction === 'sell' ? [createCloseAccountInstruction(ataIn, wallet.publicKey, wallet.publicKey)] : []),
+      ],
+    }).compileToV0Message();
+
+    const transaction = new VersionedTransaction(messageV0);
+    transaction.sign([wallet, ...innerTransaction.signers]);
+
+    return this.txExecutor.executeAndConfirm(transaction, latestBlockhash);
+  }
+
+  private async priceMatch(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
+    const profitFraction = this.config.quoteAmount.mul(this.config.takeProfit).numerator.div(new BN(100));
+    const profitAmount = new TokenAmount(this.config.quoteToken, profitFraction, true);
+    const takeProfit = this.config.quoteAmount.add(profitAmount);
+
+    const lossFraction = this.config.quoteAmount.mul(this.config.stopLoss).numerator.div(new BN(100));
+    const lossAmount = new TokenAmount(this.config.quoteToken, lossFraction, true);
+    const stopLoss = this.config.quoteAmount.subtract(lossAmount);
+    const slippage = new Percent(this.config.sellSlippage, 100);
+
+    const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
+    let timesChecked = 0;
+
+    do {
+      try {
+        const poolInfo = await Liquidity.fetchInfo({
+          connection: this.connection,
+          poolKeys,
+        });
+
+        const amountOut = Liquidity.computeAmountOut({
+          poolKeys,
+          poolInfo,
+          amountIn: amountIn,
+          currencyOut: this.config.quoteToken,
+          slippage,
+        }).amountOut;
+
+        logger.debug(
+          { mint: poolKeys.baseMint.toString() },
+          `Take profit: ${takeProfit.toFixed()} | Stop loss: ${stopLoss.toFixed()} | Current: ${amountOut.toFixed()}`,
+        );
+
+        if (amountOut.lt(stopLoss)){
+          break;
+        }
+
+        if (amountOut.gt(takeProfit)){
+          break;
+        }
+
+        await sleep(this.config.priceCheckInterval);
+      } catch (e) {
+        logger.trace({ mint: poolKeys.baseMint.toString(), e }, `Failed to check token price`);
+      } finally {
+        timesChecked++;
+      }
+    } while (timesChecked < timesToCheck);
+  }
+}

+ 0 - 555
buy.ts

@@ -1,555 +0,0 @@
-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,
-  MAX_POOL_SIZE,
-  ONE_TOKEN_AT_A_TIME,
-} 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<string> = new Set<string>();
-const existingOpenBookMarkets: Set<string> = new Set<string>();
-const existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<string, MinimalTokenAccountData>();
-
-let wallet: Keypair;
-let quoteToken: Token;
-let quoteTokenAssociatedAddress: PublicKey;
-let quoteAmount: TokenAmount;
-let quoteMinPoolSizeAmount: TokenAmount;
-let quoteMaxPoolSizeAmount: TokenAmount;
-let processingToken: Boolean = false;
-
-
-
-let snipeList: string[] = [];
-
-async function init(): Promise<void> {
-  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);
-      quoteMaxPoolSizeAmount = new TokenAmount(quoteToken, MAX_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);
-      quoteMaxPoolSizeAmount = new TokenAmount(quoteToken, MAX_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(
-    `Max pool size: ${quoteMaxPoolSizeAmount.isZero() ? 'false' : quoteMaxPoolSizeAmount.toFixed()} ${quoteToken.symbol}`,
-  );
-  logger.info(`One token at a time: ${ONE_TOKEN_AT_A_TIME}`);
-  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(), <MinimalTokenAccountData>{
-      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 = <MinimalTokenAccountData>{
-    address: ata,
-    mint: mint,
-    market: <MinimalMarketLayoutV3>{
-      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()}`,
-      );
-      logger.info(`-------------------🤖🔧------------------- \n`);
-      return;
-    }
-  }
-
-  if (!quoteMaxPoolSizeAmount.isZero()) {
-    const poolSize = new TokenAmount(quoteToken, poolState.swapQuoteInAmount, true);
-
-    if (poolSize.gt(quoteMaxPoolSizeAmount)) {
-      logger.warn(
-        {
-          mint: poolState.baseMint,
-          pooled: `${poolSize.toFixed()} ${quoteToken.symbol}`,
-        },
-        `Skipping pool, bigger than ${quoteMaxPoolSizeAmount.toFixed()} ${quoteToken.symbol}`,
-        `Swap quote in amount: ${poolSize.toFixed()}`,
-      );
-      logger.info(`-------------------🤖🔧------------------- \n`);
-      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<boolean | undefined> {
-  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<void> {
-  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`);
-    processingToken = true;
-
-    const confirmation = await solanaConnection.confirmTransaction(
-      {
-        signature,
-        lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
-        blockhash: latestBlockhash.blockhash,
-      },
-      COMMITMENT_LEVEL,
-    );
-    if (!confirmation.value.err) {
-      logger.info(`-------------------🟢------------------- `);
-      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);
-    processingToken = false;
-    logger.error({ mint: accountData.baseMint }, `Failed to buy token`);
-  }
-}
-
-async function sell(accountId: PublicKey, mint: PublicKey, amount: BigNumberish): Promise<void> {
-  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(`-------------------🔴------------------- `);
-      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;
-      processingToken = false;
-    } 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);
-  processingToken = false;
-}
-
-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 {
-  logger.info(`-------------------🤖🔧------------------- `);
-  logger.info(`Processing token: ${processingToken}`)
-  return USE_SNIPE_LIST ? snipeList.includes(key) : ONE_TOKEN_AT_A_TIME ? !processingToken : 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}`);
-
-  logger.info('------------------- 🚀 ---------------------');
-  logger.info('Bot is running! Press CTRL + C to stop it.');
-  logger.info('------------------- 🚀 ---------------------');
-
-  if (USE_SNIPE_LIST) {
-    setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
-  }
-};
-
-runListener();

+ 3 - 0
cache/index.ts

@@ -0,0 +1,3 @@
+export * from './market.cache';
+export * from './pool.cache';
+export * from './snipe-list.cache';

+ 58 - 0
cache/market.cache.ts

@@ -0,0 +1,58 @@
+import { Connection, PublicKey } from '@solana/web3.js';
+import { getMinimalMarketV3, logger, MINIMAL_MARKET_STATE_LAYOUT_V3, MinimalMarketLayoutV3 } from '../helpers';
+import { MAINNET_PROGRAM_ID, MARKET_STATE_LAYOUT_V3, Token } from '@raydium-io/raydium-sdk';
+
+export class MarketCache {
+  private readonly keys: Map<string, MinimalMarketLayoutV3> = new Map<string, MinimalMarketLayoutV3>();
+  constructor(private readonly connection: Connection) {}
+
+  async init(config: { quoteToken: Token }) {
+    logger.debug({}, `Fetching all existing ${config.quoteToken.symbol} markets...`);
+
+    const accounts = await this.connection.getProgramAccounts(MAINNET_PROGRAM_ID.OPENBOOK_MARKET, {
+      commitment: this.connection.commitment,
+      dataSlice: {
+        offset: MARKET_STATE_LAYOUT_V3.offsetOf('eventQueue'),
+        length: MINIMAL_MARKET_STATE_LAYOUT_V3.span,
+      },
+      filters: [
+        { dataSize: MARKET_STATE_LAYOUT_V3.span },
+        {
+          memcmp: {
+            offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'),
+            bytes: config.quoteToken.mint.toBase58(),
+          },
+        },
+      ],
+    });
+
+    for (const account of accounts) {
+      const market = MINIMAL_MARKET_STATE_LAYOUT_V3.decode(account.account.data);
+      this.keys.set(account.pubkey.toString(), market);
+    }
+
+    logger.debug({}, `Cached ${this.keys.size} markets`);
+  }
+
+  public save(marketId: string, keys: MinimalMarketLayoutV3) {
+    if (!this.keys.has(marketId)) {
+      logger.trace({}, `Caching new market: ${marketId}`);
+      this.keys.set(marketId, keys);
+    }
+  }
+
+  public async get(marketId: string): Promise<MinimalMarketLayoutV3> {
+    if (this.keys.has(marketId)) {
+      return this.keys.get(marketId)!;
+    }
+
+    logger.trace({}, `Fetching new market keys for ${marketId}`);
+    const market = await this.fetch(marketId);
+    this.keys.set(marketId, market);
+    return market;
+  }
+
+  private fetch(marketId: string): Promise<MinimalMarketLayoutV3> {
+    return getMinimalMarketV3(this.connection, new PublicKey(marketId), this.connection.commitment);
+  }
+}

+ 20 - 0
cache/pool.cache.ts

@@ -0,0 +1,20 @@
+import { LiquidityStateV4 } from '@raydium-io/raydium-sdk';
+import { logger } from '../helpers';
+
+export class PoolCache {
+  private readonly keys: Map<string, { id: string; state: LiquidityStateV4 }> = new Map<
+    string,
+    { id: string; state: LiquidityStateV4 }
+  >();
+
+  public save(id: string, state: LiquidityStateV4) {
+    if (!this.keys.has(state.baseMint.toString())) {
+      logger.trace(`Caching new pool for mint: ${state.baseMint.toString()}`);
+      this.keys.set(state.baseMint.toString(), { id, state });
+    }
+  }
+
+  public async get(mint: string): Promise<{ id: string; state: LiquidityStateV4 }> {
+    return this.keys.get(mint)!;
+  }
+}

+ 34 - 0
cache/snipe-list.cache.ts

@@ -0,0 +1,34 @@
+import fs from 'fs';
+import path from 'path';
+import { logger, SNIPE_LIST_REFRESH_INTERVAL } from '../helpers';
+
+export class SnipeListCache {
+  private snipeList: string[] = [];
+
+  constructor() {
+    setInterval(this.loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
+  }
+
+  public init() {
+    this.loadSnipeList();
+  }
+
+  public isInList(mint: string) {
+    return this.snipeList.includes(mint);
+  }
+
+  private loadSnipeList() {
+    logger.trace('Refreshing snipe list...');
+
+    const count = this.snipeList.length;
+    const data = fs.readFileSync(path.join(__dirname, 'snipe-list.txt'), 'utf-8');
+    this.snipeList = data
+      .split('\n')
+      .map((a) => a.trim())
+      .filter((a) => a);
+
+    if (this.snipeList.length != count) {
+      logger.info(`Loaded snipe list: ${this.snipeList.length}`);
+    }
+  }
+}

+ 0 - 20
constants/constants.ts

@@ -1,20 +0,0 @@
-import { Commitment } from "@solana/web3.js";
-import { logger, retrieveEnvVariable } from "../utils";
-
-export const NETWORK = 'mainnet-beta';
-export const COMMITMENT_LEVEL: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
-export const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
-export const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOINT', logger);
-export const LOG_LEVEL = retrieveEnvVariable('LOG_LEVEL', logger);
-export const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNCED', logger) === 'true';
-export const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
-export const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger));
-export const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger) === 'true';
-export const MAX_SELL_RETRIES = Number(retrieveEnvVariable('MAX_SELL_RETRIES', logger));
-export const AUTO_SELL_DELAY = Number(retrieveEnvVariable('AUTO_SELL_DELAY', logger));
-export const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
-export const QUOTE_MINT = retrieveEnvVariable('QUOTE_MINT', logger);
-export const QUOTE_AMOUNT = retrieveEnvVariable('QUOTE_AMOUNT', logger);
-export const MIN_POOL_SIZE = retrieveEnvVariable('MIN_POOL_SIZE', logger);
-export const MAX_POOL_SIZE = retrieveEnvVariable('MAX_POOL_SIZE', logger);
-export const ONE_TOKEN_AT_A_TIME = retrieveEnvVariable('ONE_TOKEN_AT_A_TIME', logger) === 'true';

+ 0 - 1
constants/index.ts

@@ -1 +0,0 @@
-export * from './constants';

+ 24 - 0
filters/burn.filter.ts

@@ -0,0 +1,24 @@
+import { Filter, FilterResult } from './pool-filters';
+import { Connection } from '@solana/web3.js';
+import { LiquidityStateV4 } from '@raydium-io/raydium-sdk';
+import { logger } from '../helpers';
+
+export class BurnFilter implements Filter {
+  constructor(private readonly connection: Connection) {}
+
+  async execute(poolState: LiquidityStateV4): Promise<FilterResult> {
+    try {
+      const amount = await this.connection.getTokenSupply(poolState.lpMint, this.connection.commitment);
+      const burned = amount.value.uiAmount === 0;
+      return { ok: burned, message: burned ? undefined : "Burned -> Creator didn't burn LP" };
+    } catch (e: any) {
+      if (e.code == -32602) {
+        return { ok: true };
+      }
+
+      logger.error({ mint: poolState.baseMint }, `Failed to check if LP is burned`);
+    }
+
+    return { ok: false, message: 'Failed to check if LP is burned' };
+  }
+}

+ 4 - 0
filters/index.ts

@@ -0,0 +1,4 @@
+export * from './burn.filter';
+export * from './pool-filters';
+export * from './pool-size.filter';
+export * from './renounced.filter';

+ 61 - 0
filters/pool-filters.ts

@@ -0,0 +1,61 @@
+import { Connection } from '@solana/web3.js';
+import { LiquidityStateV4, Token, TokenAmount } from '@raydium-io/raydium-sdk';
+import { BurnFilter } from './burn.filter';
+import { RenouncedFilter } from './renounced.filter';
+import { PoolSizeFilter } from './pool-size.filter';
+import { CHECK_IF_BURNED, CHECK_IF_MINT_IS_RENOUNCED, logger } from '../helpers';
+
+export interface Filter {
+  execute(poolState: LiquidityStateV4): Promise<FilterResult>;
+}
+
+export interface FilterResult {
+  ok: boolean;
+  message?: string;
+}
+
+export interface PoolFilterArgs {
+  minPoolSize: TokenAmount;
+  maxPoolSize: TokenAmount;
+  quoteToken: Token;
+}
+
+export class PoolFilters {
+  private readonly filters: Filter[] = [];
+
+  constructor(
+    readonly connection: Connection,
+    readonly args: PoolFilterArgs,
+  ) {
+    if (CHECK_IF_BURNED) {
+      this.filters.push(new BurnFilter(connection));
+    }
+
+    if (CHECK_IF_MINT_IS_RENOUNCED) {
+      this.filters.push(new RenouncedFilter(connection));
+    }
+
+    if (!args.minPoolSize.isZero() || !args.maxPoolSize.isZero()) {
+      this.filters.push(new PoolSizeFilter(connection, args.quoteToken, args.minPoolSize, args.maxPoolSize));
+    }
+  }
+
+  public async execute(poolState: LiquidityStateV4): Promise<boolean> {
+    if (this.filters.length === 0) {
+      return true;
+    }
+
+    const result = await Promise.all(this.filters.map((f) => f.execute(poolState)));
+    const pass = result.every((r) => r.ok);
+
+    if (pass) {
+      return true;
+    }
+
+    for (const filterResult of result.filter((r) => !r.ok)) {
+      logger.info(filterResult.message);
+    }
+
+    return false;
+  }
+}

+ 36 - 0
filters/pool-size.filter.ts

@@ -0,0 +1,36 @@
+import { Filter, FilterResult } from './pool-filters';
+import { LiquidityStateV4, Token, TokenAmount } from '@raydium-io/raydium-sdk';
+import { Connection } from '@solana/web3.js';
+
+export class PoolSizeFilter implements Filter {
+  constructor(
+    private readonly connection: Connection,
+    private readonly quoteToken: Token,
+    private readonly minPoolSize: TokenAmount,
+    private readonly maxPoolSize: TokenAmount,
+  ) {}
+
+  async execute(poolState: LiquidityStateV4): Promise<FilterResult> {
+    const response = await this.connection.getTokenAccountBalance(poolState.quoteVault, this.connection.commitment);
+    const poolSize = new TokenAmount(this.quoteToken, response.value.amount, true);
+    let inRange = true;
+
+    if (!this.maxPoolSize?.isZero()) {
+      inRange = poolSize.lt(this.maxPoolSize);
+
+      if (!inRange) {
+        return { ok: false, message: `PoolSize -> Pool size ${poolSize.toFixed()} > ${this.maxPoolSize.toFixed()}` };
+      }
+    }
+
+    if (!this.minPoolSize?.isZero()) {
+      inRange = poolSize.gt(this.minPoolSize);
+
+      if (!inRange) {
+        return { ok: false, message: `PoolSize -> Pool size ${poolSize.toFixed()} < ${this.minPoolSize.toFixed()}` };
+      }
+    }
+
+    return { ok: inRange };
+  }
+}

+ 26 - 0
filters/renounced.filter.ts

@@ -0,0 +1,26 @@
+import { Filter, FilterResult } from './pool-filters';
+import { MintLayout } from '@solana/spl-token';
+import { Connection } from '@solana/web3.js';
+import { LiquidityStateV4 } from '@raydium-io/raydium-sdk';
+import { logger } from '../helpers';
+
+export class RenouncedFilter implements Filter {
+  constructor(private readonly connection: Connection) {}
+
+  async execute(poolState: LiquidityStateV4): Promise<FilterResult> {
+    try {
+      const accountInfo = await this.connection.getAccountInfo(poolState.baseMint, this.connection.commitment);
+      if (!accountInfo?.data) {
+        return { ok: false, message: 'Renounced -> Failed to fetch account data' };
+      }
+
+      const deserialize = MintLayout.decode(accountInfo.data);
+      const renounced = deserialize.mintAuthorityOption === 0;
+      return { ok: renounced, message: renounced ? undefined : 'Renounced -> Creator can mint more tokens' };
+    } catch (e) {
+      logger.error({ mint: poolState.baseMint }, `Failed to check if mint is renounced`);
+    }
+
+    return { ok: false, message: 'Renounced -> Failed to check if mint is renounced' };
+  }
+}

+ 57 - 0
helpers/constants.ts

@@ -0,0 +1,57 @@
+import { Logger } from 'pino';
+import dotenv from 'dotenv';
+import { Commitment } from '@solana/web3.js';
+import { logger } from './logger';
+
+dotenv.config();
+
+const retrieveEnvVariable = (variableName: string, logger: Logger) => {
+  const variable = process.env[variableName] || '';
+  if (!variable) {
+    logger.error(`${variableName} is not set`);
+    process.exit(1);
+  }
+  return variable;
+};
+
+// Wallet
+export const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
+
+// Connection
+export const NETWORK = 'mainnet-beta';
+export const COMMITMENT_LEVEL: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
+export const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
+export const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOINT', logger);
+
+// Bot
+export const LOG_LEVEL = retrieveEnvVariable('LOG_LEVEL', logger);
+export const ONE_TOKEN_AT_A_TIME = retrieveEnvVariable('ONE_TOKEN_AT_A_TIME', logger) === 'true';
+export const COMPUTE_UNIT_LIMIT = Number(retrieveEnvVariable('COMPUTE_UNIT_LIMIT', logger));
+export const COMPUTE_UNIT_PRICE = Number(retrieveEnvVariable('COMPUTE_UNIT_PRICE', logger));
+export const PRE_LOAD_EXISTING_MARKETS = retrieveEnvVariable('PRE_LOAD_EXISTING_MARKETS', logger) === 'true';
+export const CACHE_NEW_MARKETS = retrieveEnvVariable('CACHE_NEW_MARKETS', logger) === 'true';
+
+// Buy
+export const AUTO_BUY_DELAY = Number(retrieveEnvVariable('AUTO_BUY_DELAY', logger));
+export const QUOTE_MINT = retrieveEnvVariable('QUOTE_MINT', logger);
+export const QUOTE_AMOUNT = retrieveEnvVariable('QUOTE_AMOUNT', logger);
+export const MAX_BUY_RETRIES = Number(retrieveEnvVariable('MAX_BUY_RETRIES', logger));
+export const BUY_SLIPPAGE = Number(retrieveEnvVariable('BUY_SLIPPAGE', logger));
+
+// Sell
+export const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger) === 'true';
+export const AUTO_SELL_DELAY = Number(retrieveEnvVariable('AUTO_SELL_DELAY', logger));
+export const MAX_SELL_RETRIES = Number(retrieveEnvVariable('MAX_SELL_RETRIES', logger));
+export const TAKE_PROFIT = Number(retrieveEnvVariable('TAKE_PROFIT', logger));
+export const STOP_LOSS = Number(retrieveEnvVariable('STOP_LOSS', logger));
+export const PRICE_CHECK_INTERVAL = Number(retrieveEnvVariable('PRICE_CHECK_INTERVAL', logger));
+export const PRICE_CHECK_DURATION = Number(retrieveEnvVariable('PRICE_CHECK_DURATION', logger));
+export const SELL_SLIPPAGE = Number(retrieveEnvVariable('SELL_SLIPPAGE', logger));
+
+// Filters
+export const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNCED', logger) === 'true';
+export const CHECK_IF_BURNED = retrieveEnvVariable('CHECK_IF_BURNED', logger) === 'true';
+export const MIN_POOL_SIZE = retrieveEnvVariable('MIN_POOL_SIZE', logger);
+export const MAX_POOL_SIZE = retrieveEnvVariable('MAX_POOL_SIZE', logger);
+export const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
+export const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger));

+ 7 - 0
helpers/index.ts

@@ -0,0 +1,7 @@
+export * from './market';
+export * from './liquidity';
+export * from './logger';
+export * from './constants';
+export * from './token';
+export * from './wallet';
+export * from './promises'

+ 5 - 50
liquidity/liquidity.ts → helpers/liquidity.ts

@@ -1,26 +1,6 @@
-import { Commitment, Connection, PublicKey } from '@solana/web3.js';
-import {
-  Liquidity,
-  LiquidityPoolKeys,
-  Market,
-  TokenAccount,
-  SPL_ACCOUNT_LAYOUT,
-  publicKey,
-  struct,
-  MAINNET_PROGRAM_ID,
-  LiquidityStateV4,
-} from '@raydium-io/raydium-sdk';
-import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
-import { MinimalMarketLayoutV3 } from '../market';
-
-export const RAYDIUM_LIQUIDITY_PROGRAM_ID_V4 = MAINNET_PROGRAM_ID.AmmV4;
-export const OPENBOOK_PROGRAM_ID = MAINNET_PROGRAM_ID.OPENBOOK_MARKET;
-
-export const MINIMAL_MARKET_STATE_LAYOUT_V3 = struct([
-  publicKey('eventQueue'),
-  publicKey('bids'),
-  publicKey('asks'),
-]);
+import { PublicKey } from '@solana/web3.js';
+import { Liquidity, LiquidityPoolKeys, LiquidityStateV4, MAINNET_PROGRAM_ID, Market } from '@raydium-io/raydium-sdk';
+import { MinimalMarketLayoutV3 } from './market';
 
 export function createPoolKeys(
   id: PublicKey,
@@ -36,9 +16,9 @@ export function createPoolKeys(
     quoteDecimals: accountData.quoteDecimal.toNumber(),
     lpDecimals: 5,
     version: 4,
-    programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
+    programId: MAINNET_PROGRAM_ID.AmmV4,
     authority: Liquidity.getAssociatedAuthority({
-      programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
+      programId: MAINNET_PROGRAM_ID.AmmV4,
     }).publicKey,
     openOrders: accountData.openOrders,
     targetOrders: accountData.targetOrders,
@@ -61,28 +41,3 @@ export function createPoolKeys(
     lookupTableAccount: PublicKey.default,
   };
 }
-
-export async function getTokenAccounts(
-  connection: Connection,
-  owner: PublicKey,
-  commitment?: Commitment,
-) {
-  const tokenResp = await connection.getTokenAccountsByOwner(
-    owner,
-    {
-      programId: TOKEN_PROGRAM_ID,
-    },
-    commitment,
-  );
-
-  const accounts: TokenAccount[] = [];
-  for (const { pubkey, account } of tokenResp.value) {
-    accounts.push({
-      pubkey,
-      programId: account.owner,
-      accountInfo: SPL_ACCOUNT_LAYOUT.decode(account.data),
-    });
-  }
-
-  return accounts;
-}

+ 1 - 1
utils/logger.ts → helpers/logger.ts

@@ -1,4 +1,4 @@
-import pino from "pino";
+import pino from 'pino';
 
 const transport = pino.transport({
   target: 'pino-pretty',

+ 3 - 4
market/market.ts → helpers/market.ts

@@ -1,10 +1,9 @@
 import { Commitment, Connection, PublicKey } from '@solana/web3.js';
-import { GetStructureSchema, MARKET_STATE_LAYOUT_V3 } from '@raydium-io/raydium-sdk';
-import { MINIMAL_MARKET_STATE_LAYOUT_V3 } from '../liquidity';
+import { GetStructureSchema, MARKET_STATE_LAYOUT_V3, publicKey, struct } from '@raydium-io/raydium-sdk';
 
+export const MINIMAL_MARKET_STATE_LAYOUT_V3 = struct([publicKey('eventQueue'), publicKey('bids'), publicKey('asks')]);
 export type MinimalMarketStateLayoutV3 = typeof MINIMAL_MARKET_STATE_LAYOUT_V3;
-export type MinimalMarketLayoutV3 =
-  GetStructureSchema<MinimalMarketStateLayoutV3>;
+export type MinimalMarketLayoutV3 = GetStructureSchema<MinimalMarketStateLayoutV3>;
 
 export async function getMinimalMarketV3(
   connection: Connection,

+ 1 - 0
helpers/promises.ts

@@ -0,0 +1 @@
+export const sleep = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));

+ 23 - 0
helpers/token.ts

@@ -0,0 +1,23 @@
+import { Token } from '@raydium-io/raydium-sdk';
+import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
+import { PublicKey } from '@solana/web3.js';
+
+export function getToken(token: string) {
+  switch (token) {
+    case 'WSOL': {
+      return Token.WSOL;
+    }
+    case 'USDC': {
+      return new Token(
+        TOKEN_PROGRAM_ID,
+        new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
+        6,
+        'USDC',
+        'USDC',
+      );
+    }
+    default: {
+      throw new Error(`Unsupported quote mint "${token}". Supported values are USDC and WSOL`);
+    }
+  }
+}

+ 21 - 0
helpers/wallet.ts

@@ -0,0 +1,21 @@
+import { Keypair } from '@solana/web3.js';
+import bs58 from 'bs58';
+import { mnemonicToSeedSync } from 'bip39';
+import { derivePath } from 'ed25519-hd-key';
+
+export function getWallet(wallet: string): Keypair {
+  // most likely someone pasted the private key in binary format
+  if (wallet.startsWith('[')) {
+    return Keypair.fromSecretKey(JSON.parse(wallet));
+  }
+
+  // most likely someone pasted mnemonic
+  if (wallet.split(' ').length > 1) {
+    const seed = mnemonicToSeedSync(wallet, '');
+    const path = `m/44'/501'/0'/0'`; // we assume it's first path
+    return Keypair.fromSeed(derivePath(path, seed.toString('hex')).key);
+  }
+
+  // most likely someone pasted base58 encoded private key
+  return Keypair.fromSecretKey(bs58.decode(wallet));
+}

+ 192 - 0
index.ts

@@ -0,0 +1,192 @@
+import { MarketCache, PoolCache } from './cache';
+import { Listeners } from './listeners';
+import { Connection, KeyedAccountInfo, Keypair } from '@solana/web3.js';
+import { LIQUIDITY_STATE_LAYOUT_V4, MARKET_STATE_LAYOUT_V3, Token, TokenAmount } from '@raydium-io/raydium-sdk';
+import { AccountLayout, getAssociatedTokenAddressSync } from '@solana/spl-token';
+import { Bot, BotConfig } from './bot';
+import { DefaultTransactionExecutor } from './transactions';
+import {
+  getToken,
+  getWallet,
+  logger,
+  COMMITMENT_LEVEL,
+  RPC_ENDPOINT,
+  RPC_WEBSOCKET_ENDPOINT,
+  PRE_LOAD_EXISTING_MARKETS,
+  LOG_LEVEL,
+  CHECK_IF_MINT_IS_RENOUNCED,
+  CHECK_IF_BURNED,
+  QUOTE_MINT,
+  MAX_POOL_SIZE,
+  MIN_POOL_SIZE,
+  QUOTE_AMOUNT,
+  PRIVATE_KEY,
+  USE_SNIPE_LIST,
+  ONE_TOKEN_AT_A_TIME,
+  AUTO_SELL_DELAY,
+  MAX_SELL_RETRIES,
+  AUTO_SELL,
+  MAX_BUY_RETRIES,
+  AUTO_BUY_DELAY,
+  COMPUTE_UNIT_LIMIT,
+  COMPUTE_UNIT_PRICE,
+  CACHE_NEW_MARKETS,
+  TAKE_PROFIT,
+  STOP_LOSS,
+  BUY_SLIPPAGE,
+  SELL_SLIPPAGE,
+  PRICE_CHECK_DURATION,
+  PRICE_CHECK_INTERVAL, SNIPE_LIST_REFRESH_INTERVAL,
+} from './helpers';
+import { version } from './package.json';
+
+const connection = new Connection(RPC_ENDPOINT, {
+  wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
+  commitment: COMMITMENT_LEVEL,
+});
+
+function printDetails(wallet: Keypair, quoteToken: Token, botConfig: BotConfig) {
+  logger.info(`  
+                                        ..   :-===++++-     
+                                .-==+++++++- =+++++++++-    
+            ..:::--===+=.=:     .+++++++++++:=+++++++++:    
+    .==+++++++++++++++=:+++:    .+++++++++++.=++++++++-.    
+    .-+++++++++++++++=:=++++-   .+++++++++=:.=+++++-::-.    
+     -:+++++++++++++=:+++++++-  .++++++++-:- =+++++=-:      
+      -:++++++=++++=:++++=++++= .++++++++++- =+++++:        
+       -:++++-:=++=:++++=:-+++++:+++++====--:::::::.        
+        ::=+-:::==:=+++=::-:--::::::::::---------::.        
+         ::-:  .::::::::.  --------:::..                    
+          :-    .:.-:::.                                    
+
+          WARP DRIVE ACTIVATED 🚀🐟
+          Made with ❤️ by humans.
+          Version: ${version}                                          
+  `);
+
+  logger.info('------- CONFIGURATION START -------');
+  logger.info(`Wallet: ${wallet.publicKey.toString()}`);
+
+  logger.info('- Bot -');
+  logger.info(`Compute Unit limit: ${botConfig.unitLimit}`);
+  logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`);
+  logger.info(`Single token at the time: ${botConfig.oneTokenAtATime}`);
+  logger.info(`Pre load existing markets: ${PRE_LOAD_EXISTING_MARKETS}`);
+  logger.info(`Cache new markets: ${CACHE_NEW_MARKETS}`);
+  logger.info(`Log level: ${LOG_LEVEL}`);
+
+  logger.info('- Buy -');
+  logger.info(`Buy amount: ${botConfig.quoteAmount.toFixed()} ${botConfig.quoteToken.name}`);
+  logger.info(`Auto buy delay: ${botConfig.autoBuyDelay} ms`);
+  logger.info(`Max buy retries: ${botConfig.maxBuyRetries}`);
+  logger.info(`Buy amount (${quoteToken.symbol}): ${botConfig.quoteAmount.toFixed()}`);
+  logger.info(`Buy slippage: ${botConfig.buySlippage}%`);
+
+  logger.info('- Sell -');
+  logger.info(`Auto sell: ${AUTO_SELL}`);
+  logger.info(`Auto sell delay: ${botConfig.autoSellDelay} ms`);
+  logger.info(`Max sell retries: ${botConfig.maxSellRetries}`);
+  logger.info(`Sell slippage: ${botConfig.sellSlippage}%`);
+  logger.info(`Price check interval: ${botConfig.priceCheckInterval} ms`);
+  logger.info(`Price check duration: ${botConfig.priceCheckDuration} ms`);
+  logger.info(`Take profit: ${botConfig.takeProfit}%`);
+  logger.info(`Stop loss: ${botConfig.stopLoss}%`);
+
+  logger.info('- Filters -');
+  logger.info(`Snipe list: ${botConfig.useSnipeList}`);
+  logger.info(`Snipe list refresh interval: ${SNIPE_LIST_REFRESH_INTERVAL} ms`);
+  logger.info(`Check renounced: ${botConfig.checkRenounced}`);
+  logger.info(`Check burned: ${botConfig.checkBurned}`);
+  logger.info(`Min pool size: ${botConfig.minPoolSize.toFixed()}`);
+  logger.info(`Max pool size: ${botConfig.maxPoolSize.toFixed()}`);
+
+  logger.info('------- CONFIGURATION END -------');
+
+  logger.info('Bot is running! Press CTRL + C to stop it.');
+}
+
+const runListener = async () => {
+  logger.level = LOG_LEVEL;
+  logger.info('Bot is starting...');
+
+  const marketCache = new MarketCache(connection);
+  const poolCache = new PoolCache();
+  const txExecutor = new DefaultTransactionExecutor(connection);
+  const wallet = getWallet(PRIVATE_KEY.trim());
+  const quoteToken = getToken(QUOTE_MINT);
+  const botConfig = <BotConfig>{
+    wallet,
+    quoteAta: getAssociatedTokenAddressSync(quoteToken.mint, wallet.publicKey),
+    checkRenounced: CHECK_IF_MINT_IS_RENOUNCED,
+    checkBurned: CHECK_IF_BURNED,
+    minPoolSize: new TokenAmount(quoteToken, MIN_POOL_SIZE, false),
+    maxPoolSize: new TokenAmount(quoteToken, MAX_POOL_SIZE, false),
+    quoteToken,
+    quoteAmount: new TokenAmount(quoteToken, QUOTE_AMOUNT, false),
+    oneTokenAtATime: ONE_TOKEN_AT_A_TIME,
+    useSnipeList: USE_SNIPE_LIST,
+    autoSellDelay: AUTO_SELL_DELAY,
+    maxSellRetries: MAX_SELL_RETRIES,
+    autoBuyDelay: AUTO_BUY_DELAY,
+    maxBuyRetries: MAX_BUY_RETRIES,
+    unitLimit: COMPUTE_UNIT_LIMIT,
+    unitPrice: COMPUTE_UNIT_PRICE,
+    takeProfit: TAKE_PROFIT,
+    stopLoss: STOP_LOSS,
+    buySlippage: BUY_SLIPPAGE,
+    sellSlippage: SELL_SLIPPAGE,
+    priceCheckInterval: PRICE_CHECK_INTERVAL,
+    priceCheckDuration: PRICE_CHECK_DURATION,
+  };
+
+  const bot = new Bot(connection, marketCache, poolCache, txExecutor, botConfig);
+  const valid = await bot.validate();
+
+  if (!valid) {
+    logger.info('Bot is exiting...');
+    process.exit(1);
+  }
+
+  if (PRE_LOAD_EXISTING_MARKETS) {
+    await marketCache.init({ quoteToken });
+  }
+
+  const runTimestamp = Math.floor(new Date().getTime() / 1000);
+  const listeners = new Listeners(connection);
+  await listeners.start({
+    walletPublicKey: wallet.publicKey,
+    quoteToken,
+    autoSell: AUTO_SELL,
+    cacheNewMarkets: CACHE_NEW_MARKETS,
+  });
+
+  listeners.on('market', (updatedAccountInfo: KeyedAccountInfo) => {
+    const marketState = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data);
+    marketCache.save(updatedAccountInfo.accountId.toString(), marketState);
+  });
+
+  listeners.on('pool', async (updatedAccountInfo: KeyedAccountInfo) => {
+    const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data);
+    const poolOpenTime = parseInt(poolState.poolOpenTime.toString());
+    const exists = await poolCache.get(poolState.baseMint.toString());
+
+    if (!exists && poolOpenTime > runTimestamp) {
+      poolCache.save(updatedAccountInfo.accountId.toString(), poolState);
+      await bot.buy(updatedAccountInfo.accountId, poolState);
+    }
+  });
+
+  listeners.on('wallet', async (updatedAccountInfo: KeyedAccountInfo) => {
+    const accountData = AccountLayout.decode(updatedAccountInfo.accountInfo.data);
+
+    if (accountData.mint.equals(quoteToken.mint)) {
+      return;
+    }
+
+    await bot.sell(updatedAccountInfo.accountId, accountData);
+  });
+
+  printDetails(wallet, quoteToken, botConfig);
+};
+
+runListener();

+ 0 - 1
liquidity/index.ts

@@ -1 +0,0 @@
-export * from './liquidity';

+ 1 - 0
listeners/index.ts

@@ -0,0 +1 @@
+export * from './listeners';

+ 112 - 0
listeners/listeners.ts

@@ -0,0 +1,112 @@
+import { LIQUIDITY_STATE_LAYOUT_V4, MAINNET_PROGRAM_ID, MARKET_STATE_LAYOUT_V3, Token } from '@raydium-io/raydium-sdk';
+import bs58 from 'bs58';
+import { Connection, PublicKey } from '@solana/web3.js';
+import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
+import { EventEmitter } from 'events';
+
+export class Listeners extends EventEmitter {
+  private subscriptions: number[] = [];
+
+  constructor(private readonly connection: Connection) {
+    super();
+  }
+
+  public async start(config: {
+    walletPublicKey: PublicKey;
+    quoteToken: Token;
+    autoSell: boolean;
+    cacheNewMarkets: boolean;
+  }) {
+    if (config.cacheNewMarkets) {
+      const openBookSubscription = await this.subscribeToOpenBookMarkets(config);
+      this.subscriptions.push(openBookSubscription);
+    }
+
+    const raydiumSubscription = await this.subscribeToRaydiumPools(config);
+    this.subscriptions.push(raydiumSubscription);
+
+    if (config.autoSell) {
+      const walletSubscription = await this.subscribeToWalletChanges(config);
+      this.subscriptions.push(walletSubscription);
+    }
+  }
+
+  private async subscribeToOpenBookMarkets(config: { quoteToken: Token }) {
+    return this.connection.onProgramAccountChange(
+      MAINNET_PROGRAM_ID.OPENBOOK_MARKET,
+      async (updatedAccountInfo) => {
+        this.emit('market', updatedAccountInfo);
+      },
+      this.connection.commitment,
+      [
+        { dataSize: MARKET_STATE_LAYOUT_V3.span },
+        {
+          memcmp: {
+            offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'),
+            bytes: config.quoteToken.mint.toBase58(),
+          },
+        },
+      ],
+    );
+  }
+
+  private async subscribeToRaydiumPools(config: { quoteToken: Token }) {
+    return this.connection.onProgramAccountChange(
+      MAINNET_PROGRAM_ID.AmmV4,
+      async (updatedAccountInfo) => {
+        this.emit('pool', updatedAccountInfo);
+      },
+      this.connection.commitment,
+      [
+        { dataSize: LIQUIDITY_STATE_LAYOUT_V4.span },
+        {
+          memcmp: {
+            offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
+            bytes: config.quoteToken.mint.toBase58(),
+          },
+        },
+        {
+          memcmp: {
+            offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'),
+            bytes: MAINNET_PROGRAM_ID.OPENBOOK_MARKET.toBase58(),
+          },
+        },
+        {
+          memcmp: {
+            offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'),
+            bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]),
+          },
+        },
+      ],
+    );
+  }
+
+  private async subscribeToWalletChanges(config: { walletPublicKey: PublicKey }) {
+    return this.connection.onProgramAccountChange(
+      TOKEN_PROGRAM_ID,
+      async (updatedAccountInfo) => {
+        this.emit('wallet', updatedAccountInfo);
+      },
+      this.connection.commitment,
+      [
+        {
+          dataSize: 165,
+        },
+        {
+          memcmp: {
+            offset: 32,
+            bytes: config.walletPublicKey.toBase58(),
+          },
+        },
+      ],
+    );
+  }
+
+  public async stop() {
+    for (let i = this.subscriptions.length; i >= 0; --i) {
+      const subscription = this.subscriptions[i];
+      await this.connection.removeAccountChangeListener(subscription);
+      this.subscriptions.splice(i, 1);
+    }
+  }
+}

+ 0 - 1
market/index.ts

@@ -1 +0,0 @@
-export * from './market';

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 2876 - 164
package-lock.json


+ 8 - 2
package.json

@@ -1,18 +1,24 @@
 {
   "name": "solana-sniper-bot",
   "author": "Filip Dundjer",
+  "version": "2.0.0",
   "scripts": {
-    "buy": "ts-node buy.ts",
+    "start": "ts-node index.ts",
     "tsc": "tsc --noEmit"
   },
   "dependencies": {
     "@raydium-io/raydium-sdk": "^1.3.1-beta.47",
     "@solana/spl-token": "^0.4.0",
     "@solana/web3.js": "^1.89.1",
+    "async-mutex": "^0.5.0",
     "bigint-buffer": "^1.1.5",
+    "bip39": "^3.1.0",
     "bn.js": "^5.2.1",
     "bs58": "^5.0.0",
     "dotenv": "^16.4.1",
+    "ed25519-hd-key": "^1.3.0",
+    "i": "^0.3.7",
+    "npm": "^10.5.2",
     "pino": "^8.18.0",
     "pino-pretty": "^10.3.1",
     "pino-std-serializers": "^6.2.2"
@@ -23,4 +29,4 @@
     "ts-node": "^10.9.2",
     "typescript": "^5.3.3"
   }
-}
+}

+ 37 - 0
transactions/default-transaction-executor.ts

@@ -0,0 +1,37 @@
+import { BlockhashWithExpiryBlockHeight, Connection, Transaction, VersionedTransaction } from '@solana/web3.js';
+import { TransactionExecutor } from './transaction-executor.interface';
+import { logger } from '../helpers';
+
+export class DefaultTransactionExecutor implements TransactionExecutor {
+  constructor(private readonly connection: Connection) {}
+
+  public async executeAndConfirm(
+    transaction: Transaction | VersionedTransaction,
+    latestBlockhash: BlockhashWithExpiryBlockHeight,
+  ): Promise<{ confirmed: boolean; signature: string }> {
+    logger.debug('Executing transaction...');
+    const signature = await this.execute(transaction);
+
+    logger.debug({ signature }, 'Confirming transaction...');
+    return this.confirm(signature, latestBlockhash);
+  }
+
+  private async execute(transaction: Transaction | VersionedTransaction) {
+    return this.connection.sendRawTransaction(transaction.serialize(), {
+      preflightCommitment: this.connection.commitment,
+    });
+  }
+
+  private async confirm(signature: string, latestBlockhash: BlockhashWithExpiryBlockHeight) {
+    const confirmation = await this.connection.confirmTransaction(
+      {
+        signature,
+        lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
+        blockhash: latestBlockhash.blockhash,
+      },
+      this.connection.commitment,
+    );
+
+    return { confirmed: !confirmation.value.err, signature };
+  }
+}

+ 2 - 0
transactions/index.ts

@@ -0,0 +1,2 @@
+export * from './default-transaction-executor';
+export * from './transaction-executor.interface';

+ 8 - 0
transactions/transaction-executor.interface.ts

@@ -0,0 +1,8 @@
+import { BlockhashWithExpiryBlockHeight, Transaction, VersionedTransaction } from '@solana/web3.js';
+
+export interface TransactionExecutor {
+  executeAndConfirm(
+    transaction: Transaction | VersionedTransaction,
+    latestBlockhash: BlockhashWithExpiryBlockHeight,
+  ): Promise<{ confirmed: boolean; signature: string }>;
+}

+ 0 - 1
types/index.ts

@@ -1 +0,0 @@
-export * from './mint';

+ 0 - 44
types/mint.ts

@@ -1,44 +0,0 @@
-import { struct, u32, u8 } from '@solana/buffer-layout';
-import { bool, publicKey, u64 } from '@solana/buffer-layout-utils';
-import { Commitment, Connection, PublicKey } from '@solana/web3.js';
-
-/** Information about a mint */
-export interface Mint {
-    /** Address of the mint */
-    address: PublicKey;
-    /**
-     * Optional authority used to mint new tokens. The mint authority may only be provided during mint creation.
-     * If no mint authority is present then the mint has a fixed supply and no further tokens may be minted.
-     */
-    mintAuthority: PublicKey | null;
-    /** Total supply of tokens */
-    supply: bigint;
-    /** Number of base 10 digits to the right of the decimal place */
-    decimals: number;
-    /** Is this mint initialized */
-    isInitialized: boolean;
-    /** Optional authority to freeze token accounts */
-    freezeAuthority: PublicKey | null;
-}
-
-/** Mint as stored by the program */
-export interface RawMint {
-    mintAuthorityOption: 1 | 0;
-    mintAuthority: PublicKey;
-    supply: bigint;
-    decimals: number;
-    isInitialized: boolean;
-    freezeAuthorityOption: 1 | 0;
-    freezeAuthority: PublicKey;
-}
-
-/** Buffer layout for de/serializing a mint */
-export const MintLayout = struct<RawMint>([
-    u32('mintAuthorityOption'),
-    publicKey('mintAuthority'),
-    u64('supply'),
-    u8('decimals'),
-    bool('isInitialized'),
-    u32('freezeAuthorityOption'),
-    publicKey('freezeAuthority'),
-]);

+ 0 - 2
utils/index.ts

@@ -1,2 +0,0 @@
-export * from './utils';
-export * from './logger';

+ 0 - 13
utils/utils.ts

@@ -1,13 +0,0 @@
-import { Logger } from 'pino';
-import dotenv from 'dotenv';
-
-dotenv.config();
-
-export const retrieveEnvVariable = (variableName: string, logger: Logger) => {
-  const variable = process.env[variableName] || '';
-  if (!variable) {
-    logger.error(`${variableName} is not set`);
-    process.exit(1);
-  }
-  return variable;
-};

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно