Quellcode durchsuchen

Merge pull request #35 from Journeytojah/feat-autosell

Feat autosell
Filip Dunđer vor 1 Jahr
Ursprung
Commit
b6c7c665e0
7 geänderte Dateien mit 129 neuen und 91 gelöschten Zeilen
  1. 3 1
      .env.copy
  2. 1 1
      .gitignore
  3. 2 1
      .prettierrc
  4. 9 0
      README.md
  5. 107 81
      buy.ts
  6. 1 1
      package.json
  7. 6 6
      utils/utils.ts

+ 3 - 1
.env.copy

@@ -5,4 +5,6 @@ QUOTE_MINT=WSOL
 QUOTE_AMOUNT=0.1
 COMMITMENT_LEVEL=finalized
 USE_SNIPE_LIST=false
-SNIPE_LIST_REFRESH_INTERVAL=30000
+SNIPE_LIST_REFRESH_INTERVAL=30000
+AUTO_SELL=false
+SELL_DELAY=2000

+ 1 - 1
.gitignore

@@ -129,5 +129,5 @@ dist
 .yarn/install-state.gz
 .pnp.*
 
-
+# JetBrains
 .idea

+ 2 - 1
.prettierrc

@@ -1,4 +1,5 @@
 {
   "singleQuote": true,
-  "trailingComma": "all"
+  "trailingComma": "all",
+  "printWidth": 120
 }

+ 9 - 0
README.md

@@ -40,6 +40,15 @@ You can update the list while script is running. Script will check for new value
 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
+
+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.
+
 ## Common issues
 If you have an error which is not listed here, please create a new issue in this repository.
 

+ 107 - 81
buy.ts

@@ -10,6 +10,7 @@ import {
 } from '@raydium-io/raydium-sdk';
 import {
   createAssociatedTokenAccountIdempotentInstruction,
+  createCloseAccountInstruction,
   getAssociatedTokenAddressSync,
   TOKEN_PROGRAM_ID,
 } from '@solana/spl-token';
@@ -23,30 +24,25 @@ import {
   VersionedTransaction,
   Commitment,
 } from '@solana/web3.js';
-import {
-  getTokenAccounts,
-  RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
-  OPENBOOK_PROGRAM_ID,
-  createPoolKeys,
-} from './liquidity';
+import { getTokenAccounts, RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, OPENBOOK_PROGRAM_ID, createPoolKeys } from './liquidity';
 import { retrieveEnvVariable } from './utils';
 import { getMinimalMarketV3, MinimalMarketLayoutV3 } from './market';
 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: [
-    /*
-    {
-      level: 'trace',
-      target: 'pino/file',
-      options: {
-        destination: 'buy.log',
-      },
-    },
-    */
+    // {
+    //   level: 'trace',
+    //   target: 'pino/file',
+    //   options: {
+    //     destination: 'buy.log',
+    //   },
+    // },
+
     {
       level: 'trace',
       target: 'pino-pretty',
@@ -68,10 +64,7 @@ export const logger = pino(
 
 const network = 'mainnet-beta';
 const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
-const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable(
-  'RPC_WEBSOCKET_ENDPOINT',
-  logger,
-);
+const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOINT', logger);
 
 const solanaConnection = new Connection(RPC_ENDPOINT, {
   wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
@@ -86,24 +79,20 @@ export type MinimalTokenAccountData = {
 
 let existingLiquidityPools: Set<string> = new Set<string>();
 let existingOpenBookMarkets: Set<string> = new Set<string>();
-let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<
-  string,
-  MinimalTokenAccountData
->();
+let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<string, MinimalTokenAccountData>();
 
 let wallet: Keypair;
 let quoteToken: Token;
 let quoteTokenAssociatedAddress: PublicKey;
 let quoteAmount: TokenAmount;
-let commitment: Commitment = retrieveEnvVariable(
-  'COMMITMENT_LEVEL',
-  logger,
-) as Commitment;
+let commitment: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
 
 const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
-const SNIPE_LIST_REFRESH_INTERVAL = Number(
-  retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger),
-);
+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;
+
 let snipeList: string[] = [];
 
 async function init(): Promise<void> {
@@ -133,9 +122,7 @@ async function init(): Promise<void> {
       break;
     }
     default: {
-      throw new Error(
-        `Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`,
-      );
+      throw new Error(`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`);
     }
   }
 
@@ -144,29 +131,19 @@ async function init(): Promise<void> {
   );
 
   // check existing wallet for associated token account of quote mint
-  const tokenAccounts = await getTokenAccounts(
-    solanaConnection,
-    wallet.publicKey,
-    commitment,
-  );
+  const tokenAccounts = await getTokenAccounts(solanaConnection, wallet.publicKey, commitment);
 
   for (const ta of tokenAccounts) {
-    existingTokenAccounts.set(ta.accountInfo.mint.toString(), <
-      MinimalTokenAccountData
-    >{
+    existingTokenAccounts.set(ta.accountInfo.mint.toString(), <MinimalTokenAccountData>{
       mint: ta.accountInfo.mint,
       address: ta.pubkey,
     });
   }
 
-  const tokenAccount = tokenAccounts.find(
-    (acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString(),
-  )!;
+  const tokenAccount = tokenAccounts.find((acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString())!;
 
   if (!tokenAccount) {
-    throw new Error(
-      `No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`,
-    );
+    throw new Error(`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`);
   }
 
   quoteTokenAssociatedAddress = tokenAccount.pubkey;
@@ -190,29 +167,28 @@ function saveTokenAccount(mint: PublicKey, accountData: MinimalMarketLayoutV3) {
   return tokenAccount;
 }
 
-export async function processRaydiumPool(
-  id: PublicKey,
-  poolState: LiquidityStateV4,
-) {
+export async function processRaydiumPool(id: PublicKey, poolState: LiquidityStateV4) {
   try {
     if (!shouldBuy(poolState.baseMint.toString())) {
       return;
     }
 
     await buy(id, poolState);
+
+    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);
+    }
   } catch (e) {
     logger.error({ ...poolState, error: e }, `Failed to process pool`);
   }
 }
 
-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,
-    );
+    accountData = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data);
 
     // to be competitive, we collect market data before buying the token...
     if (existingTokenAccounts.has(accountData.baseMint.toString())) {
@@ -225,27 +201,16 @@ export async function processOpenBookMarket(
   }
 }
 
-async function buy(
-  accountId: PublicKey,
-  accountData: LiquidityStateV4,
-): Promise<void> {
+async function buy(accountId: PublicKey, accountData: LiquidityStateV4): Promise<void> {
   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,
-    );
+    const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, commitment);
     tokenAccount = saveTokenAccount(accountData.baseMint, market);
   }
 
-  tokenAccount.poolKeys = createPoolKeys(
-    accountId,
-    accountData,
-    tokenAccount.market!,
-  );
+  tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!);
   const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
     {
       poolKeys: tokenAccount.poolKeys,
@@ -280,13 +245,10 @@ async function buy(
   }).compileToV0Message();
   const transaction = new VersionedTransaction(messageV0);
   transaction.sign([wallet, ...innerTransaction.signers]);
-  const signature = await solanaConnection.sendRawTransaction(
-    transaction.serialize(),
-    {
-      maxRetries: 20,
-      preflightCommitment: commitment,
-    },
-  );
+  const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
+    maxRetries: 20,
+    preflightCommitment: commitment,
+  });
   logger.info(
     {
       mint: accountData.baseMint,
@@ -296,6 +258,72 @@ async function buy(
   );
 }
 
+async function sell(accountData: LiquidityStateV4, poolKeys: LiquidityPoolKeys): Promise<void> {
+  const tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
+
+  if (!tokenAccount) {
+    return;
+  }
+
+  let retries = 0;
+  let balanceFound = false;
+  while (retries < MAX_SELL_RETRIES) {
+    try {
+      const balanceResponse = (await solanaConnection.getTokenAccountBalance(tokenAccount.address)).value.amount;
+
+      if (balanceResponse !== null && Number(balanceResponse) > 0 && !balanceFound) {
+        balanceFound = true;
+
+        const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
+          {
+            poolKeys: poolKeys,
+            userKeys: {
+              tokenAccountIn: tokenAccount.address,
+              tokenAccountOut: quoteTokenAssociatedAddress,
+              owner: wallet.publicKey,
+            },
+            amountIn: new BN(balanceResponse),
+            minAmountOut: 0,
+          },
+          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: 200000 }),
+            createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey),
+            ...innerTransaction.instructions,
+          ],
+        }).compileToV0Message();
+        const transaction = new VersionedTransaction(messageV0);
+        transaction.sign([wallet, ...innerTransaction.signers]);
+        const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
+          maxRetries: 5,
+          preflightCommitment: commitment,
+        });
+        logger.info(
+          {
+            mint: accountData.baseMint,
+            url: `https://solscan.io/tx/${signature}?cluster=${network}`,
+          },
+          'sell',
+        );
+        break;
+      }
+    } catch (error) {
+      // ignored
+    }
+    retries++;
+    await new Promise((resolve) => setTimeout(resolve, 1000));
+  }
+}
+
 function loadSnipeList() {
   if (!USE_SNIPE_LIST) {
     return;
@@ -324,9 +352,7 @@ const runListener = async () => {
     RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
     async (updatedAccountInfo) => {
       const key = updatedAccountInfo.accountId.toString();
-      const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(
-        updatedAccountInfo.accountInfo.data,
-      );
+      const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data);
       const poolOpenTime = parseInt(poolState.poolOpenTime.toString());
       const existing = existingLiquidityPools.has(key);
 

+ 1 - 1
package.json

@@ -22,4 +22,4 @@
     "ts-node": "^10.9.2",
     "typescript": "^5.3.3"
   }
-}
+}

+ 6 - 6
utils/utils.ts

@@ -3,10 +3,10 @@ import dotenv from 'dotenv';
 dotenv.config();
 
 export const retrieveEnvVariable = (variableName: string, logger: Logger) => {
-    const variable = process.env[variableName] || '';
-    if (!variable) {
-      logger.error(`${variableName} is not set`);
-      process.exit(1);
-    }
-    return variable;
+  const variable = process.env[variableName] || '';
+  if (!variable) {
+    logger.error(`${variableName} is not set`);
+    process.exit(1);
+  }
+  return variable;
 }