Procházet zdrojové kódy

Add auto sell feature

OneRobotBoii před 1 rokem
rodič
revize
71b02470cb
5 změnil soubory, kde provedl 250 přidání a 26 odebrání
  1. 49 9
      buy.ts
  2. 3 2
      package.json
  3. 28 9
      pnpm-lock.yaml
  4. 6 6
      utils/utils.ts
  5. 164 0
      worker/watcher.ts

+ 49 - 9
buy.ts

@@ -201,6 +201,22 @@ export async function processRaydiumPool(
     }
 
     await buy(id, poolState);
+
+
+    const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger);
+    if (AUTO_SELL === 'true') {
+      // wait for a bit before selling
+      const SELL_DELAY = retrieveEnvVariable('SELL_DELAY', logger);
+      const timeout = parseInt(SELL_DELAY, 10);
+      await new Promise((resolve) => setTimeout(resolve, timeout));
+
+      // log poolstate info
+      // logger.info({ poolState }, `Pool state info`);
+
+      await sell(id, poolState);
+    }
+
+    // await sell(id, poolState);
   } catch (e) {
     logger.error({ ...poolState, error: e }, `Failed to process pool`);
   }
@@ -531,16 +547,44 @@ const runListener = async () => {
   logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`);
   logger.info(`Listening for open book changes: ${openBookSubscriptionId}`);
 
-  // post to discord webhook
+
+  if (USE_SNIPE_LIST) {
+    setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
+  }
+};
+
+// runListener();
+
+// make sure we can send a message on discord if there is an error or the script exits
+process.on('unhandledRejection', (reason, promise) => {
+  logger.error(reason, 'Unhandled Rejection at:', promise);
   const message = {
     embeds: [
       {
-        title: `Listening for raydium changes: ${raydiumSubscriptionId}`,
+        title: `Unhandled Rejection: ${reason}`,
         color: 1127128,
       },
+    ],
+  };
+
+  const DISCORD_WEBHOOK = retrieveEnvVariable('DISCORD_WEBHOOK', logger);
+  // use native fetch to post to discord
+  fetch(DISCORD_WEBHOOK, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify(message),
+  });
+});
+
+process.on('uncaughtException', (err) => {
+  logger.error(err, 'Uncaught Exception thrown');
+  const message = {
+    embeds: [
       {
-        title: `Listening for open book changes: ${openBookSubscriptionId}`,
-        color: 14177041,
+        title: `Uncaught Exception: ${err}`,
+        color: 1127128,
       },
     ],
   };
@@ -554,10 +598,6 @@ const runListener = async () => {
     },
     body: JSON.stringify(message),
   });
-
-  if (USE_SNIPE_LIST) {
-    setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
-  }
-};
+});
 
 runListener();

+ 3 - 2
package.json

@@ -2,7 +2,8 @@
   "name": "solana-sniper-bot",
   "author": "Filip Dundjer",
   "scripts": {
-    "buy": "ts-node buy.ts"
+    "buy": "ts-node buy.ts",
+    "work": "ts-node worker/watcher.ts"
   },
   "dependencies": {
     "@raydium-io/raydium-sdk": "^1.3.1-beta.47",
@@ -22,4 +23,4 @@
     "ts-node": "^10.9.2",
     "typescript": "^5.3.3"
   }
-}
+}

+ 28 - 9
pnpm-lock.yaml

@@ -9,8 +9,11 @@ dependencies:
     specifier: ^1.3.1-beta.47
     version: 1.3.1-beta.47(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22)
   '@solana/spl-token':
-    specifier: ^0.3.11
-    version: 0.3.11(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22)
+    specifier: ^0.4.0
+    version: 0.4.0(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22)
+  '@solana/web3.js':
+    specifier: ^1.89.1
+    version: 1.90.0
   bigint-buffer:
     specifier: ^1.1.5
     version: 1.1.5
@@ -21,10 +24,10 @@ dependencies:
     specifier: ^5.0.0
     version: 5.0.0
   dotenv:
-    specifier: ^16.3.2
+    specifier: ^16.4.1
     version: 16.4.1
   pino:
-    specifier: ^8.17.2
+    specifier: ^8.18.0
     version: 8.18.0
   pino-pretty:
     specifier: ^10.3.1
@@ -38,7 +41,7 @@ devDependencies:
     specifier: ^5.1.5
     version: 5.1.5
   prettier:
-    specifier: ^3.2.1
+    specifier: ^3.2.4
     version: 3.2.5
   ts-node:
     specifier: ^10.9.2
@@ -204,6 +207,24 @@ packages:
       - utf-8-validate
     dev: false
 
+  /@solana/spl-token@0.4.0(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22):
+    resolution: {integrity: sha512-jjBIBG9IsclqQVl5Y82npGE6utdCh7Z9VFcF5qgJa5EUq2XgspW3Dt1wujWjH/vQDRnkp9zGO+BqQU/HhX/3wg==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.89.1
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/buffer-layout-utils': 0.2.0
+      '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22)
+      '@solana/web3.js': 1.90.0
+      buffer: 6.0.3
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - fastestsmallesttextencoderdecoder
+      - utf-8-validate
+    dev: false
+
   /@solana/spl-type-length-value@0.1.0:
     resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==}
     engines: {node: '>=16'}
@@ -260,7 +281,7 @@ packages:
   /@types/connect@3.4.38:
     resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
     dependencies:
-      '@types/node': 12.20.55
+      '@types/node': 20.11.16
     dev: false
 
   /@types/node@12.20.55:
@@ -271,12 +292,11 @@ packages:
     resolution: {integrity: sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==}
     dependencies:
       undici-types: 5.26.5
-    dev: true
 
   /@types/ws@7.4.7:
     resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
     dependencies:
-      '@types/node': 12.20.55
+      '@types/node': 20.11.16
     dev: false
 
   /JSONStream@1.3.5:
@@ -869,7 +889,6 @@ packages:
 
   /undici-types@5.26.5:
     resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
-    dev: true
 
   /utf-8-validate@5.0.10:
     resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}

+ 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;
 }

+ 164 - 0
worker/watcher.ts

@@ -0,0 +1,164 @@
+// sellWorker.ts
+import { parentPort, workerData } from 'worker_threads';
+import { PublicKey, TokenAmount, Connection, Commitment } from '@solana/web3.js';
+import { LIQUIDITY_STATE_LAYOUT_V4, Liquidity, SPL_ACCOUNT_LAYOUT, TOKEN_PROGRAM_ID, TokenAccount } from '@raydium-io/raydium-sdk';
+import { retrieveEnvVariable } from '../utils';
+import BN from 'bn.js';
+import pino from 'pino';
+
+const transport = pino.transport({
+  targets: [
+    {
+      level: 'trace',
+      target: 'pino-pretty',
+      options: {},
+    },
+  ],
+});
+
+export const logger = pino(
+  {
+    redact: ['poolKeys'],
+    serializers: {
+      error: pino.stdSerializers.err,
+    },
+    base: undefined,
+  },
+  transport,
+);
+
+async function getTokenAccounts(connection: Connection, owner: PublicKey) {
+  const tokenResp = await connection.getTokenAccountsByOwner(owner, {
+    programId: TOKEN_PROGRAM_ID
+  });
+
+  const accounts: TokenAccount[] = [];
+  for (const { pubkey, account } of tokenResp.value) {
+    accounts.push({
+      pubkey,
+      accountInfo: SPL_ACCOUNT_LAYOUT.decode(account.data),
+      programId: new PublicKey(account.owner.toBase58())
+    });
+  }
+
+  return accounts;
+}
+
+const SOL_SDC_POOL_ID = "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2";
+const OPENBOOK_PROGRAM_ID = new PublicKey(
+  "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX"
+);
+
+async function parsePoolInfo() {
+  const network = 'mainnet-beta';
+  const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
+  const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable(
+    'RPC_WEBSOCKET_ENDPOINT',
+    logger,
+  );
+
+  const connection = new Connection(RPC_ENDPOINT, {
+    wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
+  });
+  const owner = new PublicKey("VnxDzsZ7chE88e9rB6UKztCt2HUwrkgCTx8WieWf5mM");
+
+  const tokenAccounts = await getTokenAccounts(connection, owner);
+
+  // example to get pool info
+  const info = await connection.getAccountInfo(new PublicKey(SOL_SDC_POOL_ID));
+  if (!info) {
+    throw new Error("Pool not found");
+  }
+
+  const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(info.data);
+
+  const baseDecimal = 10 ** poolState.baseDecimal.toNumber();
+  const quoteDecimal = 10 ** poolState.quoteDecimal.toNumber();
+
+  const baseTokenAmount = await connection.getTokenAccountBalance(
+    poolState.baseVault
+  )
+
+  const quoteTokenAmount = await connection.getTokenAccountBalance(
+    poolState.quoteVault
+  )
+
+  const basePnl = poolState.baseNeedTakePnl.toNumber() / baseDecimal;
+  const quotePnl = poolState.quoteNeedTakePnl.toNumber() / quoteDecimal;
+
+  const base = (baseTokenAmount.value?.uiAmount || 0) - basePnl;
+  const quote = (quoteTokenAmount.value?.uiAmount || 0) - quotePnl;
+
+  const denominator = new BN(10).pow(poolState.baseDecimal);
+
+  const addedLpAccount = tokenAccounts.find((a) => a.accountInfo.mint.equals(poolState.lpMint));
+
+  const message = `
+  SOL - USDC Pool Info:
+  
+  Pool total base: ${base},
+  Pool total quote: ${quote},
+
+  Base vault balance: ${baseTokenAmount.value.uiAmount},
+  Quote vault balance: ${quoteTokenAmount.value.uiAmount},
+
+  Base token decimals: ${poolState.baseDecimal.toNumber()},
+  Quote token decimals: ${poolState.quoteDecimal.toNumber()},
+  Total LP: ${poolState.lpReserve.div(denominator).toString()},
+
+  Added LP amount: ${(addedLpAccount?.accountInfo.amount.toNumber() || 0) / baseDecimal},
+  `;
+
+  logger.info(message);
+
+  // send message to discord (embed)
+  // post to discord webhook
+  let embed = {
+    embeds: [
+      {
+        title: "SOL - USDC Pool Info",
+        description: `
+        Pool total base: **${base}**,
+        Pool total quote: **${quote}**,
+
+        Base vault balance: **${baseTokenAmount.value.uiAmount}**,
+        Quote vault balance: **${quoteTokenAmount.value.uiAmount}**,
+
+        Base token decimals:** ${poolState.baseDecimal.toNumber()}**,
+        Quote token decimals:** ${poolState.quoteDecimal.toNumber()}**,
+
+        Total LP: **${poolState.lpReserve.div(denominator).toString()}**,
+        Added LP amount: **${(addedLpAccount?.accountInfo.amount.toNumber() || 0) / baseDecimal}**
+
+        Happy trading! 🚀
+        `
+      }
+
+    ]
+
+  };
+
+  const DISCORD_WEBHOOK = retrieveEnvVariable('DISCORD_WEBHOOK', logger);
+  // use native fetch to post to discord
+  fetch(DISCORD_WEBHOOK, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify(embed),
+  });
+
+  logger.info("Message sent to Discord");
+
+}
+
+// Function to periodically check the pool
+async function checkPoolPeriodically(interval: number) {
+  while (true) {
+    await parsePoolInfo();
+    await new Promise(resolve => setTimeout(resolve, interval));
+  }
+}
+
+// Check pool periodically with a specified interval
+checkPoolPeriodically(60000); // 1 minute