bot.ts 13 KB

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