mirror of
https://github.com/fdundjer/solana-sniper-bot.git
synced 2025-11-10 04:22:05 +10:00
feat: real time filtering
This commit is contained in:
15
.env.copy
15
.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
|
||||
24
README.md
24
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.
|
||||
61
bot.ts
61
bot.ts
@ -47,6 +47,9 @@ export interface BotConfig {
|
||||
sellSlippage: number;
|
||||
priceCheckInterval: number;
|
||||
priceCheckDuration: number;
|
||||
filterCheckInterval: number;
|
||||
filterCheckDuration: number;
|
||||
consecutiveMatchCount: number;
|
||||
}
|
||||
|
||||
export class Bot {
|
||||
@ -120,15 +123,6 @@ export class Bot {
|
||||
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()),
|
||||
@ -136,6 +130,15 @@ export class Bot {
|
||||
]);
|
||||
const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(accountId, poolState, market);
|
||||
|
||||
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}`,
|
||||
@ -214,13 +217,13 @@ export class Bot {
|
||||
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);
|
||||
|
||||
for (let i = 0; i < this.config.maxSellRetries; i++) {
|
||||
try {
|
||||
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;
|
||||
|
||||
@ -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<FilterResult> {
|
||||
async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
|
||||
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' };
|
||||
|
||||
@ -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<FilterResult>;
|
||||
execute(poolKeysV4: LiquidityPoolKeysV4): Promise<FilterResult>;
|
||||
}
|
||||
|
||||
export interface FilterResult {
|
||||
@ -40,12 +40,12 @@ export class PoolFilters {
|
||||
}
|
||||
}
|
||||
|
||||
public async execute(poolState: LiquidityStateV4): Promise<boolean> {
|
||||
public async execute(poolKeys: LiquidityPoolKeysV4): Promise<boolean> {
|
||||
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;
|
||||
|
||||
@ -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<FilterResult> {
|
||||
async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
|
||||
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' };
|
||||
|
||||
@ -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<FilterResult> {
|
||||
async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
|
||||
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' };
|
||||
|
||||
@ -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);
|
||||
|
||||
13
index.ts
13
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);
|
||||
|
||||
Reference in New Issue
Block a user