Skip to main content

Monitor Solana Programs with Yellowstone gRPC Geyser Plugin (Rust)

Updated on
Apr 23, 2025

44 min read

Overview

Real-time blockchain data is crucial for DeFi developers, trading bot operators, and analysts tracking on-chain activity. In this guide, we'll write a Rust script that monitors Raydium Launchpad transactions (though capable of monitoring any program) using QuickNode's Yellowstone gRPC add-on. Unlike traditional polling methods, our stream-based approach provides ultra-low latency notification for what you care about (e.g., new token launches, AMM migrations, large purchases, etc.).

By the end of this guide, you'll have a functional program monitoring system capable of:

  • Detecting Raydium Launchpad transactions in real-time
  • Parsing transaction data to extract token balances and account interactions
  • Processing inner instructions and transaction details
  • Filtering for specific instruction types (sales, AMM migrations)
  • The script can be easily modified to monitor any Solana program you choose

Prerequisites

DependencyVersion
rustc1.85.0
cargo1.85.0
tokio1.28.0
yellowstone-grpc-client6.0.0
yellowstone-grpc-proto6.0.0
futures0.3
log0.4
env_logger0.11.8
bs580.5.0
tonic0.12.3

What is Raydium Launchpad?

Raydium Launchpad is Raydium's new platform for launching new tokens on Solana. It provides a structured way for projects to launch new tokens, automate AMM migration, and earn fees on trading activity.

When tracking Raydium Launchpad activity, there are several transaction types that might be of interest:

  • Initialize - A new token sale setup
  • BuyExactIn/BuyExactOut - Token purchase transactions
  • MigrateToAmm - Migration of tokens to AMM liquidity pools
  • CreateConfig/UpdateConfig - Configuration changes for the platform or specific sales
  • ClaimVestedToken - Token distribution to buyers after vesting periods

Why Use Yellowstone gRPC?

Yellowstone gRPC, based on Solana's Geyser plugin system, provides a streaming interface for real-time Solana data access. Yellowstone provides real-time streaming of:

  • Account updates
  • Transactions
  • Entries
  • Block notifications
  • Slot notifications

Compared to traditional WebSocket implementations, Yellowstone's gRPC interface offers lower latency and higher stability. It also includes unary operations for quick, one-time data retrievals. The combination of gRPC's efficiency and type safety makes Yellowstone particularly well-suited for cloud-based services and database updates. QuickNode supports Yellowstone through our Yellowstone gRPC Marketplace Add-on.

Setting Up the Project

Let's start by creating a new Rust project:

cargo init raydium-launchpad-tracker
cd raydium-launchpad-tracker

You should now have a new directory called raydium-launchpad-tracker with a basic Rust project structure. Inside this directory, you will find a src folder containing a main.rs file. This is where we will write our code. You will also have a Cargo.toml file, which is the configuration file for your Rust project.

Next, update your Cargo.toml file with the necessary dependencies:

[package]
name = "raydium-launchpad-tracker"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1.28", features = ["rt-multi-thread", "macros"] }
yellowstone-grpc-client = "6.0.0"
yellowstone-grpc-proto = "6.0.0"
futures = "0.3"
log = "0.4"
env_logger = "0.11.8"
bs58 = "0.5.0"
tonic = "0.12.3"

Building the Raydium Launchpad Monitor

Our application will consist of several components:

  1. Client setup and connection to Yellowstone gRPC
  2. Subscription configuration for Raydium Launchpad transactions
  3. Transaction parsing and processing
  4. Instruction type detection and filtering

Let's implement these components in our src/main.rs file.

Import Required Libraries

First, we need to import the necessary libraries and modules. Add the following code at the top of your src/main.rs file:

use {
bs58,
futures::{sink::SinkExt, stream::StreamExt},
log::{error, info, warn},
std::{collections::HashMap, env, fmt},
tokio,
tonic::{Status, service::Interceptor, transport::ClientTlsConfig},
yellowstone_grpc_client::GeyserGrpcClient,
yellowstone_grpc_proto::{
geyser::{SubscribeUpdate, SubscribeUpdatePing},
prelude::{
CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions,
subscribe_update::UpdateOneof,
},
},
};

This code imports the necessary libraries for our application, including the gRPC client, logging, and futures for asynchronous programming.

Define Constants and Main Function

Now let's define some important constants

// Constants
const RUST_LOG_LEVEL: &str = "info";
const RAYDIUM_LAUNCHPAD_PROGRAM: &str = "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj";
const TARGET_IX_TYPES: &[RaydiumInstructionType] = &[
// 👇 Uncomment to filter for specific instruction types
// RaydiumInstructionType::Initialize,
// RaydiumInstructionType::MigrateToAmm,
];
// Replace with your QuickNode Yellowstone gRPC endpoint
const ENDPOINT: &str = "https://your-quicknode-endpoint.grpc.solana-mainnet.quiknode.pro:10000";
const AUTH_TOKEN: &str = "your-auth-token"; // 👈 Replace with your token

Let's spell out what these constants do:

  • RUST_LOG_LEVEL: Sets the logging level for the application.
  • RAYDIUM_LAUNCHPAD_PROGRAM: The program ID for Raydium Launchpad. If you want to monitor a different program, replace this with the program ID of your choice.
  • TARGET_IX_TYPES: An array of instruction types to filter for. You can monitor all program transactions by leaving this empty. You can uncomment the specific instruction types you want to monitor (or add other ones). We will define the RaydiumInstructionType enum later. (Note: to monitor other programs, you would need to update this array with the appropriate instruction types for that program.)
  • ENDPOINT: The gRPC endpoint for your QuickNode Yellowstone instance. You can find information on configuring your endpoint in our docs, here.
  • AUTH_TOKEN: The authentication token for your QuickNode Yellowstone instance.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
setup_logging();
info!(
"Starting to monitor account: {}",
RAYDIUM_LAUNCHPAD_PROGRAM
);

let mut client = setup_client().await?;
info!("Connected to gRPC endpoint");

let (subscribe_tx, subscribe_rx) = client.subscribe().await?;

send_subscription_request(subscribe_tx).await?;
info!("Subscription request sent. Listening for updates...");

process_updates(subscribe_rx).await?;

info!("Stream closed");
Ok(())
}

Our main function initializes the logging system, sets up the gRPC client, and sends a subscription request to monitor transactions for the specified program using the yellowstone_grpc_client. It then processes the stream of updates. Let's define each of those functions next.

Setting Up Logging and Client Connection

Let's add the required functions for logging and client setup. Add the following code to your src/main.rs file:

fn setup_logging() {
unsafe {
env::set_var("RUST_LOG", RUST_LOG_LEVEL);
}
env_logger::init();
}

This will set up the logging system to use the specified log level. You can change RUST_LOG_LEVEL to debug or error to adjust the verbosity of the logs.

Next, let's add a function to setup our gRPC client. Add the following code:

async fn setup_client() -> Result<GeyserGrpcClient<impl Interceptor>, Box<dyn std::error::Error>> {
info!("Connecting to gRPC endpoint: {}", ENDPOINT);

// Build the gRPC client with TLS config
let client = GeyserGrpcClient::build_from_shared(ENDPOINT.to_string())?
.x_token(Some(AUTH_TOKEN.to_string()))?
.tls_config(ClientTlsConfig::new().with_native_roots())?
.connect()
.await?;

Ok(client)
}

We are using the GeyserGrpcClient to connect to the Yellowstone gRPC endpoint. The x_token method is used to set the authentication token for the connection. The tls_config method is used to configure TLS settings for secure communication. Finally, we await the connect method to establish the connection.

Setting Up Transaction Subscription

Next, let's implement the subscription request function. This will send a subscription request that specifies our monitoring filters to the Yellowstone gRPC server. Add the following code:

/// Send the subscription request with transaction filters
async fn send_subscription_request<T>(mut tx: T) -> Result<(), Box<dyn std::error::Error>>
where
T: SinkExt<SubscribeRequest> + Unpin,
<T as futures::Sink<SubscribeRequest>>::Error: std::error::Error + 'static,
{
// Create account filter with the target accounts
let mut accounts_filter = HashMap::new();
accounts_filter.insert(
"account_monitor".to_string(),
SubscribeRequestFilterTransactions {
account_include: vec![],
account_exclude: vec![],
account_required: vec![
// Replace this or add additional accounts to monitor as needed
RAYDIUM_LAUNCHPAD_PROGRAM.to_string(),
],
vote: Some(false),
failed: Some(false),
signature: None,
},
);

// Send subscription request
tx.send(SubscribeRequest {
transactions: accounts_filter,
commitment: Some(CommitmentLevel::Processed as i32),
..Default::default()
})
.await?;

Ok(())
}

This function creates a subscription request with the specified filters. The accounts_required, account_include, and account_exclude fields can be used to specify which accounts to include or exclude from the subscription. The vote and failed fields can be used to filter for vote transactions or failed transactions, respectively. We are using CommitmentLevel::Processed to get transactions as quickly as possible, but you can change this to Confirmed or Finalized if you prefer a higher degree of finality.

Processing Transaction Updates

Now, let's add the function to process the stream of updates:

async fn process_updates<S>(mut stream: S) -> Result<(), Box<dyn std::error::Error>>
where
S: StreamExt<Item = Result<SubscribeUpdate, Status>> + Unpin,
{
while let Some(message) = stream.next().await {
match message {
Ok(msg) => handle_message(msg)?,
Err(e) => {
error!("Error receiving message: {:?}", e);
break;
}
}
}

Ok(())
}

This function processes the stream of updates from the Yellowstone gRPC server. It uses a loop to continuously receive messages and calls the handle_message function to process each message as it is received.

Raydium Instruction Type Detection

Let's define an enum for Raydium Launchpad instruction types and define each instruction's discriminator. This will help us identify the specific instruction types in the transactions we receive. Add the following code:

#[derive(Debug, Clone, PartialEq)]
pub enum RaydiumInstructionType {
Initialize,
BuyExactIn,
BuyExactOut,
SellExactIn,
SellExactOut,
ClaimPlatformFee,
ClaimVestedToken,
CollectFee,
CollectMigrateFee,
CreateConfig,
CreatePlatformConfig,
CreateVestingAccount,
MigrateToAmm,
MigrateToCpswap,
UpdateConfig,
UpdatePlatformConfig,
Unknown([u8; 8]),
}

pub fn parse_raydium_instruction_type(data: &[u8]) -> RaydiumInstructionType {
if data.len() < 8 {
return RaydiumInstructionType::Unknown([0; 8]);
}

let mut discriminator = [0u8; 8];
discriminator.copy_from_slice(&data[0..8]);

match discriminator {
[175, 175, 109, 31, 13, 152, 155, 237] => RaydiumInstructionType::Initialize,
[250, 234, 13, 123, 213, 156, 19, 236] => RaydiumInstructionType::BuyExactIn,
[24, 211, 116, 40, 105, 3, 153, 56] => RaydiumInstructionType::BuyExactOut,
[149, 39, 222, 155, 211, 124, 152, 26] => RaydiumInstructionType::SellExactIn,
[95, 200, 71, 34, 8, 9, 11, 166] => RaydiumInstructionType::SellExactOut,
[156, 39, 208, 135, 76, 237, 61, 72] => RaydiumInstructionType::ClaimPlatformFee,
[49, 33, 104, 30, 189, 157, 79, 35] => RaydiumInstructionType::ClaimVestedToken,
[60, 173, 247, 103, 4, 93, 130, 48] => RaydiumInstructionType::CollectFee,
[255, 186, 150, 223, 235, 118, 201, 186] => RaydiumInstructionType::CollectMigrateFee,
[201, 207, 243, 114, 75, 111, 47, 189] => RaydiumInstructionType::CreateConfig,
[176, 90, 196, 175, 253, 113, 220, 20] => RaydiumInstructionType::CreatePlatformConfig,
[129, 178, 2, 13, 217, 172, 230, 218] => RaydiumInstructionType::CreateVestingAccount,
[207, 82, 192, 145, 254, 207, 145, 223] => RaydiumInstructionType::MigrateToAmm,
[136, 92, 200, 103, 28, 218, 144, 140] => RaydiumInstructionType::MigrateToCpswap,
[29, 158, 252, 191, 10, 83, 219, 99] => RaydiumInstructionType::UpdateConfig,
[195, 60, 76, 129, 146, 45, 67, 143] => RaydiumInstructionType::UpdatePlatformConfig,
_ => RaydiumInstructionType::Unknown(discriminator),
}
}

impl std::fmt::Display for RaydiumInstructionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RaydiumInstructionType::Initialize => write!(f, "Initialize"),
RaydiumInstructionType::BuyExactIn => write!(f, "BuyExactIn"),
RaydiumInstructionType::BuyExactOut => write!(f, "BuyExactOut"),
RaydiumInstructionType::SellExactIn => write!(f, "SellExactIn"),
RaydiumInstructionType::SellExactOut => write!(f, "SellExactOut"),
RaydiumInstructionType::ClaimPlatformFee => write!(f, "ClaimPlatformFee"),
RaydiumInstructionType::ClaimVestedToken => write!(f, "ClaimVestedToken"),
RaydiumInstructionType::CollectFee => write!(f, "CollectFee"),
RaydiumInstructionType::CollectMigrateFee => write!(f, "CollectMigrateFee"),
RaydiumInstructionType::CreateConfig => write!(f, "CreateConfig"),
RaydiumInstructionType::CreatePlatformConfig => write!(f, "CreatePlatformConfig"),
RaydiumInstructionType::CreateVestingAccount => write!(f, "CreateVestingAccount"),
RaydiumInstructionType::MigrateToAmm => write!(f, "MigrateToAmm"),
RaydiumInstructionType::MigrateToCpswap => write!(f, "MigrateToCpswap"),
RaydiumInstructionType::UpdateConfig => write!(f, "UpdateConfig"),
RaydiumInstructionType::UpdatePlatformConfig => write!(f, "UpdatePlatformConfig"),
RaydiumInstructionType::Unknown(discriminator) => {
write!(f, "Unknown(discriminator={:?})", discriminator)
}
}
}
}

We get the instructions and their discriminator's from the Program's IDL. We are using the first 8 bytes of the instruction data as the discriminator to identify the instruction type. The parse_raydium_instruction_type function takes a byte slice and returns the corresponding RaydiumInstructionType. We are also adding a Display implementation for the enum to make it easier to print the instruction type.

Transaction Parsing Structures

Let's define the structures needed for transaction parsing. This is going to look a little messy because we are going to be parsing a lot of data, and for the sake of this demo--we will parse all of the transaction data--you can limit what you want to parse based on your needs. Specifically, let's define the ParsedTransaction, ParsedInstruction, ParsedInnerInstruction, and ParsedTokenBalance structs. Add the following code:

#[derive(Debug, Default)]
struct ParsedTransaction {
signature: String,
is_vote: bool,
account_keys: Vec<String>,
recent_blockhash: String,
instructions: Vec<ParsedInstruction>,
success: bool,
fee: u64,
pre_token_balances: Vec<ParsedTokenBalance>,
post_token_balances: Vec<ParsedTokenBalance>,
logs: Vec<String>,
inner_instructions: Vec<ParsedInnerInstruction>,
slot: u64,
}

impl fmt::Display for ParsedTransaction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Transaction: {}", self.signature)?;
writeln!(
f,
"Status: {}",
if self.success { "Success" } else { "Failed" }
)?;
writeln!(f, "Slot: {}", self.slot)?;
writeln!(f, "Fee: {} lamports", self.fee)?;

writeln!(f, "\nAccount Keys:")?;
for (i, key) in self.account_keys.iter().enumerate() {
writeln!(f, " [{}] {}", i, key)?;
}

writeln!(f, "\nInstructions:")?;
for (i, ix) in self.instructions.iter().enumerate() {
writeln!(f, " Instruction {}:", i)?;
writeln!(
f,
" Program: {} (index: {})",
ix.program_id, ix.program_id_index
)?;
writeln!(f, " Accounts:")?;
for (idx, acc) in &ix.accounts {
writeln!(f, " [{}] {}", idx, acc)?;
}
writeln!(f, " Data: {} bytes", ix.data.len())?;
}

if !self.inner_instructions.is_empty() {
writeln!(f, "\nInner Instructions:")?;
for inner_ix in &self.inner_instructions {
writeln!(f, " Instruction Index: {}", inner_ix.instruction_index)?;
for (i, ix) in inner_ix.instructions.iter().enumerate() {
writeln!(f, " Inner Instruction {}:", i)?;
writeln!(
f,
" Program: {} (index: {})",
ix.program_id, ix.program_id_index
)?;
writeln!(f, " Accounts:")?;
for (idx, acc) in &ix.accounts {
writeln!(f, " [{}] {}", idx, acc)?;
}
writeln!(f, " Data: {} bytes", ix.data.len())?;
}
}
}

if !self.pre_token_balances.is_empty() || !self.post_token_balances.is_empty() {
writeln!(f, "\nToken Balances:")?;

let mut balance_changes = HashMap::new();

for balance in &self.pre_token_balances {
let key = (balance.account_index, balance.mint.clone());
balance_changes.insert(key, (balance.amount.clone(), "".to_string()));
}

for balance in &self.post_token_balances {
let key = (balance.account_index, balance.mint.clone());

if let Some((_, post)) = balance_changes.get_mut(&key) {
*post = balance.amount.clone();
} else {
balance_changes.insert(key, ("".to_string(), balance.amount.clone()));
}
}

for ((account_idx, mint), (pre_amount, post_amount)) in balance_changes {
let account_key = if (account_idx as usize) < self.account_keys.len() {
&self.account_keys[account_idx as usize]
} else {
"unknown"
};

if pre_amount.is_empty() {
writeln!(
f,
" Account {} ({}): new balance {} (mint: {})",
account_idx, account_key, post_amount, mint
)?;
} else if post_amount.is_empty() {
writeln!(
f,
" Account {} ({}): removed balance {} (mint: {})",
account_idx, account_key, pre_amount, mint
)?;
} else {
writeln!(
f,
" Account {} ({}): {} → {} (mint: {})",
account_idx, account_key, pre_amount, post_amount, mint
)?;
}
}
}

if !self.logs.is_empty() {
writeln!(f, "\nTransaction Logs:")?;
for (i, log) in self.logs.iter().enumerate() {
writeln!(f, " [{}] {}", i, log)?;
}
}

Ok(())
}
}

#[derive(Debug)]
struct ParsedInstruction {
program_id: String,
program_id_index: u8,
accounts: Vec<(usize, String)>, // (index, pubkey)
data: Vec<u8>,
}

#[derive(Debug)]
struct ParsedInnerInstruction {
instruction_index: u8,
instructions: Vec<ParsedInstruction>,
}

#[derive(Debug)]
struct ParsedTokenBalance {
account_index: u32,
mint: String,
owner: String,
amount: String,
}

In addition to defining our structs, we have also implemented the Display trait for ParsedTransaction to make it easier to print the transaction details. In addition to logging each element of our transaction, it handles iterations over vectors and maps to display the account keys, instructions, inner instructions, token balances, and logs in a readable format.

Transaction Parser Implementation

Now, let's add the transaction parser logic which will parse the transaction data and populate our ParsedTransaction struct. This function will be responsible for extracting the relevant information from the transaction update received from the Yellowstone gRPC server. Add the following code:

struct TransactionParser;

impl TransactionParser {
pub fn parse_transaction(
tx_update: &yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction,
) -> Result<ParsedTransaction, Box<dyn std::error::Error>> {
let mut parsed_tx = ParsedTransaction::default();
parsed_tx.slot = tx_update.slot;

if let Some(tx_info) = &tx_update.transaction {
parsed_tx.is_vote = tx_info.is_vote;

parsed_tx.signature = bs58::encode(&tx_info.signature).into_string();

if let Some(tx) = &tx_info.transaction {
if let Some(msg) = &tx.message {
for key in &msg.account_keys {
parsed_tx.account_keys.push(bs58::encode(key).into_string());
}

if let Some(meta) = &tx_info.meta {
for addr in &meta.loaded_writable_addresses {
let base58_addr = bs58::encode(addr).into_string();
parsed_tx.account_keys.push(base58_addr);
}

for addr in &meta.loaded_readonly_addresses {
let base58_addr = bs58::encode(addr).into_string();
parsed_tx.account_keys.push(base58_addr);
}
}

parsed_tx.recent_blockhash = bs58::encode(&msg.recent_blockhash).into_string();

for ix in &msg.instructions {
let program_id_index = ix.program_id_index;
let program_id =
if (program_id_index as usize) < parsed_tx.account_keys.len() {
parsed_tx.account_keys[program_id_index as usize].clone()
} else {
"unknown".to_string()
};

let mut accounts = Vec::new();
for &acc_idx in &ix.accounts {
let account_idx = acc_idx as usize;
if account_idx < parsed_tx.account_keys.len() {
accounts.push((
account_idx,
parsed_tx.account_keys[account_idx].clone(),
));
}
}

parsed_tx.instructions.push(ParsedInstruction {
program_id,
program_id_index: program_id_index as u8,
accounts,
data: ix.data.clone(),
});
}
}
}

if let Some(meta) = &tx_info.meta {
parsed_tx.success = meta.err.is_none();
parsed_tx.fee = meta.fee;

for balance in &meta.pre_token_balances {
if let Some(amount) = &balance.ui_token_amount {
parsed_tx.pre_token_balances.push(ParsedTokenBalance {
account_index: balance.account_index,
mint: balance.mint.clone(),
owner: balance.owner.clone(),
amount: amount.ui_amount_string.clone(),
});
}
}

for balance in &meta.post_token_balances {
if let Some(amount) = &balance.ui_token_amount {
parsed_tx.post_token_balances.push(ParsedTokenBalance {
account_index: balance.account_index,
mint: balance.mint.clone(),
owner: balance.owner.clone(),
amount: amount.ui_amount_string.clone(),
});
}
}

for inner_ix in &meta.inner_instructions {
let mut parsed_inner_ixs = Vec::new();

for ix in &inner_ix.instructions {
let program_id_index = ix.program_id_index;

let program_id =
if (program_id_index as usize) < parsed_tx.account_keys.len() {
parsed_tx.account_keys[program_id_index as usize].clone()
} else {
"unknown".to_string()
};

let mut accounts = Vec::new();
for &acc_idx in &ix.accounts {
let account_idx = acc_idx as usize;
if account_idx < parsed_tx.account_keys.len() {
accounts.push((
account_idx,
parsed_tx.account_keys[account_idx].clone(),
));
}
}

parsed_inner_ixs.push(ParsedInstruction {
program_id,
program_id_index: program_id_index as u8,
accounts,
data: ix.data.clone(),
});
}

parsed_tx.inner_instructions.push(ParsedInnerInstruction {
instruction_index: inner_ix.index as u8,
instructions: parsed_inner_ixs,
});
}

parsed_tx.logs = meta.log_messages.clone();
}
}

Ok(parsed_tx)
}
}

There is a lot here, because the transaction data is quite complex. The parse_transaction function takes a SubscribeUpdateTransaction object and extracts the relevant information into our ParsedTransaction struct. It handles parsing the transaction signature, account keys, recent blockhash, instructions, inner instructions, token balances, and logs.

Message Handler Implementation

Finally, let's implement the message handler function which we called in process_updates. This function will handle the incoming messages from the Yellowstone gRPC stream and process them accordingly. Add the following code:

fn handle_message(msg: SubscribeUpdate) -> Result<(), Box<dyn std::error::Error>> {
match msg.update_oneof {
Some(UpdateOneof::Transaction(transaction_update)) => {
match TransactionParser::parse_transaction(&transaction_update) {
Ok(parsed_tx) => {
let mut has_raydium_ix = false;
let mut found_target_ix = false;
let mut found_ix_types = Vec::new();

if TARGET_IX_TYPES.is_empty() {
found_target_ix = true;
}

for (i, ix) in parsed_tx.instructions.iter().enumerate() {
if ix.program_id == RAYDIUM_LAUNCHPAD_PROGRAM {
has_raydium_ix = true;
let raydium_ix_type = parse_raydium_instruction_type(&ix.data);
found_ix_types.push(raydium_ix_type.clone());

if TARGET_IX_TYPES.contains(&raydium_ix_type) {
found_target_ix = true;
}
if found_target_ix {
info!(
"Found target instruction: {} at index {}",
raydium_ix_type, i
);
}
}
}

for inner_ix_group in &parsed_tx.inner_instructions {
for (i, inner_ix) in inner_ix_group.instructions.iter().enumerate() {
if inner_ix.program_id == RAYDIUM_LAUNCHPAD_PROGRAM {
has_raydium_ix = true;
let raydium_ix_type =
parse_raydium_instruction_type(&inner_ix.data);
found_ix_types.push(raydium_ix_type.clone());

if TARGET_IX_TYPES.contains(&raydium_ix_type) {
found_target_ix = true;
}
if found_target_ix && !matches!(raydium_ix_type, RaydiumInstructionType::Unknown(_)) {
info!(
"Found target instruction: {} at inner index {}.{}",
raydium_ix_type, inner_ix_group.instruction_index, i
);
}
}
}
}

if found_target_ix && has_raydium_ix {
info!("Found Raydium Launchpad transaction!");
info!("Parsed Transaction:\n{}", parsed_tx);
}
}
Err(e) => {
error!("Failed to parse transaction: {:?}", e);
}
}
}
Some(UpdateOneof::Ping(SubscribeUpdatePing {})) => {
// Ignore pings
}
Some(other) => {
info!("Unexpected update received. Type of update: {:?}", other);
}
None => {
warn!("Empty update received");
}
}

Ok(())
}

There's a bit happening here, let's break it down:

  • The handle_message function takes a SubscribeUpdate message and checks if it contains a transaction update.
  • If it does, it calls the TransactionParser::parse_transaction function to parse the transaction data.
  • It checks if the transaction (including inner instructions) contains Raydium Launchpad instructions and whether they match the target instruction types specified in TARGET_IX_TYPES.
  • If a target instruction is found, it logs the details of the transaction and the instruction.
  • It also handles pings and unexpected updates gracefully.

Running the Application

To run the application, you'll need to:

  1. Update the ENDPOINT and AUTH_TOKEN constants with your QuickNode Yellowstone gRPC endpoint information
  2. Optionally customize the TARGET_IX_TYPES array to filter for specific instruction types
  3. Build and run the application
cargo build

Then run the application:

cargo run

You should start seeing logs indicating that the application is connecting to the Yellowstone gRPC endpoint and processing transactions. It should look something like this:

[2025-04-17T23:58:26Z INFO  raydium_launchpad_tracker] Found Raydium Launchpad transaction!
[2025-04-17T23:58:26Z INFO raydium_launchpad_tracker] Found target instruction: BuyExactIn at index 5
[2025-04-17T23:58:26Z INFO raydium_launchpad_tracker] Parsed Transaction:
Transaction: 4s5xG2Gf25aBxVr8ahdTvChT1ioWzNTqbCNFkchfHokHUxzyGDKTzFadsn9xAsgBgqr4EWT87NZKRncbq1ZGYvkG
Status: Success
Slot: 334162911
Fee: 53060 lamports

Account Keys:
[0] 2VSCxjXzykbjVBKrBa3we4yjxWtKAUkd8hAgNwpPkKP7
## ...
[17] SysvarRent111111111111111111111111111111111

Instructions:
Instruction 0:
Program: ComputeBudget111111111111111111111111111111 (index: 6)
Accounts:
Data: 9 bytes
Instruction 1:
Program: ComputeBudget111111111111111111111111111111 (index: 6)
Accounts:
Data: 5 bytes
Instruction 2:
Program: ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL (index: 7)
Accounts:
[0] 2VSCxjXzykbjVBKrBa3we4yjxWtKAUkd8hAgNwpPkKP7
[1] ALuscPsdi4rS2u47hSZ8vxmGDWb9csm49Sg63FSCW1gj
## ...

Token Balances:
Account 5 (7W1...Yujxc): 22.38471449423.184714494 (mint: So1...112)
## ...

Transaction Logs:
[0] Program ComputeBudget111111111111111111111111111111 invoke [1]
## ...
[49] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success

This log is intended to be comprehensive--you can customize the ParsedTransaction struct and the handle_message function to only log the information you are interested in.

Customizing Transaction Filtering

The application can be customized to focus on specific Raydium Launchpad instruction types. To do this, modify the TARGET_IX_TYPES array:

const TARGET_IX_TYPES: &[RaydiumInstructionType] = &[
RaydiumInstructionType::Initialize, // Track new token sales
RaydiumInstructionType::MigrateToAmm, // Track migrations to AMM
];

If you leave the array empty, the application will track all Raydium Launchpad instructions.

Additional Enhancements

Here are some ways you could enhance this application:

  1. Store data in a database - Connect to a database to store transaction information for later analysis
  2. Create alerts - Set up notifications for specific transaction types or thresholds
  3. Add a web interface - Create a dashboard to visualize real-time transaction data
  4. Track specific tokens - Filter for transactions involving specific token mints
  5. Analyze price impacts - Calculate price impacts of token sales and migrations

Troubleshooting

Here are solutions to common issues you might encounter:

  1. Connection issues - Ensure your endpoint URL and authentication token are correct (check our Yellowstone Docs for more information)
  2. High message volume - Consider adding more specific server-side filters to reduce the number of transactions processed--note that our script includes server and client-side filtering. In our example, this means updating the accounts_filter in send_subscription_request to include specific accounts or transaction types. If your target instruction includes a unique account, you can add it to the account_required field. For example, if you want to track the Initialize instruction, you could add the Metaplex Token Metadata program account which is required for initializing the new token but not required for swaps (metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s).
  3. Missed transactions - Check your commitment level (currently set to Processed); you might want to use Confirmed or Finalized for more reliable results
  4. Streams or Account Limits - for applications requiring multiple streams or larger filter arrays, check out our Velocity Tier or Reach out to an Enterprise Specialist for custom solutions.

Wrap Up

In this guide, we've built a Rust script that leverages QuickNode's Yellowstone gRPC add-on to monitor Raydium Launchpad transactions in real-time. This approach provides significant advantages over traditional websockets, including lower latency and more reliable transaction detection.

By understanding and monitoring Raydium Launchpad transactions, you can gain valuable insights into token launches, sales activities, and platform configurations. And more importantly, you now have the tools to build your own custom monitoring solution for other Solana programs!

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Additional Resources

Share this guide