sol-parser-sdk 0.7.6

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
//! Meteora 账户填充模块(包含 DAMM V2, Pools, DLMM)

use crate::core::events::*;
use solana_sdk::pubkey::Pubkey;

pub type AccountGetter<'a> = dyn Fn(usize) -> Pubkey + 'a;

// ============================================================================
// Meteora DAMM V2
// ============================================================================

/// Meteora DAMM V2 Swap 账户填充
///
/// swap/swap2 instruction account mapping (based on IDL):
/// 0: pool_authority
/// 1: pool
/// 2: input_token_account
/// 3: output_token_account
/// 4: token_a_vault
/// 5: token_b_vault
/// 6: token_a_mint
/// 7: token_b_mint
/// 8: payer
/// 9: token_a_program
/// 10: token_b_program
/// 11: optional referral_token_account (or program-id placeholder)
/// 12: event_authority (11 when optional account is omitted)
/// 13: program (12 when optional account is omitted)
pub fn fill_damm_v2_swap_accounts(
    e: &mut MeteoraDammV2SwapEvent,
    get: &AccountGetter<'_>,
    account_count: usize,
) {
    if !(13..=14).contains(&account_count) {
        return;
    }
    e.pool_authority = get(0);
    e.input_token_account = get(2);
    e.output_token_account = get(3);
    e.token_a_vault = get(4);
    e.token_b_vault = get(5);
    e.token_a_mint = get(6);
    e.token_b_mint = get(7);
    e.payer = get(8);
    e.token_a_program = get(9);
    e.token_b_program = get(10);
    let authority_index = account_count - 2;
    e.event_authority = get(authority_index);
    e.program = get(account_count - 1);
    e.referral_token_account = (account_count == 14 && get(11) != e.program).then(|| get(11));
}

#[cfg(test)]
mod damm_swap_tests {
    use super::*;

    #[test]
    fn optional_referral_is_not_the_program_placeholder() {
        let accounts: Vec<_> = (0..14).map(|_| Pubkey::new_unique()).collect();
        let mut event = MeteoraDammV2SwapEvent::default();
        fill_damm_v2_swap_accounts(
            &mut event,
            &|i| accounts.get(i).copied().unwrap_or_default(),
            14,
        );
        assert_eq!(event.token_a_mint, accounts[6]);
        assert_eq!(event.payer, accounts[8]);
        assert_eq!(event.referral_token_account, Some(accounts[11]));
        assert_eq!(event.event_authority, accounts[12]);
        assert_eq!(event.program, accounts[13]);

        let mut without_referral = accounts.clone();
        without_referral[11] = accounts[13];
        fill_damm_v2_swap_accounts(
            &mut event,
            &|i| without_referral.get(i).copied().unwrap_or_default(),
            14,
        );
        assert_eq!(event.referral_token_account, None);

        let mut omitted = MeteoraDammV2SwapEvent::default();
        fill_damm_v2_swap_accounts(
            &mut omitted,
            &|i| accounts.get(i).copied().unwrap_or_default(),
            13,
        );
        assert_eq!(omitted.referral_token_account, None);
        assert_eq!(omitted.event_authority, accounts[11]);
        assert_eq!(omitted.program, accounts[12]);
    }

    #[test]
    fn older_swap_json_defaults_new_accounts() {
        let mut json = serde_json::to_value(MeteoraDammV2SwapEvent::default()).unwrap();
        let map = json.as_object_mut().unwrap();
        for field in [
            "pool_authority",
            "input_token_account",
            "output_token_account",
            "payer",
            "referral_token_account",
            "event_authority",
            "program",
        ] {
            map.remove(field);
        }
        let decoded: MeteoraDammV2SwapEvent = serde_json::from_value(json).unwrap();
        assert_eq!(decoded.pool_authority, Pubkey::default());
        assert_eq!(decoded.referral_token_account, None);
    }

    #[test]
    fn invalid_account_count_does_not_panic_or_fill() {
        let mut event = MeteoraDammV2SwapEvent::default();
        fill_damm_v2_swap_accounts(&mut event, &|_| Pubkey::new_unique(), 0);
        assert_eq!(event.token_a_mint, Pubkey::default());
    }
}

pub fn fill_damm_v2_create_position_accounts(
    _e: &mut MeteoraDammV2CreatePositionEvent,
    _get: &AccountGetter<'_>,
) {
    // DAMM V2 是动态 AMM,没有传统的 position 概念
    // 此事件类型可能不适用
}

pub fn fill_damm_v2_close_position_accounts(
    _e: &mut MeteoraDammV2ClosePositionEvent,
    _get: &AccountGetter<'_>,
) {
    // DAMM V2 是动态 AMM,没有传统的 position 概念
    // 此事件类型可能不适用
}

pub fn fill_damm_v2_add_liquidity_accounts(
    _e: &mut MeteoraDammV2AddLiquidityEvent,
    _get: &AccountGetter<'_>,
) {
    // DAMM V2 流动性操作通过 initialize_virtual_pool 等指令
    // 事件数据已包含主要信息
}

pub fn fill_damm_v2_remove_liquidity_accounts(
    _e: &mut MeteoraDammV2RemoveLiquidityEvent,
    _get: &AccountGetter<'_>,
) {
    // DAMM V2 流动性移除操作
    // 事件数据已包含主要信息
}

pub fn fill_damm_v2_initialize_pool_accounts(
    e: &mut MeteoraDammV2InitializePoolEvent,
    get: &AccountGetter<'_>,
) {
    if e.creator == Pubkey::default() {
        e.creator = get(0);
    }
    if e.position_nft_mint == Pubkey::default() {
        e.position_nft_mint = get(1);
    }
    if e.pool == Pubkey::default() {
        e.pool = get(6);
    }
    if e.position == Pubkey::default() {
        e.position = get(7);
    }
    if e.token_a_mint == Pubkey::default() {
        e.token_a_mint = get(8);
    }
    if e.token_b_mint == Pubkey::default() {
        e.token_b_mint = get(9);
    }
}

// ============================================================================
// Meteora Pools
// ============================================================================

/// Meteora Pools Swap 账户填充
///
/// swap instruction account mapping (based on IDL):
/// 0: pool
/// 1: userSourceToken
/// 2: userDestinationToken
/// 3: aVault
/// 4: bVault
/// 5: aTokenVault
/// 6: bTokenVault
/// 7: aVaultLpMint
/// 8: bVaultLpMint
/// 9: aVaultLp
/// 10: bVaultLp
/// 11: adminTokenFee
/// 12: user
/// 13: vaultProgram
/// 14: tokenProgram
pub fn fill_pools_swap_accounts(_e: &mut MeteoraPoolsSwapEvent, _get: &AccountGetter<'_>) {
    // 事件数据已包含主要信息
}

/// Meteora Pools Add Liquidity 账户填充
///
/// addBalanceLiquidity/addImbalanceLiquidity instruction account mapping:
/// 0: pool
/// 1: lpMint
/// 2: userPoolLp
/// 3: aVaultLp
/// 4: bVaultLp
/// 5: aVault
/// 6: bVault
/// 7: aVaultLpMint
/// 8: bVaultLpMint
/// 9: aTokenVault
/// 10: bTokenVault
/// 11: userAToken
/// 12: userBToken
/// 13: user
/// ...
pub fn fill_pools_add_liquidity_accounts(
    _e: &mut MeteoraPoolsAddLiquidityEvent,
    _get: &AccountGetter<'_>,
) {
    // 事件数据已包含主要信息
}

/// Meteora Pools Remove Liquidity 账户填充
///
/// removeBalanceLiquidity/removeLiquiditySingleSide instruction account mapping:
/// 0: pool
/// 1: lpMint
/// 2: userPoolLp
/// 3: aVaultLp
/// 4: bVaultLp
/// 5: aVault
/// 6: bVault
/// ...
pub fn fill_pools_remove_liquidity_accounts(
    _e: &mut MeteoraPoolsRemoveLiquidityEvent,
    _get: &AccountGetter<'_>,
) {
    // 事件数据已包含主要信息
}

// ============================================================================
// Meteora DLMM
// ============================================================================

/// Meteora DLMM Swap 账户填充
///
/// `swap` (v1) fixed accounts (IDL):
/// 0..12 shared, 13: eventAuthority, 14: program, remaining: bin arrays
///
/// `swap2` fixed accounts (IDL):
/// 0: lbPair, 1: binArrayBitmapExtension (optional), 2: reserveX, 3: reserveY,
/// 4: userTokenIn, 5: userTokenOut, 6: tokenXMint, 7: tokenYMint,
/// 8: oracle, 9: hostFeeIn (optional), 10: user, 11: tokenXProgram, 12: tokenYProgram,
/// 13: memoProgram, 14: eventAuthority, 15: program, remaining: bin arrays
pub fn fill_dlmm_swap_accounts(e: &mut MeteoraDlmmSwapEvent, get: &AccountGetter<'_>) {
    /// Official Memo program id used by Meteora `swap2` / `swap_*2`.
    const MEMO_PROGRAM: Pubkey = solana_sdk::pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");

    if e.pool == Pubkey::default() {
        e.pool = get(0);
    }
    if e.user_token_in == Pubkey::default() {
        e.user_token_in = get(4);
    }
    if e.user_token_out == Pubkey::default() {
        e.user_token_out = get(5);
    }
    if e.token_x_mint == Pubkey::default() {
        e.token_x_mint = get(6);
    }
    if e.token_y_mint == Pubkey::default() {
        e.token_y_mint = get(7);
    }
    if e.reserve_x == Pubkey::default() {
        e.reserve_x = get(2);
    }
    if e.reserve_y == Pubkey::default() {
        e.reserve_y = get(3);
    }
    if e.oracle == Pubkey::default() {
        e.oracle = get(8);
    }
    if e.token_x_program == Pubkey::default() {
        e.token_x_program = get(11);
    }
    if e.token_y_program == Pubkey::default() {
        e.token_y_program = get(12);
    }

    // swap2 inserts memo_program at index 13; swap (v1) has event_authority there.
    let is_swap2 = get(13) == MEMO_PROGRAM;
    let program_idx = if is_swap2 { 15 } else { 14 };
    let bins_start = if is_swap2 { 16 } else { 15 };

    if e.bitmap_extension.is_none() {
        let ext = get(1);
        // Unused optional accounts are often the program id placeholder.
        let program = get(program_idx);
        if ext != Pubkey::default() && ext != e.pool && ext != program {
            e.bitmap_extension = Some(ext);
        }
    }
    if e.bin_arrays.is_empty() {
        let mut bins = Vec::new();
        let mut idx = bins_start;
        while idx < bins_start + 16 {
            let key = get(idx);
            if key == Pubkey::default() {
                break;
            }
            bins.push(key);
            idx += 1;
        }
        e.bin_arrays = bins;
    }
}

/// Meteora DLMM Add Liquidity 账户填充
///
/// addLiquidity instruction account mapping (based on IDL):
/// 0: position
/// 1: lbPair
/// 2: binArrayBitmapExtension
/// 3: userTokenX
/// 4: userTokenY
/// 5: reserveX
/// 6: reserveY
/// 7: tokenXMint
/// 8: tokenYMint
/// 9: binArrayLower
/// 10: binArrayUpper
/// 11: sender
/// ...
pub fn fill_dlmm_add_liquidity_accounts(
    _e: &mut MeteoraDlmmAddLiquidityEvent,
    _get: &AccountGetter<'_>,
) {
    // 事件数据已包含主要信息
}

/// Meteora DLMM Remove Liquidity 账户填充
///
/// removeLiquidity instruction account mapping (based on IDL):
/// 0: position
/// 1: lbPair
/// 2: binArrayBitmapExtension
/// 3: userTokenX
/// 4: userTokenY
/// 5: reserveX
/// 6: reserveY
/// 7: tokenXMint
/// 8: tokenYMint
/// 9: binArrayLower
/// 10: binArrayUpper
/// 11: sender
/// ...
pub fn fill_dlmm_remove_liquidity_accounts(
    _e: &mut MeteoraDlmmRemoveLiquidityEvent,
    _get: &AccountGetter<'_>,
) {
    // 事件数据已包含主要信息
}

#[cfg(test)]
mod dlmm_swap_tests {
    use super::*;

    #[test]
    fn swap2_bin_arrays_start_after_memo_and_program() {
        let memo = solana_sdk::pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
        let mut accounts: Vec<_> = (0..18).map(|_| Pubkey::new_unique()).collect();
        accounts[13] = memo;
        let bin0 = accounts[16];
        let bin1 = accounts[17];
        let mut e = MeteoraDlmmSwapEvent::default();
        fill_dlmm_swap_accounts(&mut e, &|i| accounts.get(i).copied().unwrap_or_default());
        assert_eq!(e.bin_arrays, vec![bin0, bin1]);
        assert_eq!(e.reserve_x, accounts[2]);
        assert_eq!(e.oracle, accounts[8]);
    }

    #[test]
    fn swap_v1_bin_arrays_start_at_15() {
        let accounts: Vec<_> = (0..17).map(|_| Pubkey::new_unique()).collect();
        // index 13 is NOT memo → v1 layout
        let bin0 = accounts[15];
        let bin1 = accounts[16];
        let mut e = MeteoraDlmmSwapEvent::default();
        fill_dlmm_swap_accounts(&mut e, &|i| accounts.get(i).copied().unwrap_or_default());
        assert_eq!(e.bin_arrays, vec![bin0, bin1]);
    }
}