-
-
Save endrsmar/684c336c3729ec4472b2f337c50c3cdb to your computer and use it in GitHub Desktop.
import { LiquidityPoolKeysV4, MARKET_STATE_LAYOUT_V3, Market, TOKEN_PROGRAM_ID } from "@raydium-io/raydium-sdk"; | |
import { Connection, Logs, ParsedInnerInstruction, ParsedInstruction, ParsedTransactionWithMeta, PartiallyDecodedInstruction, PublicKey } from "@solana/web3.js"; | |
const RPC_ENDPOINT = 'https://api.mainnet-beta.solana.com'; | |
const RAYDIUM_POOL_V4_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'; | |
const SERUM_OPENBOOK_PROGRAM_ID = 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX'; | |
const SOL_MINT = 'So11111111111111111111111111111111111111112'; | |
const SOL_DECIMALS = 9; | |
const connection = new Connection(RPC_ENDPOINT); | |
const seenTransactions : Array<string> = []; // The log listener is sometimes triggered multiple times for a single transaction, don't react to tranasctions we've already seen | |
subscribeToNewRaydiumPools(); | |
function subscribeToNewRaydiumPools() : void | |
{ | |
connection.onLogs(new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID), async (txLogs: Logs) => { | |
if (seenTransactions.includes(txLogs.signature)) { | |
return; | |
} | |
seenTransactions.push(txLogs.signature); | |
if (!findLogEntry('init_pc_amount', txLogs.logs)) { | |
return; // If "init_pc_amount" is not in log entries then it's not LP initialization transaction | |
} | |
const poolKeys = await fetchPoolKeysForLPInitTransactionHash(txLogs.signature); // With poolKeys you can do a swap | |
console.log(poolKeys); | |
}); | |
console.log('Listening to new pools...'); | |
} | |
function findLogEntry(needle: string, logEntries: Array<string>) : string|null | |
{ | |
for (let i = 0; i < logEntries.length; ++i) { | |
if (logEntries[i].includes(needle)) { | |
return logEntries[i]; | |
} | |
} | |
return null; | |
} | |
async function fetchPoolKeysForLPInitTransactionHash(txSignature: string) : Promise<LiquidityPoolKeysV4> | |
{ | |
const tx = await connection.getParsedTransaction(txSignature, {maxSupportedTransactionVersion: 0}); | |
if (!tx) { | |
throw new Error('Failed to fetch transaction with signature ' + txSignature); | |
} | |
const poolInfo = parsePoolInfoFromLpTransaction(tx); | |
const marketInfo = await fetchMarketInfo(poolInfo.marketId); | |
return { | |
id: poolInfo.id, | |
baseMint: poolInfo.baseMint, | |
quoteMint: poolInfo.quoteMint, | |
lpMint: poolInfo.lpMint, | |
baseDecimals: poolInfo.baseDecimals, | |
quoteDecimals: poolInfo.quoteDecimals, | |
lpDecimals: poolInfo.lpDecimals, | |
version: 4, | |
programId: poolInfo.programId, | |
authority: poolInfo.authority, | |
openOrders: poolInfo.openOrders, | |
targetOrders: poolInfo.targetOrders, | |
baseVault: poolInfo.baseVault, | |
quoteVault: poolInfo.quoteVault, | |
withdrawQueue: poolInfo.withdrawQueue, | |
lpVault: poolInfo.lpVault, | |
marketVersion: 3, | |
marketProgramId: poolInfo.marketProgramId, | |
marketId: poolInfo.marketId, | |
marketAuthority: Market.getAssociatedAuthority({programId: poolInfo.marketProgramId, marketId: poolInfo.marketId}).publicKey, | |
marketBaseVault: marketInfo.baseVault, | |
marketQuoteVault: marketInfo.quoteVault, | |
marketBids: marketInfo.bids, | |
marketAsks: marketInfo.asks, | |
marketEventQueue: marketInfo.eventQueue, | |
} as LiquidityPoolKeysV4; | |
} | |
async function fetchMarketInfo(marketId: PublicKey) { | |
const marketAccountInfo = await connection.getAccountInfo(marketId); | |
if (!marketAccountInfo) { | |
throw new Error('Failed to fetch market info for market id ' + marketId.toBase58()); | |
} | |
return MARKET_STATE_LAYOUT_V3.decode(marketAccountInfo.data); | |
} | |
function parsePoolInfoFromLpTransaction(txData: ParsedTransactionWithMeta) | |
{ | |
const initInstruction = findInstructionByProgramId(txData.transaction.message.instructions, new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID)) as PartiallyDecodedInstruction|null; | |
if (!initInstruction) { | |
throw new Error('Failed to find lp init instruction in lp init tx'); | |
} | |
const baseMint = initInstruction.accounts[8]; | |
const baseVault = initInstruction.accounts[10]; | |
const quoteMint = initInstruction.accounts[9]; | |
const quoteVault = initInstruction.accounts[11]; | |
const lpMint = initInstruction.accounts[7]; | |
const baseAndQuoteSwapped = baseMint.toBase58() === SOL_MINT; | |
const lpMintInitInstruction = findInitializeMintInInnerInstructionsByMintAddress(txData.meta?.innerInstructions ?? [], lpMint); | |
if (!lpMintInitInstruction) { | |
throw new Error('Failed to find lp mint init instruction in lp init tx'); | |
} | |
const lpMintInstruction = findMintToInInnerInstructionsByMintAddress(txData.meta?.innerInstructions ?? [], lpMint); | |
if (!lpMintInstruction) { | |
throw new Error('Failed to find lp mint to instruction in lp init tx'); | |
} | |
const baseTransferInstruction = findTransferInstructionInInnerInstructionsByDestination(txData.meta?.innerInstructions ?? [], baseVault, TOKEN_PROGRAM_ID); | |
if (!baseTransferInstruction) { | |
throw new Error('Failed to find base transfer instruction in lp init tx'); | |
} | |
const quoteTransferInstruction = findTransferInstructionInInnerInstructionsByDestination(txData.meta?.innerInstructions ?? [], quoteVault, TOKEN_PROGRAM_ID); | |
if (!quoteTransferInstruction) { | |
throw new Error('Failed to find quote transfer instruction in lp init tx'); | |
} | |
const lpDecimals = lpMintInitInstruction.parsed.info.decimals; | |
const lpInitializationLogEntryInfo = extractLPInitializationLogEntryInfoFromLogEntry(findLogEntry('init_pc_amount', txData.meta?.logMessages ?? []) ?? ''); | |
const basePreBalance = (txData.meta?.preTokenBalances ?? []).find(balance => balance.mint === baseMint.toBase58()); | |
if (!basePreBalance) { | |
throw new Error('Failed to find base tokens preTokenBalance entry to parse the base tokens decimals'); | |
} | |
const baseDecimals = basePreBalance.uiTokenAmount.decimals; | |
return { | |
id: initInstruction.accounts[4], | |
baseMint, | |
quoteMint, | |
lpMint, | |
baseDecimals: baseAndQuoteSwapped ? SOL_DECIMALS : baseDecimals, | |
quoteDecimals: baseAndQuoteSwapped ? baseDecimals : SOL_DECIMALS, | |
lpDecimals, | |
version: 4, | |
programId: new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID), | |
authority: initInstruction.accounts[5], | |
openOrders: initInstruction.accounts[6], | |
targetOrders: initInstruction.accounts[13], | |
baseVault, | |
quoteVault, | |
withdrawQueue: new PublicKey("11111111111111111111111111111111"), | |
lpVault: new PublicKey(lpMintInstruction.parsed.info.account), | |
marketVersion: 3, | |
marketProgramId: initInstruction.accounts[15], | |
marketId: initInstruction.accounts[16], | |
baseReserve: parseInt(baseTransferInstruction.parsed.info.amount), | |
quoteReserve: parseInt(quoteTransferInstruction.parsed.info.amount), | |
lpReserve: parseInt(lpMintInstruction.parsed.info.amount), | |
openTime: lpInitializationLogEntryInfo.open_time, | |
} | |
} | |
function findTransferInstructionInInnerInstructionsByDestination(innerInstructions: Array<ParsedInnerInstruction>, destinationAccount : PublicKey, programId?: PublicKey) : ParsedInstruction|null | |
{ | |
for (let i = 0; i < innerInstructions.length; i++) { | |
for (let y = 0; y < innerInstructions[i].instructions.length; y++) { | |
const instruction = innerInstructions[i].instructions[y] as ParsedInstruction; | |
if (!instruction.parsed) {continue}; | |
if (instruction.parsed.type === 'transfer' && instruction.parsed.info.destination === destinationAccount.toBase58() && (!programId || instruction.programId.equals(programId))) { | |
return instruction; | |
} | |
} | |
} | |
return null; | |
} | |
function findInitializeMintInInnerInstructionsByMintAddress(innerInstructions: Array<ParsedInnerInstruction>, mintAddress: PublicKey) : ParsedInstruction|null | |
{ | |
for (let i = 0; i < innerInstructions.length; i++) { | |
for (let y = 0; y < innerInstructions[i].instructions.length; y++) { | |
const instruction = innerInstructions[i].instructions[y] as ParsedInstruction; | |
if (!instruction.parsed) {continue}; | |
if (instruction.parsed.type === 'initializeMint' && instruction.parsed.info.mint === mintAddress.toBase58()) { | |
return instruction; | |
} | |
} | |
} | |
return null; | |
} | |
function findMintToInInnerInstructionsByMintAddress(innerInstructions: Array<ParsedInnerInstruction>, mintAddress: PublicKey) : ParsedInstruction|null | |
{ | |
for (let i = 0; i < innerInstructions.length; i++) { | |
for (let y = 0; y < innerInstructions[i].instructions.length; y++) { | |
const instruction = innerInstructions[i].instructions[y] as ParsedInstruction; | |
if (!instruction.parsed) {continue}; | |
if (instruction.parsed.type === 'mintTo' && instruction.parsed.info.mint === mintAddress.toBase58()) { | |
return instruction; | |
} | |
} | |
} | |
return null; | |
} | |
function findInstructionByProgramId(instructions: Array<ParsedInstruction|PartiallyDecodedInstruction>, programId: PublicKey) : ParsedInstruction|PartiallyDecodedInstruction|null | |
{ | |
for (let i = 0; i < instructions.length; i++) { | |
if (instructions[i].programId.equals(programId)) { | |
return instructions[i]; | |
} | |
} | |
return null; | |
} | |
function extractLPInitializationLogEntryInfoFromLogEntry(lpLogEntry: string) : {nonce: number, open_time: number, init_pc_amount: number, init_coin_amount: number} { | |
const lpInitializationLogEntryInfoStart = lpLogEntry.indexOf('{'); | |
return JSON.parse(fixRelaxedJsonInLpLogEntry(lpLogEntry.substring(lpInitializationLogEntryInfoStart))); | |
} | |
function fixRelaxedJsonInLpLogEntry(relaxedJson: string) : string | |
{ | |
return relaxedJson.replace(/([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, "$1\"$2\":"); | |
} |
it's monitoring the token account
{ memcmp: { offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'), bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]), }, },
what's the meaning this filter
After tons of testing, I believe
connection.onProgramAccountChange()
is not only a better way since it gives you the pool Id directly, it is also less intensive on the RPC node. This becomes a serious case when using a RPC node by a provider who charges by unit. From testing, by changing toonProgramAccountChange
, unit consumption dropped by about 80%. The reason is simple, an application can write tons of logs for just a single transaction while if you listen to the change itself, it is just one per transaction.
An example usage isconnection.onProgramAccountChange(new PublicKey(RAYDIUM_POOL_V4_PROGRAM_ID), async (updatedAccountInfo) => { const poolId = updatedAccountInfo.accountId.toString(); if (seenTransactions.includes(poolId)) { return } seenTransactions.push(poolId) ... })Yes, your way is actually a better way to get a new pool, but how do we suppose to get the
poolKeys
data?You can see that in endrsmar's code, the
poolKeys
data relies heavily on transaction logs, but I don't find any transaction logs or related data in the givenupdatedAccountInfo
.Also, it seems like this event will trigger a new
updateAcountInfo
when a Token Account is created, not when a Liquidity Pool is created.
Sorry it's coming late. Even if you no longer need it, for the sake of those who may need it later, I do have a function:
// You don't need all these imports I just dropped mine here so you'll know
// where the names in formatAmmKeysByState comes from
const {
LIQUIDITY_STATE_LAYOUT_V4,
Liquidity,
MARKET_STATE_LAYOUT_V3,
Market,
SPL_MINT_LAYOUT,
SPL_ACCOUNT_LAYOUT,
TxVersion,
jsonInfo2PoolKeys,
TokenAmount,
Percent,
Token,
LOOKUP_TABLE_CACHE
} = require("@raydium-io/raydium-sdk");
async function formatAmmKeysByState(account, info, id, launchTime) {
const marketId = info.marketId
const marketAccount = await connection.getAccountInfo(marketId)
if (marketAccount === null) throw Error(' get market info error')
const marketInfo = MARKET_STATE_LAYOUT_V3.decode(marketAccount.data)
const lpMint = info.lpMint
const lpMintAccount = await connection.getAccountInfo(lpMint)
if (lpMintAccount === null) throw Error(' get lp mint info error')
const lpMintInfo = SPL_MINT_LAYOUT.decode(lpMintAccount.data)
const solIsBase = info.baseMint.toString() === config.baseCurrency
return {
id,
baseMint: info.baseMint.toString(),
quoteMint: info.quoteMint.toString(),
lpMint: info.lpMint.toString(),
baseDecimals: info.baseDecimal.toNumber(),
quoteDecimals: info.quoteDecimal.toNumber(),
lpDecimals: lpMintInfo.decimals,
version: 4,
programId: account.owner.toString(),
authority: Liquidity.getAssociatedAuthority({ programId: account.owner }).publicKey.toString(),
openOrders: info.openOrders.toString(),
targetOrders: info.targetOrders.toString(),
baseVault: info.baseVault.toString(),
quoteVault: info.quoteVault.toString(),
withdrawQueue: info.withdrawQueue.toString(),
lpVault: info.lpVault.toString(),
marketVersion: 3,
marketProgramId: info.marketProgramId.toString(),
marketId: info.marketId.toString(),
marketAuthority: Market.getAssociatedAuthority({ programId: info.marketProgramId, marketId: info.marketId }).publicKey.toString(),
marketBaseVault: marketInfo.baseVault.toString(),
marketQuoteVault: marketInfo.quoteVault.toString(),
marketBids: marketInfo.bids.toString(),
marketAsks: marketInfo.asks.toString(),
marketEventQueue: marketInfo.eventQueue.toString(),
lookupTableAccount: PublicKey.default.toString(),
launchTime,
supply: lpMintInfo.supply.toNumber(),
realBaseMint: solIsBase ? info.quoteMint.toString() : info.baseMint.toString(),
realBaseDecimals: solIsBase ? info.quoteDecimal.toString() : info.baseDecimal.toString(),
}
}
To call the function, you need to first retrieve the pool state:
const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(
updatedAccountInfo.accountInfo.data,
);
Then you can call like this:
const poolkeys = await formatAmmKeysByState(updatedAccountInfo.accountInfo, poolState, id, meta.poolCreated)
If you pay close attention, most of what you need is already in the account info notification. It's a less expensive solution compared to processing all logs.
All said, I've come to move on from this kind of solution to Yellowstone Geyser GRPC myself.
Geyser GRPC is the way
https://www.helius.dev/blog/how-to-monitor-a-raydium-liquidity-pool
it's monitoring the token account