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
- Basic familiarity with Rust programming
- Understanding of Solana transaction structure
- A QuickNode account with the Yellowstone gRPC add-on enabled
- Rust and Cargo installed on your system
Dependency | Version |
---|---|
rustc | 1.85.0 |
cargo | 1.85.0 |
tokio | 1.28.0 |
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 |
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:
- Client setup and connection to Yellowstone gRPC
- Subscription configuration for Raydium Launchpad transactions
- Transaction parsing and processing
- 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 theRaydiumInstructionType
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 aSubscribeUpdate
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:
- Update the
ENDPOINT
andAUTH_TOKEN
constants with your QuickNode Yellowstone gRPC endpoint information - Optionally customize the
TARGET_IX_TYPES
array to filter for specific instruction types - 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.384714494 → 23.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:
- Store data in a database - Connect to a database to store transaction information for later analysis
- Create alerts - Set up notifications for specific transaction types or thresholds
- Add a web interface - Create a dashboard to visualize real-time transaction data
- Track specific tokens - Filter for transactions involving specific token mints
- Analyze price impacts - Calculate price impacts of token sales and migrations
Troubleshooting
Here are solutions to common issues you might encounter:
- Connection issues - Ensure your endpoint URL and authentication token are correct (check our Yellowstone Docs for more information)
- 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
insend_subscription_request
to include specific accounts or transaction types. If your target instruction includes a unique account, you can add it to theaccount_required
field. For example, if you want to track theInitialize
instruction, you could add the Metaplex Token Metadata program account which is required for initializing the new token but not required for swaps (metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
). - Missed transactions - Check your commitment level (currently set to
Processed
); you might want to useConfirmed
orFinalized
for more reliable results - 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.