buy.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import {
  2. Liquidity,
  3. LIQUIDITY_STATE_LAYOUT_V4,
  4. LiquidityPoolKeys,
  5. LiquidityStateV4,
  6. MARKET_STATE_LAYOUT_V2,
  7. MARKET_STATE_LAYOUT_V3,
  8. MarketStateV3,
  9. Token,
  10. TokenAmount,
  11. } from '@raydium-io/raydium-sdk';
  12. import {
  13. createAssociatedTokenAccountIdempotentInstruction,
  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 {
  28. getAllAccountsV4,
  29. getTokenAccounts,
  30. RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
  31. OPENBOOK_PROGRAM_ID,
  32. createPoolKeys,
  33. } from './liquidity';
  34. import { retrieveEnvVariable } from './utils';
  35. import { getAllMarketsV3, MinimalMarketLayoutV3 } from './market';
  36. import pino from 'pino';
  37. import bs58 from 'bs58';
  38. const transport = pino.transport({
  39. targets: [
  40. /*
  41. {
  42. level: 'trace',
  43. target: 'pino/file',
  44. options: {
  45. destination: 'buy.log',
  46. },
  47. },
  48. */
  49. {
  50. level: 'trace',
  51. target: 'pino-pretty',
  52. options: {},
  53. },
  54. ],
  55. });
  56. export const logger = pino(
  57. {
  58. redact: ['poolKeys'],
  59. serializers: {
  60. error: pino.stdSerializers.err,
  61. },
  62. base: undefined,
  63. },
  64. transport,
  65. );
  66. const network = 'mainnet-beta';
  67. const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
  68. const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable(
  69. 'RPC_WEBSOCKET_ENDPOINT',
  70. logger,
  71. );
  72. const solanaConnection = new Connection(RPC_ENDPOINT, {
  73. wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
  74. });
  75. export type MinimalTokenAccountData = {
  76. mint: PublicKey;
  77. address: PublicKey;
  78. ata: PublicKey;
  79. poolKeys?: LiquidityPoolKeys;
  80. market?: MinimalMarketLayoutV3;
  81. };
  82. let existingLiquidityPools: Set<string> = new Set<string>();
  83. let existingOpenBookMarkets: Set<string> = new Set<string>();
  84. let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<
  85. string,
  86. MinimalTokenAccountData
  87. >();
  88. let wallet: Keypair;
  89. let quoteToken: Token;
  90. let quoteTokenAssociatedAddress: PublicKey;
  91. let quoteAmount: TokenAmount;
  92. let commitment: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
  93. async function init(): Promise<void> {
  94. // get wallet
  95. const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
  96. wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY));
  97. logger.info(`Wallet Address: ${wallet.publicKey}`);
  98. // get quote mint and amount
  99. const QUOTE_MINT = retrieveEnvVariable('QUOTE_MINT', logger);
  100. const QUOTE_AMOUNT = retrieveEnvVariable('QUOTE_AMOUNT', logger);
  101. switch (QUOTE_MINT) {
  102. case 'WSOL': {
  103. quoteToken = Token.WSOL;
  104. quoteAmount = new TokenAmount(Token.WSOL, QUOTE_AMOUNT, false);
  105. break;
  106. }
  107. case 'USDC': {
  108. quoteToken = new Token(
  109. TOKEN_PROGRAM_ID,
  110. new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
  111. 6,
  112. 'USDC',
  113. 'USDC',
  114. );
  115. quoteAmount = new TokenAmount(quoteToken, QUOTE_AMOUNT, false);
  116. break;
  117. }
  118. default: {
  119. throw new Error(
  120. `Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`,
  121. );
  122. }
  123. }
  124. logger.info(
  125. `Script will buy all new tokens using ${QUOTE_MINT}. Amount that will be used to buy each token is: ${quoteAmount.toFixed().toString()}`
  126. );
  127. // get all existing liquidity pools
  128. const allLiquidityPools = await getAllAccountsV4(
  129. solanaConnection,
  130. quoteToken.mint,
  131. commitment,
  132. );
  133. existingLiquidityPools = new Set(
  134. allLiquidityPools.map((p) => p.id.toString()),
  135. );
  136. // get all open-book markets
  137. const allMarkets = await getAllMarketsV3(solanaConnection, quoteToken.mint, commitment);
  138. existingOpenBookMarkets = new Set(allMarkets.map((p) => p.id.toString()));
  139. const tokenAccounts = await getTokenAccounts(
  140. solanaConnection,
  141. wallet.publicKey,
  142. commitment,
  143. );
  144. logger.info(
  145. `Total ${quoteToken.symbol} markets ${existingOpenBookMarkets.size}`,
  146. );
  147. logger.info(
  148. `Total ${quoteToken.symbol} pools ${existingLiquidityPools.size}`,
  149. );
  150. // check existing wallet for associated token account of quote mint
  151. for (const ta of tokenAccounts) {
  152. existingTokenAccounts.set(ta.accountInfo.mint.toString(), <
  153. MinimalTokenAccountData
  154. >{
  155. mint: ta.accountInfo.mint,
  156. address: ta.pubkey,
  157. });
  158. }
  159. const tokenAccount = tokenAccounts.find(
  160. (acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString(),
  161. )!;
  162. if (!tokenAccount) {
  163. throw new Error(
  164. `No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`,
  165. );
  166. }
  167. quoteTokenAssociatedAddress = tokenAccount.pubkey;
  168. }
  169. export async function processRaydiumPool(updatedAccountInfo: KeyedAccountInfo) {
  170. let accountData: LiquidityStateV4 | undefined;
  171. try {
  172. accountData = LIQUIDITY_STATE_LAYOUT_V4.decode(
  173. updatedAccountInfo.accountInfo.data,
  174. );
  175. await buy(updatedAccountInfo.accountId, accountData);
  176. } catch (e) {
  177. logger.error({ ...accountData, error: e }, `Failed to process pool`);
  178. }
  179. }
  180. export async function processOpenBookMarket(
  181. updatedAccountInfo: KeyedAccountInfo,
  182. ) {
  183. let accountData: MarketStateV3 | undefined;
  184. try {
  185. accountData = MARKET_STATE_LAYOUT_V3.decode(
  186. updatedAccountInfo.accountInfo.data,
  187. );
  188. // to be competitive, we collect market data before buying the token...
  189. if (existingTokenAccounts.has(accountData.baseMint.toString())) {
  190. return;
  191. }
  192. const ata = getAssociatedTokenAddressSync(
  193. accountData.baseMint,
  194. wallet.publicKey,
  195. );
  196. existingTokenAccounts.set(accountData.baseMint.toString(), <
  197. MinimalTokenAccountData
  198. >{
  199. address: ata,
  200. mint: accountData.baseMint,
  201. market: <MinimalMarketLayoutV3>{
  202. bids: accountData.bids,
  203. asks: accountData.asks,
  204. eventQueue: accountData.eventQueue,
  205. },
  206. });
  207. } catch (e) {
  208. logger.error({ ...accountData, error: e }, `Failed to process market`);
  209. }
  210. }
  211. async function buy(
  212. accountId: PublicKey,
  213. accountData: LiquidityStateV4,
  214. ): Promise<void> {
  215. const tokenAccount = existingTokenAccounts.get(
  216. accountData.baseMint.toString(),
  217. );
  218. if (!tokenAccount) {
  219. return;
  220. }
  221. tokenAccount.poolKeys = createPoolKeys(
  222. accountId,
  223. accountData,
  224. tokenAccount.market!,
  225. );
  226. const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
  227. {
  228. poolKeys: tokenAccount.poolKeys,
  229. userKeys: {
  230. tokenAccountIn: quoteTokenAssociatedAddress,
  231. tokenAccountOut: tokenAccount.address,
  232. owner: wallet.publicKey,
  233. },
  234. amountIn: quoteAmount.raw,
  235. minAmountOut: 0,
  236. },
  237. tokenAccount.poolKeys.version,
  238. );
  239. const latestBlockhash = await solanaConnection.getLatestBlockhash({
  240. commitment: commitment,
  241. });
  242. const messageV0 = new TransactionMessage({
  243. payerKey: wallet.publicKey,
  244. recentBlockhash: latestBlockhash.blockhash,
  245. instructions: [
  246. ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
  247. ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30000 }),
  248. createAssociatedTokenAccountIdempotentInstruction(
  249. wallet.publicKey,
  250. tokenAccount.address,
  251. wallet.publicKey,
  252. accountData.baseMint,
  253. ),
  254. ...innerTransaction.instructions,
  255. ],
  256. }).compileToV0Message();
  257. const transaction = new VersionedTransaction(messageV0);
  258. transaction.sign([wallet, ...innerTransaction.signers]);
  259. const signature = await solanaConnection.sendRawTransaction(
  260. transaction.serialize(),
  261. {
  262. maxRetries: 5,
  263. preflightCommitment: commitment,
  264. },
  265. );
  266. logger.info(
  267. {
  268. mint: accountData.baseMint,
  269. url: `https://solscan.io/tx/${signature}?cluster=${network}`,
  270. },
  271. 'Buy',
  272. );
  273. }
  274. const runListener = async () => {
  275. await init();
  276. const raydiumSubscriptionId = solanaConnection.onProgramAccountChange(
  277. RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
  278. async (updatedAccountInfo) => {
  279. const existing = existingLiquidityPools.has(
  280. updatedAccountInfo.accountId.toString(),
  281. );
  282. if (!existing) {
  283. existingLiquidityPools.add(updatedAccountInfo.accountId.toString());
  284. const _ = processRaydiumPool(updatedAccountInfo);
  285. }
  286. },
  287. commitment,
  288. [
  289. { dataSize: LIQUIDITY_STATE_LAYOUT_V4.span },
  290. {
  291. memcmp: {
  292. offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
  293. bytes: quoteToken.mint.toBase58(),
  294. },
  295. },
  296. {
  297. memcmp: {
  298. offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'),
  299. bytes: OPENBOOK_PROGRAM_ID.toBase58(),
  300. },
  301. },
  302. {
  303. memcmp: {
  304. offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'),
  305. bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]),
  306. },
  307. },
  308. ],
  309. );
  310. const openBookSubscriptionId = solanaConnection.onProgramAccountChange(
  311. OPENBOOK_PROGRAM_ID,
  312. async (updatedAccountInfo) => {
  313. const existing = existingOpenBookMarkets.has(
  314. updatedAccountInfo.accountId.toString(),
  315. );
  316. if (!existing) {
  317. existingOpenBookMarkets.add(updatedAccountInfo.accountId.toString());
  318. const _ = processOpenBookMarket(updatedAccountInfo);
  319. }
  320. },
  321. commitment,
  322. [
  323. { dataSize: MARKET_STATE_LAYOUT_V2.span },
  324. {
  325. memcmp: {
  326. offset: MARKET_STATE_LAYOUT_V2.offsetOf('quoteMint'),
  327. bytes: quoteToken.mint.toBase58(),
  328. },
  329. },
  330. ],
  331. );
  332. logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`);
  333. logger.info(`Listening for open book changes: ${openBookSubscriptionId}`);
  334. };
  335. runListener();