浏览代码

Merge pull request #58 from fdundjer/fix/sell-buy-improvements

fix: remove delay and listen to wallet changes
Filip Dunđer 1 年之前
父节点
当前提交
d2a55599d3
共有 4 个文件被更改,包括 208 次插入146 次删除
  1. 3 3
      .env.copy
  2. 8 6
      README.md
  3. 194 135
      buy.ts
  4. 3 2
      utils/utils.ts

+ 3 - 3
.env.copy

@@ -2,10 +2,10 @@ PRIVATE_KEY=
 RPC_ENDPOINT=https://api.mainnet-beta.solana.com
 RPC_WEBSOCKET_ENDPOINT=wss://api.mainnet-beta.solana.com
 QUOTE_MINT=WSOL
-QUOTE_AMOUNT=0.1
+QUOTE_AMOUNT=0.01
 COMMITMENT_LEVEL=finalized
 USE_SNIPE_LIST=false
 SNIPE_LIST_REFRESH_INTERVAL=30000
 CHECK_IF_MINT_IS_RENOUNCED=false
-AUTO_SELL=false
-SELL_DELAY=2000
+AUTO_SELL=true
+MAX_SELL_RETRIES=5

+ 8 - 6
README.md

@@ -42,13 +42,11 @@ Pool must not exist before the script starts.
 It will buy only when new pool is open for trading. If you want to buy token that will be launched in the future, make sure that script is running before the launch.
 
 ## Auto Sell
-By default, auto sell is disabled. If you want to enable it, you need to:
-- Change variable `AUTO_SELL` to `true`
-- Update `SELL_DELAY` to the number of milliseconds you want to wait before selling the token
+By default, auto sell is enabled. If you want to disable it, you need to:
+- Change variable `AUTO_SELL` to `false`
+- Update `MAX_SELL_RETRIES` to set the maximum number of retries for selling token
 
-This will sell the token after the specified delay. (+- RPC node speed)
-
-This feature is **experimental** and should be used with caution. Make sure you understand the risks before enabling it. There is no guarantee that the token will be sold at a profit or even sold at all. The developer is not responsible for any losses incurred by using this feature.
+Token will be sold immediately after it is bought.
 
 ## Common issues
 If you have an error which is not listed here, please create a new issue in this repository.
@@ -72,3 +70,7 @@ If you have an error which is not listed here, please create a new issue in this
 
 ## Contact
 [![](https://img.shields.io/discord/1201826085655023616?color=5865F2&logo=Discord&style=flat-square)](https://discord.gg/xYUETCA2aP)
+
+## Disclaimer
+
+Use this script at your own risk.

+ 194 - 135
buy.ts

@@ -1,4 +1,5 @@
 import {
+  BigNumberish,
   Liquidity,
   LIQUIDITY_STATE_LAYOUT_V4,
   LiquidityPoolKeys,
@@ -9,6 +10,7 @@ import {
   TokenAmount,
 } from '@raydium-io/raydium-sdk';
 import {
+  AccountLayout,
   createAssociatedTokenAccountIdempotentInstruction,
   createCloseAccountInstruction,
   getAssociatedTokenAddressSync,
@@ -32,7 +34,6 @@ import pino from 'pino';
 import bs58 from 'bs58';
 import * as fs from 'fs';
 import * as path from 'path';
-import BN from 'bn.js';
 
 const transport = pino.transport({
   targets: [
@@ -45,7 +46,7 @@ const transport = pino.transport({
     // },
 
     {
-      level: 'trace',
+      level: 'info',
       target: 'pino-pretty',
       options: {},
     },
@@ -54,6 +55,7 @@ const transport = pino.transport({
 
 export const logger = pino(
   {
+    level: 'info',
     redact: ['poolKeys'],
     serializers: {
       error: pino.stdSerializers.err,
@@ -92,8 +94,7 @@ const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNC
 const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
 const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger));
 const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger) === 'true';
-const SELL_DELAY = Number(retrieveEnvVariable('SELL_DELAY', logger));
-const MAX_SELL_RETRIES = 60;
+const MAX_SELL_RETRIES = Number(retrieveEnvVariable('MAX_SELL_RETRIES', logger));
 
 let snipeList: string[] = [];
 
@@ -170,30 +171,20 @@ function saveTokenAccount(mint: PublicKey, accountData: MinimalMarketLayoutV3) {
 }
 
 export async function processRaydiumPool(id: PublicKey, poolState: LiquidityStateV4) {
-  try {
-    if (!shouldBuy(poolState.baseMint.toString())) {
-      return;
-    }
-
-    if (CHECK_IF_MINT_IS_RENOUNCED) {
-      const mintOption = await checkMintable(poolState.baseMint);
-
-      if (mintOption !== true) {
-        logger.warn({ ...poolState, }, 'Skipping, owner can mint tokens!');
-        return;
-      }
-    }
+  if (!shouldBuy(poolState.baseMint.toString())) {
+    return;
+  }
 
-    await buy(id, poolState);
+  if (CHECK_IF_MINT_IS_RENOUNCED) {
+    const mintOption = await checkMintable(poolState.baseMint);
 
-    if (AUTO_SELL) {
-      await new Promise((resolve) => setTimeout(resolve, SELL_DELAY));
-      const poolKeys = existingTokenAccounts.get(poolState.baseMint.toString())!.poolKeys;
-      await sell(poolState, poolKeys as LiquidityPoolKeys);
+    if (mintOption !== true) {
+      logger.warn({ mint: poolState.baseMint }, 'Skipping, owner can mint tokens!');
+      return;
     }
-  } catch (e) {
-    logger.error({ ...poolState, error: e }, `Failed to process pool`);
   }
+
+  await buy(id, poolState);
 }
 
 export async function checkMintable(vault: PublicKey): Promise<boolean | undefined> {
@@ -202,16 +193,15 @@ export async function checkMintable(vault: PublicKey): Promise<boolean | undefin
     if (!data) {
       return;
     }
-    const deserialize = MintLayout.decode(data), mintAuthorityOption = deserialize.mintAuthorityOption;
-    return mintAuthorityOption === 0;
+    const deserialize = MintLayout.decode(data);
+    return deserialize.mintAuthorityOption === 0;
   } catch (e) {
-    logger.error({ mint: vault, error: e }, `Failed to check if mint is renounced`);
+    logger.debug(e);
+    logger.error({ mint: vault }, `Failed to check if mint is renounced`);
   }
 }
 
-export async function processOpenBookMarket(
-  updatedAccountInfo: KeyedAccountInfo,
-) {
+export async function processOpenBookMarket(updatedAccountInfo: KeyedAccountInfo) {
   let accountData: MarketStateV3 | undefined;
   try {
     accountData = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data);
@@ -223,132 +213,172 @@ export async function processOpenBookMarket(
 
     saveTokenAccount(accountData.baseMint, accountData);
   } catch (e) {
-    logger.error({ ...accountData, error: e }, `Failed to process market`);
+    logger.debug(e);
+    logger.error({ mint: accountData?.baseMint }, `Failed to process market`);
   }
 }
 
 async function buy(accountId: PublicKey, accountData: LiquidityStateV4): Promise<void> {
-  let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
+  try {
+    let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
 
-  if (!tokenAccount) {
-    // it's possible that we didn't have time to fetch open book data
-    const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, commitment);
-    tokenAccount = saveTokenAccount(accountData.baseMint, market);
-  }
+    if (!tokenAccount) {
+      // it's possible that we didn't have time to fetch open book data
+      const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, commitment);
+      tokenAccount = saveTokenAccount(accountData.baseMint, market);
+    }
 
-  tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!);
-  const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
-    {
-      poolKeys: tokenAccount.poolKeys,
-      userKeys: {
-        tokenAccountIn: quoteTokenAssociatedAddress,
-        tokenAccountOut: tokenAccount.address,
-        owner: wallet.publicKey,
+    tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!);
+    const { innerTransaction } = Liquidity.makeSwapFixedInInstruction(
+      {
+        poolKeys: tokenAccount.poolKeys,
+        userKeys: {
+          tokenAccountIn: quoteTokenAssociatedAddress,
+          tokenAccountOut: tokenAccount.address,
+          owner: wallet.publicKey,
+        },
+        amountIn: quoteAmount.raw,
+        minAmountOut: 0,
       },
-      amountIn: quoteAmount.raw,
-      minAmountOut: 0,
-    },
-    tokenAccount.poolKeys.version,
-  );
+      tokenAccount.poolKeys.version,
+    );
 
-  const latestBlockhash = await solanaConnection.getLatestBlockhash({
-    commitment: commitment,
-  });
-  const messageV0 = new TransactionMessage({
-    payerKey: wallet.publicKey,
-    recentBlockhash: latestBlockhash.blockhash,
-    instructions: [
-      ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
-      ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30000 }),
-      createAssociatedTokenAccountIdempotentInstruction(
-        wallet.publicKey,
-        tokenAccount.address,
-        wallet.publicKey,
-        accountData.baseMint,
-      ),
-      ...innerTransaction.instructions,
-    ],
-  }).compileToV0Message();
-  const transaction = new VersionedTransaction(messageV0);
-  transaction.sign([wallet, ...innerTransaction.signers]);
-  const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
-    maxRetries: 20,
-    preflightCommitment: commitment,
-  });
-  logger.info(
-    {
-      mint: accountData.baseMint,
-      url: `https://solscan.io/tx/${signature}?cluster=${network}`,
-      dexURL: `https://dexscreener.com/solana/${accountData.baseMint}?maker=${wallet.publicKey}`,
-    },
-    'Buy',
-  );
-}
-
-async function sell(accountData: LiquidityStateV4, poolKeys: LiquidityPoolKeys): Promise<void> {
-  const tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
-
-  if (!tokenAccount) {
-    return;
+    const latestBlockhash = await solanaConnection.getLatestBlockhash({
+      commitment: commitment,
+    });
+    const messageV0 = new TransactionMessage({
+      payerKey: wallet.publicKey,
+      recentBlockhash: latestBlockhash.blockhash,
+      instructions: [
+        ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 421197 }),
+        ComputeBudgetProgram.setComputeUnitLimit({ units: 101337 }),
+        createAssociatedTokenAccountIdempotentInstruction(
+          wallet.publicKey,
+          tokenAccount.address,
+          wallet.publicKey,
+          accountData.baseMint,
+        ),
+        ...innerTransaction.instructions,
+      ],
+    }).compileToV0Message();
+    const transaction = new VersionedTransaction(messageV0);
+    transaction.sign([wallet, ...innerTransaction.signers]);
+    const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
+      preflightCommitment: commitment,
+    });
+    logger.info({ mint: accountData.baseMint, signature }, `Sent buy tx`);
+    const confirmation = await solanaConnection.confirmTransaction(
+      {
+        signature,
+        lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
+        blockhash: latestBlockhash.blockhash,
+      },
+      commitment,
+    );
+    if (!confirmation.value.err) {
+      logger.info(
+        {
+          mint: accountData.baseMint,
+          signature,
+          url: `https://solscan.io/tx/${signature}?cluster=${network}`,
+        },
+        `Confirmed buy tx`,
+      );
+    } else {
+      logger.debug(confirmation.value.err);
+      logger.info({ mint: accountData.baseMint, signature }, `Error confirming buy tx`);
+    }
+  } catch (e) {
+    logger.debug(e);
+    logger.error({ mint: accountData.baseMint }, `Failed to buy token`);
   }
+}
 
+async function sell(accountId: PublicKey, mint: PublicKey, amount: BigNumberish): Promise<void> {
+  let sold = false;
   let retries = 0;
-  let balanceFound = false;
-  while (retries < MAX_SELL_RETRIES) {
+
+  do {
     try {
-      const balanceResponse = (await solanaConnection.getTokenAccountBalance(tokenAccount.address)).value.amount;
+      const tokenAccount = existingTokenAccounts.get(mint.toString());
 
-      if (balanceResponse !== null && Number(balanceResponse) > 0 && !balanceFound) {
-        balanceFound = true;
+      if (!tokenAccount) {
+        return;
+      }
 
-        const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
-          {
-            poolKeys: poolKeys,
-            userKeys: {
-              tokenAccountIn: tokenAccount.address,
-              tokenAccountOut: quoteTokenAssociatedAddress,
-              owner: wallet.publicKey,
-            },
-            amountIn: new BN(balanceResponse),
-            minAmountOut: 0,
-          },
-          poolKeys.version,
-        );
+      if (!tokenAccount.poolKeys) {
+        logger.warn({ mint }, 'No pool keys found');
+        continue;
+      }
 
-        const latestBlockhash = await solanaConnection.getLatestBlockhash({
-          commitment: commitment,
-        });
-        const messageV0 = new TransactionMessage({
-          payerKey: wallet.publicKey,
-          recentBlockhash: latestBlockhash.blockhash,
-          instructions: [
-            ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
-            ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 200000 }),
-            ...innerTransaction.instructions,
-            createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey),
-          ],
-        }).compileToV0Message();
-        const transaction = new VersionedTransaction(messageV0);
-        transaction.sign([wallet, ...innerTransaction.signers]);
-        const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
-          maxRetries: 5,
-          preflightCommitment: commitment,
-        });
+      if (amount === 0) {
         logger.info(
           {
-            mint: accountData.baseMint,
-            url: `https://solscan.io/tx/${signature}?cluster=${network}`,
+            mint: tokenAccount.mint,
           },
-          'sell',
+          `Empty balance, can't sell`,
         );
-        break;
+        return;
+      }
+
+      const { innerTransaction } = Liquidity.makeSwapFixedInInstruction(
+        {
+          poolKeys: tokenAccount.poolKeys!,
+          userKeys: {
+            tokenAccountOut: quoteTokenAssociatedAddress,
+            tokenAccountIn: tokenAccount.address,
+            owner: wallet.publicKey,
+          },
+          amountIn: amount,
+          minAmountOut: 0,
+        },
+        tokenAccount.poolKeys!.version,
+      );
+
+      const latestBlockhash = await solanaConnection.getLatestBlockhash({
+        commitment: commitment,
+      });
+      const messageV0 = new TransactionMessage({
+        payerKey: wallet.publicKey,
+        recentBlockhash: latestBlockhash.blockhash,
+        instructions: [
+          ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 421197 }),
+          ComputeBudgetProgram.setComputeUnitLimit({ units: 101337 }),
+          ...innerTransaction.instructions,
+          createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey),
+        ],
+      }).compileToV0Message();
+      const transaction = new VersionedTransaction(messageV0);
+      transaction.sign([wallet, ...innerTransaction.signers]);
+      const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
+        preflightCommitment: commitment,
+      });
+      logger.info({ mint, signature }, `Sent sell tx`);
+      const confirmation = await solanaConnection.confirmTransaction(
+        {
+          signature,
+          lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
+          blockhash: latestBlockhash.blockhash,
+        },
+        commitment,
+      );
+      if (confirmation.value.err) {
+        logger.debug(confirmation.value.err);
+        logger.info({ mint, signature }, `Error confirming sell tx`);
+        continue;
       }
-    } catch (error) {
-      // ignored
+
+      logger.info(
+        { mint, signature, url: `https://solscan.io/tx/${signature}?cluster=${network}` },
+        `Confirmed sell tx`,
+      );
+      sold = true;
+    } catch (e: any) {
+      retries++;
+      logger.debug(e);
+      logger.error({ mint }, `Failed to sell token, retry: ${retries}/${MAX_SELL_RETRIES}`);
     }
-    retries++;
-    await new Promise((resolve) => setTimeout(resolve, 1000));
-  }
+  } while (!sold && retries < MAX_SELL_RETRIES);
 }
 
 function loadSnipeList() {
@@ -434,6 +464,35 @@ const runListener = async () => {
     ],
   );
 
+  if (AUTO_SELL) {
+    const walletSubscriptionId = solanaConnection.onProgramAccountChange(
+      TOKEN_PROGRAM_ID,
+      async (updatedAccountInfo) => {
+        const accountData = AccountLayout.decode(updatedAccountInfo.accountInfo!.data);
+
+        if (updatedAccountInfo.accountId.equals(quoteTokenAssociatedAddress)) {
+          return;
+        }
+
+        const _ = sell(updatedAccountInfo.accountId, accountData.mint, accountData.amount);
+      },
+      commitment,
+      [
+        {
+          dataSize: 165,
+        },
+        {
+          memcmp: {
+            offset: 32,
+            bytes: wallet.publicKey.toBase58(),
+          },
+        },
+      ],
+    );
+
+    logger.info(`Listening for wallet changes: ${walletSubscriptionId}`);
+  }
+
   logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`);
   logger.info(`Listening for open book changes: ${openBookSubscriptionId}`);
 

+ 3 - 2
utils/utils.ts

@@ -1,5 +1,6 @@
-import { Logger } from "pino";
+import { Logger } from 'pino';
 import dotenv from 'dotenv';
+
 dotenv.config();
 
 export const retrieveEnvVariable = (variableName: string, logger: Logger) => {
@@ -9,4 +10,4 @@ export const retrieveEnvVariable = (variableName: string, logger: Logger) => {
     process.exit(1);
   }
   return variable;
-}
+};