mirror of
https://github.com/fdundjer/solana-sniper-bot.git
synced 2025-11-10 04:22:05 +10:00
Merge branch 'refs/heads/social-filter' into fd-master
This commit is contained in:
12
.env.copy
12
.env.copy
@ -11,13 +11,13 @@ LOG_LEVEL=trace
|
||||
ONE_TOKEN_AT_A_TIME=true
|
||||
PRE_LOAD_EXISTING_MARKETS=false
|
||||
CACHE_NEW_MARKETS=false
|
||||
# default or warp
|
||||
# default or warp or jito
|
||||
TRANSACTION_EXECUTOR=default
|
||||
# if using default executor fee below will be applied
|
||||
# if using default executor, fee below will be applied
|
||||
COMPUTE_UNIT_LIMIT=101337
|
||||
COMPUTE_UNIT_PRICE=421197
|
||||
# if using warp executor fee below will be applied
|
||||
WARP_FEE=0.006
|
||||
# if using warp or jito executor, fee below will be applied
|
||||
CUSTOM_FEE=0.006
|
||||
|
||||
# Buy
|
||||
QUOTE_MINT=WSOL
|
||||
@ -42,8 +42,10 @@ SNIPE_LIST_REFRESH_INTERVAL=30000
|
||||
FILTER_CHECK_DURATION=60000
|
||||
FILTER_CHECK_INTERVAL=2000
|
||||
CONSECUTIVE_FILTER_MATCHES=3
|
||||
CHECK_IF_MUTABLE=false
|
||||
CHECK_IF_SOCIALS=true
|
||||
CHECK_IF_MINT_IS_RENOUNCED=true
|
||||
CHECK_IF_FREEZABLE=true
|
||||
CHECK_IF_FREEZABLE=false
|
||||
CHECK_IF_BURNED=true
|
||||
MIN_POOL_SIZE=5
|
||||
MAX_POOL_SIZE=50
|
||||
|
||||
36
README.md
36
README.md
@ -1,12 +1,14 @@
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
- Create a new empty Solana wallet
|
||||
- Transfer some SOL to it.
|
||||
- Convert some SOL to USDC or WSOL.
|
||||
@ -22,14 +24,17 @@ You should see the following output:
|
||||
### Configuration
|
||||
|
||||
#### Wallet
|
||||
|
||||
- `PRIVATE_KEY` - Your wallet's private key.
|
||||
|
||||
#### Connection
|
||||
|
||||
- `RPC_ENDPOINT` - HTTPS RPC endpoint for interacting with the Solana network.
|
||||
- `RPC_WEBSOCKET_ENDPOINT` - WebSocket RPC endpoint for real-time updates from the Solana network.
|
||||
- `COMMITMENT_LEVEL`- The commitment level of transactions (e.g., "finalized" for the highest level of security).
|
||||
|
||||
#### Bot
|
||||
|
||||
- `LOG_LEVEL` - Set logging level, e.g., `info`, `debug`, `trace`, etc.
|
||||
- `ONE_TOKEN_AT_A_TIME` - Set to `true` to process buying one token at a time.
|
||||
- `COMPUTE_UNIT_LIMIT` - Compute limit used to calculate fees.
|
||||
@ -38,13 +43,14 @@ You should see the following output:
|
||||
- This option should not be used with public RPC.
|
||||
- `CACHE_NEW_MARKETS` - Set to `true` to cache new markets.
|
||||
- This option should not be used with public RPC.
|
||||
- `TRANSACTION_EXECUTOR` - Set to `warp` to use warp infrastructure for executing transactions
|
||||
- `TRANSACTION_EXECUTOR` - Set to `warp` to use warp infrastructure for executing transactions, or set it to jito to use JSON-RPC jito executer
|
||||
- For more details checkout [warp](#warp-transactions-beta) section
|
||||
- `WARP_FEE` - If using warp executor this value will be used for transaction fees instead of `COMPUTE_UNIT_LIMIT` and `COMPUTE_UNIT_LIMIT`
|
||||
- Minimum value is 0.0001 SOL, but we recommend using 0.006 SOL or above
|
||||
- `CUSTOM_FEE` - If using warp or jito executors this value will be used for transaction fees instead of `COMPUTE_UNIT_LIMIT` and `COMPUTE_UNIT_LIMIT`
|
||||
- Minimum value is 0.0001 SOL, but we recommend using 0.006 SOL or above
|
||||
- On top of this fee, minimal solana network fee will be applied
|
||||
|
||||
#### Buy
|
||||
|
||||
- `QUOTE_MINT` - Which pools to snipe, USDC or WSOL.
|
||||
- `QUOTE_AMOUNT` - Amount used to buy each new token.
|
||||
- `AUTO_BUY_DELAY` - Delay in milliseconds before buying a token.
|
||||
@ -52,10 +58,11 @@ You should see the following output:
|
||||
- `BUY_SLIPPAGE` - Slippage %
|
||||
|
||||
#### 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.
|
||||
- `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.
|
||||
@ -68,6 +75,7 @@ You should see the following output:
|
||||
- `SELL_SLIPPAGE` - Slippage %.
|
||||
|
||||
#### Snipe list
|
||||
|
||||
- `USE_SNIPE_LIST` - Set to `true` to enable buying only tokens listed in `snipe-list.txt`.
|
||||
- Pool must not exist before the bot starts.
|
||||
- If token can be traded before bot starts nothing will happen. Bot will not buy the token.
|
||||
@ -77,6 +85,7 @@ You should see the following output:
|
||||
Note: When using snipe list filters below will be disabled.
|
||||
|
||||
#### 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.
|
||||
@ -84,6 +93,8 @@ Note: When using snipe list filters below will be disabled.
|
||||
- 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. eg. pool size may still have old value
|
||||
- `CHECK_IF_MUTABLE` - Set to `true` to buy tokens only if their metadata are not mutable.
|
||||
- `CHECK_IF_SOCIALS` - Set to `true` to buy tokens only if they have at least 1 social.
|
||||
- `CHECK_IF_MINT_IS_RENOUNCED` - Set to `true` to buy tokens only if their mint is renounced.
|
||||
- `CHECK_IF_FREEZABLE` - Set to `true` to buy tokens only if they are not freezable.
|
||||
- `CHECK_IF_BURNED` - Set to `true` to buy tokens only if their liquidity pool is burned.
|
||||
@ -93,12 +104,14 @@ Note: When using snipe list filters below will be disabled.
|
||||
- Set `0` to disable.
|
||||
|
||||
## Warp transactions (beta)
|
||||
|
||||
In case you experience a lot of failed transactions or transaction performance is too slow, you can try using `warp` for executing transactions.
|
||||
Warp is hosted service that executes transactions using integrations with third party providers.
|
||||
|
||||
Using warp for transactions supports the team behind this project.
|
||||
|
||||
### Security
|
||||
|
||||
When using warp, transaction is sent to the hosted service.
|
||||
**Payload that is being sent will NOT contain your wallet private key**. Fee transaction is signed on your machine.
|
||||
Each request is processed by hosted service and sent to third party provider.
|
||||
@ -107,20 +120,24 @@ Each request is processed by hosted service and sent to third party provider.
|
||||
Note: Warp transactions are disabled by default.
|
||||
|
||||
### Fees
|
||||
|
||||
When using warp for transactions, fee is distributed between developers of warp and third party providers.
|
||||
In case TX fails, no fee will be taken from your account.
|
||||
|
||||
## Common issues
|
||||
|
||||
If you have an error which is not listed here, please create a new issue in this repository.
|
||||
To collect more information on an issue, please change `LOG_LEVEL` to `debug`.
|
||||
|
||||
### Unsupported RPC node
|
||||
|
||||
- If you see following error in your log file:
|
||||
`Error: 410 Gone: {"jsonrpc":"2.0","error":{"code": 410, "message":"The RPC call or parameters have been disabled."}, "id": "986f3599-b2b7-47c4-b951-074c19842bad" }`
|
||||
it means your RPC node doesn't support methods needed to execute script.
|
||||
- FIX: Change your RPC node. You can use Helius or Quicknode.
|
||||
|
||||
### No token account
|
||||
|
||||
- If you see following error in your log file:
|
||||
`Error: No SOL token account found in wallet: `
|
||||
it means that wallet you provided doesn't have USDC/WSOL token account.
|
||||
@ -129,10 +146,11 @@ To collect more information on an issue, please change `LOG_LEVEL` to `debug`.
|
||||

|
||||
|
||||
## Contact
|
||||
|
||||
[](https://discord.gg/xYUETCA2aP)
|
||||
|
||||
- If you want to leave a tip, you can send it to the following address:
|
||||
`7gm6BPQrSBaTAYaJheuRevBNXcmKsgbkfBCVSjBnt9aP`
|
||||
`7gm6BPQrSBaTAYaJheuRevBNXcmKsgbkfBCVSjBnt9aP`
|
||||
|
||||
- If you need custom features or assistance, feel free to contact the admin team on discord for dedicated support.
|
||||
|
||||
@ -140,4 +158,4 @@ To collect more information on an issue, please change `LOG_LEVEL` to `debug`.
|
||||
|
||||
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.
|
||||
The use of this bot is at your own risk, and we are not responsible for any losses incurred while using the bot.
|
||||
|
||||
5
bot.ts
5
bot.ts
@ -22,6 +22,7 @@ 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;
|
||||
@ -63,6 +64,7 @@ export class Bot {
|
||||
private readonly mutex: Mutex;
|
||||
private sellExecutionCount = 0;
|
||||
public readonly isWarp: boolean = false;
|
||||
public readonly isJito: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly connection: Connection,
|
||||
@ -72,6 +74,7 @@ export class Bot {
|
||||
readonly config: BotConfig,
|
||||
) {
|
||||
this.isWarp = txExecutor instanceof WarpTransactionExecutor;
|
||||
this.isJito = txExecutor instanceof JitoTransactionExecutor;
|
||||
|
||||
this.mutex = new Mutex();
|
||||
this.poolFilters = new PoolFilters(connection, {
|
||||
@ -324,7 +327,7 @@ export class Bot {
|
||||
payerKey: wallet.publicKey,
|
||||
recentBlockhash: latestBlockhash.blockhash,
|
||||
instructions: [
|
||||
...(this.isWarp
|
||||
...(this.isWarp || this.isJito
|
||||
? []
|
||||
: [
|
||||
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export * from './burn.filter';
|
||||
export * from './mutable.filter';
|
||||
export * from './pool-filters';
|
||||
export * from './pool-size.filter';
|
||||
export * from './renounced.filter';
|
||||
|
||||
66
filters/mutable.filter.ts
Normal file
66
filters/mutable.filter.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Filter, FilterResult } from './pool-filters';
|
||||
import { Connection } from '@solana/web3.js';
|
||||
import { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk';
|
||||
import { getPdaMetadataKey } from '@raydium-io/raydium-sdk';
|
||||
import { MetadataAccountData, MetadataAccountDataArgs } from '@metaplex-foundation/mpl-token-metadata';
|
||||
import { Serializer } from '@metaplex-foundation/umi/serializers';
|
||||
import { logger } from '../helpers';
|
||||
|
||||
export class MutableFilter implements Filter {
|
||||
private readonly errorMessage: string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly connection: Connection,
|
||||
private readonly metadataSerializer: Serializer<MetadataAccountDataArgs, MetadataAccountData>,
|
||||
private readonly checkMutable: boolean,
|
||||
private readonly checkSocials: boolean,
|
||||
) {
|
||||
if (this.checkMutable) {
|
||||
this.errorMessage.push('mutable');
|
||||
}
|
||||
|
||||
if (this.checkSocials) {
|
||||
this.errorMessage.push('socials');
|
||||
}
|
||||
}
|
||||
|
||||
async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
|
||||
try {
|
||||
const metadataPDA = getPdaMetadataKey(poolKeys.baseMint);
|
||||
const metadataAccount = await this.connection.getAccountInfo(metadataPDA.publicKey, this.connection.commitment);
|
||||
|
||||
if (!metadataAccount?.data) {
|
||||
return { ok: false, message: 'Mutable -> Failed to fetch account data' };
|
||||
}
|
||||
|
||||
const deserialize = this.metadataSerializer.deserialize(metadataAccount.data);
|
||||
const mutable = !this.checkMutable || deserialize[0].isMutable;
|
||||
const hasSocials = !this.checkSocials || (await this.hasSocials(deserialize[0]));
|
||||
const ok = !mutable && hasSocials;
|
||||
const message: string[] = [];
|
||||
|
||||
if (mutable) {
|
||||
message.push('metadata can be changed');
|
||||
}
|
||||
|
||||
if (!hasSocials) {
|
||||
message.push('has no socials');
|
||||
}
|
||||
|
||||
return { ok: ok, message: ok ? undefined : `MutableSocials -> Token ${message.join(' and ')}` };
|
||||
} catch (e) {
|
||||
logger.error({ mint: poolKeys.baseMint }, `MutableSocials -> Failed to check ${this.errorMessage.join(' and ')}`);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
message: `MutableSocials -> Failed to check ${this.errorMessage.join(' and ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
private async hasSocials(metadata: MetadataAccountData) {
|
||||
const response = await fetch(metadata.uri);
|
||||
const data = await response.json();
|
||||
return Object.values(data?.extensions ?? {}).some((value: any) => value !== null && value.length > 0);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
import { Connection } from '@solana/web3.js';
|
||||
import { LiquidityPoolKeysV4, Token, TokenAmount } from '@raydium-io/raydium-sdk';
|
||||
import { getMetadataAccountDataSerializer } from '@metaplex-foundation/mpl-token-metadata';
|
||||
import { BurnFilter } from './burn.filter';
|
||||
import { MutableFilter } from './mutable.filter';
|
||||
import { RenouncedFreezeFilter } from './renounced.filter';
|
||||
import { PoolSizeFilter } from './pool-size.filter';
|
||||
import { CHECK_IF_BURNED, CHECK_IF_FREEZABLE, CHECK_IF_MINT_IS_RENOUNCED, logger } from '../helpers';
|
||||
import { CHECK_IF_BURNED, CHECK_IF_FREEZABLE, CHECK_IF_MINT_IS_RENOUNCED, CHECK_IF_MUTABLE, CHECK_IF_SOCIALS, logger } from '../helpers';
|
||||
|
||||
export interface Filter {
|
||||
execute(poolKeysV4: LiquidityPoolKeysV4): Promise<FilterResult>;
|
||||
@ -35,6 +37,10 @@ export class PoolFilters {
|
||||
this.filters.push(new RenouncedFreezeFilter(connection, CHECK_IF_MINT_IS_RENOUNCED, CHECK_IF_FREEZABLE));
|
||||
}
|
||||
|
||||
if (CHECK_IF_MUTABLE || CHECK_IF_SOCIALS) {
|
||||
this.filters.push(new MutableFilter(connection, getMetadataAccountDataSerializer(), CHECK_IF_MUTABLE, CHECK_IF_SOCIALS));
|
||||
}
|
||||
|
||||
if (!args.minPoolSize.isZero() || !args.maxPoolSize.isZero()) {
|
||||
this.filters.push(new PoolSizeFilter(connection, args.quoteToken, args.minPoolSize, args.maxPoolSize));
|
||||
}
|
||||
|
||||
@ -5,10 +5,23 @@ import { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk';
|
||||
import { logger } from '../helpers';
|
||||
|
||||
export class RenouncedFreezeFilter implements Filter {
|
||||
constructor(private readonly connection: Connection, private readonly checkRenounced: boolean, private readonly checkFreezable: boolean) {}
|
||||
private readonly errorMessage: string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly connection: Connection,
|
||||
private readonly checkRenounced: boolean,
|
||||
private readonly checkFreezable: boolean,
|
||||
) {
|
||||
if (this.checkRenounced) {
|
||||
this.errorMessage.push('mint');
|
||||
}
|
||||
|
||||
if (this.checkFreezable) {
|
||||
this.errorMessage.push('freeze');
|
||||
}
|
||||
}
|
||||
|
||||
async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
|
||||
const errorMessage = [ this.checkRenounced ? 'mint' : undefined, this.checkFreezable ? 'freeze' : undefined ].filter((e) => e !== undefined);
|
||||
try {
|
||||
const accountInfo = await this.connection.getAccountInfo(poolKeys.baseMint, this.connection.commitment);
|
||||
if (!accountInfo?.data) {
|
||||
@ -18,15 +31,28 @@ export class RenouncedFreezeFilter implements Filter {
|
||||
const deserialize = MintLayout.decode(accountInfo.data);
|
||||
const renounced = !this.checkRenounced || deserialize.mintAuthorityOption === 0;
|
||||
const freezable = !this.checkFreezable || deserialize.freezeAuthorityOption !== 0;
|
||||
|
||||
const message = [ renounced ? undefined : 'mint', !freezable ? undefined : 'freeze' ].filter((e) => e !== undefined);
|
||||
const ok = renounced && !freezable;
|
||||
const message: string[] = [];
|
||||
|
||||
if (!renounced) {
|
||||
message.push('mint');
|
||||
}
|
||||
|
||||
if (freezable) {
|
||||
message.push('freeze');
|
||||
}
|
||||
|
||||
return { ok: ok, message: ok ? undefined : `RenouncedFreeze -> Creator can ${message.join(' and ')} tokens` };
|
||||
} catch (e) {
|
||||
logger.error({ mint: poolKeys.baseMint }, `RenouncedFreeze -> Failed to check if creator can ${errorMessage.join(' and ')} tokens`);
|
||||
logger.error(
|
||||
{ mint: poolKeys.baseMint },
|
||||
`RenouncedFreeze -> Failed to check if creator can ${this.errorMessage.join(' and ')} tokens`,
|
||||
);
|
||||
}
|
||||
|
||||
return { ok: false, message: `RenouncedFreeze -> Failed to check if creator can ${errorMessage.join(' and ')} tokens` };
|
||||
return {
|
||||
ok: false,
|
||||
message: `RenouncedFreeze -> Failed to check if creator can ${this.errorMessage.join(' and ')} tokens`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ export const COMPUTE_UNIT_PRICE = Number(retrieveEnvVariable('COMPUTE_UNIT_PRICE
|
||||
export const PRE_LOAD_EXISTING_MARKETS = retrieveEnvVariable('PRE_LOAD_EXISTING_MARKETS', logger) === 'true';
|
||||
export const CACHE_NEW_MARKETS = retrieveEnvVariable('CACHE_NEW_MARKETS', logger) === 'true';
|
||||
export const TRANSACTION_EXECUTOR = retrieveEnvVariable('TRANSACTION_EXECUTOR', logger);
|
||||
export const WARP_FEE = retrieveEnvVariable('WARP_FEE', logger);
|
||||
export const CUSTOM_FEE = retrieveEnvVariable('CUSTOM_FEE', logger);
|
||||
|
||||
// Buy
|
||||
export const AUTO_BUY_DELAY = Number(retrieveEnvVariable('AUTO_BUY_DELAY', logger));
|
||||
@ -54,6 +54,8 @@ export const SELL_SLIPPAGE = Number(retrieveEnvVariable('SELL_SLIPPAGE', logger)
|
||||
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_MUTABLE = retrieveEnvVariable('CHECK_IF_MUTABLE', logger) === 'true';
|
||||
export const CHECK_IF_SOCIALS = retrieveEnvVariable('CHECK_IF_SOCIALS', logger) === 'true';
|
||||
export const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNCED', logger) === 'true';
|
||||
export const CHECK_IF_FREEZABLE = retrieveEnvVariable('CHECK_IF_FREEZABLE', logger) === 'true';
|
||||
export const CHECK_IF_BURNED = retrieveEnvVariable('CHECK_IF_BURNED', logger) === 'true';
|
||||
|
||||
18
index.ts
18
index.ts
@ -14,6 +14,7 @@ import {
|
||||
RPC_WEBSOCKET_ENDPOINT,
|
||||
PRE_LOAD_EXISTING_MARKETS,
|
||||
LOG_LEVEL,
|
||||
CHECK_IF_MUTABLE,
|
||||
CHECK_IF_MINT_IS_RENOUNCED,
|
||||
CHECK_IF_FREEZABLE,
|
||||
CHECK_IF_BURNED,
|
||||
@ -40,13 +41,14 @@ import {
|
||||
PRICE_CHECK_INTERVAL,
|
||||
SNIPE_LIST_REFRESH_INTERVAL,
|
||||
TRANSACTION_EXECUTOR,
|
||||
WARP_FEE,
|
||||
CUSTOM_FEE,
|
||||
FILTER_CHECK_INTERVAL,
|
||||
FILTER_CHECK_DURATION,
|
||||
CONSECUTIVE_FILTER_MATCHES,
|
||||
} from './helpers';
|
||||
import { version } from './package.json';
|
||||
import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
|
||||
import { JitoTransactionExecutor } from './transactions/jito-rpc-transaction-executor';
|
||||
|
||||
const connection = new Connection(RPC_ENDPOINT, {
|
||||
wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
|
||||
@ -79,9 +81,11 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
|
||||
|
||||
logger.info('- Bot -');
|
||||
|
||||
logger.info(`Using warp: ${bot.isWarp}`);
|
||||
if (bot.isWarp) {
|
||||
logger.info(`Warp fee: ${WARP_FEE}`);
|
||||
logger.info(
|
||||
`Using ${TRANSACTION_EXECUTOR} executer: ${bot.isWarp || bot.isJito || (TRANSACTION_EXECUTOR === 'default' ? true : false)}`,
|
||||
);
|
||||
if (bot.isWarp || bot.isJito) {
|
||||
logger.info(`${TRANSACTION_EXECUTOR} fee: ${CUSTOM_FEE}`);
|
||||
} else {
|
||||
logger.info(`Compute Unit limit: ${botConfig.unitLimit}`);
|
||||
logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`);
|
||||
@ -143,7 +147,11 @@ const runListener = async () => {
|
||||
|
||||
switch (TRANSACTION_EXECUTOR) {
|
||||
case 'warp': {
|
||||
txExecutor = new WarpTransactionExecutor(WARP_FEE);
|
||||
txExecutor = new WarpTransactionExecutor(CUSTOM_FEE);
|
||||
break;
|
||||
}
|
||||
case 'jito': {
|
||||
txExecutor = new JitoTransactionExecutor(CUSTOM_FEE, connection);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
83
package-lock.json
generated
83
package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "warp-solana-bot",
|
||||
"version": "2.0.1",
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/mpl-token-metadata": "^3.2.1",
|
||||
"@raydium-io/raydium-sdk": "^1.3.1-beta.47",
|
||||
"@solana/spl-token": "^0.4.0",
|
||||
"@solana/web3.js": "^1.89.1",
|
||||
@ -75,6 +76,88 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@metaplex-foundation/mpl-token-metadata": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-3.2.1.tgz",
|
||||
"integrity": "sha512-26W1NhQwDWmLOg/pBRYut7x/vEs/5kFS2sWVEY5/X0f2jJOLhnd4NaZQcq+5u+XZsXvm1jq2AtrRGPNK43oqWQ==",
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/mpl-toolbox": "^0.9.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@metaplex-foundation/umi": ">= 0.8.2 < 1"
|
||||
}
|
||||
},
|
||||
"node_modules/@metaplex-foundation/mpl-toolbox": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-toolbox/-/mpl-toolbox-0.9.4.tgz",
|
||||
"integrity": "sha512-fd6JxfoLbj/MM8FG2x91KYVy1U6AjBQw4qjt7+Da3trzQaWnSaYHDcYRG/53xqfvZ9qofY1T2t53GXPlD87lnQ==",
|
||||
"peerDependencies": {
|
||||
"@metaplex-foundation/umi": ">= 0.8.2 < 1"
|
||||
}
|
||||
},
|
||||
"node_modules/@metaplex-foundation/umi": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi/-/umi-0.9.1.tgz",
|
||||
"integrity": "sha512-IhHoOvp4vfO/++YL+78+iVuLM53+FDwUOZDYgH6lx0jYXyQ27BeaieeR5i+q3A9dz4KxQo5Nzc5aCA1109QGCQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/umi-options": "^0.8.9",
|
||||
"@metaplex-foundation/umi-public-keys": "^0.8.9",
|
||||
"@metaplex-foundation/umi-serializers": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@metaplex-foundation/umi-options": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-options/-/umi-options-0.8.9.tgz",
|
||||
"integrity": "sha512-jSQ61sZMPSAk/TXn8v8fPqtz3x8d0/blVZXLLbpVbo2/T5XobiI6/MfmlUosAjAUaQl6bHRF8aIIqZEFkJiy4A==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@metaplex-foundation/umi-public-keys": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-public-keys/-/umi-public-keys-0.8.9.tgz",
|
||||
"integrity": "sha512-CxMzN7dgVGOq9OcNCJe2casKUpJ3RmTVoOvDFyeoTQuK+vkZ1YSSahbqC1iGuHEtKTLSjtWjKvUU6O7zWFTw3Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/umi-serializers-encodings": "^0.8.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@metaplex-foundation/umi-serializers": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers/-/umi-serializers-0.9.0.tgz",
|
||||
"integrity": "sha512-hAOW9Djl4w4ioKeR4erDZl5IG4iJdP0xA19ZomdaCbMhYAAmG/FEs5khh0uT2mq53/MnzWcXSUPoO8WBN4Q+Vg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/umi-options": "^0.8.9",
|
||||
"@metaplex-foundation/umi-public-keys": "^0.8.9",
|
||||
"@metaplex-foundation/umi-serializers-core": "^0.8.9",
|
||||
"@metaplex-foundation/umi-serializers-encodings": "^0.8.9",
|
||||
"@metaplex-foundation/umi-serializers-numbers": "^0.8.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@metaplex-foundation/umi-serializers-core": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-core/-/umi-serializers-core-0.8.9.tgz",
|
||||
"integrity": "sha512-WT82tkiYJ0Qmscp7uTj1Hz6aWQPETwaKLAENAUN5DeWghkuBKtuxyBKVvEOuoXerJSdhiAk0e8DWA4cxcTTQ/w==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@metaplex-foundation/umi-serializers-encodings": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-encodings/-/umi-serializers-encodings-0.8.9.tgz",
|
||||
"integrity": "sha512-N3VWLDTJ0bzzMKcJDL08U3FaqRmwlN79FyE4BHj6bbAaJ9LEHjDQ9RJijZyWqTm0jE7I750fU7Ow5EZL38Xi6Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/umi-serializers-core": "^0.8.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@metaplex-foundation/umi-serializers-numbers": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-numbers/-/umi-serializers-numbers-0.8.9.tgz",
|
||||
"integrity": "sha512-NtBf1fnVNQJHFQjLFzRu2i9GGnigb9hOm/Gfrk628d0q0tRJB7BOM3bs5C61VAs7kJs4yd+pDNVAERJkknQ7Lg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/umi-serializers-core": "^0.8.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
"tsc": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/mpl-token-metadata": "^3.2.1",
|
||||
"@raydium-io/raydium-sdk": "^1.3.1-beta.47",
|
||||
"@solana/spl-token": "^0.4.0",
|
||||
"@solana/web3.js": "^1.89.1",
|
||||
|
||||
131
transactions/jito-rpc-transaction-executor.ts
Normal file
131
transactions/jito-rpc-transaction-executor.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import {
|
||||
BlockhashWithExpiryBlockHeight,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Connection,
|
||||
TransactionMessage,
|
||||
VersionedTransaction,
|
||||
} from '@solana/web3.js';
|
||||
import { TransactionExecutor } from './transaction-executor.interface';
|
||||
import { logger } from '../helpers';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import bs58 from 'bs58';
|
||||
import { Currency, CurrencyAmount } from '@raydium-io/raydium-sdk';
|
||||
|
||||
export class JitoTransactionExecutor implements TransactionExecutor {
|
||||
// https://jito-labs.gitbook.io/mev/searcher-resources/json-rpc-api-reference/bundles/gettipaccounts
|
||||
private jitpTipAccounts = [
|
||||
'Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY',
|
||||
'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL',
|
||||
'96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5',
|
||||
'3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT',
|
||||
'HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe',
|
||||
'ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49',
|
||||
'ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt',
|
||||
'DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh',
|
||||
];
|
||||
|
||||
private JitoFeeWallet: PublicKey;
|
||||
|
||||
constructor(
|
||||
private readonly jitoFee: string,
|
||||
private readonly connection: Connection,
|
||||
) {
|
||||
this.JitoFeeWallet = this.getRandomValidatorKey();
|
||||
}
|
||||
|
||||
private getRandomValidatorKey(): PublicKey {
|
||||
const randomValidator = this.jitpTipAccounts[Math.floor(Math.random() * this.jitpTipAccounts.length)];
|
||||
return new PublicKey(randomValidator);
|
||||
}
|
||||
|
||||
public async executeAndConfirm(
|
||||
transaction: VersionedTransaction,
|
||||
payer: Keypair,
|
||||
latestBlockhash: BlockhashWithExpiryBlockHeight,
|
||||
): Promise<{ confirmed: boolean; signature?: string }> {
|
||||
logger.debug('Starting Jito transaction execution...');
|
||||
this.JitoFeeWallet = this.getRandomValidatorKey(); // Update wallet key each execution
|
||||
logger.trace(`Selected Jito fee wallet: ${this.JitoFeeWallet.toBase58()}`);
|
||||
|
||||
try {
|
||||
const fee = new CurrencyAmount(Currency.SOL, this.jitoFee, false).raw.toNumber();
|
||||
logger.trace(`Calculated fee: ${fee} lamports`);
|
||||
|
||||
const jitTipTxFeeMessage = new TransactionMessage({
|
||||
payerKey: payer.publicKey,
|
||||
recentBlockhash: latestBlockhash.blockhash,
|
||||
instructions: [
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: payer.publicKey,
|
||||
toPubkey: this.JitoFeeWallet,
|
||||
lamports: fee,
|
||||
}),
|
||||
],
|
||||
}).compileToV0Message();
|
||||
|
||||
const jitoFeeTx = new VersionedTransaction(jitTipTxFeeMessage);
|
||||
jitoFeeTx.sign([payer]);
|
||||
|
||||
const jitoTxsignature = bs58.encode(jitoFeeTx.signatures[0]);
|
||||
|
||||
// Serialize the transactions once here
|
||||
const serializedjitoFeeTx = bs58.encode(jitoFeeTx.serialize());
|
||||
const serializedTransaction = bs58.encode(transaction.serialize());
|
||||
const serializedTransactions = [serializedjitoFeeTx, serializedTransaction];
|
||||
|
||||
// https://jito-labs.gitbook.io/mev/searcher-resources/json-rpc-api-reference/url
|
||||
const endpoints = [
|
||||
'https://mainnet.block-engine.jito.wtf/api/v1/bundles',
|
||||
'https://amsterdam.mainnet.block-engine.jito.wtf/api/v1/bundles',
|
||||
'https://frankfurt.mainnet.block-engine.jito.wtf/api/v1/bundles',
|
||||
'https://ny.mainnet.block-engine.jito.wtf/api/v1/bundles',
|
||||
'https://tokyo.mainnet.block-engine.jito.wtf/api/v1/bundles',
|
||||
];
|
||||
|
||||
const requests = endpoints.map((url) =>
|
||||
axios.post(url, {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'sendBundle',
|
||||
params: [serializedTransactions],
|
||||
}),
|
||||
);
|
||||
|
||||
logger.trace('Sending transactions to endpoints...');
|
||||
const results = await Promise.all(requests.map((p) => p.catch((e) => e)));
|
||||
|
||||
const successfulResults = results.filter((result) => !(result instanceof Error));
|
||||
|
||||
if (successfulResults.length > 0) {
|
||||
logger.trace(`At least one successful response`);
|
||||
logger.debug(`Confirming jito transaction...`);
|
||||
return await this.confirm(jitoTxsignature, latestBlockhash);
|
||||
} else {
|
||||
logger.debug(`No successful responses received for jito`);
|
||||
}
|
||||
|
||||
return { confirmed: false };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
logger.trace({ error: error.response?.data }, 'Failed to execute jito transaction');
|
||||
}
|
||||
logger.error('Error during transaction execution', error);
|
||||
return { confirmed: false };
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user