buy.ts 15 KB

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