-
-
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\":"); | |
} |
@endrsmar Hi Martin, thanks for your sharing the script. I am trying to find out the Liquidity of the new created LP? Can you give me some instructions to find it out?
Thank you so much!
@lmharpae you're right about it. It's the fastest way to listen to new pools
https://github.com/warp-id/solana-trading-bot uses following functions to monitor the new pools.
private async subscribeToRaydiumPools(config: { quoteToken: Token }) {
return this.connection.onProgramAccountChange(
RAYDIUM_PROGRAM_ID.AmmV4,
async (updatedAccountInfo) => {
this.emit('pool', updatedAccountInfo);
},
this.connection.commitment,
[
{ dataSize: LIQUIDITY_STATE_LAYOUT_V4.span },
{
memcmp: {
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
bytes: config.quoteToken.mint.toBase58(),
},
},
{
memcmp: {
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'),
bytes: RAYDIUM_PROGRAM_ID.OPENBOOK_MARKET.toBase58(),
},
},
{
memcmp: {
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'),
bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]),
},
},
],
);
}
listeners.on('pool', async (updatedAccountInfo: KeyedAccountInfo) => {
const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data);
const poolOpenTime = parseInt(poolState.poolOpenTime.toString());
const exists = await poolCache.get(poolState.baseMint.toString());
// Pool is created after the bot started
if (!exists && poolOpenTime > runTimestamp) {
logger.debug({accountId: updatedAccountInfo.accountId}, 'A new pool was found.');
poolCache.save(updatedAccountInfo.accountId.toString(), poolState);
await bot.buy(updatedAccountInfo.accountId, poolState);
}
});
@lmharpae you're right about it. It's the fastest way to listen to new pools https://github.com/warp-id/solana-trading-bot uses following functions to monitor the new pools.
private async subscribeToRaydiumPools(config: { quoteToken: Token }) { return this.connection.onProgramAccountChange( RAYDIUM_PROGRAM_ID.AmmV4, async (updatedAccountInfo) => { this.emit('pool', updatedAccountInfo); }, this.connection.commitment, [ { dataSize: LIQUIDITY_STATE_LAYOUT_V4.span }, { memcmp: { offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'), bytes: config.quoteToken.mint.toBase58(), }, }, { memcmp: { offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'), bytes: RAYDIUM_PROGRAM_ID.OPENBOOK_MARKET.toBase58(), }, }, { memcmp: { offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'), bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]), }, }, ], ); }listeners.on('pool', async (updatedAccountInfo: KeyedAccountInfo) => { const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data); const poolOpenTime = parseInt(poolState.poolOpenTime.toString()); const exists = await poolCache.get(poolState.baseMint.toString()); // Pool is created after the bot started if (!exists && poolOpenTime > runTimestamp) { logger.debug({accountId: updatedAccountInfo.accountId}, 'A new pool was found.'); poolCache.save(updatedAccountInfo.accountId.toString(), poolState); await bot.buy(updatedAccountInfo.accountId, poolState); } });
Nice! How does your sell logic work? Doing limits or monitoring the token?
it's monitoring the token account
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
Hi all, Hope someone can help me swap without pool key or pool information. I accept the swap failure, and swap continuously until LP is added.