From a5d9fc1a889447ed73e1fcf0cd38b36e29690d21 Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sun, 21 Apr 2024 15:35:40 +0200 Subject: [PATCH 01/10] Add mutable filter Check if owner can change metadata --- .env.copy | 1 + README.md | 1 + filters/index.ts | 1 + filters/pool-filters.ts | 7 ++++++- helpers/constants.ts | 1 + index.ts | 1 + 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.env.copy b/.env.copy index 2a4bdfd..18de2a2 100644 --- a/.env.copy +++ b/.env.copy @@ -42,6 +42,7 @@ SNIPE_LIST_REFRESH_INTERVAL=30000 FILTER_CHECK_DURATION=60000 FILTER_CHECK_INTERVAL=2000 CONSECUTIVE_FILTER_MATCHES=3 +CHECK_IF_MUTABLE=true CHECK_IF_MINT_IS_RENOUNCED=true CHECK_IF_FREEZABLE=true CHECK_IF_BURNED=true diff --git a/README.md b/README.md index 799613b..7ecffbf 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ You should see the following output: - `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. +- `CHECK_IF_MUTABLE` - Set to `true` to buy tokens only if their metadata are not mutable. - `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. diff --git a/filters/index.ts b/filters/index.ts index d2ba963..697388b 100644 --- a/filters/index.ts +++ b/filters/index.ts @@ -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'; diff --git a/filters/pool-filters.ts b/filters/pool-filters.ts index fc7d457..350b158 100644 --- a/filters/pool-filters.ts +++ b/filters/pool-filters.ts @@ -1,9 +1,10 @@ import { Connection } from '@solana/web3.js'; import { LiquidityPoolKeysV4, Token, TokenAmount } from '@raydium-io/raydium-sdk'; 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, logger } from '../helpers'; export interface Filter { execute(poolKeysV4: LiquidityPoolKeysV4): Promise; @@ -35,6 +36,10 @@ export class PoolFilters { this.filters.push(new RenouncedFreezeFilter(connection, CHECK_IF_MINT_IS_RENOUNCED, CHECK_IF_FREEZABLE)); } + if (CHECK_IF_MUTABLE) { + this.filters.push(new MutableFilter(connection)); + } + if (!args.minPoolSize.isZero() || !args.maxPoolSize.isZero()) { this.filters.push(new PoolSizeFilter(connection, args.quoteToken, args.minPoolSize, args.maxPoolSize)); } diff --git a/helpers/constants.ts b/helpers/constants.ts index 2e8da8f..1ad8e76 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -54,6 +54,7 @@ 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_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'; diff --git a/index.ts b/index.ts index 39acf85..4d31fec 100644 --- a/index.ts +++ b/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, From a34a60978679a977e466070e704231260b491242 Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sun, 21 Apr 2024 15:37:30 +0200 Subject: [PATCH 02/10] add mutable.filter.ts file --- filters/mutable.filter.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 filters/mutable.filter.ts diff --git a/filters/mutable.filter.ts b/filters/mutable.filter.ts new file mode 100644 index 0000000..6a6b9b3 --- /dev/null +++ b/filters/mutable.filter.ts @@ -0,0 +1,28 @@ +import { Filter, FilterResult } from './pool-filters'; +import { Connection, PublicKey } from '@solana/web3.js'; +import { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk'; +import { getPdaMetadataKey } from '@raydium-io/raydium-sdk'; +import { getMetadataAccountDataSerializer, MetadataAccountData } from '@metaplex-foundation/mpl-token-metadata'; +import { logger } from '../helpers'; + +export class MutableFilter implements Filter { + constructor(private readonly connection: Connection) {} + + async execute(poolKeys: LiquidityPoolKeysV4): Promise { + try { + const metadataPDA = getPdaMetadataKey(poolKeys.baseMint); + const metadataAccount = await this.connection.getAccountInfo(new PublicKey(metadataPDA.publicKey.toString())); + if (!metadataAccount?.data) { + return { ok: false, message: 'Mutable -> Failed to fetch account data' }; + } + const deserialize = getMetadataAccountDataSerializer().deserialize(metadataAccount.data); + const mutable = deserialize[0].isMutable; + + return { ok: !mutable, message: !mutable ? undefined : "Mutable -> Creator can change metadata" }; + } catch (e: any) { + logger.error({ mint: poolKeys.baseMint }, `Mutable -> Failed to check if metadata are mutable`); + } + + return { ok: false, message: 'Mutable -> Failed to check if metadata are mutable' }; + } +} From c7a7c9fb0e80cbd44c8509a860c3352020337d09 Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sun, 21 Apr 2024 15:52:23 +0200 Subject: [PATCH 03/10] pass metadataPDA.publicKey directly --- filters/mutable.filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filters/mutable.filter.ts b/filters/mutable.filter.ts index 6a6b9b3..4956726 100644 --- a/filters/mutable.filter.ts +++ b/filters/mutable.filter.ts @@ -11,7 +11,7 @@ export class MutableFilter implements Filter { async execute(poolKeys: LiquidityPoolKeysV4): Promise { try { const metadataPDA = getPdaMetadataKey(poolKeys.baseMint); - const metadataAccount = await this.connection.getAccountInfo(new PublicKey(metadataPDA.publicKey.toString())); + const metadataAccount = await this.connection.getAccountInfo(metadataPDA.publicKey); if (!metadataAccount?.data) { return { ok: false, message: 'Mutable -> Failed to fetch account data' }; } From 3c850c4c1ab24a5b6b1e1af17669c8749bfc7b38 Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sun, 21 Apr 2024 15:58:58 +0200 Subject: [PATCH 04/10] move metadataSerializer into constructor --- filters/mutable.filter.ts | 7 ++++--- filters/pool-filters.ts | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/filters/mutable.filter.ts b/filters/mutable.filter.ts index 4956726..a12c74e 100644 --- a/filters/mutable.filter.ts +++ b/filters/mutable.filter.ts @@ -2,11 +2,12 @@ import { Filter, FilterResult } from './pool-filters'; import { Connection, PublicKey } from '@solana/web3.js'; import { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk'; import { getPdaMetadataKey } from '@raydium-io/raydium-sdk'; -import { getMetadataAccountDataSerializer, MetadataAccountData } from '@metaplex-foundation/mpl-token-metadata'; +import { getMetadataAccountDataSerializer, MetadataAccountData, MetadataAccountDataArgs } from '@metaplex-foundation/mpl-token-metadata'; +import { Serializer } from '@metaplex-foundation/umi/serializers'; import { logger } from '../helpers'; export class MutableFilter implements Filter { - constructor(private readonly connection: Connection) {} + constructor(private readonly connection: Connection, private readonly metadataSerializer: Serializer) {} async execute(poolKeys: LiquidityPoolKeysV4): Promise { try { @@ -15,7 +16,7 @@ export class MutableFilter implements Filter { if (!metadataAccount?.data) { return { ok: false, message: 'Mutable -> Failed to fetch account data' }; } - const deserialize = getMetadataAccountDataSerializer().deserialize(metadataAccount.data); + const deserialize = this.metadataSerializer.deserialize(metadataAccount.data); const mutable = deserialize[0].isMutable; return { ok: !mutable, message: !mutable ? undefined : "Mutable -> Creator can change metadata" }; diff --git a/filters/pool-filters.ts b/filters/pool-filters.ts index 350b158..350a4d9 100644 --- a/filters/pool-filters.ts +++ b/filters/pool-filters.ts @@ -1,5 +1,6 @@ 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'; @@ -37,7 +38,7 @@ export class PoolFilters { } if (CHECK_IF_MUTABLE) { - this.filters.push(new MutableFilter(connection)); + this.filters.push(new MutableFilter(connection, getMetadataAccountDataSerializer())); } if (!args.minPoolSize.isZero() || !args.maxPoolSize.isZero()) { From 19913901218d3cad39a92baa79e1be72a85ede52 Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sun, 21 Apr 2024 15:59:39 +0200 Subject: [PATCH 05/10] add @metaplex-foundation/mpl-token-metadata to package.json --- package-lock.json | 83 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 84 insertions(+) diff --git a/package-lock.json b/package-lock.json index dc75a61..f00d3f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "warp-solana-bot", "version": "2.0.0", "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", diff --git a/package.json b/package.json index a783d18..64668d5 100644 --- a/package.json +++ b/package.json @@ -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", From 19e012c8e1410bf4455d444ad4b1347bc15fc05e Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sun, 21 Apr 2024 16:28:33 +0200 Subject: [PATCH 06/10] set freezable and mutable filter to false by default --- .env.copy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.copy b/.env.copy index 18de2a2..3e24846 100644 --- a/.env.copy +++ b/.env.copy @@ -42,9 +42,9 @@ SNIPE_LIST_REFRESH_INTERVAL=30000 FILTER_CHECK_DURATION=60000 FILTER_CHECK_INTERVAL=2000 CONSECUTIVE_FILTER_MATCHES=3 -CHECK_IF_MUTABLE=true +CHECK_IF_MUTABLE=false 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 From 39ef59f5a6f80bd8225e4f580d23f327f4ccddd4 Mon Sep 17 00:00:00 2001 From: engmabdulwahab <49817513+mcoder9@users.noreply.github.com> Date: Mon, 22 Apr 2024 07:27:59 +0400 Subject: [PATCH 07/10] Added Jito-RPC Executer --- .env.copy | 8 +- README.md | 34 +++-- bot.ts | 5 +- helpers/constants.ts | 2 +- index.ts | 12 +- transactions/jito-rpc-transaction-executor.ts | 131 ++++++++++++++++++ 6 files changed, 174 insertions(+), 18 deletions(-) create mode 100644 transactions/jito-rpc-transaction-executor.ts diff --git a/.env.copy b/.env.copy index 2a4bdfd..5f4cdda 100644 --- a/.env.copy +++ b/.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 diff --git a/README.md b/README.md index 9f9957d..1755978 100644 --- a/README.md +++ b/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. @@ -93,12 +102,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 +118,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 +144,11 @@ To collect more information on an issue, please change `LOG_LEVEL` to `debug`. ![wsol](readme/wsol.png) ## Contact + [![](https://img.shields.io/discord/1201826085655023616?color=5865F2&logo=Discord&style=flat-square)](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 +156,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. \ No newline at end of file +The use of this bot is at your own risk, and we are not responsible for any losses incurred while using the bot. diff --git a/bot.ts b/bot.ts index 449bd18..835aca8 100644 --- a/bot.ts +++ b/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 }), diff --git a/helpers/constants.ts b/helpers/constants.ts index 2e8da8f..c8c8ea4 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -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)); diff --git a/index.ts b/index.ts index 2c7ae33..83e7655 100644 --- a/index.ts +++ b/index.ts @@ -40,13 +40,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, @@ -80,8 +81,9 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) { logger.info('- Bot -'); logger.info(`Using warp: ${bot.isWarp}`); + logger.info(`Using jito: ${bot.isJito}`); if (bot.isWarp) { - logger.info(`Warp fee: ${WARP_FEE}`); + 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 +145,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: { diff --git a/transactions/jito-rpc-transaction-executor.ts b/transactions/jito-rpc-transaction-executor.ts new file mode 100644 index 0000000..42d0d71 --- /dev/null +++ b/transactions/jito-rpc-transaction-executor.ts @@ -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 warp 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 }; + } +} From 35cc982e8ac14607d3b8e65541d6bd7ab51d261d Mon Sep 17 00:00:00 2001 From: engmabdulwahab <49817513+mcoder9@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:09:15 +0400 Subject: [PATCH 08/10] fixed some typos --- index.ts | 5 +++-- transactions/jito-rpc-transaction-executor.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index 83e7655..53d78d4 100644 --- a/index.ts +++ b/index.ts @@ -80,8 +80,9 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) { logger.info('- Bot -'); - logger.info(`Using warp: ${bot.isWarp}`); - logger.info(`Using jito: ${bot.isJito}`); + logger.info( + `Using ${TRANSACTION_EXECUTOR} executer: ${bot.isWarp || bot.isJito || (TRANSACTION_EXECUTOR === 'default' ? true : false)}`, + ); if (bot.isWarp) { logger.info(`${TRANSACTION_EXECUTOR} fee: ${CUSTOM_FEE}`); } else { diff --git a/transactions/jito-rpc-transaction-executor.ts b/transactions/jito-rpc-transaction-executor.ts index 42d0d71..bcb8da1 100644 --- a/transactions/jito-rpc-transaction-executor.ts +++ b/transactions/jito-rpc-transaction-executor.ts @@ -109,7 +109,7 @@ export class JitoTransactionExecutor implements TransactionExecutor { return { confirmed: false }; } catch (error) { if (error instanceof AxiosError) { - logger.trace({ error: error.response?.data }, 'Failed to execute warp transaction'); + logger.trace({ error: error.response?.data }, 'Failed to execute jito transaction'); } logger.error('Error during transaction execution', error); return { confirmed: false }; From 1abcaac1edc6fcbb3fbf50882b36b5f6212aec7e Mon Sep 17 00:00:00 2001 From: engmabdulwahab <49817513+mcoder9@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:17:03 +0400 Subject: [PATCH 09/10] custom fee checking if using jito --- index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 53d78d4..7b6620c 100644 --- a/index.ts +++ b/index.ts @@ -83,7 +83,7 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) { logger.info( `Using ${TRANSACTION_EXECUTOR} executer: ${bot.isWarp || bot.isJito || (TRANSACTION_EXECUTOR === 'default' ? true : false)}`, ); - if (bot.isWarp) { + if (bot.isWarp || bot.isJito) { logger.info(`${TRANSACTION_EXECUTOR} fee: ${CUSTOM_FEE}`); } else { logger.info(`Compute Unit limit: ${botConfig.unitLimit}`); From b1bdcd9365133ebabd7857183dc379d0d8f47fb2 Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sat, 27 Apr 2024 10:46:10 +0200 Subject: [PATCH 10/10] Add social filter --- .env.copy | 1 + README.md | 1 + filters/mutable.filter.ts | 30 ++++++++++++++++++++++-------- filters/pool-filters.ts | 6 +++--- helpers/constants.ts | 1 + 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.env.copy b/.env.copy index de70fb7..07681b9 100644 --- a/.env.copy +++ b/.env.copy @@ -43,6 +43,7 @@ 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=false CHECK_IF_BURNED=true diff --git a/README.md b/README.md index 4e6658f..92f00ae 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Note: When using snipe list filters below will be disabled. - `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. diff --git a/filters/mutable.filter.ts b/filters/mutable.filter.ts index a12c74e..52f370f 100644 --- a/filters/mutable.filter.ts +++ b/filters/mutable.filter.ts @@ -1,15 +1,16 @@ import { Filter, FilterResult } from './pool-filters'; -import { Connection, PublicKey } from '@solana/web3.js'; +import { Connection } from '@solana/web3.js'; import { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk'; import { getPdaMetadataKey } from '@raydium-io/raydium-sdk'; -import { getMetadataAccountDataSerializer, MetadataAccountData, MetadataAccountDataArgs } from '@metaplex-foundation/mpl-token-metadata'; +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 { - constructor(private readonly connection: Connection, private readonly metadataSerializer: Serializer) {} + constructor(private readonly connection: Connection, private readonly metadataSerializer: Serializer, private readonly checkMutable: boolean, private readonly checkSocials: boolean) {} async execute(poolKeys: LiquidityPoolKeysV4): Promise { + const errorMessage = [ this.checkMutable ? 'mutable' : undefined, this.checkSocials ? 'socials' : undefined ].filter((e) => e !== undefined); try { const metadataPDA = getPdaMetadataKey(poolKeys.baseMint); const metadataAccount = await this.connection.getAccountInfo(metadataPDA.publicKey); @@ -17,13 +18,26 @@ export class MutableFilter implements Filter { return { ok: false, message: 'Mutable -> Failed to fetch account data' }; } const deserialize = this.metadataSerializer.deserialize(metadataAccount.data); - const mutable = deserialize[0].isMutable; + const mutable = this.checkMutable ? deserialize[0].isMutable: false; - return { ok: !mutable, message: !mutable ? undefined : "Mutable -> Creator can change metadata" }; - } catch (e: any) { - logger.error({ mint: poolKeys.baseMint }, `Mutable -> Failed to check if metadata are mutable`); + const hasSocials = this.checkSocials ? (Object.values(await this.getSocials(deserialize[0])).some((value: any) => value !== null && value.length > 0)) === true: true; + + const message = [ !mutable ? undefined : 'metadata can be changed', hasSocials ? undefined : 'has no socials' ].filter((e) => e !== undefined); + const ok = !mutable && hasSocials; + + return { ok: ok, message: ok ? undefined : `MutableSocials -> Token ${message.join(' and ')}` }; + } catch (e) { + logger.error({ mint: poolKeys.baseMint, error: e }, `MutableSocials -> Failed to check ${errorMessage.join(' and ')}`); + return { ok: false, message: `MutableSocials -> Failed to check ${errorMessage.join(' and ')}` }; } - return { ok: false, message: 'Mutable -> Failed to check if metadata are mutable' }; + logger.error({ mint: poolKeys.baseMint }, `MutableSocials -> Failed to check ${errorMessage.join(' and ')}`); + return { ok: false, message: `MutableSocials -> Failed to check ${errorMessage.join(' and ')}` }; + } + + async getSocials(metadata: MetadataAccountData): Promise { + const response = await fetch(metadata.uri); + const data = await response.json(); + return data?.extensions; } } diff --git a/filters/pool-filters.ts b/filters/pool-filters.ts index 350a4d9..f74d925 100644 --- a/filters/pool-filters.ts +++ b/filters/pool-filters.ts @@ -5,7 +5,7 @@ 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, CHECK_IF_MUTABLE, 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; @@ -37,8 +37,8 @@ export class PoolFilters { this.filters.push(new RenouncedFreezeFilter(connection, CHECK_IF_MINT_IS_RENOUNCED, CHECK_IF_FREEZABLE)); } - if (CHECK_IF_MUTABLE) { - this.filters.push(new MutableFilter(connection, getMetadataAccountDataSerializer())); + 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()) { diff --git a/helpers/constants.ts b/helpers/constants.ts index ae50b61..96ca547 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -55,6 +55,7 @@ export const FILTER_CHECK_INTERVAL = Number(retrieveEnvVariable('FILTER_CHECK_IN 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';