buy.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import {
  2. BigNumberish,
  3. Liquidity,
  4. LIQUIDITY_STATE_LAYOUT_V4,
  5. LiquidityPoolKeys,
  6. LiquidityStateV4,
  7. MARKET_STATE_LAYOUT_V3,
  8. MarketStateV3,
  9. Token,
  10. TokenAmount,
  11. } from '@raydium-io/raydium-sdk';
  12. import {
  13. AccountLayout,
  14. createAssociatedTokenAccountIdempotentInstruction,
  15. createCloseAccountInstruction,
  16. getAssociatedTokenAddressSync,
  17. TOKEN_PROGRAM_ID,
  18. } from '@solana/spl-token';
  19. import {
  20. Keypair,
  21. Connection,
  22. PublicKey,
  23. ComputeBudgetProgram,
  24. KeyedAccountInfo,
  25. TransactionMessage,
  26. VersionedTransaction,
  27. Commitment,
  28. } from '@solana/web3.js';
  29. import { getTokenAccounts, RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, OPENBOOK_PROGRAM_ID, createPoolKeys } from './liquidity';
  30. import { retrieveEnvVariable } from './utils';
  31. import { getMinimalMarketV3, MinimalMarketLayoutV3 } from './market';
  32. import { MintLayout } from './types';
  33. import pino from 'pino';
  34. import bs58 from 'bs58';
  35. import * as fs from 'fs';
  36. import * as path from 'path';
  37. const transport = pino.transport({
  38. target: 'pino-pretty',
  39. });
  40. export const logger = pino(
  41. {
  42. level: 'info',
  43. redact: ['poolKeys'],
  44. serializers: {
  45. error: pino.stdSerializers.err,
  46. },
  47. base: undefined,
  48. },
  49. transport,
  50. );
  51. const network = 'mainnet-beta';
  52. const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
  53. const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOINT', logger);
  54. const LOG_LEVEL = retrieveEnvVariable('LOG_LEVEL', logger);
  55. const solanaConnection = new Connection(RPC_ENDPOINT, {
  56. wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
  57. });
  58. export type MinimalTokenAccountData = {
  59. mint: PublicKey;
  60. address: PublicKey;
  61. poolKeys?: LiquidityPoolKeys;
  62. market?: MinimalMarketLayoutV3;
  63. };
  64. let existingLiquidityPools: Set<string> = new Set<string>();
  65. let existingOpenBookMarkets: Set<string> = new Set<string>();
  66. let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<string, MinimalTokenAccountData>();
  67. let wallet: Keypair;
  68. let quoteToken: Token;
  69. let quoteTokenAssociatedAddress: PublicKey;
  70. let quoteAmount: TokenAmount;
  71. let commitment: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
  72. const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNCED', logger) === 'true';
  73. const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
  74. const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger));
  75. const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger) === 'true';
  76. const MAX_SELL_RETRIES = Number(retrieveEnvVariable('MAX_SELL_RETRIES', logger));
  77. const AUTO_SELL_DELAY = Number(retrieveEnvVariable('AUTO_SELL_DELAY', logger));
  78. let snipeList: string[] = [];
  79. async function init(): Promise<void> {
  80. logger.level = LOG_LEVEL;
  81. // get wallet
  82. const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
  83. wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY));
  84. logger.info(`Wallet Address: ${wallet.publicKey}`);
  85. // get quote mint and amount
  86. const QUOTE_MINT = retrieveEnvVariable('QUOTE_MINT', logger);
  87. const QUOTE_AMOUNT = retrieveEnvVariable('QUOTE_AMOUNT', logger);
  88. switch (QUOTE_MINT) {
  89. case 'WSOL': {
  90. quoteToken = Token.WSOL;
  91. quoteAmount = new TokenAmount(Token.WSOL, QUOTE_AMOUNT, false);
  92. break;
  93. }
  94. case 'USDC': {
  95. quoteToken = new Token(
  96. TOKEN_PROGRAM_ID,
  97. new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
  98. 6,
  99. 'USDC',
  100. 'USDC',
  101. );
  102. quoteAmount = new TokenAmount(quoteToken, QUOTE_AMOUNT, false);
  103. break;
  104. }
  105. default: {
  106. throw new Error(`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`);
  107. }
  108. }
  109. logger.info(
  110. `Script will buy all new tokens using ${QUOTE_MINT}. Amount that will be used to buy each token is: ${quoteAmount.toFixed().toString()}`,
  111. );
  112. // check existing wallet for associated token account of quote mint
  113. const tokenAccounts = await getTokenAccounts(solanaConnection, wallet.publicKey, commitment);
  114. for (const ta of tokenAccounts) {
  115. existingTokenAccounts.set(ta.accountInfo.mint.toString(), <MinimalTokenAccountData>{
  116. mint: ta.accountInfo.mint,
  117. address: ta.pubkey,
  118. });
  119. }
  120. const tokenAccount = tokenAccounts.find((acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString())!;
  121. if (!tokenAccount) {
  122. throw new Error(`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`);
  123. }
  124. quoteTokenAssociatedAddress = tokenAccount.pubkey;
  125. // load tokens to snipe
  126. loadSnipeList();
  127. }
  128. function saveTokenAccount(mint: PublicKey, accountData: MinimalMarketLayoutV3) {
  129. const ata = getAssociatedTokenAddressSync(mint, wallet.publicKey);
  130. const tokenAccount = <MinimalTokenAccountData>{
  131. address: ata,
  132. mint: mint,
  133. market: <MinimalMarketLayoutV3>{
  134. bids: accountData.bids,
  135. asks: accountData.asks,
  136. eventQueue: accountData.eventQueue,
  137. },
  138. };
  139. existingTokenAccounts.set(mint.toString(), tokenAccount);
  140. return tokenAccount;
  141. }
  142. export async function processRaydiumPool(id: PublicKey, poolState: LiquidityStateV4) {
  143. if (!shouldBuy(poolState.baseMint.toString())) {
  144. return;
  145. }
  146. if (CHECK_IF_MINT_IS_RENOUNCED) {
  147. const mintOption = await checkMintable(poolState.baseMint);
  148. if (mintOption !== true) {
  149. logger.warn({ mint: poolState.baseMint }, 'Skipping, owner can mint tokens!');
  150. return;
  151. }
  152. }
  153. await buy(id, poolState);
  154. }
  155. export async function checkMintable(vault: PublicKey): Promise<boolean | undefined> {
  156. try {
  157. let { data } = (await solanaConnection.getAccountInfo(vault)) || {};
  158. if (!data) {
  159. return;
  160. }
  161. const deserialize = MintLayout.decode(data);
  162. return deserialize.mintAuthorityOption === 0;
  163. } catch (e) {
  164. logger.debug(e);
  165. logger.error({ mint: vault }, `Failed to check if mint is renounced`);
  166. }
  167. }
  168. export async function processOpenBookMarket(updatedAccountInfo: KeyedAccountInfo) {
  169. let accountData: MarketStateV3 | undefined;
  170. try {
  171. accountData = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data);
  172. // to be competitive, we collect market data before buying the token...
  173. if (existingTokenAccounts.has(accountData.baseMint.toString())) {
  174. return;
  175. }
  176. saveTokenAccount(accountData.baseMint, accountData);
  177. } catch (e) {
  178. logger.debug(e);
  179. logger.error({ mint: accountData?.baseMint }, `Failed to process market`);
  180. }
  181. }
  182. async function buy(accountId: PublicKey, accountData: LiquidityStateV4): Promise<void> {
  183. try {
  184. let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
  185. if (!tokenAccount) {
  186. // it's possible that we didn't have time to fetch open book data
  187. const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, commitment);
  188. tokenAccount = saveTokenAccount(accountData.baseMint, market);
  189. }
  190. tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!);
  191. const { innerTransaction } = Liquidity.makeSwapFixedInInstruction(
  192. {
  193. poolKeys: tokenAccount.poolKeys,
  194. userKeys: {
  195. tokenAccountIn: quoteTokenAssociatedAddress,
  196. tokenAccountOut: tokenAccount.address,
  197. owner: wallet.publicKey,
  198. },
  199. amountIn: quoteAmount.raw,
  200. minAmountOut: 0,
  201. },
  202. tokenAccount.poolKeys.version,
  203. );
  204. const latestBlockhash = await solanaConnection.getLatestBlockhash({
  205. commitment: commitment,
  206. });
  207. const messageV0 = new TransactionMessage({
  208. payerKey: wallet.publicKey,
  209. recentBlockhash: latestBlockhash.blockhash,
  210. instructions: [
  211. ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 421197 }),
  212. ComputeBudgetProgram.setComputeUnitLimit({ units: 101337 }),
  213. createAssociatedTokenAccountIdempotentInstruction(
  214. wallet.publicKey,
  215. tokenAccount.address,
  216. wallet.publicKey,
  217. accountData.baseMint,
  218. ),
  219. ...innerTransaction.instructions,
  220. ],
  221. }).compileToV0Message();
  222. const transaction = new VersionedTransaction(messageV0);
  223. transaction.sign([wallet, ...innerTransaction.signers]);
  224. const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
  225. preflightCommitment: commitment,
  226. });
  227. logger.info({ mint: accountData.baseMint, signature }, `Sent buy tx`);
  228. const confirmation = await solanaConnection.confirmTransaction(
  229. {
  230. signature,
  231. lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
  232. blockhash: latestBlockhash.blockhash,
  233. },
  234. commitment,
  235. );
  236. if (!confirmation.value.err) {
  237. logger.info(
  238. {
  239. mint: accountData.baseMint,
  240. signature,
  241. url: `https://solscan.io/tx/${signature}?cluster=${network}`,
  242. },
  243. `Confirmed buy tx`,
  244. );
  245. } else {
  246. logger.debug(confirmation.value.err);
  247. logger.info({ mint: accountData.baseMint, signature }, `Error confirming buy tx`);
  248. }
  249. } catch (e) {
  250. logger.debug(e);
  251. logger.error({ mint: accountData.baseMint }, `Failed to buy token`);
  252. }
  253. }
  254. async function sell(accountId: PublicKey, mint: PublicKey, amount: BigNumberish): Promise<void> {
  255. let sold = false;
  256. let retries = 0;
  257. if (AUTO_SELL_DELAY > 0) {
  258. await new Promise((resolve) => setTimeout(resolve, AUTO_SELL_DELAY));
  259. }
  260. do {
  261. try {
  262. const tokenAccount = existingTokenAccounts.get(mint.toString());
  263. if (!tokenAccount) {
  264. return;
  265. }
  266. if (!tokenAccount.poolKeys) {
  267. logger.warn({ mint }, 'No pool keys found');
  268. return;
  269. }
  270. if (amount === 0) {
  271. logger.info(
  272. {
  273. mint: tokenAccount.mint,
  274. },
  275. `Empty balance, can't sell`,
  276. );
  277. return;
  278. }
  279. const { innerTransaction } = Liquidity.makeSwapFixedInInstruction(
  280. {
  281. poolKeys: tokenAccount.poolKeys!,
  282. userKeys: {
  283. tokenAccountOut: quoteTokenAssociatedAddress,
  284. tokenAccountIn: tokenAccount.address,
  285. owner: wallet.publicKey,
  286. },
  287. amountIn: amount,
  288. minAmountOut: 0,
  289. },
  290. tokenAccount.poolKeys!.version,
  291. );
  292. const latestBlockhash = await solanaConnection.getLatestBlockhash({
  293. commitment: commitment,
  294. });
  295. const messageV0 = new TransactionMessage({
  296. payerKey: wallet.publicKey,
  297. recentBlockhash: latestBlockhash.blockhash,
  298. instructions: [
  299. ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 421197 }),
  300. ComputeBudgetProgram.setComputeUnitLimit({ units: 101337 }),
  301. ...innerTransaction.instructions,
  302. createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey),
  303. ],
  304. }).compileToV0Message();
  305. const transaction = new VersionedTransaction(messageV0);
  306. transaction.sign([wallet, ...innerTransaction.signers]);
  307. const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
  308. preflightCommitment: commitment,
  309. });
  310. logger.info({ mint, signature }, `Sent sell tx`);
  311. const confirmation = await solanaConnection.confirmTransaction(
  312. {
  313. signature,
  314. lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
  315. blockhash: latestBlockhash.blockhash,
  316. },
  317. commitment,
  318. );
  319. if (confirmation.value.err) {
  320. logger.debug(confirmation.value.err);
  321. logger.info({ mint, signature }, `Error confirming sell tx`);
  322. continue;
  323. }
  324. logger.info(
  325. {
  326. dex: `https://dexscreener.com/solana/${mint}?maker=${wallet.publicKey}`,
  327. mint,
  328. signature,
  329. url: `https://solscan.io/tx/${signature}?cluster=${network}`,
  330. },
  331. `Confirmed sell tx`,
  332. );
  333. sold = true;
  334. } catch (e: any) {
  335. retries++;
  336. logger.debug(e);
  337. logger.error({ mint }, `Failed to sell token, retry: ${retries}/${MAX_SELL_RETRIES}`);
  338. }
  339. } while (!sold && retries < MAX_SELL_RETRIES);
  340. }
  341. function loadSnipeList() {
  342. if (!USE_SNIPE_LIST) {
  343. return;
  344. }
  345. const count = snipeList.length;
  346. const data = fs.readFileSync(path.join(__dirname, 'snipe-list.txt'), 'utf-8');
  347. snipeList = data
  348. .split('\n')
  349. .map((a) => a.trim())
  350. .filter((a) => a);
  351. if (snipeList.length != count) {
  352. logger.info(`Loaded snipe list: ${snipeList.length}`);
  353. }
  354. }
  355. function shouldBuy(key: string): boolean {
  356. return USE_SNIPE_LIST ? snipeList.includes(key) : true;
  357. }
  358. const runListener = async () => {
  359. await init();
  360. const runTimestamp = Math.floor(new Date().getTime() / 1000);
  361. const raydiumSubscriptionId = solanaConnection.onProgramAccountChange(
  362. RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
  363. async (updatedAccountInfo) => {
  364. const key = updatedAccountInfo.accountId.toString();
  365. const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data);
  366. const poolOpenTime = parseInt(poolState.poolOpenTime.toString());
  367. const existing = existingLiquidityPools.has(key);
  368. if (poolOpenTime > runTimestamp && !existing) {
  369. existingLiquidityPools.add(key);
  370. const _ = processRaydiumPool(updatedAccountInfo.accountId, poolState);
  371. }
  372. },
  373. commitment,
  374. [
  375. { dataSize: LIQUIDITY_STATE_LAYOUT_V4.span },
  376. {
  377. memcmp: {
  378. offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
  379. bytes: quoteToken.mint.toBase58(),
  380. },
  381. },
  382. {
  383. memcmp: {
  384. offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'),
  385. bytes: OPENBOOK_PROGRAM_ID.toBase58(),
  386. },
  387. },
  388. {
  389. memcmp: {
  390. offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'),
  391. bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]),
  392. },
  393. },
  394. ],
  395. );
  396. const openBookSubscriptionId = solanaConnection.onProgramAccountChange(
  397. OPENBOOK_PROGRAM_ID,
  398. async (updatedAccountInfo) => {
  399. const key = updatedAccountInfo.accountId.toString();
  400. const existing = existingOpenBookMarkets.has(key);
  401. if (!existing) {
  402. existingOpenBookMarkets.add(key);
  403. const _ = processOpenBookMarket(updatedAccountInfo);
  404. }
  405. },
  406. commitment,
  407. [
  408. { dataSize: MARKET_STATE_LAYOUT_V3.span },
  409. {
  410. memcmp: {
  411. offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'),
  412. bytes: quoteToken.mint.toBase58(),
  413. },
  414. },
  415. ],
  416. );
  417. if (AUTO_SELL) {
  418. const walletSubscriptionId = solanaConnection.onProgramAccountChange(
  419. TOKEN_PROGRAM_ID,
  420. async (updatedAccountInfo) => {
  421. const accountData = AccountLayout.decode(updatedAccountInfo.accountInfo!.data);
  422. if (updatedAccountInfo.accountId.equals(quoteTokenAssociatedAddress)) {
  423. return;
  424. }
  425. const _ = sell(updatedAccountInfo.accountId, accountData.mint, accountData.amount);
  426. },
  427. commitment,
  428. [
  429. {
  430. dataSize: 165,
  431. },
  432. {
  433. memcmp: {
  434. offset: 32,
  435. bytes: wallet.publicKey.toBase58(),
  436. },
  437. },
  438. ],
  439. );
  440. logger.info(`Listening for wallet changes: ${walletSubscriptionId}`);
  441. }
  442. logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`);
  443. logger.info(`Listening for open book changes: ${openBookSubscriptionId}`);
  444. if (USE_SNIPE_LIST) {
  445. setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
  446. }
  447. };
  448. runListener();