Bläddra i källkod

Merge pull request #84 from fdundjer/feat/v2

feat: warp transactions
Filip Dunđer 1 år sedan
förälder
incheckning
0925fd8032

+ 8 - 3
.env.copy

@@ -9,10 +9,15 @@ COMMITMENT_LEVEL=confirmed
 # Bot
 LOG_LEVEL=debug
 ONE_TOKEN_AT_A_TIME=true
-COMPUTE_UNIT_LIMIT=421197
-COMPUTE_UNIT_PRICE=101337
 PRE_LOAD_EXISTING_MARKETS=false
 CACHE_NEW_MARKETS=false
+# default or warp
+TRANSACTION_EXECUTOR=default
+# if using default executor fee below will be applied
+COMPUTE_UNIT_LIMIT=421197
+COMPUTE_UNIT_PRICE=101337
+# if using warp executor fee below will be applied
+WARP_FEE=0.006
 
 # Buy
 QUOTE_MINT=WSOL
@@ -27,7 +32,7 @@ MAX_SELL_RETRIES=10
 AUTO_SELL_DELAY=0
 PRICE_CHECK_INTERVAL=2000
 PRICE_CHECK_DURATION=60000
-TAKE_PROFIT=25
+TAKE_PROFIT=20
 STOP_LOSS=15
 SELL_SLIPPAGE=5
 

+ 26 - 3
README.md

@@ -40,6 +40,11 @@ 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
+  - 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 
+  - On top of this fee, minimal solana network fee will be applied
 
 #### Buy
 - `QUOTE_MINT` - Amount used to buy each new token.
@@ -66,12 +71,30 @@ You should see the following output:
   - Pool must not exist before the script starts.
 - `SNIPE_LIST_REFRESH_INTERVAL` - Interval in milliseconds to refresh the snipe list.
 - `CHECK_IF_MINT_IS_RENOUNCED` - Set to `true` to buy tokens only if their mint is renounced.
-- `CHECK_IF_BURNED` - Set to `true` to buy tokens only if their liqudity pool is burned.
-- `MIN_POOL_SIZE` - Bot will buy only if the pool size is greater than the specified amount.
+- `CHECK_IF_BURNED` - Set to `true` to buy tokens only if their liquidity pool is burned.
+- `MIN_POOL_SIZE` - Bot will buy only if the pool size is greater than or equal the specified amount.
   - Set `0` to disable.
-- `MAX_POOL_SIZE` - Bot will buy only if the pool size is less than the specified amount.
+- `MAX_POOL_SIZE` - Bot will buy only if the pool size is less than or equal the specified amount.
   - 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.
+**We don't store your transactions, nor we store your private key.**
+
+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`.

+ 20 - 16
bot.ts

@@ -14,20 +14,14 @@ import {
   RawAccount,
   TOKEN_PROGRAM_ID,
 } from '@solana/spl-token';
-import {
-  Liquidity,
-  LiquidityPoolKeysV4,
-  LiquidityStateV4,
-  Percent,
-  Token,
-  TokenAmount,
-} from '@raydium-io/raydium-sdk';
+import { Liquidity, LiquidityPoolKeysV4, LiquidityStateV4, Percent, Token, TokenAmount } from '@raydium-io/raydium-sdk';
 import { MarketCache, PoolCache, SnipeListCache } from './cache';
 import { PoolFilters } from './filters';
 import { TransactionExecutor } from './transactions';
 import { createPoolKeys, logger, NETWORK, sleep } from './helpers';
 import { Mutex } from 'async-mutex';
 import BN from 'bn.js';
+import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
 
 export interface BotConfig {
   wallet: Keypair;
@@ -64,14 +58,17 @@ export class Bot {
   // one token at the time
   private readonly mutex: Mutex;
   private sellExecutionCount = 0;
+  public readonly isWarp: boolean = false;
 
   constructor(
     private readonly connection: Connection,
     private readonly marketStorage: MarketCache,
     private readonly poolStorage: PoolCache,
     private readonly txExecutor: TransactionExecutor,
-    private readonly config: BotConfig,
+    readonly config: BotConfig,
   ) {
+    this.isWarp = txExecutor instanceof WarpTransactionExecutor;
+
     this.mutex = new Mutex();
     this.poolFilters = new PoolFilters(connection, {
       quoteToken: this.config.quoteToken,
@@ -318,8 +315,12 @@ export class Bot {
       payerKey: wallet.publicKey,
       recentBlockhash: latestBlockhash.blockhash,
       instructions: [
-        ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }),
-        ComputeBudgetProgram.setComputeUnitLimit({ units: this.config.unitLimit }),
+        ...(this.isWarp
+          ? []
+          : [
+              ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }),
+              ComputeBudgetProgram.setComputeUnitLimit({ units: this.config.unitLimit }),
+            ]),
         ...(direction === 'buy'
           ? [
               createAssociatedTokenAccountIdempotentInstruction(
@@ -338,10 +339,15 @@ export class Bot {
     const transaction = new VersionedTransaction(messageV0);
     transaction.sign([wallet, ...innerTransaction.signers]);
 
-    return this.txExecutor.executeAndConfirm(transaction, latestBlockhash);
+    return this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash);
   }
 
   private async priceMatch(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
+    if (this.config.priceCheckDuration === 0 || this.config.priceCheckInterval === 0) {
+      return;
+    }
+
+    const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
     const profitFraction = this.config.quoteAmount.mul(this.config.takeProfit).numerator.div(new BN(100));
     const profitAmount = new TokenAmount(this.config.quoteToken, profitFraction, true);
     const takeProfit = this.config.quoteAmount.add(profitAmount);
@@ -350,8 +356,6 @@ export class Bot {
     const lossAmount = new TokenAmount(this.config.quoteToken, lossFraction, true);
     const stopLoss = this.config.quoteAmount.subtract(lossAmount);
     const slippage = new Percent(this.config.sellSlippage, 100);
-
-    const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
     let timesChecked = 0;
 
     do {
@@ -374,11 +378,11 @@ export class Bot {
           `Take profit: ${takeProfit.toFixed()} | Stop loss: ${stopLoss.toFixed()} | Current: ${amountOut.toFixed()}`,
         );
 
-        if (amountOut.lt(stopLoss)){
+        if (amountOut.lt(stopLoss)) {
           break;
         }
 
-        if (amountOut.gt(takeProfit)){
+        if (amountOut.gt(takeProfit)) {
           break;
         }
 

+ 2 - 2
filters/pool-size.filter.ts

@@ -18,7 +18,7 @@ export class PoolSizeFilter implements Filter {
       let inRange = true;
 
       if (!this.maxPoolSize?.isZero()) {
-        inRange = poolSize.lt(this.maxPoolSize);
+        inRange = poolSize.raw.lte(this.maxPoolSize.raw);
 
         if (!inRange) {
           return { ok: false, message: `PoolSize -> Pool size ${poolSize.toFixed()} > ${this.maxPoolSize.toFixed()}` };
@@ -26,7 +26,7 @@ export class PoolSizeFilter implements Filter {
       }
 
       if (!this.minPoolSize?.isZero()) {
-        inRange = poolSize.gt(this.minPoolSize);
+        inRange = poolSize.raw.gte(this.minPoolSize.raw);
 
         if (!inRange) {
           return { ok: false, message: `PoolSize -> Pool size ${poolSize.toFixed()} < ${this.minPoolSize.toFixed()}` };

+ 2 - 0
helpers/constants.ts

@@ -30,6 +30,8 @@ export const COMPUTE_UNIT_LIMIT = Number(retrieveEnvVariable('COMPUTE_UNIT_LIMIT
 export const COMPUTE_UNIT_PRICE = Number(retrieveEnvVariable('COMPUTE_UNIT_PRICE', logger));
 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);
 
 // Buy
 export const AUTO_BUY_DELAY = Number(retrieveEnvVariable('AUTO_BUY_DELAY', logger));

+ 33 - 7
index.ts

@@ -4,7 +4,7 @@ import { Connection, KeyedAccountInfo, Keypair } from '@solana/web3.js';
 import { LIQUIDITY_STATE_LAYOUT_V4, MARKET_STATE_LAYOUT_V3, Token, TokenAmount } from '@raydium-io/raydium-sdk';
 import { AccountLayout, getAssociatedTokenAddressSync } from '@solana/spl-token';
 import { Bot, BotConfig } from './bot';
-import { DefaultTransactionExecutor } from './transactions';
+import { DefaultTransactionExecutor, TransactionExecutor } from './transactions';
 import {
   getToken,
   getWallet,
@@ -36,16 +36,20 @@ import {
   BUY_SLIPPAGE,
   SELL_SLIPPAGE,
   PRICE_CHECK_DURATION,
-  PRICE_CHECK_INTERVAL, SNIPE_LIST_REFRESH_INTERVAL,
+  PRICE_CHECK_INTERVAL,
+  SNIPE_LIST_REFRESH_INTERVAL,
+  TRANSACTION_EXECUTOR,
+  WARP_FEE,
 } from './helpers';
 import { version } from './package.json';
+import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
 
 const connection = new Connection(RPC_ENDPOINT, {
   wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
   commitment: COMMITMENT_LEVEL,
 });
 
-function printDetails(wallet: Keypair, quoteToken: Token, botConfig: BotConfig) {
+function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
   logger.info(`  
                                         ..   :-===++++-     
                                 .-==+++++++- =+++++++++-    
@@ -64,12 +68,22 @@ function printDetails(wallet: Keypair, quoteToken: Token, botConfig: BotConfig)
           Version: ${version}                                          
   `);
 
+  const botConfig = bot.config;
+
   logger.info('------- CONFIGURATION START -------');
   logger.info(`Wallet: ${wallet.publicKey.toString()}`);
 
   logger.info('- Bot -');
-  logger.info(`Compute Unit limit: ${botConfig.unitLimit}`);
-  logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`);
+
+  logger.info(`Using warp: ${bot.isWarp}`);
+  if (bot.isWarp) {
+    logger.info(`Warp fee: ${WARP_FEE}`);
+  }
+  else {
+    logger.info(`Compute Unit limit: ${botConfig.unitLimit}`);
+    logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`);
+  }
+
   logger.info(`Single token at the time: ${botConfig.oneTokenAtATime}`);
   logger.info(`Pre load existing markets: ${PRE_LOAD_EXISTING_MARKETS}`);
   logger.info(`Cache new markets: ${CACHE_NEW_MARKETS}`);
@@ -111,7 +125,19 @@ const runListener = async () => {
 
   const marketCache = new MarketCache(connection);
   const poolCache = new PoolCache();
-  const txExecutor = new DefaultTransactionExecutor(connection);
+  let txExecutor: TransactionExecutor;
+
+  switch (TRANSACTION_EXECUTOR) {
+    case 'warp': {
+      txExecutor = new WarpTransactionExecutor(WARP_FEE);
+      break;
+    }
+    default: {
+      txExecutor = new DefaultTransactionExecutor(connection);
+      break;
+    }
+  }
+
   const wallet = getWallet(PRIVATE_KEY.trim());
   const quoteToken = getToken(QUOTE_MINT);
   const botConfig = <BotConfig>{
@@ -186,7 +212,7 @@ const runListener = async () => {
     await bot.sell(updatedAccountInfo.accountId, accountData);
   });
 
-  printDetails(wallet, quoteToken, botConfig);
+  printDetails(wallet, quoteToken, bot);
 };
 
 runListener();

+ 8 - 5
package-lock.json

@@ -12,6 +12,7 @@
         "@solana/spl-token": "^0.4.0",
         "@solana/web3.js": "^1.89.1",
         "async-mutex": "^0.5.0",
+        "axios": "^1.6.8",
         "bigint-buffer": "^1.1.5",
         "bip39": "^3.1.0",
         "bn.js": "^5.2.1",
@@ -396,10 +397,11 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.6.7",
-      "license": "MIT",
+      "version": "1.6.8",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
+      "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
       "dependencies": {
-        "follow-redirects": "^1.15.4",
+        "follow-redirects": "^1.15.6",
         "form-data": "^4.0.0",
         "proxy-from-env": "^1.1.0"
       }
@@ -724,14 +726,15 @@
       "license": "MIT"
     },
     "node_modules/follow-redirects": {
-      "version": "1.15.5",
+      "version": "1.15.6",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+      "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
       "funding": [
         {
           "type": "individual",
           "url": "https://github.com/sponsors/RubenVerborgh"
         }
       ],
-      "license": "MIT",
       "engines": {
         "node": ">=4.0"
       },

+ 1 - 0
package.json

@@ -12,6 +12,7 @@
     "@solana/spl-token": "^0.4.0",
     "@solana/web3.js": "^1.89.1",
     "async-mutex": "^0.5.0",
+    "axios": "^1.6.8",
     "bigint-buffer": "^1.1.5",
     "bip39": "^3.1.0",
     "bn.js": "^5.2.1",

+ 10 - 3
transactions/default-transaction-executor.ts

@@ -1,4 +1,10 @@
-import { BlockhashWithExpiryBlockHeight, Connection, Transaction, VersionedTransaction } from '@solana/web3.js';
+import {
+  BlockhashWithExpiryBlockHeight,
+  Connection,
+  Keypair,
+  Transaction,
+  VersionedTransaction,
+} from '@solana/web3.js';
 import { TransactionExecutor } from './transaction-executor.interface';
 import { logger } from '../helpers';
 
@@ -6,9 +12,10 @@ export class DefaultTransactionExecutor implements TransactionExecutor {
   constructor(private readonly connection: Connection) {}
 
   public async executeAndConfirm(
-    transaction: Transaction | VersionedTransaction,
+    transaction: VersionedTransaction,
+    payer: Keypair,
     latestBlockhash: BlockhashWithExpiryBlockHeight,
-  ): Promise<{ confirmed: boolean; signature: string }> {
+  ): Promise<{ confirmed: boolean; signature?: string }> {
     logger.debug('Executing transaction...');
     const signature = await this.execute(transaction);
 

+ 5 - 4
transactions/transaction-executor.interface.ts

@@ -1,8 +1,9 @@
-import { BlockhashWithExpiryBlockHeight, Transaction, VersionedTransaction } from '@solana/web3.js';
+import { BlockhashWithExpiryBlockHeight, Keypair, MessageV0, Signer, VersionedTransaction } from '@solana/web3.js';
 
 export interface TransactionExecutor {
   executeAndConfirm(
-    transaction: Transaction | VersionedTransaction,
-    latestBlockhash: BlockhashWithExpiryBlockHeight,
-  ): Promise<{ confirmed: boolean; signature: string }>;
+    transaction: VersionedTransaction,
+    payer: Keypair,
+    latestBlockHash: BlockhashWithExpiryBlockHeight,
+  ): Promise<{ confirmed: boolean; signature?: string }>;
 }

+ 64 - 0
transactions/warp-transaction-executor.ts

@@ -0,0 +1,64 @@
+import {
+  BlockhashWithExpiryBlockHeight,
+  Keypair,
+  PublicKey,
+  SystemProgram,
+  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 WarpTransactionExecutor implements TransactionExecutor {
+  private readonly warpFeeWallet = new PublicKey('WARPzUMPnycu9eeCZ95rcAUxorqpBqHndfV3ZP5FSyS');
+
+  constructor(private readonly warpFee: string) {}
+
+  public async executeAndConfirm(
+    transaction: VersionedTransaction,
+    payer: Keypair,
+    latestBlockhash: BlockhashWithExpiryBlockHeight,
+  ): Promise<{ confirmed: boolean; signature?: string }> {
+    logger.debug('Executing transaction...');
+
+    try {
+      const fee = new CurrencyAmount(Currency.SOL, this.warpFee, false).raw.toNumber();
+      const warpFeeMessage = new TransactionMessage({
+        payerKey: payer.publicKey,
+        recentBlockhash: latestBlockhash.blockhash,
+        instructions: [
+          SystemProgram.transfer({
+            fromPubkey: payer.publicKey,
+            toPubkey: this.warpFeeWallet,
+            lamports: fee,
+          }),
+        ],
+      }).compileToV0Message();
+
+      const warpFeeTx = new VersionedTransaction(warpFeeMessage);
+      warpFeeTx.sign([payer]);
+
+      const response = await axios.post<{ confirmed: boolean; signature: string }>(
+        'https://tx.warp.id/transaction/execute',
+        {
+          transactions: [bs58.encode(warpFeeTx.serialize()), bs58.encode(transaction.serialize())],
+          latestBlockhash,
+        },
+        {
+          timeout: 100000,
+        },
+      );
+
+      return response.data;
+    } catch (error) {
+      if (error instanceof AxiosError) {
+        logger.trace({ error: error.response?.data }, 'Failed to execute warp transaction');
+      }
+    }
+
+    return { confirmed: false };
+  }
+}