// file: kb_lib/src/dex/raydium_clmm.rs

//! Raydium CLMM instruction decoder.

const RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR: [u8; 8] = [43, 4, 237, 11, 26, 201, 30, 98];

const RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR: [u8; 8] = [248, 198, 158, 145, 225, 117, 135, 200];

const RAYDIUM_CLMM_CREATE_POOL_DISCRIMINATOR: [u8; 8] = [233, 146, 209, 142, 207, 104, 64, 188];

const RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE_DISCRIMINATOR: [u8; 8] =
    [136, 136, 252, 221, 194, 66, 126, 89];

const RAYDIUM_CLMM_ANCHOR_SELF_CPI_LOG_SELECTOR: [u8; 8] = [228, 69, 165, 46, 81, 203, 154, 29];

const RAYDIUM_CLMM_COLLECT_PERSONAL_FEE_EVENT_DISCRIMINATOR: [u8; 8] =
    [166, 174, 105, 192, 81, 161, 83, 105];
const RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE_EVENT_DISCRIMINATOR: [u8; 8] =
    [206, 87, 17, 79, 45, 41, 213, 61];
const RAYDIUM_CLMM_CONFIG_CHANGE_EVENT_DISCRIMINATOR: [u8; 8] =
    [247, 189, 7, 119, 106, 112, 95, 151];
const RAYDIUM_CLMM_CREATE_PERSONAL_POSITION_EVENT_DISCRIMINATOR: [u8; 8] =
    [100, 30, 87, 249, 196, 223, 154, 206];
const RAYDIUM_CLMM_DECREASE_LIQUIDITY_EVENT_DISCRIMINATOR: [u8; 8] =
    [58, 222, 86, 58, 68, 50, 85, 56];
const RAYDIUM_CLMM_INCREASE_LIQUIDITY_EVENT_DISCRIMINATOR: [u8; 8] =
    [49, 79, 105, 212, 32, 34, 30, 84];
const RAYDIUM_CLMM_LIQUIDITY_CALCULATE_EVENT_DISCRIMINATOR: [u8; 8] =
    [237, 112, 148, 230, 57, 84, 180, 162];
const RAYDIUM_CLMM_LIQUIDITY_CHANGE_EVENT_DISCRIMINATOR: [u8; 8] =
    [126, 240, 175, 206, 158, 88, 153, 107];
const RAYDIUM_CLMM_POOL_CREATED_EVENT_DISCRIMINATOR: [u8; 8] = [25, 94, 75, 47, 112, 99, 53, 63];
const RAYDIUM_CLMM_SWAP_EVENT_DISCRIMINATOR: [u8; 8] = [64, 198, 205, 232, 38, 8, 113, 226];
const RAYDIUM_CLMM_UPDATE_REWARD_INFOS_EVENT_DISCRIMINATOR: [u8; 8] =
    [109, 127, 186, 78, 114, 65, 37, 236];

const OBSERVED_JUPITER_V6_PROGRAM_ID: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";

/// Decoded Raydium CLMM event.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum RaydiumClmmDecodedEvent {
    /// Raydium CLMM legacy swap instruction.
    Swap(crate::RaydiumClmmSwapLegacyDecoded),
    /// Raydium CLMM swap_v2 instruction.
    SwapV2(crate::RaydiumClmmSwapV2Decoded),
    /// Raydium CLMM create_pool instruction.
    CreatePool(crate::RaydiumClmmCreatePoolDecoded),
    /// Raydium CLMM collect_protocol_fee instruction.
    CollectProtocolFee(crate::RaydiumClmmCollectProtocolFeeDecoded),
    /// Raydium CLMM CollectPersonalFeeEvent `Program data` log.
    CollectPersonalFeeEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM CollectProtocolFeeEvent `Program data` log.
    CollectProtocolFeeEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM ConfigChangeEvent `Program data` log.
    ConfigChangeEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM CreatePersonalPositionEvent `Program data` log.
    CreatePersonalPositionEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM DecreaseLiquidityEvent `Program data` log.
    DecreaseLiquidityEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM IncreaseLiquidityEvent `Program data` log.
    IncreaseLiquidityEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM LiquidityCalculateEvent `Program data` log.
    LiquidityCalculateEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM LiquidityChangeEvent `Program data` log.
    LiquidityChangeEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM PoolCreatedEvent `Program data` log.
    PoolCreatedEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM SwapEvent `Program data` log retained as audit evidence.
    SwapEvent(crate::RaydiumClmmProgramDataEventDecoded),
    /// Raydium CLMM UpdateRewardInfosEvent `Program data` log.
    UpdateRewardInfosEvent(crate::RaydiumClmmProgramDataEventDecoded),
}

impl RaydiumClmmDecodedEvent {
    /// Returns the normalized event kind.
    pub fn event_kind(&self) -> &'static str {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(_) => return "raydium_clmm.swap",
            crate::RaydiumClmmDecodedEvent::SwapV2(_) => return "raydium_clmm.swap_v2",
            crate::RaydiumClmmDecodedEvent::CreatePool(_) => return "raydium_clmm.create_pool",
            crate::RaydiumClmmDecodedEvent::CollectProtocolFee(_) => {
                return "raydium_clmm.collect_protocol_fee";
            },
            crate::RaydiumClmmDecodedEvent::CollectPersonalFeeEvent(_) => {
                return "raydium_clmm.collect_personal_fee_event";
            },
            crate::RaydiumClmmDecodedEvent::CollectProtocolFeeEvent(_) => {
                return "raydium_clmm.collect_protocol_fee_event";
            },
            crate::RaydiumClmmDecodedEvent::ConfigChangeEvent(_) => {
                return "raydium_clmm.config_change_event";
            },
            crate::RaydiumClmmDecodedEvent::CreatePersonalPositionEvent(_) => {
                return "raydium_clmm.create_personal_position_event";
            },
            crate::RaydiumClmmDecodedEvent::DecreaseLiquidityEvent(_) => {
                return "raydium_clmm.decrease_liquidity_event";
            },
            crate::RaydiumClmmDecodedEvent::IncreaseLiquidityEvent(_) => {
                return "raydium_clmm.increase_liquidity_event";
            },
            crate::RaydiumClmmDecodedEvent::LiquidityCalculateEvent(_) => {
                return "raydium_clmm.liquidity_calculate_event";
            },
            crate::RaydiumClmmDecodedEvent::LiquidityChangeEvent(_) => {
                return "raydium_clmm.liquidity_change_event";
            },
            crate::RaydiumClmmDecodedEvent::PoolCreatedEvent(_) => {
                return "raydium_clmm.pool_created_event";
            },
            crate::RaydiumClmmDecodedEvent::SwapEvent(_) => return "raydium_clmm.swap_event",
            crate::RaydiumClmmDecodedEvent::UpdateRewardInfosEvent(_) => {
                return "raydium_clmm.update_reward_infos_event";
            },
        }
    }

    /// Returns the pool account when the decoded payload exposes one.
    pub fn pool_account_option(&self) -> std::option::Option<&str> {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(event) => return Some(event.pool_state.as_str()),
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => {
                return Some(event.pool_state.as_str());
            },
            crate::RaydiumClmmDecodedEvent::CreatePool(event) => {
                return Some(event.pool_state.as_str());
            },
            crate::RaydiumClmmDecodedEvent::CollectProtocolFee(event) => {
                return Some(event.pool_state.as_str());
            },
            crate::RaydiumClmmDecodedEvent::CollectPersonalFeeEvent(event)
            | crate::RaydiumClmmDecodedEvent::CollectProtocolFeeEvent(event)
            | crate::RaydiumClmmDecodedEvent::ConfigChangeEvent(event)
            | crate::RaydiumClmmDecodedEvent::CreatePersonalPositionEvent(event)
            | crate::RaydiumClmmDecodedEvent::DecreaseLiquidityEvent(event)
            | crate::RaydiumClmmDecodedEvent::IncreaseLiquidityEvent(event)
            | crate::RaydiumClmmDecodedEvent::LiquidityCalculateEvent(event)
            | crate::RaydiumClmmDecodedEvent::LiquidityChangeEvent(event)
            | crate::RaydiumClmmDecodedEvent::PoolCreatedEvent(event)
            | crate::RaydiumClmmDecodedEvent::SwapEvent(event)
            | crate::RaydiumClmmDecodedEvent::UpdateRewardInfosEvent(event) => {
                match event.pool_state.as_ref() {
                    Some(pool_state) => return Some(pool_state.as_str()),
                    None => return None,
                }
            },
        }
    }

    /// Returns the pool account, or an empty string for audit events without pool scope.
    pub fn pool_account(&self) -> &str {
        match self.pool_account_option() {
            Some(pool_account) => return pool_account,
            None => return "",
        }
    }

    /// Returns the normalized base mint when known.
    pub fn base_mint_option(&self) -> std::option::Option<&str> {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(event) => return Some(event.base_mint.as_str()),
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => return Some(event.base_mint.as_str()),
            crate::RaydiumClmmDecodedEvent::CreatePool(event) => {
                return Some(event.token_mint_0.as_str());
            },
            crate::RaydiumClmmDecodedEvent::CollectProtocolFee(event) => {
                return Some(event.vault_0_mint.as_str());
            },
            crate::RaydiumClmmDecodedEvent::CollectPersonalFeeEvent(event)
            | crate::RaydiumClmmDecodedEvent::CollectProtocolFeeEvent(event)
            | crate::RaydiumClmmDecodedEvent::ConfigChangeEvent(event)
            | crate::RaydiumClmmDecodedEvent::CreatePersonalPositionEvent(event)
            | crate::RaydiumClmmDecodedEvent::DecreaseLiquidityEvent(event)
            | crate::RaydiumClmmDecodedEvent::IncreaseLiquidityEvent(event)
            | crate::RaydiumClmmDecodedEvent::LiquidityCalculateEvent(event)
            | crate::RaydiumClmmDecodedEvent::LiquidityChangeEvent(event)
            | crate::RaydiumClmmDecodedEvent::PoolCreatedEvent(event)
            | crate::RaydiumClmmDecodedEvent::SwapEvent(event)
            | crate::RaydiumClmmDecodedEvent::UpdateRewardInfosEvent(event) => {
                match event.token_mint0.as_ref() {
                    Some(token_mint0) => return Some(token_mint0.as_str()),
                    None => return None,
                }
            },
        }
    }

    /// Returns the normalized base mint, or an empty string for audit events without mint scope.
    pub fn base_mint(&self) -> &str {
        match self.base_mint_option() {
            Some(base_mint) => return base_mint,
            None => return "",
        }
    }

    /// Returns the normalized quote mint when known.
    pub fn quote_mint_option(&self) -> std::option::Option<&str> {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(event) => return Some(event.quote_mint.as_str()),
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => {
                return Some(event.quote_mint.as_str());
            },
            crate::RaydiumClmmDecodedEvent::CreatePool(event) => {
                return Some(event.token_mint_1.as_str());
            },
            crate::RaydiumClmmDecodedEvent::CollectProtocolFee(event) => {
                return Some(event.vault_1_mint.as_str());
            },
            crate::RaydiumClmmDecodedEvent::CollectPersonalFeeEvent(event)
            | crate::RaydiumClmmDecodedEvent::CollectProtocolFeeEvent(event)
            | crate::RaydiumClmmDecodedEvent::ConfigChangeEvent(event)
            | crate::RaydiumClmmDecodedEvent::CreatePersonalPositionEvent(event)
            | crate::RaydiumClmmDecodedEvent::DecreaseLiquidityEvent(event)
            | crate::RaydiumClmmDecodedEvent::IncreaseLiquidityEvent(event)
            | crate::RaydiumClmmDecodedEvent::LiquidityCalculateEvent(event)
            | crate::RaydiumClmmDecodedEvent::LiquidityChangeEvent(event)
            | crate::RaydiumClmmDecodedEvent::PoolCreatedEvent(event)
            | crate::RaydiumClmmDecodedEvent::SwapEvent(event)
            | crate::RaydiumClmmDecodedEvent::UpdateRewardInfosEvent(event) => {
                match event.token_mint1.as_ref() {
                    Some(token_mint1) => return Some(token_mint1.as_str()),
                    None => return None,
                }
            },
        }
    }

    /// Returns the normalized quote mint, or an empty string for audit events without mint scope.
    pub fn quote_mint(&self) -> &str {
        match self.quote_mint_option() {
            Some(quote_mint) => return quote_mint,
            None => return "",
        }
    }

    /// Converts the decoded event to JSON payload.
    pub fn to_payload_json(&self) -> std::option::Option<std::string::String> {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(event) => return serde_json_string(event),
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => return serde_json_string(event),
            crate::RaydiumClmmDecodedEvent::CreatePool(event) => return serde_json_string(event),
            crate::RaydiumClmmDecodedEvent::CollectProtocolFee(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::CollectPersonalFeeEvent(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::CollectProtocolFeeEvent(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::ConfigChangeEvent(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::CreatePersonalPositionEvent(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::DecreaseLiquidityEvent(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::IncreaseLiquidityEvent(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::LiquidityCalculateEvent(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::LiquidityChangeEvent(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::PoolCreatedEvent(event) => {
                return serde_json_string(event);
            },
            crate::RaydiumClmmDecodedEvent::SwapEvent(event) => return serde_json_string(event),
            crate::RaydiumClmmDecodedEvent::UpdateRewardInfosEvent(event) => {
                return serde_json_string(event);
            },
        }
    }
}

/// One decoded Raydium CLMM instruction associated with its projected instruction id.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct RaydiumClmmDecodedInstructionEvent {
    /// Projected chain instruction id.
    pub instruction_id: i64,
    /// Decoded Raydium CLMM event.
    pub decoded_event: crate::RaydiumClmmDecodedEvent,
}

/// Decoded Raydium CLMM Anchor `Program data` event.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumClmmProgramDataEventDecoded {
    /// Upstream Anchor event name.
    pub event_name: std::string::String,
    /// Event discriminator in lowercase hexadecimal.
    pub event_discriminator_hex: std::string::String,
    /// Whether this was decoded from a `Program data:` log.
    pub program_data_log: bool,
    /// CLMM pool account when present in the event layout.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub pool_state: std::option::Option<std::string::String>,
    /// Position NFT mint when present in the event layout.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub position_nft_mint: std::option::Option<std::string::String>,
    /// Event sender/minter/owner authority when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub actor_wallet: std::option::Option<std::string::String>,
    /// First token mint when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub token_mint0: std::option::Option<std::string::String>,
    /// Second token mint when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub token_mint1: std::option::Option<std::string::String>,
    /// First token account when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub token_account0: std::option::Option<std::string::String>,
    /// Second token account when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub token_account1: std::option::Option<std::string::String>,
    /// First token vault when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub token_vault0: std::option::Option<std::string::String>,
    /// Second token vault when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub token_vault1: std::option::Option<std::string::String>,
    /// Lower tick index when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub tick_lower_index: std::option::Option<i32>,
    /// Upper tick index when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub tick_upper_index: std::option::Option<i32>,
    /// Current or initial tick when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub tick: std::option::Option<i32>,
    /// CLMM tick spacing when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub tick_spacing: std::option::Option<u16>,
    /// Liquidity value as decimal string when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub liquidity_raw: std::option::Option<std::string::String>,
    /// Pool liquidity before an event, as decimal string.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub liquidity_before_raw: std::option::Option<std::string::String>,
    /// Pool liquidity after an event, as decimal string.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub liquidity_after_raw: std::option::Option<std::string::String>,
    /// Sqrt price Q64.64 as decimal string when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub sqrt_price_x64: std::option::Option<std::string::String>,
    /// First token amount as decimal string.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub amount0_raw: std::option::Option<std::string::String>,
    /// Second token amount as decimal string.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub amount1_raw: std::option::Option<std::string::String>,
    /// First token transfer fee as decimal string.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub transfer_fee0_raw: std::option::Option<std::string::String>,
    /// Second token transfer fee as decimal string.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub transfer_fee1_raw: std::option::Option<std::string::String>,
    /// First fee amount as decimal string.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub fee_amount0_raw: std::option::Option<std::string::String>,
    /// Second fee amount as decimal string.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub fee_amount1_raw: std::option::Option<std::string::String>,
    /// Reward growth values as decimal strings.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub reward_growth_global_x64: std::option::Option<std::vec::Vec<std::string::String>>,
    /// Reward amounts as decimal strings.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub reward_amounts_raw: std::option::Option<std::vec::Vec<std::string::String>>,
    /// Zero-for-one swap flag when present.
    #[serde(skip_serializing_if = "std::option::Option::is_none")]
    pub zero_for_one: std::option::Option<bool>,
    /// Explicitly prevents Anchor audit events from double-counting instruction swaps.
    pub trade_candidate: bool,
    /// Explicitly prevents Anchor audit events from double-counting instruction candles.
    pub candle_candidate: bool,
    /// Trade skip reason for audit-only Program data events.
    pub skip_trade_reason: std::string::String,
    /// Candle skip reason for audit-only Program data events.
    pub skip_candle_reason: std::string::String,
}

fn serde_json_string<T: serde::Serialize>(value: &T) -> std::option::Option<std::string::String> {
    let result = serde_json::to_string(value);
    match result {
        Ok(payload_json) => return Some(payload_json),
        Err(_) => return None,
    }
}

/// Decoded Raydium CLMM legacy swap instruction.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct RaydiumClmmSwapLegacyDecoded {
    /// User performing the swap.
    pub payer: std::string::String,
    /// AMM config account.
    pub amm_config: std::string::String,
    /// CLMM pool state account.
    pub pool_state: std::string::String,
    /// User input token account.
    pub input_token_account: std::string::String,
    /// User output token account.
    pub output_token_account: std::string::String,
    /// Pool input vault.
    pub input_vault: std::string::String,
    /// Pool output vault.
    pub output_vault: std::string::String,
    /// Pool oracle observation state.
    pub observation_state: std::string::String,
    /// Input vault mint inferred from transaction token balances.
    pub input_vault_mint: std::string::String,
    /// Output vault mint inferred from transaction token balances.
    pub output_vault_mint: std::string::String,
    /// Canonical base mint.
    pub base_mint: std::string::String,
    /// Canonical quote mint.
    pub quote_mint: std::string::String,
    /// Canonical base vault.
    pub base_vault: std::string::String,
    /// Canonical quote vault.
    pub quote_vault: std::string::String,
    /// Trade side relative to the canonical base mint.
    #[serde(rename = "tradeSide")]
    pub trade_side: std::string::String,
    /// Optional raw base amount inferred from vault balance deltas.
    #[serde(rename = "baseAmountRaw")]
    pub base_amount_raw: std::option::Option<std::string::String>,
    /// Optional raw quote amount inferred from vault balance deltas.
    #[serde(rename = "quoteAmountRaw")]
    pub quote_amount_raw: std::option::Option<std::string::String>,
    /// Amount argument.
    pub amount: u64,
    /// Other amount threshold argument.
    pub other_amount_threshold: u64,
    /// Sqrt price limit as decimal string.
    pub sqrt_price_limit_x64: std::string::String,
    /// Whether the instruction uses exact input mode.
    pub is_base_input: bool,
    /// Whether this decoded event comes from the legacy CLMM swap discriminator.
    #[serde(rename = "legacyInstruction")]
    pub legacy_instruction: bool,
    /// Optional top-level caller inferred from parent instruction.
    #[serde(rename = "routeSource")]
    pub route_source: std::option::Option<std::string::String>,
}

/// Decoded Raydium CLMM swap_v2 instruction.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct RaydiumClmmSwapV2Decoded {
    /// User performing the swap.
    pub payer: std::string::String,
    /// AMM config account.
    pub amm_config: std::string::String,
    /// CLMM pool state account.
    pub pool_state: std::string::String,
    /// User input token account.
    pub input_token_account: std::string::String,
    /// User output token account.
    pub output_token_account: std::string::String,
    /// Pool input vault.
    pub input_vault: std::string::String,
    /// Pool output vault.
    pub output_vault: std::string::String,
    /// Pool oracle observation state.
    pub observation_state: std::string::String,
    /// Input vault mint.
    pub input_vault_mint: std::string::String,
    /// Output vault mint.
    pub output_vault_mint: std::string::String,
    /// Canonical base mint.
    pub base_mint: std::string::String,
    /// Canonical quote mint.
    pub quote_mint: std::string::String,
    /// Canonical base vault.
    pub base_vault: std::string::String,
    /// Canonical quote vault.
    pub quote_vault: std::string::String,
    /// Trade side relative to the canonical base mint.
    #[serde(rename = "tradeSide")]
    pub trade_side: std::string::String,
    /// Amount argument.
    pub amount: u64,
    /// Other amount threshold argument.
    pub other_amount_threshold: u64,
    /// Sqrt price limit as decimal string.
    pub sqrt_price_limit_x64: std::string::String,
    /// Whether the instruction uses exact input mode.
    pub is_base_input: bool,
}

/// Decoded Raydium CLMM create_pool instruction.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct RaydiumClmmCreatePoolDecoded {
    /// Pool creator or payer account.
    pub payer: std::string::String,
    /// AMM config account.
    pub amm_config: std::string::String,
    /// CLMM pool state account.
    pub pool_state: std::string::String,
    /// First pool token mint in the instruction account order.
    pub token_mint_0: std::string::String,
    /// Second pool token mint in the instruction account order.
    pub token_mint_1: std::string::String,
    /// First pool vault.
    pub token_vault_0: std::string::String,
    /// Second pool vault.
    pub token_vault_1: std::string::String,
    /// Observation state account.
    pub observation_state: std::string::String,
    /// Initial sqrt price as decimal string.
    #[serde(rename = "sqrtPriceX64")]
    pub sqrt_price_x64: std::string::String,
    /// Pool open time argument.
    #[serde(rename = "openTime")]
    pub open_time: u64,
    /// Instruction discriminator used for audit cleanup and coverage reconciliation.
    #[serde(rename = "instructionDiscriminatorHex")]
    pub instruction_discriminator_hex: std::string::String,
}

/// Decoded Raydium CLMM collect_protocol_fee instruction.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct RaydiumClmmCollectProtocolFeeDecoded {
    /// Fee collection authority.
    pub authority: std::string::String,
    /// CLMM pool state account.
    pub pool_state: std::string::String,
    /// First recipient token account.
    pub recipient_token_account_0: std::string::String,
    /// Second recipient token account.
    pub recipient_token_account_1: std::string::String,
    /// First pool vault.
    pub token_vault_0: std::string::String,
    /// Second pool vault.
    pub token_vault_1: std::string::String,
    /// First pool vault mint.
    pub vault_0_mint: std::string::String,
    /// Second pool vault mint.
    pub vault_1_mint: std::string::String,
    /// Requested first token amount.
    #[serde(rename = "amount0RequestedRaw")]
    pub amount_0_requested_raw: std::string::String,
    /// Requested second token amount.
    #[serde(rename = "amount1RequestedRaw")]
    pub amount_1_requested_raw: std::string::String,
    /// Alias used by generic fee materialization.
    #[serde(rename = "tokenAAmount")]
    pub token_a_amount: std::string::String,
    /// Alias used by generic fee materialization.
    #[serde(rename = "tokenBAmount")]
    pub token_b_amount: std::string::String,
    /// Instruction discriminator used for audit cleanup and coverage reconciliation.
    #[serde(rename = "instructionDiscriminatorHex")]
    pub instruction_discriminator_hex: std::string::String,
}

/// Raydium CLMM transaction decoder.
#[derive(Clone, Debug, Default)]
pub struct RaydiumClmmDecoder;

impl RaydiumClmmDecoder {
    /// Creates a new Raydium CLMM decoder.
    pub fn new() -> Self {
        return Self;
    }

    /// Decodes one projected transaction into Raydium CLMM instruction events.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::RaydiumClmmDecodedInstructionEvent>, crate::Error> {
        let transaction_json_result =
            serde_json::from_str::<serde_json::Value>(transaction.transaction_json.as_str());
        let transaction_json = match transaction_json_result {
            Ok(transaction_json) => transaction_json,
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot parse transaction_json for signature '{}': {}",
                    transaction.signature, error
                )));
            },
        };
        let meta_value_result = parse_transaction_meta_value(transaction, &transaction_json);
        let meta_value = match meta_value_result {
            Ok(meta_value) => meta_value,
            Err(error) => return Err(error),
        };
        let account_keys = extract_transaction_account_keys(&transaction_json, meta_value.as_ref());
        let token_balances =
            extract_token_balance_records(meta_value.as_ref(), account_keys.as_slice());
        let mut decoded = std::vec::Vec::new();
        for instruction in instructions {
            let program_id = match instruction.program_id.as_ref() {
                Some(program_id) => program_id,
                None => continue,
            };
            if program_id.as_str() != crate::RAYDIUM_CLMM_PROGRAM_ID {
                continue;
            }
            let instruction_id = match instruction.id {
                Some(instruction_id) => instruction_id,
                None => continue,
            };
            let data_json = match instruction.data_json.as_ref() {
                Some(data_json) => data_json,
                None => continue,
            };
            let decoded_events_result = decode_raydium_clmm_instruction_with_token_balances(
                instruction.accounts_json.as_str(),
                data_json.as_str(),
                instructions,
                instruction,
                token_balances.as_slice(),
            );
            let decoded_events = match decoded_events_result {
                Ok(decoded_events) => decoded_events,
                Err(error) => return Err(error),
            };
            for decoded_event in decoded_events {
                decoded.push(crate::RaydiumClmmDecodedInstructionEvent {
                    instruction_id,
                    decoded_event,
                });
            }
        }
        return Ok(decoded);
    }
}

/// Decodes a Raydium CLMM instruction.
pub fn decode_raydium_clmm_instruction(
    accounts_json: &str,
    data_json: &str,
) -> std::vec::Vec<crate::RaydiumClmmDecodedEvent> {
    let mut decoded = std::vec::Vec::new();
    let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
    let accounts = match accounts_result {
        Ok(accounts) => accounts,
        Err(_) => return decoded,
    };
    let data_base58_result = serde_json::from_str::<std::string::String>(data_json);
    let data_base58 = match data_base58_result {
        Ok(data_base58) => data_base58,
        Err(_) => data_json.to_string(),
    };
    let data_option = decode_base58(data_base58.as_str());
    let data = match data_option {
        Some(data) => data,
        None => return decoded,
    };
    if data.len() < 8 {
        return decoded;
    }
    let discriminator_option = read_discriminator(data.as_slice());
    let discriminator = match discriminator_option {
        Some(discriminator) => discriminator,
        None => return decoded,
    };
    if discriminator == RAYDIUM_CLMM_CREATE_POOL_DISCRIMINATOR {
        let event_option = decode_create_pool(accounts.as_slice(), data.as_slice());
        let event = match event_option {
            Some(event) => event,
            None => return decoded,
        };
        decoded.push(crate::RaydiumClmmDecodedEvent::CreatePool(event));
        return decoded;
    }
    if discriminator == RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE_DISCRIMINATOR {
        let event_option = decode_collect_protocol_fee(accounts.as_slice(), data.as_slice());
        let event = match event_option {
            Some(event) => event,
            None => return decoded,
        };
        decoded.push(crate::RaydiumClmmDecodedEvent::CollectProtocolFee(event));
        return decoded;
    }
    if discriminator == RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR {
        return decoded;
    }
    if discriminator != RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR {
        return decoded;
    }
    let event_option = decode_swap_v2(accounts.as_slice(), data.as_slice());
    let event = match event_option {
        Some(event) => event,
        None => return decoded,
    };
    decoded.push(crate::RaydiumClmmDecodedEvent::SwapV2(event));
    return decoded;
}

/// Decodes one Raydium CLMM Anchor event emitted in a `Program data:` log.
pub fn decode_raydium_clmm_program_data_event(
    data_base64: &str,
) -> std::option::Option<crate::RaydiumClmmDecodedEvent> {
    let decoded = decode_base64_standard(data_base64);
    let data = match decoded {
        Some(data) => data,
        None => return None,
    };
    let event = decode_raydium_clmm_program_data_event_bytes(data.as_slice());
    if event.is_some() {
        return event;
    }
    if data.len() < 16 {
        return None;
    }
    let selector = read_discriminator(data.as_slice());
    let selector = match selector {
        Some(selector) => selector,
        None => return None,
    };
    if selector != RAYDIUM_CLMM_ANCHOR_SELF_CPI_LOG_SELECTOR {
        return None;
    }
    return decode_raydium_clmm_program_data_event_bytes(&data[8..]);
}

fn decode_raydium_clmm_program_data_event_bytes(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmDecodedEvent> {
    let discriminator = match read_discriminator(data) {
        Some(discriminator) => discriminator,
        None => return None,
    };
    if discriminator == RAYDIUM_CLMM_COLLECT_PERSONAL_FEE_EVENT_DISCRIMINATOR {
        let event = decode_clmm_collect_personal_fee_event(data);
        match event {
            Some(event) => {
                return Some(crate::RaydiumClmmDecodedEvent::CollectPersonalFeeEvent(event));
            },
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE_EVENT_DISCRIMINATOR {
        let event = decode_clmm_collect_protocol_fee_event(data);
        match event {
            Some(event) => {
                return Some(crate::RaydiumClmmDecodedEvent::CollectProtocolFeeEvent(event));
            },
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_CONFIG_CHANGE_EVENT_DISCRIMINATOR {
        let event = decode_clmm_config_change_event(data);
        match event {
            Some(event) => return Some(crate::RaydiumClmmDecodedEvent::ConfigChangeEvent(event)),
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_CREATE_PERSONAL_POSITION_EVENT_DISCRIMINATOR {
        let event = decode_clmm_create_personal_position_event(data);
        match event {
            Some(event) => {
                return Some(crate::RaydiumClmmDecodedEvent::CreatePersonalPositionEvent(event));
            },
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_DECREASE_LIQUIDITY_EVENT_DISCRIMINATOR {
        let event = decode_clmm_decrease_liquidity_event(data);
        match event {
            Some(event) => {
                return Some(crate::RaydiumClmmDecodedEvent::DecreaseLiquidityEvent(event));
            },
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_INCREASE_LIQUIDITY_EVENT_DISCRIMINATOR {
        let event = decode_clmm_increase_liquidity_event(data);
        match event {
            Some(event) => {
                return Some(crate::RaydiumClmmDecodedEvent::IncreaseLiquidityEvent(event));
            },
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_LIQUIDITY_CALCULATE_EVENT_DISCRIMINATOR {
        let event = decode_clmm_liquidity_calculate_event(data);
        match event {
            Some(event) => {
                return Some(crate::RaydiumClmmDecodedEvent::LiquidityCalculateEvent(event));
            },
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_LIQUIDITY_CHANGE_EVENT_DISCRIMINATOR {
        let event = decode_clmm_liquidity_change_event(data);
        match event {
            Some(event) => {
                return Some(crate::RaydiumClmmDecodedEvent::LiquidityChangeEvent(event));
            },
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_POOL_CREATED_EVENT_DISCRIMINATOR {
        let event = decode_clmm_pool_created_event(data);
        match event {
            Some(event) => return Some(crate::RaydiumClmmDecodedEvent::PoolCreatedEvent(event)),
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_SWAP_EVENT_DISCRIMINATOR {
        let event = decode_clmm_swap_event(data);
        match event {
            Some(event) => return Some(crate::RaydiumClmmDecodedEvent::SwapEvent(event)),
            None => return None,
        }
    }
    if discriminator == RAYDIUM_CLMM_UPDATE_REWARD_INFOS_EVENT_DISCRIMINATOR {
        let event = decode_clmm_update_reward_infos_event(data);
        match event {
            Some(event) => {
                return Some(crate::RaydiumClmmDecodedEvent::UpdateRewardInfosEvent(event));
            },
            None => return None,
        }
    }
    return None;
}

fn new_clmm_program_data_event(
    event_name: &str,
    event_discriminator_hex: &str,
) -> crate::RaydiumClmmProgramDataEventDecoded {
    return crate::RaydiumClmmProgramDataEventDecoded {
        event_name: event_name.to_string(),
        event_discriminator_hex: event_discriminator_hex.to_string(),
        program_data_log: true,
        pool_state: None,
        position_nft_mint: None,
        actor_wallet: None,
        token_mint0: None,
        token_mint1: None,
        token_account0: None,
        token_account1: None,
        token_vault0: None,
        token_vault1: None,
        tick_lower_index: None,
        tick_upper_index: None,
        tick: None,
        tick_spacing: None,
        liquidity_raw: None,
        liquidity_before_raw: None,
        liquidity_after_raw: None,
        sqrt_price_x64: None,
        amount0_raw: None,
        amount1_raw: None,
        transfer_fee0_raw: None,
        transfer_fee1_raw: None,
        fee_amount0_raw: None,
        fee_amount1_raw: None,
        reward_growth_global_x64: None,
        reward_amounts_raw: None,
        zero_for_one: None,
        trade_candidate: false,
        candle_candidate: false,
        skip_trade_reason: "raydium_clmm_program_data_event_audit_only".to_string(),
        skip_candle_reason: "raydium_clmm_program_data_event_audit_only".to_string(),
    };
}

fn decode_clmm_collect_personal_fee_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 120 {
        return None;
    }
    let mut event = new_clmm_program_data_event("collect_personal_fee_event", "a6ae69c051a15369");
    event.position_nft_mint = read_pubkey_base58(data, 8);
    event.token_account0 = read_pubkey_base58(data, 40);
    event.token_account1 = read_pubkey_base58(data, 72);
    event.amount0_raw = read_u64_le(data, 104).map(|value| return value.to_string());
    event.amount1_raw = read_u64_le(data, 112).map(|value| return value.to_string());
    return Some(event);
}

fn decode_clmm_collect_protocol_fee_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 120 {
        return None;
    }
    let mut event = new_clmm_program_data_event("collect_protocol_fee_event", "ce57114f2d29d53d");
    event.pool_state = read_pubkey_base58(data, 8);
    event.token_account0 = read_pubkey_base58(data, 40);
    event.token_account1 = read_pubkey_base58(data, 72);
    event.amount0_raw = read_u64_le(data, 104).map(|value| return value.to_string());
    event.amount1_raw = read_u64_le(data, 112).map(|value| return value.to_string());
    return Some(event);
}

fn decode_clmm_config_change_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 88 {
        return None;
    }
    let mut event = new_clmm_program_data_event("config_change_event", "f7bd07776a705f97");
    event.tick_spacing = read_u16_le(data, 50);
    event.actor_wallet = read_pubkey_base58(data, 10);
    event.fee_amount0_raw = read_u32_le(data, 42).map(|value| return value.to_string());
    event.fee_amount1_raw = read_u32_le(data, 46).map(|value| return value.to_string());
    event.token_account0 = read_pubkey_base58(data, 56);
    return Some(event);
}

fn decode_clmm_create_personal_position_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 160 {
        return None;
    }
    let mut event =
        new_clmm_program_data_event("create_personal_position_event", "641e57f9c4df9ace");
    event.pool_state = read_pubkey_base58(data, 8);
    event.actor_wallet = read_pubkey_base58(data, 40);
    event.token_account0 = read_pubkey_base58(data, 72);
    event.tick_lower_index = read_i32_le(data, 104);
    event.tick_upper_index = read_i32_le(data, 108);
    event.liquidity_raw = read_u128_le(data, 112).map(|value| return value.to_string());
    event.amount0_raw = read_u64_le(data, 128).map(|value| return value.to_string());
    event.amount1_raw = read_u64_le(data, 136).map(|value| return value.to_string());
    event.transfer_fee0_raw = read_u64_le(data, 144).map(|value| return value.to_string());
    event.transfer_fee1_raw = read_u64_le(data, 152).map(|value| return value.to_string());
    return Some(event);
}

fn decode_clmm_decrease_liquidity_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 128 {
        return None;
    }
    let mut event = new_clmm_program_data_event("decrease_liquidity_event", "3ade563a44325538");
    event.position_nft_mint = read_pubkey_base58(data, 8);
    event.liquidity_raw = read_u128_le(data, 40).map(|value| return value.to_string());
    event.amount0_raw = read_u64_le(data, 56).map(|value| return value.to_string());
    event.amount1_raw = read_u64_le(data, 64).map(|value| return value.to_string());
    event.fee_amount0_raw = read_u64_le(data, 72).map(|value| return value.to_string());
    event.fee_amount1_raw = read_u64_le(data, 80).map(|value| return value.to_string());
    event.reward_amounts_raw = read_u64_vec(data, 88, 3);
    event.transfer_fee0_raw = read_u64_le(data, 112).map(|value| return value.to_string());
    event.transfer_fee1_raw = read_u64_le(data, 120).map(|value| return value.to_string());
    return Some(event);
}

fn decode_clmm_increase_liquidity_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 88 {
        return None;
    }
    let mut event = new_clmm_program_data_event("increase_liquidity_event", "314f69d420221e54");
    event.position_nft_mint = read_pubkey_base58(data, 8);
    event.liquidity_raw = read_u128_le(data, 40).map(|value| return value.to_string());
    event.amount0_raw = read_u64_le(data, 56).map(|value| return value.to_string());
    event.amount1_raw = read_u64_le(data, 64).map(|value| return value.to_string());
    event.transfer_fee0_raw = read_u64_le(data, 72).map(|value| return value.to_string());
    event.transfer_fee1_raw = read_u64_le(data, 80).map(|value| return value.to_string());
    return Some(event);
}

fn decode_clmm_liquidity_calculate_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 92 {
        return None;
    }
    let mut event = new_clmm_program_data_event("liquidity_calculate_event", "ed7094e63954b4a2");
    event.liquidity_before_raw = read_u128_le(data, 8).map(|value| return value.to_string());
    event.sqrt_price_x64 = read_u128_le(data, 24).map(|value| return value.to_string());
    event.tick = read_i32_le(data, 40);
    event.amount0_raw = read_u64_le(data, 44).map(|value| return value.to_string());
    event.amount1_raw = read_u64_le(data, 52).map(|value| return value.to_string());
    event.fee_amount0_raw = read_u64_le(data, 60).map(|value| return value.to_string());
    event.fee_amount1_raw = read_u64_le(data, 68).map(|value| return value.to_string());
    event.transfer_fee0_raw = read_u64_le(data, 76).map(|value| return value.to_string());
    event.transfer_fee1_raw = read_u64_le(data, 84).map(|value| return value.to_string());
    return Some(event);
}

fn decode_clmm_liquidity_change_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 84 {
        return None;
    }
    let mut event = new_clmm_program_data_event("liquidity_change_event", "7ef0afce9e58996b");
    event.pool_state = read_pubkey_base58(data, 8);
    event.tick = read_i32_le(data, 40);
    event.tick_lower_index = read_i32_le(data, 44);
    event.tick_upper_index = read_i32_le(data, 48);
    event.liquidity_before_raw = read_u128_le(data, 52).map(|value| return value.to_string());
    event.liquidity_after_raw = read_u128_le(data, 68).map(|value| return value.to_string());
    return Some(event);
}

fn decode_clmm_pool_created_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 190 {
        return None;
    }
    let mut event = new_clmm_program_data_event("pool_created_event", "195e4b2f7063353f");
    event.token_mint0 = read_pubkey_base58(data, 8);
    event.token_mint1 = read_pubkey_base58(data, 40);
    event.tick_spacing = read_u16_le(data, 72);
    event.pool_state = read_pubkey_base58(data, 74);
    event.sqrt_price_x64 = read_u128_le(data, 106).map(|value| return value.to_string());
    event.tick = read_i32_le(data, 122);
    event.token_vault0 = read_pubkey_base58(data, 126);
    event.token_vault1 = read_pubkey_base58(data, 158);
    return Some(event);
}

fn decode_clmm_swap_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 205 {
        return None;
    }
    let mut event = new_clmm_program_data_event("swap_event", "40c6cde8260871e2");
    event.pool_state = read_pubkey_base58(data, 8);
    event.actor_wallet = read_pubkey_base58(data, 40);
    event.token_account0 = read_pubkey_base58(data, 72);
    event.token_account1 = read_pubkey_base58(data, 104);
    event.amount0_raw = read_u64_le(data, 136).map(|value| return value.to_string());
    event.transfer_fee0_raw = read_u64_le(data, 144).map(|value| return value.to_string());
    event.amount1_raw = read_u64_le(data, 152).map(|value| return value.to_string());
    event.transfer_fee1_raw = read_u64_le(data, 160).map(|value| return value.to_string());
    event.zero_for_one = read_bool(data, 168);
    event.sqrt_price_x64 = read_u128_le(data, 169).map(|value| return value.to_string());
    event.liquidity_raw = read_u128_le(data, 185).map(|value| return value.to_string());
    event.tick = read_i32_le(data, 201);
    return Some(event);
}

fn decode_clmm_update_reward_infos_event(
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmProgramDataEventDecoded> {
    if data.len() < 56 {
        return None;
    }
    let mut event = new_clmm_program_data_event("update_reward_infos_event", "6d7fba4e724125ec");
    event.reward_growth_global_x64 = read_u128_vec(data, 8, 3);
    return Some(event);
}

fn decode_raydium_clmm_instruction_with_token_balances(
    accounts_json: &str,
    data_json: &str,
    transaction_instructions: &[crate::ChainInstructionDto],
    instruction: &crate::ChainInstructionDto,
    token_balances: &[TokenBalanceRecord],
) -> Result<std::vec::Vec<crate::RaydiumClmmDecodedEvent>, crate::Error> {
    let mut decoded = std::vec::Vec::new();
    let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
    let accounts = match accounts_result {
        Ok(accounts) => accounts,
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse raydium clmm accounts json: {}",
                error
            )));
        },
    };
    let data_base58_result = serde_json::from_str::<std::string::String>(data_json);
    let data_base58 = match data_base58_result {
        Ok(data_base58) => data_base58,
        Err(_) => data_json.to_string(),
    };
    let data_option = decode_base58(data_base58.as_str());
    let data = match data_option {
        Some(data) => data,
        None => return Ok(decoded),
    };
    if data.len() < 8 {
        return Ok(decoded);
    }
    let discriminator_option = read_discriminator(data.as_slice());
    let discriminator = match discriminator_option {
        Some(discriminator) => discriminator,
        None => return Ok(decoded),
    };
    if discriminator == RAYDIUM_CLMM_CREATE_POOL_DISCRIMINATOR {
        let event_option = decode_create_pool(accounts.as_slice(), data.as_slice());
        if let Some(event) = event_option {
            decoded.push(crate::RaydiumClmmDecodedEvent::CreatePool(event));
        }
        return Ok(decoded);
    }
    if discriminator == RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE_DISCRIMINATOR {
        let event_option = decode_collect_protocol_fee(accounts.as_slice(), data.as_slice());
        if let Some(event) = event_option {
            decoded.push(crate::RaydiumClmmDecodedEvent::CollectProtocolFee(event));
        }
        return Ok(decoded);
    }
    if discriminator == RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR {
        let event_option = decode_swap_v2(accounts.as_slice(), data.as_slice());
        if let Some(event) = event_option {
            decoded.push(crate::RaydiumClmmDecodedEvent::SwapV2(event));
        }
        return Ok(decoded);
    }
    if discriminator == RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR {
        let event_option = decode_swap_legacy(
            accounts.as_slice(),
            data.as_slice(),
            transaction_instructions,
            instruction,
            token_balances,
        );
        if let Some(event) = event_option {
            decoded.push(crate::RaydiumClmmDecodedEvent::Swap(event));
        }
        return Ok(decoded);
    }
    return Ok(decoded);
}

fn decode_create_pool(
    accounts: &[std::string::String],
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmCreatePoolDecoded> {
    let payer = match clone_account(accounts, 0) {
        Some(value) => value,
        None => return None,
    };
    let amm_config = match clone_account(accounts, 1) {
        Some(value) => value,
        None => return None,
    };
    let pool_state = match clone_account(accounts, 2) {
        Some(value) => value,
        None => return None,
    };
    let token_mint_0 = match clone_account(accounts, 3) {
        Some(value) => value,
        None => return None,
    };
    let token_mint_1 = match clone_account(accounts, 4) {
        Some(value) => value,
        None => return None,
    };
    let token_vault_0 = match clone_account(accounts, 5) {
        Some(value) => value,
        None => return None,
    };
    let token_vault_1 = match clone_account(accounts, 6) {
        Some(value) => value,
        None => return None,
    };
    let observation_state = match clone_account(accounts, 7) {
        Some(value) => value,
        None => return None,
    };
    let sqrt_price_x64 = match read_u128_le(data, 8) {
        Some(value) => value,
        None => return None,
    };
    let open_time = match read_u64_le(data, 24) {
        Some(value) => value,
        None => return None,
    };
    return Some(crate::RaydiumClmmCreatePoolDecoded {
        payer,
        amm_config,
        pool_state,
        token_mint_0,
        token_mint_1,
        token_vault_0,
        token_vault_1,
        observation_state,
        sqrt_price_x64: sqrt_price_x64.to_string(),
        open_time,
        instruction_discriminator_hex: "e992d18ecf6840bc".to_string(),
    });
}

fn decode_collect_protocol_fee(
    accounts: &[std::string::String],
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmCollectProtocolFeeDecoded> {
    let authority = match clone_account(accounts, 0) {
        Some(value) => value,
        None => return None,
    };
    let pool_state = match clone_account(accounts, 1) {
        Some(value) => value,
        None => return None,
    };
    let recipient_token_account_0 = match clone_account(accounts, 2) {
        Some(value) => value,
        None => return None,
    };
    let recipient_token_account_1 = match clone_account(accounts, 3) {
        Some(value) => value,
        None => return None,
    };
    let token_vault_0 = match clone_account(accounts, 4) {
        Some(value) => value,
        None => return None,
    };
    let token_vault_1 = match clone_account(accounts, 7) {
        Some(value) => value,
        None => return None,
    };
    let vault_0_mint = match clone_account(accounts, 5) {
        Some(value) => value,
        None => return None,
    };
    let vault_1_mint = match clone_account(accounts, 6) {
        Some(value) => value,
        None => return None,
    };
    let amount_0_requested = match read_u64_le(data, 8) {
        Some(value) => value.to_string(),
        None => return None,
    };
    let amount_1_requested = match read_u64_le(data, 16) {
        Some(value) => value.to_string(),
        None => return None,
    };
    return Some(crate::RaydiumClmmCollectProtocolFeeDecoded {
        authority,
        pool_state,
        recipient_token_account_0,
        recipient_token_account_1,
        token_vault_0,
        token_vault_1,
        vault_0_mint,
        vault_1_mint,
        amount_0_requested_raw: amount_0_requested.clone(),
        amount_1_requested_raw: amount_1_requested.clone(),
        token_a_amount: amount_0_requested,
        token_b_amount: amount_1_requested,
        instruction_discriminator_hex: "8888fcddc2427e59".to_string(),
    });
}

fn decode_swap_legacy(
    accounts: &[std::string::String],
    data: &[u8],
    transaction_instructions: &[crate::ChainInstructionDto],
    instruction: &crate::ChainInstructionDto,
    token_balances: &[TokenBalanceRecord],
) -> std::option::Option<crate::RaydiumClmmSwapLegacyDecoded> {
    let payer = match clone_account(accounts, 0) {
        Some(value) => value,
        None => return None,
    };
    let amm_config = match clone_account(accounts, 1) {
        Some(value) => value,
        None => return None,
    };
    let pool_state = match clone_account(accounts, 2) {
        Some(value) => value,
        None => return None,
    };
    let input_token_account = match clone_account(accounts, 3) {
        Some(value) => value,
        None => return None,
    };
    let output_token_account = match clone_account(accounts, 4) {
        Some(value) => value,
        None => return None,
    };
    let input_vault = match clone_account(accounts, 5) {
        Some(value) => value,
        None => return None,
    };
    let output_vault = match clone_account(accounts, 6) {
        Some(value) => value,
        None => return None,
    };
    let observation_state = match clone_account(accounts, 7) {
        Some(value) => value,
        None => return None,
    };
    let input_vault_mint = match mint_for_account(token_balances, input_vault.as_str()) {
        Some(value) => value,
        None => return None,
    };
    let output_vault_mint = match mint_for_account(token_balances, output_vault.as_str()) {
        Some(value) => value,
        None => return None,
    };
    if input_vault_mint == output_vault_mint {
        return None;
    }
    let amount = match read_u64_le(data, 8) {
        Some(value) => value,
        None => return None,
    };
    let other_amount_threshold = match read_u64_le(data, 16) {
        Some(value) => value,
        None => return None,
    };
    let sqrt_price_limit_x64 = match read_u128_le(data, 24) {
        Some(value) => value,
        None => return None,
    };
    let is_base_input = match read_bool(data, 40) {
        Some(value) => value,
        None => return None,
    };
    let mut base_mint = input_vault_mint.clone();
    let mut quote_mint = output_vault_mint.clone();
    let mut base_vault = input_vault.clone();
    let mut quote_vault = output_vault.clone();
    if is_quote_mint(input_vault_mint.as_str()) && !is_quote_mint(output_vault_mint.as_str()) {
        base_mint = output_vault_mint.clone();
        quote_mint = input_vault_mint.clone();
        base_vault = output_vault.clone();
        quote_vault = input_vault.clone();
    } else if !is_quote_mint(input_vault_mint.as_str()) && is_quote_mint(output_vault_mint.as_str())
    {
        base_mint = input_vault_mint.clone();
        quote_mint = output_vault_mint.clone();
        base_vault = input_vault.clone();
        quote_vault = output_vault.clone();
    } else if output_vault_mint.as_str() < input_vault_mint.as_str() {
        base_mint = output_vault_mint.clone();
        quote_mint = input_vault_mint.clone();
        base_vault = output_vault.clone();
        quote_vault = input_vault.clone();
    }
    let base_amount_raw = amount_delta_abs_for_account(token_balances, base_vault.as_str());
    let quote_amount_raw = amount_delta_abs_for_account(token_balances, quote_vault.as_str());
    if base_amount_raw.is_none() || quote_amount_raw.is_none() {
        return None;
    }
    let trade_side = match infer_trade_side_from_vault_deltas(
        token_balances,
        base_vault.as_str(),
        quote_vault.as_str(),
    ) {
        Some(trade_side) => trade_side,
        None => {
            if base_vault == input_vault {
                "SellBase".to_string()
            } else {
                "BuyBase".to_string()
            }
        },
    };
    let parent_program = parent_program_id_for_instruction(instruction, transaction_instructions);
    let route_source = route_source_from_parent(parent_program.as_deref());
    return Some(crate::RaydiumClmmSwapLegacyDecoded {
        payer,
        amm_config,
        pool_state,
        input_token_account,
        output_token_account,
        input_vault,
        output_vault,
        observation_state,
        input_vault_mint,
        output_vault_mint,
        base_mint,
        quote_mint,
        base_vault,
        quote_vault,
        trade_side,
        base_amount_raw,
        quote_amount_raw,
        amount,
        other_amount_threshold,
        sqrt_price_limit_x64: sqrt_price_limit_x64.to_string(),
        is_base_input,
        legacy_instruction: true,
        route_source,
    });
}

fn decode_swap_v2(
    accounts: &[std::string::String],
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmSwapV2Decoded> {
    let payer = match clone_account(accounts, 0) {
        Some(value) => value,
        None => return None,
    };
    let amm_config = match clone_account(accounts, 1) {
        Some(value) => value,
        None => return None,
    };
    let pool_state = match clone_account(accounts, 2) {
        Some(value) => value,
        None => return None,
    };
    let input_token_account = match clone_account(accounts, 3) {
        Some(value) => value,
        None => return None,
    };
    let output_token_account = match clone_account(accounts, 4) {
        Some(value) => value,
        None => return None,
    };
    let input_vault = match clone_account(accounts, 5) {
        Some(value) => value,
        None => return None,
    };
    let output_vault = match clone_account(accounts, 6) {
        Some(value) => value,
        None => return None,
    };
    let observation_state = match clone_account(accounts, 7) {
        Some(value) => value,
        None => return None,
    };
    let input_vault_mint = match clone_account(accounts, 11) {
        Some(value) => value,
        None => return None,
    };
    let output_vault_mint = match clone_account(accounts, 12) {
        Some(value) => value,
        None => return None,
    };
    let amount = match read_u64_le(data, 8) {
        Some(value) => value,
        None => return None,
    };
    let other_amount_threshold = match read_u64_le(data, 16) {
        Some(value) => value,
        None => return None,
    };
    let sqrt_price_limit_x64 = match read_u128_le(data, 24) {
        Some(value) => value,
        None => return None,
    };
    let is_base_input = match read_bool(data, 40) {
        Some(value) => value,
        None => return None,
    };
    let mut base_mint = input_vault_mint.clone();
    let mut quote_mint = output_vault_mint.clone();
    let mut base_vault = input_vault.clone();
    let mut quote_vault = output_vault.clone();
    let mut trade_side = "SellBase".to_string();
    if output_vault_mint.as_str() < input_vault_mint.as_str() {
        base_mint = output_vault_mint.clone();
        quote_mint = input_vault_mint.clone();
        base_vault = output_vault.clone();
        quote_vault = input_vault.clone();
        trade_side = "BuyBase".to_string();
    }
    return Some(crate::RaydiumClmmSwapV2Decoded {
        payer,
        amm_config,
        pool_state,
        input_token_account,
        output_token_account,
        input_vault,
        output_vault,
        observation_state,
        input_vault_mint,
        output_vault_mint,
        base_mint,
        quote_mint,
        base_vault,
        quote_vault,
        trade_side,
        amount,
        other_amount_threshold,
        sqrt_price_limit_x64: sqrt_price_limit_x64.to_string(),
        is_base_input,
    });
}

fn clone_account(
    accounts: &[std::string::String],
    index: usize,
) -> std::option::Option<std::string::String> {
    let account_option = accounts.get(index);
    match account_option {
        Some(account) => return Some(account.clone()),
        None => return None,
    }
}

fn read_discriminator(data: &[u8]) -> std::option::Option<[u8; 8]> {
    if data.len() < 8 {
        return None;
    }
    let mut bytes = [0_u8; 8];
    let mut index = 0_usize;
    while index < 8 {
        bytes[index] = data[index];
        index += 1;
    }
    return Some(bytes);
}

fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
    if data.len() < offset + 8 {
        return None;
    }
    let mut bytes = [0_u8; 8];
    let mut index = 0_usize;
    while index < 8 {
        bytes[index] = data[offset + index];
        index += 1;
    }
    return Some(u64::from_le_bytes(bytes));
}

fn read_u128_le(data: &[u8], offset: usize) -> std::option::Option<u128> {
    if data.len() < offset + 16 {
        return None;
    }
    let mut bytes = [0_u8; 16];
    let mut index = 0_usize;
    while index < 16 {
        bytes[index] = data[offset + index];
        index += 1;
    }
    return Some(u128::from_le_bytes(bytes));
}

fn read_bool(data: &[u8], offset: usize) -> std::option::Option<bool> {
    if data.len() <= offset {
        return None;
    }
    match data[offset] {
        0 => return Some(false),
        1 => return Some(true),
        _ => return None,
    }
}

fn decode_base64_standard(encoded: &str) -> std::option::Option<std::vec::Vec<u8>> {
    use base64::Engine as _;
    let decoded = base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes());
    match decoded {
        Ok(decoded) => return Some(decoded),
        Err(_) => return None,
    }
}

fn read_pubkey_base58(data: &[u8], offset: usize) -> std::option::Option<std::string::String> {
    if data.len() < offset + 32 {
        return None;
    }
    return Some(bs58::encode(&data[offset..offset + 32]).into_string());
}

fn read_u16_le(data: &[u8], offset: usize) -> std::option::Option<u16> {
    if data.len() < offset + 2 {
        return None;
    }
    let bytes = [data[offset], data[offset + 1]];
    return Some(u16::from_le_bytes(bytes));
}

fn read_u32_le(data: &[u8], offset: usize) -> std::option::Option<u32> {
    if data.len() < offset + 4 {
        return None;
    }
    let bytes = [data[offset], data[offset + 1], data[offset + 2], data[offset + 3]];
    return Some(u32::from_le_bytes(bytes));
}

fn read_i32_le(data: &[u8], offset: usize) -> std::option::Option<i32> {
    if data.len() < offset + 4 {
        return None;
    }
    let bytes = [data[offset], data[offset + 1], data[offset + 2], data[offset + 3]];
    return Some(i32::from_le_bytes(bytes));
}

fn read_u64_vec(
    data: &[u8],
    offset: usize,
    count: usize,
) -> std::option::Option<std::vec::Vec<std::string::String>> {
    let mut values = std::vec::Vec::new();
    let mut index = 0_usize;
    while index < count {
        let value = match read_u64_le(data, offset + index * 8) {
            Some(value) => value,
            None => return None,
        };
        values.push(value.to_string());
        index += 1;
    }
    return Some(values);
}

fn read_u128_vec(
    data: &[u8],
    offset: usize,
    count: usize,
) -> std::option::Option<std::vec::Vec<std::string::String>> {
    let mut values = std::vec::Vec::new();
    let mut index = 0_usize;
    while index < count {
        let value = match read_u128_le(data, offset + index * 16) {
            Some(value) => value,
            None => return None,
        };
        values.push(value.to_string());
        index += 1;
    }
    return Some(values);
}

fn decode_base58(input: &str) -> std::option::Option<std::vec::Vec<u8>> {
    match bs58::decode(input).into_vec() {
        Ok(decoded) => return Some(decoded),
        Err(_) => return None,
    }
}

#[derive(Debug, Clone)]
struct TokenBalanceRecord {
    account_address: std::option::Option<std::string::String>,
    mint: std::string::String,
    pre_amount_raw: std::option::Option<std::string::String>,
    post_amount_raw: std::option::Option<std::string::String>,
}

#[derive(Debug, Clone)]
struct TokenBalanceAccumulator {
    account_index: std::option::Option<i64>,
    account_address: std::option::Option<std::string::String>,
    mint: std::string::String,
    pre_amount_raw: std::option::Option<std::string::String>,
    post_amount_raw: std::option::Option<std::string::String>,
}

#[derive(Debug, Clone)]
struct AccountKeyInfo {
    index: i64,
    address: std::string::String,
}

fn parse_transaction_meta_value(
    transaction: &crate::ChainTransactionDto,
    transaction_json: &serde_json::Value,
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
    if let Some(meta_json) = transaction.meta_json.as_deref() {
        let meta_result = serde_json::from_str::<serde_json::Value>(meta_json);
        match meta_result {
            Ok(meta) => return Ok(Some(meta)),
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot parse meta_json for signature '{}': {}",
                    transaction.signature, error
                )));
            },
        }
    }
    let meta = transaction_json.get("meta");
    match meta {
        Some(meta) => return Ok(Some(meta.clone())),
        None => return Ok(None),
    }
}

fn extract_transaction_account_keys(
    transaction: &serde_json::Value,
    meta: std::option::Option<&serde_json::Value>,
) -> std::vec::Vec<AccountKeyInfo> {
    let mut account_keys = std::vec::Vec::new();
    let values = transaction
        .get("transaction")
        .and_then(|value| return value.get("message"))
        .and_then(|value| return value.get("accountKeys"))
        .and_then(serde_json::Value::as_array);
    if let Some(values) = values {
        let mut index = 0usize;
        for value in values {
            let parsed = parse_account_key_info(value, index as i64);
            if let Some(parsed) = parsed {
                account_keys.push(parsed);
            }
            index += 1;
        }
    }
    append_loaded_addresses(&mut account_keys, meta, "writable");
    append_loaded_addresses(&mut account_keys, meta, "readonly");
    return account_keys;
}

fn parse_account_key_info(
    value: &serde_json::Value,
    index: i64,
) -> std::option::Option<AccountKeyInfo> {
    if let Some(address) = value.as_str() {
        return Some(AccountKeyInfo { index, address: address.to_string() });
    }
    let address = match value.get("pubkey").and_then(serde_json::Value::as_str) {
        Some(address) => address.to_string(),
        None => return None,
    };
    return Some(AccountKeyInfo { index, address });
}

fn append_loaded_addresses(
    account_keys: &mut std::vec::Vec<AccountKeyInfo>,
    meta: std::option::Option<&serde_json::Value>,
    key: &str,
) {
    let addresses = meta
        .and_then(|value| return value.get("loadedAddresses"))
        .and_then(|value| return value.get(key))
        .and_then(serde_json::Value::as_array);
    let addresses = match addresses {
        Some(addresses) => addresses,
        None => return,
    };
    for value in addresses {
        let address = match value.as_str() {
            Some(address) => address,
            None => continue,
        };
        let index = account_keys.len() as i64;
        account_keys.push(AccountKeyInfo { index, address: address.to_string() });
    }
}

fn extract_token_balance_records(
    meta: std::option::Option<&serde_json::Value>,
    account_keys: &[AccountKeyInfo],
) -> std::vec::Vec<TokenBalanceRecord> {
    let mut accumulators = std::vec::Vec::new();
    collect_token_balance_side(meta, account_keys, "preTokenBalances", true, &mut accumulators);
    collect_token_balance_side(meta, account_keys, "postTokenBalances", false, &mut accumulators);
    let mut records = std::vec::Vec::new();
    for accumulator in accumulators {
        records.push(TokenBalanceRecord {
            account_address: accumulator.account_address,
            mint: accumulator.mint,
            pre_amount_raw: accumulator.pre_amount_raw,
            post_amount_raw: accumulator.post_amount_raw,
        });
    }
    return records;
}

fn collect_token_balance_side(
    meta: std::option::Option<&serde_json::Value>,
    account_keys: &[AccountKeyInfo],
    key: &str,
    is_pre: bool,
    accumulators: &mut std::vec::Vec<TokenBalanceAccumulator>,
) {
    let values = meta
        .and_then(|value| return value.get(key))
        .and_then(serde_json::Value::as_array);
    let values = match values {
        Some(values) => values,
        None => return,
    };
    for value in values {
        let account_index = value.get("accountIndex").and_then(serde_json::Value::as_i64);
        let mint = match value.get("mint").and_then(serde_json::Value::as_str) {
            Some(mint) => mint.to_string(),
            None => continue,
        };
        let amount = value
            .get("uiTokenAmount")
            .and_then(|amount| return amount.get("amount"))
            .and_then(serde_json::Value::as_str)
            .map(|text| return text.to_string());
        let account_address = match account_index {
            Some(account_index) => account_address_by_index(account_keys, account_index),
            None => None,
        };
        let accumulator_index =
            find_token_balance_accumulator(accumulators.as_slice(), account_index, mint.as_str());
        let index = match accumulator_index {
            Some(index) => index,
            None => {
                accumulators.push(TokenBalanceAccumulator {
                    account_index,
                    account_address,
                    mint,
                    pre_amount_raw: None,
                    post_amount_raw: None,
                });
                accumulators.len() - 1
            },
        };
        if is_pre {
            accumulators[index].pre_amount_raw = amount;
        } else {
            accumulators[index].post_amount_raw = amount;
        }
    }
}

fn find_token_balance_accumulator(
    accumulators: &[TokenBalanceAccumulator],
    account_index: std::option::Option<i64>,
    mint: &str,
) -> std::option::Option<usize> {
    let mut index = 0usize;
    while index < accumulators.len() {
        let accumulator = &accumulators[index];
        if accumulator.account_index == account_index && accumulator.mint == mint {
            return Some(index);
        }
        index += 1;
    }
    return None;
}

fn account_address_by_index(
    account_keys: &[AccountKeyInfo],
    account_index: i64,
) -> std::option::Option<std::string::String> {
    for account_key in account_keys {
        if account_key.index == account_index {
            return Some(account_key.address.clone());
        }
    }
    return None;
}

impl TokenBalanceRecord {
    fn delta_raw(&self) -> std::option::Option<i128> {
        let pre = parse_i128_or_zero(self.pre_amount_raw.as_deref());
        let post = parse_i128_or_zero(self.post_amount_raw.as_deref());
        match (pre, post) {
            (Some(pre), Some(post)) => return Some(post - pre),
            _ => return None,
        }
    }
}

fn mint_for_account(
    token_balances: &[TokenBalanceRecord],
    account: &str,
) -> std::option::Option<std::string::String> {
    let record = token_balance_record_for_account(token_balances, account);
    match record {
        Some(record) => return Some(record.mint.clone()),
        None => return None,
    }
}

fn token_balance_record_for_account<'a>(
    token_balances: &'a [TokenBalanceRecord],
    account: &str,
) -> std::option::Option<&'a TokenBalanceRecord> {
    for record in token_balances {
        if record.account_address.as_deref() == Some(account) {
            return Some(record);
        }
    }
    return None;
}

fn amount_delta_abs_for_account(
    token_balances: &[TokenBalanceRecord],
    account: &str,
) -> std::option::Option<std::string::String> {
    let record = token_balance_record_for_account(token_balances, account);
    let record = match record {
        Some(record) => record,
        None => return None,
    };
    let delta = record.delta_raw();
    let delta = match delta {
        Some(delta) => delta,
        None => return None,
    };
    if delta < 0 {
        return Some((-delta).to_string());
    }
    return Some(delta.to_string());
}

fn infer_trade_side_from_vault_deltas(
    token_balances: &[TokenBalanceRecord],
    base_vault: &str,
    quote_vault: &str,
) -> std::option::Option<std::string::String> {
    let base_record = token_balance_record_for_account(token_balances, base_vault);
    let base_record = match base_record {
        Some(base_record) => base_record,
        None => return None,
    };
    let quote_record = token_balance_record_for_account(token_balances, quote_vault);
    let quote_record = match quote_record {
        Some(quote_record) => quote_record,
        None => return None,
    };
    let base_delta = base_record.delta_raw();
    let quote_delta = quote_record.delta_raw();
    match (base_delta, quote_delta) {
        (Some(base_delta), Some(quote_delta)) => {
            if base_delta < 0 && quote_delta > 0 {
                return Some("BuyBase".to_string());
            }
            if base_delta > 0 && quote_delta < 0 {
                return Some("SellBase".to_string());
            }
            return None;
        },
        _ => return None,
    }
}

fn is_quote_mint(mint: &str) -> bool {
    return mint == crate::WSOL_MINT_ID
        || mint == crate::USDC_MINT_ID
        || mint == crate::USDT_MINT_ID;
}

fn parent_program_id_for_instruction(
    instruction: &crate::ChainInstructionDto,
    instructions: &[crate::ChainInstructionDto],
) -> std::option::Option<std::string::String> {
    let parent_id = match instruction.parent_instruction_id {
        Some(parent_id) => parent_id,
        None => return None,
    };
    for candidate in instructions {
        if candidate.id == Some(parent_id) {
            return candidate.program_id.clone();
        }
    }
    return None;
}

fn route_source_from_parent(
    parent_program: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
    let parent_program = match parent_program {
        Some(parent_program) => parent_program,
        None => return None,
    };
    if parent_program == OBSERVED_JUPITER_V6_PROGRAM_ID {
        return Some("jupiter".to_string());
    }
    return Some(parent_program.to_string());
}

fn parse_i128_or_zero(value: std::option::Option<&str>) -> std::option::Option<i128> {
    let value = match value {
        Some(value) => value.trim(),
        None => return Some(0),
    };
    if value.is_empty() {
        return Some(0);
    }
    let parsed_result = value.parse::<i128>();
    match parsed_result {
        Ok(parsed) => return Some(parsed),
        Err(_) => return None,
    }
}

#[cfg(test)]
mod tests {
    fn sample_swap_v2_accounts_json() -> &'static str {
        return r#"[
            "8NQ32SyFKD1d5kenq4oM8Da6C6J9TQSMW1uAgFRveEQr",
            "A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x",
            "GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR",
            "D2frZyyQ7NQaXRiEoBUM9S64Ckv7KZ7wuqupqdMhpsHy",
            "H7qe6sAyEyqztyMtRrDf5J1gugLx6yuyKPy5veVmR14W",
            "AvRzvwpSVnxsinLGQS3vZLqkZxhXZDM8F2qKccAo7rSq",
            "CTkc4xDrpzjWcFLC1cxmUZZjZLSRV46HZa8wu5eKTbuh",
            "8QtFSxNzD3zmEX8nzQKZB83TH4WGUAkLkQoRHAw5fuhn",
            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
            "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
            "CKvjP8FrZpaKXjASEtX2nEU9w7M4RKskfnLQbKJBodV",
            "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs",
            "8ovxZR2Gv9Mr73aoXLQYTMaZvHCSpnEohzgjVHQwmyHr",
            "9MssDxndh2Rn8DmGWL94hXVv22zxfDYHV7tvzfPgcaWe"
        ]"#;
    }

    #[test]
    fn decodes_swap_v2() {
        let events = crate::decode_raydium_clmm_instruction(
            sample_swap_v2_accounts_json(),
            r#""ASCsAbe1UnDnCsnGLPALJUXSS5JREycfhGyTzKh7xRWNyRHCqBuzR23S""#,
        );
        assert_eq!(events.len(), 1);
        match &events[0] {
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => {
                assert_eq!(events[0].event_kind(), "raydium_clmm.swap_v2");
                assert_eq!(
                    events[0].pool_account(),
                    "GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR"
                );
                assert_eq!(event.pool_state, "GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR");
                assert_eq!(event.input_vault, "AvRzvwpSVnxsinLGQS3vZLqkZxhXZDM8F2qKccAo7rSq");
                assert_eq!(event.output_vault, "CTkc4xDrpzjWcFLC1cxmUZZjZLSRV46HZa8wu5eKTbuh");
                assert_eq!(event.input_vault_mint, "CKvjP8FrZpaKXjASEtX2nEU9w7M4RKskfnLQbKJBodV");
                assert_eq!(event.output_vault_mint, "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs");
                assert_eq!(event.base_mint, "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs");
                assert_eq!(event.quote_mint, "CKvjP8FrZpaKXjASEtX2nEU9w7M4RKskfnLQbKJBodV");
                assert_eq!(event.base_vault, "CTkc4xDrpzjWcFLC1cxmUZZjZLSRV46HZa8wu5eKTbuh");
                assert_eq!(event.quote_vault, "AvRzvwpSVnxsinLGQS3vZLqkZxhXZDM8F2qKccAo7rSq");
                assert_eq!(event.trade_side, "BuyBase");
                assert_eq!(event.amount, 148441657491969);
                assert_eq!(event.other_amount_threshold, 0);
                assert_eq!(event.sqrt_price_limit_x64, "0");
                assert!(event.is_base_input);
            },
            _ => panic!("expected swap_v2 event"),
        }
    }

    #[test]
    fn serializes_swap_v2_payload_json() {
        let events = crate::decode_raydium_clmm_instruction(
            sample_swap_v2_accounts_json(),
            r#""ASCsAbe1UnDnCsnGLPALJUXSS5JREycfhGyTzKh7xRWNyRHCqBuzR23S""#,
        );
        assert_eq!(events.len(), 1);
        let payload_option = events[0].to_payload_json();
        let payload = match payload_option {
            Some(payload) => payload,
            None => panic!("payload json must be available"),
        };
        assert!(payload.contains("GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR"));
        assert!(payload.contains("input_vault"));
        assert!(payload.contains("output_vault"));
        assert!(payload.contains("tradeSide"));
    }

    #[test]
    fn decodes_create_pool() {
        let accounts_json = r#"[
            "Creator111111111111111111111111111111111111",
            "AmmConfig1111111111111111111111111111111111",
            "PoolState111111111111111111111111111111111",
            "TokenMint0111111111111111111111111111111111",
            "TokenMint1111111111111111111111111111111111",
            "TokenVault011111111111111111111111111111111",
            "TokenVault111111111111111111111111111111111",
            "Observation11111111111111111111111111111111",
            "Bitmap111111111111111111111111111111111111",
            "TokenProgram0111111111111111111111111111111",
            "TokenProgram1111111111111111111111111111111",
            "System111111111111111111111111111111111111",
            "Rent11111111111111111111111111111111111111"
        ]"#;
        let mut data = std::vec::Vec::from(super::RAYDIUM_CLMM_CREATE_POOL_DISCRIMINATOR);
        data.extend_from_slice(&123_u128.to_le_bytes());
        data.extend_from_slice(&456_u64.to_le_bytes());
        let encoded = bs58::encode(data).into_string();
        let data_json = format!("\"{}\"", encoded);
        let events = crate::decode_raydium_clmm_instruction(accounts_json, data_json.as_str());
        assert_eq!(events.len(), 1);
        match &events[0] {
            crate::RaydiumClmmDecodedEvent::CreatePool(event) => {
                assert_eq!(events[0].event_kind(), "raydium_clmm.create_pool");
                assert_eq!(event.pool_state, "PoolState111111111111111111111111111111111");
                assert_eq!(event.token_mint_0, "TokenMint0111111111111111111111111111111111");
                assert_eq!(event.token_mint_1, "TokenMint1111111111111111111111111111111111");
                assert_eq!(event.sqrt_price_x64, "123");
                assert_eq!(event.open_time, 456);
            },
            _ => panic!("expected create_pool event"),
        }
    }

    #[test]
    fn decodes_collect_protocol_fee() {
        let accounts_json = r#"[
            "Authority1111111111111111111111111111111111",
            "PoolState111111111111111111111111111111111",
            "Recipient0111111111111111111111111111111111",
            "Recipient1111111111111111111111111111111111",
            "TokenVault011111111111111111111111111111111",
            "VaultMint0111111111111111111111111111111111",
            "VaultMint1111111111111111111111111111111111",
            "TokenVault111111111111111111111111111111111",
            "TokenProgram0111111111111111111111111111111",
            "TokenProgram1111111111111111111111111111111",
            "Memo11111111111111111111111111111111111111"
        ]"#;
        let mut data = std::vec::Vec::from(super::RAYDIUM_CLMM_COLLECT_PROTOCOL_FEE_DISCRIMINATOR);
        data.extend_from_slice(&10_u64.to_le_bytes());
        data.extend_from_slice(&20_u64.to_le_bytes());
        let encoded = bs58::encode(data).into_string();
        let data_json = format!("\"{}\"", encoded);
        let events = crate::decode_raydium_clmm_instruction(accounts_json, data_json.as_str());
        assert_eq!(events.len(), 1);
        match &events[0] {
            crate::RaydiumClmmDecodedEvent::CollectProtocolFee(event) => {
                assert_eq!(events[0].event_kind(), "raydium_clmm.collect_protocol_fee");
                assert_eq!(event.pool_state, "PoolState111111111111111111111111111111111");
                assert_eq!(event.vault_0_mint, "VaultMint0111111111111111111111111111111111");
                assert_eq!(event.vault_1_mint, "VaultMint1111111111111111111111111111111111");
                assert_eq!(event.amount_0_requested_raw, "10");
                assert_eq!(event.amount_1_requested_raw, "20");
            },
            _ => panic!("expected collect_protocol_fee event"),
        }
    }

    #[test]
    fn ignores_invalid_data() {
        let events = crate::decode_raydium_clmm_instruction(
            sample_swap_v2_accounts_json(),
            r#""not-base58-data-0""#,
        );
        assert_eq!(events.len(), 0);
    }

    #[test]
    fn ignores_incomplete_accounts() {
        let accounts_json = r#"[
            "8NQ32SyFKD1d5kenq4oM8Da6C6J9TQSMW1uAgFRveEQr",
            "A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x",
            "GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR"
        ]"#;
        let events = crate::decode_raydium_clmm_instruction(
            accounts_json,
            r#""ASCsAbe1UnDnCsnGLPALJUXSS5JREycfhGyTzKh7xRWNyRHCqBuzR23S""#,
        );
        assert_eq!(events.len(), 0);
    }

    #[test]
    fn ignores_legacy_swap_for_now() {
        let mut data: std::vec::Vec<u8> = vec![248, 198, 158, 145, 225, 117, 135, 200];
        while data.len() < 41 {
            data.push(0);
        }
        data[40] = 1;
        let encoded = bs58::encode(data).into_string();
        let data_json = format!("\"{}\"", encoded);
        let events = crate::decode_raydium_clmm_instruction(
            sample_swap_v2_accounts_json(),
            data_json.as_str(),
        );
        assert_eq!(events.len(), 0);
    }
}
