bot.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import {
  2. ComputeBudgetProgram,
  3. Connection,
  4. Keypair,
  5. PublicKey,
  6. TransactionMessage,
  7. VersionedTransaction,
  8. } from '@solana/web3.js';
  9. import {
  10. createAssociatedTokenAccountIdempotentInstruction,
  11. createCloseAccountInstruction,
  12. getAccount,
  13. getAssociatedTokenAddress,
  14. RawAccount,
  15. TOKEN_PROGRAM_ID,
  16. } from '@solana/spl-token';
  17. import { Liquidity, LiquidityPoolKeysV4, LiquidityStateV4, Percent, Token, TokenAmount } from '@raydium-io/raydium-sdk';
  18. import { MarketCache, PoolCache, SnipeListCache } from './cache';
  19. import { PoolFilters } from './filters';
  20. import { TransactionExecutor } from './transactions';
  21. import { createPoolKeys, logger, NETWORK, sleep } from './helpers';
  22. import { Mutex } from 'async-mutex';
  23. import BN from 'bn.js';
  24. import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
  25. import { JitoTransactionExecutor } from './transactions/jito-rpc-transaction-executor';
  26. export interface BotConfig {
  27. wallet: Keypair;
  28. checkRenounced: boolean;
  29. checkFreezable: boolean;
  30. checkBurned: boolean;
  31. minPoolSize: TokenAmount;
  32. maxPoolSize: TokenAmount;
  33. quoteToken: Token;
  34. quoteAmount: TokenAmount;
  35. quoteAta: PublicKey;
  36. oneTokenAtATime: boolean;
  37. useSnipeList: boolean;
  38. autoSell: boolean;
  39. autoBuyDelay: number;
  40. autoSellDelay: number;
  41. maxBuyRetries: number;
  42. maxSellRetries: number;
  43. unitLimit: number;
  44. unitPrice: number;
  45. takeProfit: number;
  46. stopLoss: number;
  47. buySlippage: number;
  48. sellSlippage: number;
  49. priceCheckInterval: number;
  50. priceCheckDuration: number;
  51. filterCheckInterval: number;
  52. filterCheckDuration: number;
  53. consecutiveMatchCount: number;
  54. }
  55. export class Bot {
  56. private readonly poolFilters: PoolFilters;
  57. // snipe list
  58. private readonly snipeListCache?: SnipeListCache;
  59. // one token at the time
  60. private readonly mutex: Mutex;
  61. private sellExecutionCount = 0;
  62. public readonly isWarp: boolean = false;
  63. public readonly isJito: boolean = false;
  64. constructor(
  65. private readonly connection: Connection,
  66. private readonly marketStorage: MarketCache,
  67. private readonly poolStorage: PoolCache,
  68. private readonly txExecutor: TransactionExecutor,
  69. readonly config: BotConfig,
  70. ) {
  71. this.isWarp = txExecutor instanceof WarpTransactionExecutor;
  72. this.isJito = txExecutor instanceof JitoTransactionExecutor;
  73. this.mutex = new Mutex();
  74. this.poolFilters = new PoolFilters(connection, {
  75. quoteToken: this.config.quoteToken,
  76. minPoolSize: this.config.minPoolSize,
  77. maxPoolSize: this.config.maxPoolSize,
  78. });
  79. if (this.config.useSnipeList) {
  80. this.snipeListCache = new SnipeListCache();
  81. this.snipeListCache.init();
  82. }
  83. }
  84. async validate() {
  85. try {
  86. await getAccount(this.connection, this.config.quoteAta, this.connection.commitment);
  87. } catch (error) {
  88. logger.error(
  89. `${this.config.quoteToken.symbol} token account not found in wallet: ${this.config.wallet.publicKey.toString()}`,
  90. );
  91. return false;
  92. }
  93. return true;
  94. }
  95. public async buy(accountId: PublicKey, poolState: LiquidityStateV4) {
  96. logger.trace({ mint: poolState.baseMint }, `Processing new pool...`);
  97. if (this.config.useSnipeList && !this.snipeListCache?.isInList(poolState.baseMint.toString())) {
  98. logger.debug({ mint: poolState.baseMint.toString() }, `Skipping buy because token is not in a snipe list`);
  99. return;
  100. }
  101. if (this.config.autoBuyDelay > 0) {
  102. logger.debug({ mint: poolState.baseMint }, `Waiting for ${this.config.autoBuyDelay} ms before buy`);
  103. await sleep(this.config.autoBuyDelay);
  104. }
  105. if (this.config.oneTokenAtATime) {
  106. if (this.mutex.isLocked() || this.sellExecutionCount > 0) {
  107. logger.debug(
  108. { mint: poolState.baseMint.toString() },
  109. `Skipping buy because one token at a time is turned on and token is already being processed`,
  110. );
  111. return;
  112. }
  113. await this.mutex.acquire();
  114. }
  115. try {
  116. const [market, mintAta] = await Promise.all([
  117. this.marketStorage.get(poolState.marketId.toString()),
  118. getAssociatedTokenAddress(poolState.baseMint, this.config.wallet.publicKey),
  119. ]);
  120. const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(accountId, poolState, market);
  121. if (!this.config.useSnipeList) {
  122. const match = await this.filterMatch(poolKeys);
  123. if (!match) {
  124. logger.trace({ mint: poolKeys.baseMint.toString() }, `Skipping buy because pool doesn't match filters`);
  125. return;
  126. }
  127. }
  128. for (let i = 0; i < this.config.maxBuyRetries; i++) {
  129. try {
  130. logger.info(
  131. { mint: poolState.baseMint.toString() },
  132. `Send buy transaction attempt: ${i + 1}/${this.config.maxBuyRetries}`,
  133. );
  134. const tokenOut = new Token(TOKEN_PROGRAM_ID, poolKeys.baseMint, poolKeys.baseDecimals);
  135. const result = await this.swap(
  136. poolKeys,
  137. this.config.quoteAta,
  138. mintAta,
  139. this.config.quoteToken,
  140. tokenOut,
  141. this.config.quoteAmount,
  142. this.config.buySlippage,
  143. this.config.wallet,
  144. 'buy',
  145. );
  146. if (result.confirmed) {
  147. logger.info(
  148. {
  149. mint: poolState.baseMint.toString(),
  150. signature: result.signature,
  151. url: `https://solscan.io/tx/${result.signature}?cluster=${NETWORK}`,
  152. },
  153. `Confirmed buy tx`,
  154. );
  155. break;
  156. }
  157. logger.info(
  158. {
  159. mint: poolState.baseMint.toString(),
  160. signature: result.signature,
  161. error: result.error,
  162. },
  163. `Error confirming buy tx`,
  164. );
  165. } catch (error) {
  166. logger.debug({ mint: poolState.baseMint.toString(), error }, `Error confirming buy transaction`);
  167. }
  168. }
  169. } catch (error) {
  170. logger.error({ mint: poolState.baseMint.toString(), error }, `Failed to buy token`);
  171. } finally {
  172. if (this.config.oneTokenAtATime) {
  173. this.mutex.release();
  174. }
  175. }
  176. }
  177. public async sell(accountId: PublicKey, rawAccount: RawAccount) {
  178. if (this.config.oneTokenAtATime) {
  179. this.sellExecutionCount++;
  180. }
  181. try {
  182. logger.trace({ mint: rawAccount.mint }, `Processing new token...`);
  183. const poolData = await this.poolStorage.get(rawAccount.mint.toString());
  184. if (!poolData) {
  185. logger.trace({ mint: rawAccount.mint.toString() }, `Token pool data is not found, can't sell`);
  186. return;
  187. }
  188. const tokenIn = new Token(TOKEN_PROGRAM_ID, poolData.state.baseMint, poolData.state.baseDecimal.toNumber());
  189. const tokenAmountIn = new TokenAmount(tokenIn, rawAccount.amount, true);
  190. if (tokenAmountIn.isZero()) {
  191. logger.info({ mint: rawAccount.mint.toString() }, `Empty balance, can't sell`);
  192. return;
  193. }
  194. if (this.config.autoSellDelay > 0) {
  195. logger.debug({ mint: rawAccount.mint }, `Waiting for ${this.config.autoSellDelay} ms before sell`);
  196. await sleep(this.config.autoSellDelay);
  197. }
  198. const market = await this.marketStorage.get(poolData.state.marketId.toString());
  199. const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(new PublicKey(poolData.id), poolData.state, market);
  200. await this.priceMatch(tokenAmountIn, poolKeys);
  201. for (let i = 0; i < this.config.maxSellRetries; i++) {
  202. try {
  203. logger.info(
  204. { mint: rawAccount.mint },
  205. `Send sell transaction attempt: ${i + 1}/${this.config.maxSellRetries}`,
  206. );
  207. const result = await this.swap(
  208. poolKeys,
  209. accountId,
  210. this.config.quoteAta,
  211. tokenIn,
  212. this.config.quoteToken,
  213. tokenAmountIn,
  214. this.config.sellSlippage,
  215. this.config.wallet,
  216. 'sell',
  217. );
  218. if (result.confirmed) {
  219. logger.info(
  220. {
  221. dex: `https://dexscreener.com/solana/${rawAccount.mint.toString()}?maker=${this.config.wallet.publicKey}`,
  222. mint: rawAccount.mint.toString(),
  223. signature: result.signature,
  224. url: `https://solscan.io/tx/${result.signature}?cluster=${NETWORK}`,
  225. },
  226. `Confirmed sell tx`,
  227. );
  228. break;
  229. }
  230. logger.info(
  231. {
  232. mint: rawAccount.mint.toString(),
  233. signature: result.signature,
  234. error: result.error,
  235. },
  236. `Error confirming sell tx`,
  237. );
  238. } catch (error) {
  239. logger.debug({ mint: rawAccount.mint.toString(), error }, `Error confirming sell transaction`);
  240. }
  241. }
  242. } catch (error) {
  243. logger.error({ mint: rawAccount.mint.toString(), error }, `Failed to sell token`);
  244. } finally {
  245. if (this.config.oneTokenAtATime) {
  246. this.sellExecutionCount--;
  247. }
  248. }
  249. }
  250. // noinspection JSUnusedLocalSymbols
  251. private async swap(
  252. poolKeys: LiquidityPoolKeysV4,
  253. ataIn: PublicKey,
  254. ataOut: PublicKey,
  255. tokenIn: Token,
  256. tokenOut: Token,
  257. amountIn: TokenAmount,
  258. slippage: number,
  259. wallet: Keypair,
  260. direction: 'buy' | 'sell',
  261. ) {
  262. const slippagePercent = new Percent(slippage, 100);
  263. const poolInfo = await Liquidity.fetchInfo({
  264. connection: this.connection,
  265. poolKeys,
  266. });
  267. const computedAmountOut = Liquidity.computeAmountOut({
  268. poolKeys,
  269. poolInfo,
  270. amountIn,
  271. currencyOut: tokenOut,
  272. slippage: slippagePercent,
  273. });
  274. const latestBlockhash = await this.connection.getLatestBlockhash();
  275. const { innerTransaction } = Liquidity.makeSwapFixedInInstruction(
  276. {
  277. poolKeys: poolKeys,
  278. userKeys: {
  279. tokenAccountIn: ataIn,
  280. tokenAccountOut: ataOut,
  281. owner: wallet.publicKey,
  282. },
  283. amountIn: amountIn.raw,
  284. minAmountOut: computedAmountOut.minAmountOut.raw,
  285. },
  286. poolKeys.version,
  287. );
  288. const messageV0 = new TransactionMessage({
  289. payerKey: wallet.publicKey,
  290. recentBlockhash: latestBlockhash.blockhash,
  291. instructions: [
  292. ...(this.isWarp || this.isJito
  293. ? []
  294. : [
  295. ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }),
  296. ComputeBudgetProgram.setComputeUnitLimit({ units: this.config.unitLimit }),
  297. ]),
  298. ...(direction === 'buy'
  299. ? [
  300. createAssociatedTokenAccountIdempotentInstruction(
  301. wallet.publicKey,
  302. ataOut,
  303. wallet.publicKey,
  304. tokenOut.mint,
  305. ),
  306. ]
  307. : []),
  308. ...innerTransaction.instructions,
  309. ...(direction === 'sell' ? [createCloseAccountInstruction(ataIn, wallet.publicKey, wallet.publicKey)] : []),
  310. ],
  311. }).compileToV0Message();
  312. const transaction = new VersionedTransaction(messageV0);
  313. transaction.sign([wallet, ...innerTransaction.signers]);
  314. return this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash);
  315. }
  316. private async filterMatch(poolKeys: LiquidityPoolKeysV4) {
  317. if (this.config.filterCheckInterval === 0 || this.config.filterCheckDuration === 0) {
  318. return true;
  319. }
  320. const timesToCheck = this.config.filterCheckDuration / this.config.filterCheckInterval;
  321. let timesChecked = 0;
  322. let matchCount = 0;
  323. do {
  324. try {
  325. const shouldBuy = await this.poolFilters.execute(poolKeys);
  326. if (shouldBuy) {
  327. matchCount++;
  328. if (this.config.consecutiveMatchCount <= matchCount) {
  329. logger.debug(
  330. { mint: poolKeys.baseMint.toString() },
  331. `Filter match ${matchCount}/${this.config.consecutiveMatchCount}`,
  332. );
  333. return true;
  334. }
  335. } else {
  336. matchCount = 0;
  337. }
  338. await sleep(this.config.filterCheckInterval);
  339. } finally {
  340. timesChecked++;
  341. }
  342. } while (timesChecked < timesToCheck);
  343. return false;
  344. }
  345. private async priceMatch(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
  346. if (this.config.priceCheckDuration === 0 || this.config.priceCheckInterval === 0) {
  347. return;
  348. }
  349. const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
  350. const profitFraction = this.config.quoteAmount.mul(this.config.takeProfit).numerator.div(new BN(100));
  351. const profitAmount = new TokenAmount(this.config.quoteToken, profitFraction, true);
  352. const takeProfit = this.config.quoteAmount.add(profitAmount);
  353. const lossFraction = this.config.quoteAmount.mul(this.config.stopLoss).numerator.div(new BN(100));
  354. const lossAmount = new TokenAmount(this.config.quoteToken, lossFraction, true);
  355. const stopLoss = this.config.quoteAmount.subtract(lossAmount);
  356. const slippage = new Percent(this.config.sellSlippage, 100);
  357. let timesChecked = 0;
  358. do {
  359. try {
  360. const poolInfo = await Liquidity.fetchInfo({
  361. connection: this.connection,
  362. poolKeys,
  363. });
  364. const amountOut = Liquidity.computeAmountOut({
  365. poolKeys,
  366. poolInfo,
  367. amountIn: amountIn,
  368. currencyOut: this.config.quoteToken,
  369. slippage,
  370. }).amountOut;
  371. logger.debug(
  372. { mint: poolKeys.baseMint.toString() },
  373. `Take profit: ${takeProfit.toFixed()} | Stop loss: ${stopLoss.toFixed()} | Current: ${amountOut.toFixed()}`,
  374. );
  375. if (amountOut.lt(stopLoss)) {
  376. break;
  377. }
  378. if (amountOut.gt(takeProfit)) {
  379. break;
  380. }
  381. await sleep(this.config.priceCheckInterval);
  382. } catch (e) {
  383. logger.trace({ mint: poolKeys.baseMint.toString(), e }, `Failed to check token price`);
  384. } finally {
  385. timesChecked++;
  386. }
  387. } while (timesChecked < timesToCheck);
  388. }
  389. }