123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- 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';
- import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
- import { JitoTransactionExecutor } from './transactions/jito-rpc-transaction-executor';
- export interface BotConfig {
- wallet: Keypair;
- checkRenounced: boolean;
- checkFreezable: 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;
- filterCheckInterval: number;
- filterCheckDuration: number;
- consecutiveMatchCount: 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;
- public readonly isWarp: boolean = false;
- public readonly isJito: boolean = false;
- constructor(
- private readonly connection: Connection,
- private readonly marketStorage: MarketCache,
- private readonly poolStorage: PoolCache,
- private readonly txExecutor: TransactionExecutor,
- readonly config: BotConfig,
- ) {
- this.isWarp = txExecutor instanceof WarpTransactionExecutor;
- this.isJito = txExecutor instanceof JitoTransactionExecutor;
- 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 new pool...`);
- 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 [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);
- if (!this.config.useSnipeList) {
- const match = await this.filterMatch(poolKeys);
- if (!match) {
- logger.trace({ mint: poolKeys.baseMint.toString() }, `Skipping buy because pool doesn't match filters`);
- return;
- }
- }
- for (let i = 0; i < this.config.maxBuyRetries; i++) {
- try {
- 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.info(
- {
- mint: poolState.baseMint.toString(),
- signature: result.signature,
- error: result.error,
- },
- `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 new token...`);
- 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);
- }
- 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);
- for (let i = 0; i < this.config.maxSellRetries; i++) {
- try {
- 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: result.error,
- },
- `Error confirming sell tx`,
- );
- } catch (error) {
- logger.debug({ mint: rawAccount.mint.toString(), error }, `Error confirming sell transaction`);
- }
- }
- } catch (error) {
- logger.error({ mint: rawAccount.mint.toString(), error }, `Failed to sell token`);
- } finally {
- if (this.config.oneTokenAtATime) {
- this.sellExecutionCount--;
- }
- }
- }
- // noinspection JSUnusedLocalSymbols
- 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: [
- ...(this.isWarp || this.isJito
- ? []
- : [
- 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, wallet, latestBlockhash);
- }
- private async filterMatch(poolKeys: LiquidityPoolKeysV4) {
- if (this.config.filterCheckInterval === 0 || this.config.filterCheckDuration === 0) {
- return true;
- }
- const timesToCheck = this.config.filterCheckDuration / this.config.filterCheckInterval;
- let timesChecked = 0;
- let matchCount = 0;
- do {
- try {
- const shouldBuy = await this.poolFilters.execute(poolKeys);
- if (shouldBuy) {
- matchCount++;
- if (this.config.consecutiveMatchCount <= matchCount) {
- logger.debug(
- { mint: poolKeys.baseMint.toString() },
- `Filter match ${matchCount}/${this.config.consecutiveMatchCount}`,
- );
- return true;
- }
- } else {
- matchCount = 0;
- }
- await sleep(this.config.filterCheckInterval);
- } finally {
- timesChecked++;
- }
- } while (timesChecked < timesToCheck);
- return false;
- }
- private async priceMatch(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
- if (this.config.priceCheckDuration === 0 || this.config.priceCheckInterval === 0) {
- return;
- }
- const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
- 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);
- 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);
- }
- }
|