From da9512b8bb3be52ee36f9f15aa534453bddd51d4 Mon Sep 17 00:00:00 2001 From: Filip Dunder Date: Fri, 19 Apr 2024 12:09:56 +0200 Subject: [PATCH] feat: real time filtering --- .env.copy | 15 +++++---- README.md | 24 +++++++++---- bot.ts | 67 +++++++++++++++++++++++++++++-------- filters/burn.filter.ts | 8 ++--- filters/pool-filters.ts | 10 +++--- filters/pool-size.filter.ts | 8 ++--- filters/renounced.filter.ts | 8 ++--- helpers/constants.ts | 3 ++ index.ts | 13 +++++-- 9 files changed, 110 insertions(+), 46 deletions(-) diff --git a/.env.copy b/.env.copy index 4fb77cc..b3946e4 100644 --- a/.env.copy +++ b/.env.copy @@ -24,22 +24,25 @@ QUOTE_MINT=WSOL QUOTE_AMOUNT=0.001 AUTO_BUY_DELAY=0 MAX_BUY_RETRIES=10 -BUY_SLIPPAGE=5 +BUY_SLIPPAGE=20 # Sell AUTO_SELL=true MAX_SELL_RETRIES=10 AUTO_SELL_DELAY=0 PRICE_CHECK_INTERVAL=2000 -PRICE_CHECK_DURATION=60000 -TAKE_PROFIT=20 -STOP_LOSS=15 -SELL_SLIPPAGE=5 +PRICE_CHECK_DURATION=600000 +TAKE_PROFIT=40 +STOP_LOSS=20 +SELL_SLIPPAGE=20 # Filters USE_SNIPE_LIST=false SNIPE_LIST_REFRESH_INTERVAL=30000 +FILTER_CHECK_DURATION=60000 +FILTER_CHECK_INTERVAL=2000 +CONSECUTIVE_FILTER_MATCHES=3 CHECK_IF_MINT_IS_RENOUNCED=true -CHECK_IF_BURNED=false +CHECK_IF_BURNED=true MIN_POOL_SIZE=5 MAX_POOL_SIZE=50 \ No newline at end of file diff --git a/README.md b/README.md index f0494e9..e670daf 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ -# Solana Sniper Bot (Poc) -This code is written as proof of concept to demonstrate how we can buy new tokens immediately after the liquidity pool is open for trading. +# Solana Trading Bot (Beta) +The Solana Trading Bot is a software tool designed to automate the buying and selling of tokens on the Solana blockchain. +It is configured to execute trades based on predefined parameters and strategies set by the user. -Script listens to new Raydium USDC or SOL pools and buys tokens for a fixed amount in USDC/SOL. -Depending on the speed of the RPC node, the purchase usually happens before the token is available on Raydium UI for swapping. - -This is provided as is, for learning purposes. +The bot can monitor market conditions in real-time, such as pool burn, mint renounced and other factors, and it will execute trades when these conditions are fulfilled. ## Setup To run the script you need to: @@ -55,11 +53,14 @@ You should see the following output: #### Sell - `AUTO_SELL` - Set to `true` to enable automatic selling of tokens. + - If you want to manually sell bought tokens, disable this option. - `MAX_SELL_RETRIES` - Maximum number of retries for selling a token. - `AUTO_SELL_DELAY` - Delay in milliseconds before auto-selling a token. - `PRICE_CHECK_INTERVAL` - Interval in milliseconds for checking the take profit and stop loss conditions. + - Set to zero to disable take profit and stop loss. - `PRICE_CHECK_DURATION` - Time in milliseconds to wait for stop loss/take profit conditions. - If you don't reach profit or loss bot will auto sell after this time. + - Set to zero to disable take profit and stop loss. - `TAKE_PROFIT` - Percentage profit at which to take profit. - Take profit is calculated based on quote mint. - `STOP_LOSS` - Percentage loss at which to stop the loss. @@ -67,6 +68,13 @@ You should see the following output: - `SELL_SLIPPAGE` - Slippage %. #### Filters +- `FILTER_CHECK_INTERVAL` - Interval in milliseconds for checking if pool match the filters. + - Set to zero to disable filters. +- `FILTER_CHECK_DURATION` - Time in milliseconds to wait for pool to match the filters. + - If pool doesn't match the filter buy will not happen. + - Set to zero to disable filters. +- `CONSECUTIVE_FILTER_MATCHES` - How many times in a row pool needs to match the filters. + - This is useful because when pool is burned (and rugged), other filters may not report the same behavior because of distributed RPC endpoints (eg. helius network) - `USE_SNIPE_LIST` - Set to `true` to enable buying only tokens listed in `snipe-list.txt`. - Pool must not exist before the script starts. - `SNIPE_LIST_REFRESH_INTERVAL` - Interval in milliseconds to refresh the snipe list. @@ -123,4 +131,6 @@ To collect more information on an issue, please change `LOG_LEVEL` to `debug`. ## Disclaimer -Use this script at your own risk. +The Solana Trading Bot is provided as is, for learning purposes. +Trading cryptocurrencies and tokens involves risk, and past performance is not indicative of future results. +The use of this bot is at your own risk, and we are not responsible for any losses incurred while using the bot. \ No newline at end of file diff --git a/bot.ts b/bot.ts index c93504c..7e80090 100644 --- a/bot.ts +++ b/bot.ts @@ -47,6 +47,9 @@ export interface BotConfig { sellSlippage: number; priceCheckInterval: number; priceCheckDuration: number; + filterCheckInterval: number; + filterCheckDuration: number; + consecutiveMatchCount: number; } export class Bot { @@ -121,21 +124,21 @@ export class Bot { } try { - const shouldBuy = await this.poolFilters.execute(poolState); + 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 (!shouldBuy) { - logger.debug({ mint: poolState.baseMint.toString() }, `Skipping buy because pool doesn't match filters`); + 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 { - 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}`, @@ -214,13 +217,13 @@ export class Bot { 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 { - 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}`, @@ -342,6 +345,42 @@ export class Bot { return this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash); } + private async filterMatch(poolKeys: LiquidityPoolKeysV4) { + if (this.config.filterCheckInterval === 0 || this.config.filterCheckDuration === 0) { + return; + } + + 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; diff --git a/filters/burn.filter.ts b/filters/burn.filter.ts index 18acc5c..226699f 100644 --- a/filters/burn.filter.ts +++ b/filters/burn.filter.ts @@ -1,14 +1,14 @@ import { Filter, FilterResult } from './pool-filters'; import { Connection } from '@solana/web3.js'; -import { LiquidityStateV4 } from '@raydium-io/raydium-sdk'; +import { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk'; import { logger } from '../helpers'; export class BurnFilter implements Filter { constructor(private readonly connection: Connection) {} - async execute(poolState: LiquidityStateV4): Promise { + async execute(poolKeys: LiquidityPoolKeysV4): Promise { try { - const amount = await this.connection.getTokenSupply(poolState.lpMint, this.connection.commitment); + const amount = await this.connection.getTokenSupply(poolKeys.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) { @@ -16,7 +16,7 @@ export class BurnFilter implements Filter { return { ok: true }; } - logger.error({ mint: poolState.baseMint }, `Failed to check if LP is burned`); + logger.error({ mint: poolKeys.baseMint }, `Failed to check if LP is burned`); } return { ok: false, message: 'Failed to check if LP is burned' }; diff --git a/filters/pool-filters.ts b/filters/pool-filters.ts index 3a6e91d..9af1310 100644 --- a/filters/pool-filters.ts +++ b/filters/pool-filters.ts @@ -1,12 +1,12 @@ import { Connection } from '@solana/web3.js'; -import { LiquidityStateV4, Token, TokenAmount } from '@raydium-io/raydium-sdk'; +import { LiquidityPoolKeysV4, 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; + execute(poolKeysV4: LiquidityPoolKeysV4): Promise; } export interface FilterResult { @@ -40,12 +40,12 @@ export class PoolFilters { } } - public async execute(poolState: LiquidityStateV4): Promise { + public async execute(poolKeys: LiquidityPoolKeysV4): Promise { if (this.filters.length === 0) { return true; } - const result = await Promise.all(this.filters.map((f) => f.execute(poolState))); + const result = await Promise.all(this.filters.map((f) => f.execute(poolKeys))); const pass = result.every((r) => r.ok); if (pass) { @@ -53,7 +53,7 @@ export class PoolFilters { } for (const filterResult of result.filter((r) => !r.ok)) { - logger.info(filterResult.message); + logger.trace(filterResult.message); } return false; diff --git a/filters/pool-size.filter.ts b/filters/pool-size.filter.ts index df1b7f2..8bc5246 100644 --- a/filters/pool-size.filter.ts +++ b/filters/pool-size.filter.ts @@ -1,5 +1,5 @@ import { Filter, FilterResult } from './pool-filters'; -import { LiquidityStateV4, Token, TokenAmount } from '@raydium-io/raydium-sdk'; +import { LiquidityPoolKeysV4, Token, TokenAmount } from '@raydium-io/raydium-sdk'; import { Connection } from '@solana/web3.js'; import { logger } from '../helpers'; @@ -11,9 +11,9 @@ export class PoolSizeFilter implements Filter { private readonly maxPoolSize: TokenAmount, ) {} - async execute(poolState: LiquidityStateV4): Promise { + async execute(poolKeys: LiquidityPoolKeysV4): Promise { try { - const response = await this.connection.getTokenAccountBalance(poolState.quoteVault, this.connection.commitment); + const response = await this.connection.getTokenAccountBalance(poolKeys.quoteVault, this.connection.commitment); const poolSize = new TokenAmount(this.quoteToken, response.value.amount, true); let inRange = true; @@ -35,7 +35,7 @@ export class PoolSizeFilter implements Filter { return { ok: inRange }; } catch (error) { - logger.error({ mint: poolState.baseMint }, `Failed to check pool size`); + logger.error({ mint: poolKeys.baseMint }, `Failed to check pool size`); } return { ok: false, message: 'PoolSize -> Failed to check pool size' }; diff --git a/filters/renounced.filter.ts b/filters/renounced.filter.ts index 56ece08..c5d6884 100644 --- a/filters/renounced.filter.ts +++ b/filters/renounced.filter.ts @@ -1,15 +1,15 @@ 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 { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk'; import { logger } from '../helpers'; export class RenouncedFilter implements Filter { constructor(private readonly connection: Connection) {} - async execute(poolState: LiquidityStateV4): Promise { + async execute(poolKeys: LiquidityPoolKeysV4): Promise { try { - const accountInfo = await this.connection.getAccountInfo(poolState.baseMint, this.connection.commitment); + const accountInfo = await this.connection.getAccountInfo(poolKeys.baseMint, this.connection.commitment); if (!accountInfo?.data) { return { ok: false, message: 'Renounced -> Failed to fetch account data' }; } @@ -18,7 +18,7 @@ export class RenouncedFilter implements Filter { 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`); + logger.error({ mint: poolKeys.baseMint }, `Failed to check if mint is renounced`); } return { ok: false, message: 'Renounced -> Failed to check if mint is renounced' }; diff --git a/helpers/constants.ts b/helpers/constants.ts index a60a8ee..81f42e2 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -51,6 +51,9 @@ export const PRICE_CHECK_DURATION = Number(retrieveEnvVariable('PRICE_CHECK_DURA export const SELL_SLIPPAGE = Number(retrieveEnvVariable('SELL_SLIPPAGE', logger)); // Filters +export const FILTER_CHECK_INTERVAL = Number(retrieveEnvVariable('FILTER_CHECK_INTERVAL', logger)); +export const FILTER_CHECK_DURATION = Number(retrieveEnvVariable('FILTER_CHECK_DURATION', logger)); +export const CONSECUTIVE_FILTER_MATCHES = Number(retrieveEnvVariable('CONSECUTIVE_FILTER_MATCHES', logger)); 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); diff --git a/index.ts b/index.ts index 8859ac1..3c27f75 100644 --- a/index.ts +++ b/index.ts @@ -40,6 +40,9 @@ import { SNIPE_LIST_REFRESH_INTERVAL, TRANSACTION_EXECUTOR, WARP_FEE, + FILTER_CHECK_INTERVAL, + FILTER_CHECK_DURATION, + CONSECUTIVE_FILTER_MATCHES, } from './helpers'; import { version } from './package.json'; import { WarpTransactionExecutor } from './transactions/warp-transaction-executor'; @@ -78,8 +81,7 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) { logger.info(`Using warp: ${bot.isWarp}`); if (bot.isWarp) { logger.info(`Warp fee: ${WARP_FEE}`); - } - else { + } else { logger.info(`Compute Unit limit: ${botConfig.unitLimit}`); logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`); } @@ -109,6 +111,9 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) { logger.info('- Filters -'); logger.info(`Snipe list: ${botConfig.useSnipeList}`); logger.info(`Snipe list refresh interval: ${SNIPE_LIST_REFRESH_INTERVAL} ms`); + logger.info(`Filter check interval: ${botConfig.filterCheckInterval} ms`); + logger.info(`Filter check duration: ${botConfig.filterCheckDuration} ms`); + logger.info(`Consecutive filter matches: ${botConfig.consecutiveMatchCount} ms`); logger.info(`Check renounced: ${botConfig.checkRenounced}`); logger.info(`Check burned: ${botConfig.checkBurned}`); logger.info(`Min pool size: ${botConfig.minPoolSize.toFixed()}`); @@ -151,6 +156,7 @@ const runListener = async () => { quoteAmount: new TokenAmount(quoteToken, QUOTE_AMOUNT, false), oneTokenAtATime: ONE_TOKEN_AT_A_TIME, useSnipeList: USE_SNIPE_LIST, + autoSell: AUTO_SELL, autoSellDelay: AUTO_SELL_DELAY, maxSellRetries: MAX_SELL_RETRIES, autoBuyDelay: AUTO_BUY_DELAY, @@ -163,6 +169,9 @@ const runListener = async () => { sellSlippage: SELL_SLIPPAGE, priceCheckInterval: PRICE_CHECK_INTERVAL, priceCheckDuration: PRICE_CHECK_DURATION, + filterCheckInterval: FILTER_CHECK_INTERVAL, + filterCheckDuration: FILTER_CHECK_DURATION, + consecutiveMatchCount: CONSECUTIVE_FILTER_MATCHES, }; const bot = new Bot(connection, marketCache, poolCache, txExecutor, botConfig);