bot.ts 14 KB

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