sol-parser-sdk 0.7.10

A lightweight Rust library for real-time event streaming from Solana DEX trading programs. Supports PumpFun, PumpSwap, LaunchLab, and Raydium protocols with Yellowstone gRPC and ShredStream.
Documentation
#[path = "support/pumpfun_rpc.rs"]
mod support;
use sol_parser_sdk::{parse_rpc_transaction, DexEvent};
use solana_sdk::{pubkey::Pubkey, signature::Signature};
use solana_transaction_status::EncodedConfirmedTransactionWithStatusMeta;
use std::{fs, path::PathBuf};

#[test]
fn mainnet_create_account_regressions() {
    let directory =
        std::env::var_os("PUMPFUN_CREATE_CORPUS").map(PathBuf::from).unwrap_or_else(|| {
            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/pumpfun_create")
        });
    let mut successful = 0;
    for file in fs::read_dir(directory).unwrap() {
        let path = file.unwrap().path();
        if path.extension().is_none_or(|ext| ext != "json") {
            continue;
        }
        let raw: serde_json::Value =
            serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();

        use base64::{engine::general_purpose::STANDARD, Engine};
        let mut binary = raw.clone();
        binary["transaction"] = serde_json::json!([STANDARD.encode(support::wire(&raw)), "base64"]);
        let tx: EncodedConfirmedTransactionWithStatusMeta = serde_json::from_value(binary).unwrap();
        let events = parse_rpc_transaction(&tx, None).unwrap();
        if !raw["meta"]["err"].is_null() {
            assert!(events.is_empty(), "rolled-back events: {}", path.display());
            continue;
        }
        successful += 1;
        let creates: Vec<_> = events
            .iter()
            .filter_map(|e| match e {
                DexEvent::PumpFunCreate(c) => Some(c),
                _ => None,
            })
            .collect();
        assert!(!creates.is_empty(), "{}", path.display());
        let mut keys: Vec<_> = raw["transaction"]["message"]["accountKeys"]
            .as_array()
            .unwrap()
            .iter()
            .map(|k| k.as_str().unwrap())
            .collect();
        for field in ["writable", "readonly"] {
            keys.extend(
                raw["meta"]["loadedAddresses"][field]
                    .as_array()
                    .unwrap()
                    .iter()
                    .map(|k| k.as_str().unwrap()),
            );
        }
        for c in creates {
            let ix = raw["transaction"]["message"]["instructions"]
                .as_array()
                .unwrap()
                .iter()
                .find(|ix| {
                    let data = bs58::decode(ix["data"].as_str().unwrap()).into_vec().unwrap();
                    keys[ix["programIdIndex"].as_u64().unwrap() as usize]
                        == "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
                        && (data.starts_with(&[24, 30, 200, 40, 5, 28, 7, 119])
                            || data.starts_with(&[214, 144, 76, 236, 95, 139, 49, 180]))
                        && keys[ix["accounts"][0].as_u64().unwrap() as usize] == c.mint.to_string()
                })
                .expect("matching create instruction");
            let data = bs58::decode(ix["data"].as_str().unwrap()).into_vec().unwrap();
            let v2 = data.starts_with(&[214, 144, 76, 236, 95, 139, 49, 180]);
            assert_eq!(
                c.user.to_string(),
                keys[ix["accounts"][if v2 { 5 } else { 7 }].as_u64().unwrap() as usize],
                "{}",
                path.display()
            );
            assert_eq!(
                c.token_program.to_string(),
                keys[ix["accounts"][if v2 { 7 } else { 9 }].as_u64().unwrap() as usize],
                "{}",
                path.display()
            );
            assert!(
                [
                    "So11111111111111111111111111111111111111111",
                    "So11111111111111111111111111111111111111112"
                ]
                .contains(&c.quote_mint.to_string().as_str()),
                "{}",
                path.display()
            );
            assert_eq!(c.quote_vault, Pubkey::default());
        }
    }
    assert!(successful >= 2);
}

#[test]
fn historical_create_log_exact_layout() {
    use base64::{engine::general_purpose::STANDARD, Engine};
    let mut data = vec![27, 114, 169, 77, 222, 235, 99, 118];
    for value in ["name", "SYM", "uri"] {
        data.extend((value.len() as u32).to_le_bytes());
        data.extend(value.as_bytes());
    }
    for seed in [1, 2, 3] {
        data.extend([seed; 32]);
    }
    let parse = |bytes: &[u8]| {
        sol_parser_sdk::logs::pump::parse_log(
            &format!("Program data: {}", STANDARD.encode(bytes)),
            Signature::default(),
            1,
            0,
            None,
            0,
            false,
        )
    };
    let event = parse(&data).expect("historical event");
    match event {
        DexEvent::PumpFunCreate(c) => {
            assert_eq!(c.mint, Pubkey::new_from_array([1; 32]));
            assert_eq!(c.creator, Pubkey::default());
            assert_eq!(c.timestamp, 0);
        }
        _ => panic!("create"),
    }
    let mut disc = [0u8; 16];
    disc[..8].copy_from_slice(&data[..8]);
    disc[8..].copy_from_slice(&[155, 167, 108, 32, 122, 76, 173, 64]);
    assert!(matches!(
        sol_parser_sdk::instr::pump_inner::parse_pumpfun_inner_instruction(
            &disc,
            &data[8..],
            Default::default(),
            false
        ),
        Some(DexEvent::PumpFunCreate(_))
    ));
    assert!(matches!(
        sol_parser_sdk::logs::pump::parse_create_from_data(&data[8..], Default::default()),
        Some(DexEvent::PumpFunCreate(_))
    ));
    assert!(parse(&data[..data.len() - 1]).is_none());
    data.push(0);
    assert!(parse(&data).is_none());
}