sof 0.17.1

Solana Observer Framework for low-latency shred ingestion and plugin-driven transaction observation
Documentation
//! Plugin example that filters and logs Raydium transactions by program ID.
#![doc(hidden)]

use std::sync::{
    OnceLock,
    atomic::{AtomicU64, Ordering},
};

use async_trait::async_trait;
use sof::framework::{Plugin, PluginConfig, PluginHost, TransactionEvent};
use solana_pubkey::Pubkey;
use thiserror::Error;

pub const RAYDIUM_STANDARD_AMM_PROGRAM_ID: &str = "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C";
pub const RAYDIUM_V4_PROGRAM_ID: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
pub const RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID: &str = "5quBtoiQqxF9Jv6KYKctB59NT3gtJD2Y65kdnB1Uev3h";
pub const RAYDIUM_CLMM_PROGRAM_ID: &str = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
pub const RAYDIUM_LAUNCHLAB_PROGRAM_ID: &str = "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj";
pub const RAYDIUM_BURN_EARN_PROGRAM_ID: &str = "LockrWmn6K5twhz3y9w1dQERbmgSaRkfnTeTKbpofwE";
pub const RAYDIUM_ROUTING_PROGRAM_ID: &str = "routeUGWgWzqBWFcrCfv8tritsqukccJPu3q5GPP3xS";
pub const RAYDIUM_STAKING_PROGRAM_ID: &str = "EhhTKczWMGQt46ynNeRX1WfeagwwJd7ufHvCDjRxjo5Q";
pub const RAYDIUM_FARM_STAKING_PROGRAM_ID: &str = "9KEPoZmtHUrBbhWN1v1KWLMkkvwY6WLtAVUCPRtRjP4z";
pub const RAYDIUM_ECOSYSTEM_FARM_PROGRAM_ID: &str = "FarmqiPv5eAj3j1GMdMCMUGXqPUvmquZtMy86QH6rzhG";
const MISSING_SIGNATURE_LABEL: &str = "NO_SIGNATURE";

static RAYDIUM_TX_COUNT: AtomicU64 = AtomicU64::new(0);

#[derive(Debug, Clone, Copy, Default)]
struct RaydiumTxFilterLoggerPlugin;

#[derive(Debug, Clone, Copy, Default)]
struct RaydiumProgramsTouched {
    launchlab: bool,
    cpmm: bool,
    v4: bool,
    stable_swap: bool,
    clmm: bool,
    burn_earn: bool,
    routing: bool,
    staking: bool,
    farm_staking: bool,
    ecosystem_farm: bool,
    direct_program_invocation: bool,
    account_reference: bool,
    instruction_match_count: u64,
    account_match_count: u64,
}

impl RaydiumProgramsTouched {
    const fn any(self) -> bool {
        self.launchlab
            || self.cpmm
            || self.v4
            || self.stable_swap
            || self.clmm
            || self.burn_earn
            || self.routing
            || self.staking
            || self.farm_staking
            || self.ecosystem_farm
    }
}

#[async_trait]
impl Plugin for RaydiumTxFilterLoggerPlugin {
    fn name(&self) -> &'static str {
        "raydium-tx-filter-logger"
    }

    fn config(&self) -> PluginConfig {
        PluginConfig::new().with_transaction()
    }

    async fn on_transaction(&self, event: &TransactionEvent) {
        let Some(touched) = classify_raydium_transaction(event) else {
            return;
        };

        let signature = event
            .signature
            .map(|signature| signature.to_string())
            .unwrap_or_else(|| MISSING_SIGNATURE_LABEL.to_owned());
        let seen = RAYDIUM_TX_COUNT
            .fetch_add(1, Ordering::Relaxed)
            .saturating_add(1);

        tracing::info!(
            slot = event.slot,
            signature = %signature,
            tx_kind = ?event.kind,
            seen,
            launchlab = touched.launchlab,
            cpmm = touched.cpmm,
            v4 = touched.v4,
            stable_swap = touched.stable_swap,
            clmm = touched.clmm,
            burn_earn = touched.burn_earn,
            routing = touched.routing,
            staking = touched.staking,
            farm_staking = touched.farm_staking,
            ecosystem_farm = touched.ecosystem_farm,
            direct_program_invocation = touched.direct_program_invocation,
            account_reference = touched.account_reference,
            instruction_match_count = touched.instruction_match_count,
            account_match_count = touched.account_match_count,
            "raydium transaction observed"
        );
    }
}

fn classify_raydium_transaction(event: &TransactionEvent) -> Option<RaydiumProgramsTouched> {
    let message = &event.tx.message;
    let keys = message.static_account_keys();
    let raydium_launchlab = raydium_launchlab_pubkey()?;
    let raydium_cpmm = raydium_cpmm_pubkey()?;
    let raydium_v4 = raydium_v4_pubkey()?;
    let raydium_stable_swap = raydium_stable_swap_pubkey()?;
    let raydium_clmm = raydium_clmm_pubkey()?;
    let raydium_burn_earn = raydium_burn_earn_pubkey()?;
    let raydium_routing = raydium_routing_pubkey()?;
    let raydium_staking = raydium_staking_pubkey()?;
    let raydium_farm_staking = raydium_farm_staking_pubkey()?;
    let raydium_ecosystem_farm = raydium_ecosystem_farm_pubkey()?;
    let mut touched = RaydiumProgramsTouched::default();
    let mut match_program = |program_id: &Pubkey, direct_invocation: bool| {
        let mut matched = false;
        if *program_id == raydium_launchlab {
            touched.launchlab = true;
            matched = true;
        } else if *program_id == raydium_cpmm {
            touched.cpmm = true;
            matched = true;
        } else if *program_id == raydium_v4 {
            touched.v4 = true;
            matched = true;
        } else if *program_id == raydium_stable_swap {
            touched.stable_swap = true;
            matched = true;
        } else if *program_id == raydium_clmm {
            touched.clmm = true;
            matched = true;
        } else if *program_id == raydium_burn_earn {
            touched.burn_earn = true;
            matched = true;
        } else if *program_id == raydium_routing {
            touched.routing = true;
            matched = true;
        } else if *program_id == raydium_staking {
            touched.staking = true;
            matched = true;
        } else if *program_id == raydium_farm_staking {
            touched.farm_staking = true;
            matched = true;
        } else if *program_id == raydium_ecosystem_farm {
            touched.ecosystem_farm = true;
            matched = true;
        }
        if !matched {
            return;
        }
        if direct_invocation {
            touched.direct_program_invocation = true;
            touched.instruction_match_count = touched.instruction_match_count.saturating_add(1);
        } else {
            touched.account_reference = true;
            touched.account_match_count = touched.account_match_count.saturating_add(1);
        }
    };

    for ix in message.instructions() {
        let Some(program_id) = keys.get(usize::from(ix.program_id_index)) else {
            continue;
        };
        match_program(program_id, true);
    }

    for account_key in keys {
        match_program(account_key, false);
    }

    touched.any().then_some(touched)
}

fn parse_program_pubkey(value: &str) -> Option<Pubkey> {
    value.parse().ok()
}

fn raydium_launchlab_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_LAUNCHLAB_PROGRAM_ID))
        .to_owned()
}

fn raydium_cpmm_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_STANDARD_AMM_PROGRAM_ID))
        .to_owned()
}

fn raydium_v4_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_V4_PROGRAM_ID))
        .to_owned()
}

fn raydium_stable_swap_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID))
        .to_owned()
}

fn raydium_clmm_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_CLMM_PROGRAM_ID))
        .to_owned()
}

fn raydium_burn_earn_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_BURN_EARN_PROGRAM_ID))
        .to_owned()
}

fn raydium_routing_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_ROUTING_PROGRAM_ID))
        .to_owned()
}

fn raydium_staking_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_STAKING_PROGRAM_ID))
        .to_owned()
}

fn raydium_farm_staking_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_FARM_STAKING_PROGRAM_ID))
        .to_owned()
}

fn raydium_ecosystem_farm_pubkey() -> Option<Pubkey> {
    static PK: OnceLock<Option<Pubkey>> = OnceLock::new();
    PK.get_or_init(|| parse_program_pubkey(RAYDIUM_ECOSYSTEM_FARM_PROGRAM_ID))
        .to_owned()
}

#[derive(Debug, Error)]
enum RaydiumTxFilterExampleError {
    #[error("examples are release-only; run with `{command}`")]
    ReleaseModeRequired { command: &'static str },
    #[error(transparent)]
    Runtime(#[from] sof::runtime::RuntimeError),
}

const fn require_release_mode() -> Result<(), RaydiumTxFilterExampleError> {
    if cfg!(debug_assertions) {
        return Err(RaydiumTxFilterExampleError::ReleaseModeRequired {
            command: "cargo run --release -p sof --example raydium_contract",
        });
    }
    Ok(())
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), RaydiumTxFilterExampleError> {
    require_release_mode()?;
    let host = PluginHost::builder()
        .add_plugin(RaydiumTxFilterLoggerPlugin)
        .build();

    tracing::warn!(plugins = ?host.plugin_names(), "starting SOF runtime with plugin host");
    Ok(sof::runtime::run_async_with_plugin_host(host).await?)
}