buy.ts 14 KB

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