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

//! Raydium AmmV4 transaction decoder.

const OBSERVED_JUPITER_V6_PROGRAM_ID: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";

/// Decoded Raydium AmmV4 initialize2 pool event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RaydiumAmmV4Initialize2PoolDecoded {
    /// Parent transaction id.
    pub transaction_id: i64,
    /// Parent instruction id.
    pub instruction_id: i64,
    /// Transaction signature.
    pub signature: std::string::String,
    /// Program id.
    pub program_id: std::string::String,
    /// Optional pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional lp mint.
    pub lp_mint: std::option::Option<std::string::String>,
    /// Optional token A mint.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional token B mint.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Optional market account.
    pub market_account: std::option::Option<std::string::String>,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Raydium AmmV4 swap event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RaydiumAmmV4SwapDecoded {
    /// Parent transaction id.
    pub transaction_id: i64,
    /// Parent instruction id.
    pub instruction_id: i64,
    /// Transaction signature.
    pub signature: std::string::String,
    /// Program id.
    pub program_id: std::string::String,
    /// Local decoded event kind derived from the AMM v4 instruction discriminator.
    pub event_kind: std::string::String,
    /// Upstream instruction name derived from the AMM v4 instruction discriminator.
    pub instruction_name: std::string::String,
    /// One-byte Raydium AMM v4 instruction discriminator in hex form.
    pub discriminator_hex: std::string::String,
    /// AMM pool/state account.
    pub pool_account: std::string::String,
    /// Raydium AMM authority account.
    pub authority: std::string::String,
    /// Base token mint after base/quote normalization.
    pub token_a_mint: std::string::String,
    /// Quote token mint after base/quote normalization.
    pub token_b_mint: std::string::String,
    /// Base-side vault after base/quote normalization.
    pub base_vault: std::string::String,
    /// Quote-side vault after base/quote normalization.
    pub quote_vault: std::string::String,
    /// Raw vault account found at the AMM instruction vault A position.
    pub raw_vault_a: std::string::String,
    /// Raw vault account found at the AMM instruction vault B position.
    pub raw_vault_b: std::string::String,
    /// Optional input vault inferred from vault balance deltas.
    pub input_vault: std::option::Option<std::string::String>,
    /// Optional output vault inferred from vault balance deltas.
    pub output_vault: std::option::Option<std::string::String>,
    /// Optional input user token account inferred from token balance deltas.
    pub input_token_account: std::option::Option<std::string::String>,
    /// Optional output user token account inferred from token balance deltas.
    pub output_token_account: std::option::Option<std::string::String>,
    /// Optional trade side inferred from vault balance deltas.
    pub trade_side: std::option::Option<std::string::String>,
    /// Optional raw base amount inferred from vault balance deltas.
    pub base_amount_raw: std::option::Option<std::string::String>,
    /// Optional raw quote amount inferred from vault balance deltas.
    pub quote_amount_raw: std::option::Option<std::string::String>,
    /// Whether this instruction is an inner instruction.
    pub inner_instruction: bool,
    /// Optional top-level caller program inferred from the parent instruction.
    pub route_source: std::option::Option<std::string::String>,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Raydium AmmV4 non-swap or decoded-only instruction event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RaydiumAmmV4InstructionDecoded {
    /// Parent transaction id.
    pub transaction_id: i64,
    /// Parent instruction id.
    pub instruction_id: i64,
    /// Transaction signature.
    pub signature: std::string::String,
    /// Program id.
    pub program_id: std::string::String,
    /// Local decoded event kind derived from the AMM v4 instruction discriminator.
    pub event_kind: std::string::String,
    /// Upstream instruction name derived from the AMM v4 instruction discriminator.
    pub instruction_name: std::string::String,
    /// One-byte Raydium AMM v4 instruction discriminator in hex form.
    pub discriminator_hex: std::string::String,
    /// Optional AMM pool/state account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional Raydium AMM authority account.
    pub authority: std::option::Option<std::string::String>,
    /// Optional AMM open-orders account.
    pub open_orders: std::option::Option<std::string::String>,
    /// Optional AMM target-orders account.
    pub target_orders: std::option::Option<std::string::String>,
    /// Optional market program account.
    pub market_program: std::option::Option<std::string::String>,
    /// Optional market account.
    pub market_account: std::option::Option<std::string::String>,
    /// Optional LP mint account.
    pub lp_mint: std::option::Option<std::string::String>,
    /// Optional token A mint after best-effort account layout extraction.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional token B mint after best-effort account layout extraction.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Optional AMM coin/base vault account.
    pub base_vault: std::option::Option<std::string::String>,
    /// Optional AMM pc/quote vault account.
    pub quote_vault: std::option::Option<std::string::String>,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Raydium AmmV4 event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum RaydiumAmmV4DecodedEvent {
    /// `initialize2` pool creation-like event.
    Initialize2Pool(std::boxed::Box<RaydiumAmmV4Initialize2PoolDecoded>),
    /// Swap event decoded from a direct or inner Raydium AMM v4 instruction.
    Swap(std::boxed::Box<RaydiumAmmV4SwapDecoded>),
    /// Known Raydium AMM v4 instruction decoded without direct trade materialization.
    Instruction(std::boxed::Box<RaydiumAmmV4InstructionDecoded>),
}

/// Raydium AmmV4 decoder.
#[derive(Debug, Clone, Default)]
pub struct RaydiumAmmV4Decoder;

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

    /// Decodes one projected transaction into zero or more Raydium AmmV4 events.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::RaydiumAmmV4DecodedEvent>, crate::Error> {
        let transaction_id_option = transaction.id;
        let transaction_id = match transaction_id_option {
            Some(transaction_id) => transaction_id,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "chain transaction '{}' has no internal id",
                    transaction.signature
                )));
            },
        };
        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 log_messages = extract_log_messages(&transaction_json);
        let has_initialize2_log = log_messages_contain_initialize2(&log_messages);
        let mut decoded_events = std::vec::Vec::new();
        for instruction in instructions {
            let program_id_option = &instruction.program_id;
            let program_id = match program_id_option {
                Some(program_id) => program_id,
                None => continue,
            };
            if program_id.as_str() != crate::RAYDIUM_AMM_V4_PROGRAM_ID {
                continue;
            }
            let instruction_id_option = instruction.id;
            let instruction_id = match instruction_id_option {
                Some(instruction_id) => instruction_id,
                None => continue,
            };
            let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
            let accounts = match accounts_result {
                Ok(accounts) => accounts,
                Err(error) => return Err(error),
            };
            if instruction.parent_instruction_id.is_none() && has_initialize2_log {
                let initialize_event = decode_initialize2_event(
                    transaction,
                    transaction_id,
                    instruction,
                    instruction_id,
                    program_id,
                    accounts.as_slice(),
                    log_messages.as_slice(),
                );
                if let Some(initialize_event) = initialize_event {
                    decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(
                        std::boxed::Box::new(initialize_event),
                    ));
                    continue;
                }
            }
            let swap_result = decode_swap_event(
                transaction,
                transaction_id,
                instruction,
                instruction_id,
                program_id,
                accounts.as_slice(),
                instructions,
                token_balances.as_slice(),
            );
            match swap_result {
                Ok(Some(swap)) => {
                    decoded_events
                        .push(crate::RaydiumAmmV4DecodedEvent::Swap(std::boxed::Box::new(swap)));
                    continue;
                },
                Ok(None) => {},
                Err(error) => return Err(error),
            }
            let instruction_event = decode_known_instruction_event(
                transaction,
                transaction_id,
                instruction,
                instruction_id,
                program_id,
                accounts.as_slice(),
            );
            if let Some(instruction_event) = instruction_event {
                decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Instruction(
                    std::boxed::Box::new(instruction_event),
                ));
            }
        }
        return Ok(decoded_events);
    }
}

fn decode_initialize2_event(
    transaction: &crate::ChainTransactionDto,
    transaction_id: i64,
    instruction: &crate::ChainInstructionDto,
    instruction_id: i64,
    program_id: &str,
    accounts: &[std::string::String],
    log_messages: &[std::string::String],
) -> std::option::Option<crate::RaydiumAmmV4Initialize2PoolDecoded> {
    if accounts.len() < 10 {
        return None;
    }
    let pool_account = extract_account(accounts, 4);
    let lp_mint = extract_account(accounts, 7);
    let token_a_mint = extract_account(accounts, 8);
    let token_b_mint = extract_account(accounts, 9);
    let market_account = extract_account(accounts, 16);
    let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
    let discriminator_hex = raydium_amm_v4_instruction_discriminator_hex(data_base58.as_deref());
    let payload_json = serde_json::json!({
        "decoder": "raydium_amm_v4",
        "eventKind": "initialize2_pool",
        "instructionName": "initialize2",
        "upstreamInstructionName": "initialize2",
        "discriminatorHex": discriminator_hex,
        "instructionDiscriminatorHex": discriminator_hex,
        "signature": transaction.signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "accounts": accounts,
        "logMessages": log_messages,
        "poolAccount": pool_account,
        "lpMint": lp_mint,
        "tokenAMint": token_a_mint,
        "tokenBMint": token_b_mint,
        "marketAccount": market_account
    });
    return Some(crate::RaydiumAmmV4Initialize2PoolDecoded {
        transaction_id,
        instruction_id,
        signature: transaction.signature.clone(),
        program_id: program_id.to_string(),
        pool_account,
        lp_mint,
        token_a_mint,
        token_b_mint,
        market_account,
        payload_json,
    });
}

fn decode_swap_event(
    transaction: &crate::ChainTransactionDto,
    transaction_id: i64,
    instruction: &crate::ChainInstructionDto,
    instruction_id: i64,
    program_id: &str,
    accounts: &[std::string::String],
    transaction_instructions: &[crate::ChainInstructionDto],
    token_balances: &[TokenBalanceRecord],
) -> Result<std::option::Option<crate::RaydiumAmmV4SwapDecoded>, crate::Error> {
    if accounts.len() < 8 {
        return Ok(None);
    }
    let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
    let swap_instruction = match raydium_amm_v4_swap_instruction(data_base58.as_deref()) {
        Some(swap_instruction) => swap_instruction,
        None => return Ok(None),
    };
    let pool_account = match extract_account(accounts, 1) {
        Some(pool_account) => pool_account,
        None => return Ok(None),
    };
    let authority = match extract_account(accounts, 2) {
        Some(authority) => authority,
        None => return Ok(None),
    };
    let vault_pair_option =
        infer_raydium_amm_v4_vault_pair(accounts, authority.as_str(), token_balances);
    let vault_pair = match vault_pair_option {
        Some(vault_pair) => vault_pair,
        None => return Ok(None),
    };
    let normalized_pair = normalize_pool_pair(vault_pair);
    let base_amount_raw =
        amount_delta_abs_for_account(token_balances, normalized_pair.base_vault.as_str());
    let quote_amount_raw =
        amount_delta_abs_for_account(token_balances, normalized_pair.quote_vault.as_str());
    let trade_side = infer_trade_side_from_vault_deltas(
        token_balances,
        normalized_pair.base_vault.as_str(),
        normalized_pair.quote_vault.as_str(),
    );
    if trade_side.is_none() {
        return Ok(None);
    }
    let token_accounts = infer_user_token_accounts(
        accounts,
        token_balances,
        authority.as_str(),
        normalized_pair.base_mint.as_str(),
        normalized_pair.quote_mint.as_str(),
        normalized_pair.base_vault.as_str(),
        normalized_pair.quote_vault.as_str(),
        trade_side.as_deref(),
    );
    let input_output_vaults = input_output_vaults_for_trade_side(
        trade_side.as_deref(),
        normalized_pair.base_vault.as_str(),
        normalized_pair.quote_vault.as_str(),
    );
    let parent_program = parent_program_id_for_instruction(instruction, transaction_instructions);
    let trade_candidate = base_amount_raw.is_some() && quote_amount_raw.is_some();
    let payload_json = serde_json::json!({
        "decoder": "raydium_amm_v4",
        "eventKind": swap_instruction.event_kind,
        "instructionName": swap_instruction.instruction_name,
        "upstreamInstructionName": swap_instruction.instruction_name,
        "discriminatorHex": swap_instruction.discriminator_hex,
        "instructionDiscriminatorHex": swap_instruction.discriminator_hex,
        "signature": transaction.signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "innerInstruction": instruction.inner_instruction_index.is_some(),
        "parentInstructionId": instruction.parent_instruction_id,
        "parentProgramId": parent_program.clone(),
        "routeSource": route_source_from_parent(parent_program.as_deref()),
        "programId": program_id,
        "accounts": accounts,
        "data": data_base58,
        "poolAccount": pool_account.clone(),
        "authority": authority.clone(),
        "rawVaultA": normalized_pair.raw_vault_a.clone(),
        "rawVaultB": normalized_pair.raw_vault_b.clone(),
        "baseVault": normalized_pair.base_vault.clone(),
        "quoteVault": normalized_pair.quote_vault.clone(),
        "inputVault": input_output_vaults.0.clone(),
        "outputVault": input_output_vaults.1.clone(),
        "inputTokenAccount": token_accounts.0.clone(),
        "outputTokenAccount": token_accounts.1.clone(),
        "tokenAMint": normalized_pair.base_mint.clone(),
        "tokenBMint": normalized_pair.quote_mint.clone(),
        "baseMint": normalized_pair.base_mint.clone(),
        "quoteMint": normalized_pair.quote_mint.clone(),
        "tradeSide": trade_side.clone(),
        "baseAmountRaw": base_amount_raw.clone(),
        "quoteAmountRaw": quote_amount_raw.clone(),
        "tradeCandidate": trade_candidate,
        "candleCandidate": trade_candidate,
        "skipTradeReason": if trade_candidate { serde_json::Value::Null } else { serde_json::Value::String("missing_vault_balance_amounts".to_string()) },
        "skipCandleReason": if trade_candidate { serde_json::Value::Null } else { serde_json::Value::String("missing_vault_balance_amounts".to_string()) }
    });
    return Ok(Some(crate::RaydiumAmmV4SwapDecoded {
        transaction_id,
        instruction_id,
        signature: transaction.signature.clone(),
        program_id: program_id.to_string(),
        event_kind: swap_instruction.event_kind.to_string(),
        instruction_name: swap_instruction.instruction_name.to_string(),
        discriminator_hex: swap_instruction.discriminator_hex.to_string(),
        pool_account,
        authority,
        token_a_mint: normalized_pair.base_mint,
        token_b_mint: normalized_pair.quote_mint,
        base_vault: normalized_pair.base_vault,
        quote_vault: normalized_pair.quote_vault,
        raw_vault_a: normalized_pair.raw_vault_a,
        raw_vault_b: normalized_pair.raw_vault_b,
        input_vault: input_output_vaults.0,
        output_vault: input_output_vaults.1,
        input_token_account: token_accounts.0,
        output_token_account: token_accounts.1,
        trade_side,
        base_amount_raw,
        quote_amount_raw,
        inner_instruction: instruction.inner_instruction_index.is_some(),
        route_source: route_source_from_parent(parent_program.as_deref()),
        payload_json,
    }));
}

fn decode_known_instruction_event(
    transaction: &crate::ChainTransactionDto,
    transaction_id: i64,
    instruction: &crate::ChainInstructionDto,
    instruction_id: i64,
    program_id: &str,
    accounts: &[std::string::String],
) -> std::option::Option<crate::RaydiumAmmV4InstructionDecoded> {
    let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
    let data_bytes = raydium_amm_v4_instruction_data_bytes(data_base58.as_deref());
    let identity = match raydium_amm_v4_instruction_identity(data_base58.as_deref()) {
        Some(identity) => identity,
        None => return None,
    };
    let account_layout = raydium_amm_v4_account_layout(identity.discriminator_hex);
    let pool_account = extract_account_by_index(accounts, account_layout.pool_account_index);
    let authority = extract_account_by_index(accounts, account_layout.authority_index);
    let open_orders = extract_account_by_index(accounts, account_layout.open_orders_index);
    let target_orders = extract_account_by_index(accounts, account_layout.target_orders_index);
    let market_program = extract_account_by_index(accounts, account_layout.market_program_index);
    let market_account = extract_account_by_index(accounts, account_layout.market_account_index);
    let lp_mint = extract_account_by_index(accounts, account_layout.lp_mint_index);
    let token_a_mint = extract_account_by_index(accounts, account_layout.token_a_mint_index);
    let token_b_mint = extract_account_by_index(accounts, account_layout.token_b_mint_index);
    let base_vault = extract_account_by_index(accounts, account_layout.base_vault_index);
    let quote_vault = extract_account_by_index(accounts, account_layout.quote_vault_index);
    let mut payload_json = serde_json::json!({
        "decoder": "raydium_amm_v4",
        "eventKind": identity.event_kind,
        "instructionName": identity.instruction_name,
        "upstreamInstructionName": identity.instruction_name,
        "discriminatorHex": identity.discriminator_hex,
        "instructionDiscriminatorHex": identity.discriminator_hex,
        "signature": transaction.signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "innerInstruction": instruction.inner_instruction_index.is_some(),
        "parentInstructionId": instruction.parent_instruction_id,
        "programId": program_id,
        "accounts": accounts,
        "data": data_base58,
        "instructionDataLength": data_bytes.as_ref().map(std::vec::Vec::len),
        "poolAccount": pool_account.clone(),
        "authority": authority.clone(),
        "openOrders": open_orders.clone(),
        "targetOrders": target_orders.clone(),
        "marketProgram": market_program.clone(),
        "marketAccount": market_account.clone(),
        "lpMint": lp_mint.clone(),
        "tokenAMint": token_a_mint.clone(),
        "tokenBMint": token_b_mint.clone(),
        "baseVault": base_vault.clone(),
        "quoteVault": quote_vault.clone(),
        "tradeCandidate": false,
        "candleCandidate": false,
        "skipTradeReason": "decoded_only_instruction",
        "skipCandleReason": "decoded_only_instruction"
    });
    enrich_known_instruction_payload(&mut payload_json, identity.discriminator_hex, data_bytes.as_deref());
    return Some(crate::RaydiumAmmV4InstructionDecoded {
        transaction_id,
        instruction_id,
        signature: transaction.signature.clone(),
        program_id: program_id.to_string(),
        event_kind: identity.event_kind.to_string(),
        instruction_name: identity.instruction_name.to_string(),
        discriminator_hex: identity.discriminator_hex.to_string(),
        pool_account,
        authority,
        open_orders,
        target_orders,
        market_program,
        market_account,
        lp_mint,
        token_a_mint,
        token_b_mint,
        base_vault,
        quote_vault,
        payload_json,
    });
}

#[derive(Clone, Copy)]
struct RaydiumAmmV4AccountLayout {
    pool_account_index: std::option::Option<usize>,
    authority_index: std::option::Option<usize>,
    open_orders_index: std::option::Option<usize>,
    target_orders_index: std::option::Option<usize>,
    market_program_index: std::option::Option<usize>,
    market_account_index: std::option::Option<usize>,
    lp_mint_index: std::option::Option<usize>,
    token_a_mint_index: std::option::Option<usize>,
    token_b_mint_index: std::option::Option<usize>,
    base_vault_index: std::option::Option<usize>,
    quote_vault_index: std::option::Option<usize>,
}

fn raydium_amm_v4_empty_account_layout() -> RaydiumAmmV4AccountLayout {
    return RaydiumAmmV4AccountLayout {
        pool_account_index: None,
        authority_index: None,
        open_orders_index: None,
        target_orders_index: None,
        market_program_index: None,
        market_account_index: None,
        lp_mint_index: None,
        token_a_mint_index: None,
        token_b_mint_index: None,
        base_vault_index: None,
        quote_vault_index: None,
    };
}

fn raydium_amm_v4_account_layout(discriminator_hex: &str) -> RaydiumAmmV4AccountLayout {
    let mut layout = raydium_amm_v4_empty_account_layout();
    match discriminator_hex {
        "00" => {
            layout.pool_account_index = Some(3);
        },
        "01" => {
            layout.pool_account_index = Some(4);
            layout.authority_index = Some(5);
            layout.open_orders_index = Some(6);
            layout.lp_mint_index = Some(7);
            layout.token_a_mint_index = Some(8);
            layout.token_b_mint_index = Some(9);
            layout.base_vault_index = Some(10);
            layout.quote_vault_index = Some(11);
            layout.target_orders_index = Some(12);
            layout.market_program_index = Some(15);
            layout.market_account_index = Some(16);
        },
        "02" => {
            layout.pool_account_index = Some(3);
            layout.authority_index = Some(4);
            layout.open_orders_index = Some(5);
            layout.target_orders_index = Some(6);
            layout.base_vault_index = Some(7);
            layout.quote_vault_index = Some(8);
            layout.market_program_index = Some(9);
            layout.market_account_index = Some(10);
        },
        "03" => {
            layout.pool_account_index = Some(1);
            layout.authority_index = Some(2);
            layout.open_orders_index = Some(3);
            layout.target_orders_index = Some(4);
            layout.lp_mint_index = Some(5);
            layout.base_vault_index = Some(6);
            layout.quote_vault_index = Some(7);
            layout.market_account_index = Some(8);
        },
        "04" => {
            layout.pool_account_index = Some(1);
            layout.authority_index = Some(2);
            layout.open_orders_index = Some(3);
            layout.target_orders_index = Some(4);
            layout.lp_mint_index = Some(5);
            layout.base_vault_index = Some(6);
            layout.quote_vault_index = Some(7);
            layout.market_program_index = Some(8);
            layout.market_account_index = Some(9);
        },
        "05" => {
            layout.pool_account_index = Some(3);
            layout.authority_index = Some(4);
            layout.open_orders_index = Some(5);
            layout.base_vault_index = Some(6);
            layout.quote_vault_index = Some(7);
            layout.target_orders_index = Some(8);
            layout.market_program_index = Some(9);
            layout.market_account_index = Some(10);
        },
        "06" => {
            layout.pool_account_index = Some(1);
            layout.authority_index = Some(2);
            layout.open_orders_index = Some(3);
            layout.target_orders_index = Some(4);
            layout.base_vault_index = Some(5);
            layout.quote_vault_index = Some(6);
            layout.market_program_index = Some(7);
            layout.market_account_index = Some(8);
        },
        "07" => {
            layout.pool_account_index = Some(1);
            layout.authority_index = Some(3);
            layout.open_orders_index = Some(4);
            layout.base_vault_index = Some(5);
            layout.quote_vault_index = Some(6);
            layout.target_orders_index = Some(10);
            layout.market_program_index = Some(11);
            layout.market_account_index = Some(12);
        },
        "08" => {
            layout.pool_account_index = Some(1);
            layout.authority_index = Some(3);
        },
        "09" | "0b" => {
            layout.pool_account_index = Some(1);
            layout.authority_index = Some(2);
            layout.open_orders_index = Some(3);
            layout.target_orders_index = Some(4);
            layout.base_vault_index = Some(5);
            layout.quote_vault_index = Some(6);
            layout.market_program_index = Some(7);
            layout.market_account_index = Some(8);
        },
        "0a" => {
            layout.pool_account_index = Some(4);
        },
        "0c" => {
            layout.pool_account_index = Some(1);
        },
        "0d" => {
            layout.pool_account_index = Some(1);
        },
        "10" | "11" => {
            layout.pool_account_index = Some(1);
            layout.authority_index = Some(2);
            layout.base_vault_index = Some(3);
            layout.quote_vault_index = Some(4);
        },
        _ => {},
    }
    return layout;
}

fn extract_account_by_index(
    accounts: &[std::string::String],
    index: std::option::Option<usize>,
) -> std::option::Option<std::string::String> {
    let index = match index {
        Some(index) => index,
        None => return None,
    };
    return extract_account(accounts, index);
}

fn raydium_amm_v4_instruction_data_bytes(
    data_base58: std::option::Option<&str>,
) -> std::option::Option<std::vec::Vec<u8>> {
    let data_base58 = match data_base58 {
        Some(data_base58) => data_base58,
        None => return None,
    };
    let bytes_result = bs58::decode(data_base58).into_vec();
    match bytes_result {
        Ok(bytes) => return Some(bytes),
        Err(_) => return None,
    }
}

fn enrich_known_instruction_payload(
    payload_json: &mut serde_json::Value,
    discriminator_hex: &str,
    data: std::option::Option<&[u8]>,
) {
    let data = match data {
        Some(data) => data,
        None => return,
    };
    match discriminator_hex {
        "00" => {
            insert_u8(payload_json, "nonce", data, 1);
            insert_u64_string(payload_json, "openTime", data, 2);
        },
        "01" => {
            insert_u8(payload_json, "nonce", data, 1);
            insert_u64_string(payload_json, "openTime", data, 2);
            insert_u64_string(payload_json, "initPcAmount", data, 10);
            insert_u64_string(payload_json, "initCoinAmount", data, 18);
            insert_u64_string(payload_json, "tokenAAmount", data, 18);
            insert_u64_string(payload_json, "tokenBAmount", data, 10);
        },
        "02" => {
            insert_u16(payload_json, "planOrderLimit", data, 1);
            insert_u16(payload_json, "placeOrderLimit", data, 3);
            insert_u16(payload_json, "cancelOrderLimit", data, 5);
        },
        "03" => {
            insert_u64_string(payload_json, "maxCoinAmount", data, 1);
            insert_u64_string(payload_json, "maxPcAmount", data, 9);
            insert_u64_string(payload_json, "baseSide", data, 17);
            insert_u64_string(payload_json, "otherAmountMin", data, 25);
            insert_u64_string(payload_json, "tokenAAmount", data, 1);
            insert_u64_string(payload_json, "tokenBAmount", data, 9);
        },
        "04" => {
            insert_u64_string(payload_json, "lpAmountRaw", data, 1);
            insert_u64_string(payload_json, "liquidity", data, 1);
            insert_u64_string(payload_json, "minCoinAmount", data, 9);
            insert_u64_string(payload_json, "minPcAmount", data, 17);
        },
        "06" => {
            insert_u8(payload_json, "configParam", data, 1);
            insert_u64_string(payload_json, "configValue", data, 2);
            insert_fixed_hex(payload_json, "configPubkeyHex", data, 2, 32);
            insert_u64_string(payload_json, "lastOrderNumerator", data, 2);
            insert_u64_string(payload_json, "lastOrderDenominator", data, 10);
        },
        "08" => {
            insert_u64_string(payload_json, "amountRaw", data, 1);
        },
        "09" | "10" => {
            insert_u64_string(payload_json, "amountIn", data, 1);
            insert_u64_string(payload_json, "minimumAmountOut", data, 9);
        },
        "0a" => {
            insert_u8(payload_json, "nonce", data, 1);
        },
        "0b" | "11" => {
            insert_u64_string(payload_json, "maxAmountIn", data, 1);
            insert_u64_string(payload_json, "amountOut", data, 9);
        },
        "0c" => {
            insert_u8(payload_json, "simulateParam", data, 1);
            insert_u64_string(payload_json, "amountIn", data, 2);
            insert_u64_string(payload_json, "minimumAmountOut", data, 10);
            insert_u64_string(payload_json, "maxAmountIn", data, 2);
            insert_u64_string(payload_json, "amountOut", data, 10);
        },
        "0d" => {
            insert_u16(payload_json, "orderCancelLimit", data, 1);
        },
        "0f" => {
            insert_u8(payload_json, "configParam", data, 1);
            insert_fixed_hex(payload_json, "configOwnerHex", data, 2, 32);
            insert_u64_string(payload_json, "createPoolFee", data, 2);
        },
        _ => {},
    }
}

fn insert_u8(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) {
    let value = match read_u8(data, offset) {
        Some(value) => value,
        None => return,
    };
    insert_json_value(
        payload_json,
        key,
        serde_json::Value::Number(serde_json::Number::from(value as u64)),
    );
}

fn insert_u16(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) {
    let value = match read_u16_le(data, offset) {
        Some(value) => value,
        None => return,
    };
    insert_json_value(
        payload_json,
        key,
        serde_json::Value::Number(serde_json::Number::from(value as u64)),
    );
}

fn insert_u64_string(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) {
    let value = match read_u64_le(data, offset) {
        Some(value) => value,
        None => return,
    };
    insert_json_value(
        payload_json,
        key,
        serde_json::Value::String(value.to_string()),
    );
}

fn insert_fixed_hex(
    payload_json: &mut serde_json::Value,
    key: &str,
    data: &[u8],
    offset: usize,
    len: usize,
) {
    if data.len() < offset + len {
        return;
    }
    let slice = &data[offset..offset + len];
    insert_json_value(payload_json, key, serde_json::Value::String(bytes_to_hex(slice)));
}

fn insert_json_value(payload_json: &mut serde_json::Value, key: &str, value: serde_json::Value) {
    let object_option = payload_json.as_object_mut();
    let object = match object_option {
        Some(object) => object,
        None => return,
    };
    object.insert(key.to_string(), value);
}

fn read_u8(data: &[u8], offset: usize) -> std::option::Option<u8> {
    if data.len() < offset + 1 {
        return None;
    }
    return Some(data[offset]);
}

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_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
    if data.len() < offset + 8 {
        return None;
    }
    let bytes = [
        data[offset],
        data[offset + 1],
        data[offset + 2],
        data[offset + 3],
        data[offset + 4],
        data[offset + 5],
        data[offset + 6],
        data[offset + 7],
    ];
    return Some(u64::from_le_bytes(bytes));
}

fn bytes_to_hex(bytes: &[u8]) -> std::string::String {
    let mut output = std::string::String::new();
    for byte in bytes {
        output.push_str(format!("{byte:02x}").as_str());
    }
    return output;
}

#[derive(Debug, Clone)]
struct TokenBalanceRecord {
    account_address: std::option::Option<std::string::String>,
    mint: std::string::String,
    owner: std::option::Option<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,
    owner: std::option::Option<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,
}

#[derive(Debug, Clone)]
struct VaultPair {
    vault_a: std::string::String,
    vault_a_mint: std::string::String,
    vault_b: std::string::String,
    vault_b_mint: std::string::String,
}

#[derive(Debug, Clone)]
struct NormalizedVaultPair {
    raw_vault_a: std::string::String,
    raw_vault_b: std::string::String,
    base_vault: std::string::String,
    quote_vault: std::string::String,
    base_mint: std::string::String,
    quote_mint: std::string::String,
}

#[derive(Clone, Copy)]
struct RaydiumAmmV4InstructionIdentity {
    instruction_name: &'static str,
    event_kind: &'static str,
    discriminator_hex: &'static str,
}

fn raydium_amm_v4_swap_instruction(
    data_base58: std::option::Option<&str>,
) -> std::option::Option<RaydiumAmmV4InstructionIdentity> {
    let identity = match raydium_amm_v4_instruction_identity(data_base58) {
        Some(identity) => identity,
        None => return None,
    };
    match identity.discriminator_hex {
        "09" | "0b" | "10" | "11" => return Some(identity),
        _ => return None,
    }
}

fn raydium_amm_v4_instruction_identity(
    data_base58: std::option::Option<&str>,
) -> std::option::Option<RaydiumAmmV4InstructionIdentity> {
    let discriminator_hex = match raydium_amm_v4_instruction_discriminator_hex(data_base58) {
        Some(discriminator_hex) => discriminator_hex,
        None => return None,
    };
    match discriminator_hex.as_str() {
        "00" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "initialize",
                event_kind: "raydium_amm_v4.initialize",
                discriminator_hex: "00",
            });
        },
        "01" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "initialize2",
                event_kind: "raydium_amm_v4.initialize2_pool",
                discriminator_hex: "01",
            });
        },
        "02" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "monitor_step",
                event_kind: "raydium_amm_v4.monitor_step",
                discriminator_hex: "02",
            });
        },
        "03" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "deposit",
                event_kind: "raydium_amm_v4.deposit",
                discriminator_hex: "03",
            });
        },
        "04" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "withdraw",
                event_kind: "raydium_amm_v4.withdraw",
                discriminator_hex: "04",
            });
        },
        "05" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "migrate_to_open_book",
                event_kind: "raydium_amm_v4.migrate_to_open_book",
                discriminator_hex: "05",
            });
        },
        "06" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "set_params",
                event_kind: "raydium_amm_v4.set_params",
                discriminator_hex: "06",
            });
        },
        "07" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "withdraw_pnl",
                event_kind: "raydium_amm_v4.withdraw_pnl",
                discriminator_hex: "07",
            });
        },
        "08" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "withdraw_srm",
                event_kind: "raydium_amm_v4.withdraw_srm",
                discriminator_hex: "08",
            });
        },
        "09" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "swap_base_in",
                event_kind: "raydium_amm_v4.swap_base_in",
                discriminator_hex: "09",
            });
        },
        "0a" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "pre_initialize",
                event_kind: "raydium_amm_v4.pre_initialize",
                discriminator_hex: "0a",
            });
        },
        "0b" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "swap_base_out",
                event_kind: "raydium_amm_v4.swap_base_out",
                discriminator_hex: "0b",
            });
        },
        "0c" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "simulate_info",
                event_kind: "raydium_amm_v4.simulate_info",
                discriminator_hex: "0c",
            });
        },
        "0d" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "admin_cancel_orders",
                event_kind: "raydium_amm_v4.admin_cancel_orders",
                discriminator_hex: "0d",
            });
        },
        "0e" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "create_config_account",
                event_kind: "raydium_amm_v4.create_config_account",
                discriminator_hex: "0e",
            });
        },
        "0f" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "update_config_account",
                event_kind: "raydium_amm_v4.update_config_account",
                discriminator_hex: "0f",
            });
        },
        "10" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "swap_base_in_v2",
                event_kind: "raydium_amm_v4.swap_base_in_v2",
                discriminator_hex: "10",
            });
        },
        "11" => {
            return Some(RaydiumAmmV4InstructionIdentity {
                instruction_name: "swap_base_out_v2",
                event_kind: "raydium_amm_v4.swap_base_out_v2",
                discriminator_hex: "11",
            });
        },
        _ => return None,
    }
}

fn raydium_amm_v4_instruction_discriminator_hex(
    data_base58: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
    let data_base58 = match data_base58 {
        Some(data_base58) => data_base58,
        None => return None,
    };
    let bytes_result = bs58::decode(data_base58).into_vec();
    let bytes = match bytes_result {
        Ok(bytes) => bytes,
        Err(_) => return None,
    };
    let first_byte = match bytes.first() {
        Some(first_byte) => first_byte,
        None => return None,
    };
    return Some(format!("{first_byte:02x}"));
}

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,
            owner: accumulator.owner,
            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 owner = value
            .get("owner")
            .and_then(serde_json::Value::as_str)
            .map(|text| return text.to_string());
        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,
                    owner: owner.clone(),
                    pre_amount_raw: None,
                    post_amount_raw: None,
                });
                accumulators.len() - 1
            },
        };
        if accumulators[index].owner.is_none() {
            accumulators[index].owner = owner;
        }
        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;
}

fn infer_raydium_amm_v4_vault_pair(
    accounts: &[std::string::String],
    authority: &str,
    token_balances: &[TokenBalanceRecord],
) -> std::option::Option<VaultPair> {
    let positional = infer_positional_vault_pair(accounts, token_balances);
    if positional.is_some() {
        return positional;
    }
    return infer_authority_owned_vault_pair(accounts, authority, token_balances);
}

fn infer_positional_vault_pair(
    accounts: &[std::string::String],
    token_balances: &[TokenBalanceRecord],
) -> std::option::Option<VaultPair> {
    if accounts.len() < 5 {
        return None;
    }
    let vault_a = accounts[3].clone();
    let vault_b = accounts[4].clone();
    let mint_a = mint_for_account(token_balances, vault_a.as_str());
    let mint_b = mint_for_account(token_balances, vault_b.as_str());
    match (mint_a, mint_b) {
        (Some(mint_a), Some(mint_b)) => {
            if mint_a == mint_b {
                return None;
            }
            return Some(VaultPair {
                vault_a,
                vault_a_mint: mint_a,
                vault_b,
                vault_b_mint: mint_b,
            });
        },
        _ => return None,
    }
}

fn infer_authority_owned_vault_pair(
    accounts: &[std::string::String],
    authority: &str,
    token_balances: &[TokenBalanceRecord],
) -> std::option::Option<VaultPair> {
    let mut candidates = std::vec::Vec::new();
    for account in accounts {
        if is_known_program_or_sysvar(account.as_str()) {
            continue;
        }
        let record = token_balance_record_for_account(token_balances, account.as_str());
        let record = match record {
            Some(record) => record,
            None => continue,
        };
        if record.owner.as_deref() != Some(authority) {
            continue;
        }
        if record.delta_raw().is_none() {
            continue;
        }
        if candidates
            .iter()
            .any(|candidate: &TokenBalanceRecord| return candidate.mint == record.mint)
        {
            continue;
        }
        candidates.push(record.clone());
        if candidates.len() >= 2 {
            break;
        }
    }
    if candidates.len() < 2 {
        return None;
    }
    let first_address = match &candidates[0].account_address {
        Some(address) => address.clone(),
        None => return None,
    };
    let second_address = match &candidates[1].account_address {
        Some(address) => address.clone(),
        None => return None,
    };
    return Some(VaultPair {
        vault_a: first_address,
        vault_a_mint: candidates[0].mint.clone(),
        vault_b: second_address,
        vault_b_mint: candidates[1].mint.clone(),
    });
}

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 normalize_pool_pair(vault_pair: VaultPair) -> NormalizedVaultPair {
    let vault_a_is_quote = is_quote_mint(vault_pair.vault_a_mint.as_str());
    let vault_b_is_quote = is_quote_mint(vault_pair.vault_b_mint.as_str());
    if vault_a_is_quote && !vault_b_is_quote {
        return NormalizedVaultPair {
            raw_vault_a: vault_pair.vault_a.clone(),
            raw_vault_b: vault_pair.vault_b.clone(),
            base_vault: vault_pair.vault_b,
            quote_vault: vault_pair.vault_a,
            base_mint: vault_pair.vault_b_mint,
            quote_mint: vault_pair.vault_a_mint,
        };
    }
    if vault_b_is_quote && !vault_a_is_quote {
        return NormalizedVaultPair {
            raw_vault_a: vault_pair.vault_a.clone(),
            raw_vault_b: vault_pair.vault_b.clone(),
            base_vault: vault_pair.vault_a,
            quote_vault: vault_pair.vault_b,
            base_mint: vault_pair.vault_a_mint,
            quote_mint: vault_pair.vault_b_mint,
        };
    }
    return NormalizedVaultPair {
        raw_vault_a: vault_pair.vault_a.clone(),
        raw_vault_b: vault_pair.vault_b.clone(),
        base_vault: vault_pair.vault_a,
        quote_vault: vault_pair.vault_b,
        base_mint: vault_pair.vault_a_mint,
        quote_mint: vault_pair.vault_b_mint,
    };
}

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

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 infer_user_token_accounts(
    instruction_accounts: &[std::string::String],
    token_balances: &[TokenBalanceRecord],
    authority: &str,
    base_mint: &str,
    quote_mint: &str,
    base_vault: &str,
    quote_vault: &str,
    trade_side: std::option::Option<&str>,
) -> (
    std::option::Option<std::string::String>,
    std::option::Option<std::string::String>,
) {
    let user_base = find_user_token_account(
        instruction_accounts,
        token_balances,
        authority,
        base_mint,
        base_vault,
    );
    let user_quote = find_user_token_account(
        instruction_accounts,
        token_balances,
        authority,
        quote_mint,
        quote_vault,
    );
    match trade_side {
        Some("BuyBase") => return (user_quote, user_base),
        Some("SellBase") => return (user_base, user_quote),
        _ => return (None, None),
    }
}

fn find_user_token_account(
    instruction_accounts: &[std::string::String],
    token_balances: &[TokenBalanceRecord],
    authority: &str,
    mint: &str,
    vault: &str,
) -> std::option::Option<std::string::String> {
    for account in instruction_accounts {
        if account == vault {
            continue;
        }
        let record = token_balance_record_for_account(token_balances, account.as_str());
        let record = match record {
            Some(record) => record,
            None => continue,
        };
        if record.mint != mint {
            continue;
        }
        if record.owner.as_deref() == Some(authority) {
            continue;
        }
        return Some(account.clone());
    }
    return None;
}

fn input_output_vaults_for_trade_side(
    trade_side: std::option::Option<&str>,
    base_vault: &str,
    quote_vault: &str,
) -> (
    std::option::Option<std::string::String>,
    std::option::Option<std::string::String>,
) {
    match trade_side {
        Some("BuyBase") => return (Some(quote_vault.to_string()), Some(base_vault.to_string())),
        Some("SellBase") => return (Some(base_vault.to_string()), Some(quote_vault.to_string())),
        _ => return (None, None),
    }
}

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,
    }
}

fn is_known_program_or_sysvar(account: &str) -> bool {
    if account == crate::SYSTEM_PROGRAM_ID {
        return true;
    }
    if account == crate::SPL_TOKEN_PROGRAM_ID {
        return true;
    }
    if account == crate::SPL_TOKEN_2022_PROGRAM_ID {
        return true;
    }
    if account == crate::ASSOCIATED_TOKEN_PROGRAM_ID {
        return true;
    }
    if account == crate::RAYDIUM_AMM_V4_PROGRAM_ID {
        return true;
    }
    if account == OBSERVED_JUPITER_V6_PROGRAM_ID {
        return true;
    }
    return false;
}

fn extract_log_messages(
    transaction_json: &serde_json::Value,
) -> std::vec::Vec<std::string::String> {
    let mut messages = std::vec::Vec::new();
    let meta_option = transaction_json.get("meta");
    let meta = match meta_option {
        Some(meta) => meta,
        None => return messages,
    };
    let logs_option = meta.get("logMessages");
    let logs = match logs_option {
        Some(logs) => logs,
        None => return messages,
    };
    let logs_array_option = logs.as_array();
    let logs_array = match logs_array_option {
        Some(logs_array) => logs_array,
        None => return messages,
    };
    for value in logs_array {
        let text_option = value.as_str();
        if let Some(text) = text_option {
            messages.push(text.to_string());
        }
    }
    return messages;
}

fn log_messages_contain_initialize2(log_messages: &[std::string::String]) -> bool {
    for log_message in log_messages {
        if log_message.contains("initialize2") {
            return true;
        }
    }
    return false;
}

fn parse_accounts_json(
    accounts_json: &str,
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
    let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
    let values = match values_result {
        Ok(values) => values,
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse instruction accounts_json '{}': {}",
                accounts_json, error
            )));
        },
    };
    let mut accounts = std::vec::Vec::new();
    for value in values {
        let text_option = value.as_str();
        if let Some(text) = text_option {
            accounts.push(text.to_string());
        }
    }
    return Ok(accounts);
}

fn parse_optional_data_json_as_base58(
    data_json: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
    let data_json = match data_json {
        Some(data_json) => data_json,
        None => return None,
    };
    let json_string_result = serde_json::from_str::<std::string::String>(data_json);
    if let Ok(value) = json_string_result {
        return Some(value);
    }
    let trimmed = data_json.trim();
    if trimmed.is_empty() {
        return None;
    }
    let without_quotes = trimmed.trim_matches('"');
    if without_quotes.is_empty() {
        return None;
    }
    return Some(without_quotes.to_string());
}

fn extract_account(
    accounts: &[std::string::String],
    index: usize,
) -> std::option::Option<std::string::String> {
    if index >= accounts.len() {
        return None;
    }
    return Some(accounts[index].clone());
}

#[cfg(test)]
mod tests {
    fn make_transaction() -> crate::ChainTransactionDto {
        let mut dto = crate::ChainTransactionDto::new(
            "sig-raydium-test-1".to_string(),
            Some(888888),
            Some(1778000000),
            Some("helius_primary_http".to_string()),
            Some("0".to_string()),
            None,
            None,
            serde_json::json!({
                "slot": 888888,
                "meta": {
                    "logMessages": [
                        "Program log: initialize2"
                    ]
                },
                "transaction": {
                    "message": {
                        "instructions": [],
                        "accountKeys": []
                    }
                }
            })
            .to_string(),
        );
        dto.id = Some(42);
        return dto;
    }

    fn make_instruction() -> crate::ChainInstructionDto {
        let mut dto = crate::ChainInstructionDto::new(
            42,
            None,
            0,
            None,
            Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
            Some("raydium_amm_v4".to_string()),
            Some(1),
            serde_json::json!([
                "Account0",
                "Account1",
                "Account2",
                "Account3",
                "Pool111",
                "Account5",
                "Account6",
                "LpMint111",
                "TokenA111",
                "TokenB111",
                "Account10",
                "Account11",
                "Account12",
                "Account13",
                "Account14",
                "Account15",
                "Market111"
            ])
            .to_string(),
            None,
            None,
            None,
        );
        dto.id = Some(7);
        return dto;
    }

    fn make_swap_transaction() -> crate::ChainTransactionDto {
        let meta = serde_json::json!({
            "err": null,
            "preTokenBalances": [
                {
                    "accountIndex": 3,
                    "mint": "BaseMint111",
                    "owner": "Authority111",
                    "programId": crate::SPL_TOKEN_PROGRAM_ID,
                    "uiTokenAmount": { "amount": "1000", "decimals": 6 }
                },
                {
                    "accountIndex": 4,
                    "mint": crate::WSOL_MINT_ID,
                    "owner": "Authority111",
                    "programId": crate::SPL_TOKEN_PROGRAM_ID,
                    "uiTokenAmount": { "amount": "2000", "decimals": 9 }
                },
                {
                    "accountIndex": 6,
                    "mint": "BaseMint111",
                    "owner": "User111",
                    "programId": crate::SPL_TOKEN_PROGRAM_ID,
                    "uiTokenAmount": { "amount": "50", "decimals": 6 }
                },
                {
                    "accountIndex": 7,
                    "mint": crate::WSOL_MINT_ID,
                    "owner": "User111",
                    "programId": crate::SPL_TOKEN_PROGRAM_ID,
                    "uiTokenAmount": { "amount": "500", "decimals": 9 }
                }
            ],
            "postTokenBalances": [
                {
                    "accountIndex": 3,
                    "mint": "BaseMint111",
                    "owner": "Authority111",
                    "programId": crate::SPL_TOKEN_PROGRAM_ID,
                    "uiTokenAmount": { "amount": "900", "decimals": 6 }
                },
                {
                    "accountIndex": 4,
                    "mint": crate::WSOL_MINT_ID,
                    "owner": "Authority111",
                    "programId": crate::SPL_TOKEN_PROGRAM_ID,
                    "uiTokenAmount": { "amount": "2100", "decimals": 9 }
                },
                {
                    "accountIndex": 6,
                    "mint": "BaseMint111",
                    "owner": "User111",
                    "programId": crate::SPL_TOKEN_PROGRAM_ID,
                    "uiTokenAmount": { "amount": "150", "decimals": 6 }
                },
                {
                    "accountIndex": 7,
                    "mint": crate::WSOL_MINT_ID,
                    "owner": "User111",
                    "programId": crate::SPL_TOKEN_PROGRAM_ID,
                    "uiTokenAmount": { "amount": "400", "decimals": 9 }
                }
            ],
            "innerInstructions": []
        });
        let transaction_json = serde_json::json!({
            "slot": 999999,
            "meta": meta,
            "transaction": {
                "message": {
                    "accountKeys": [
                        crate::SPL_TOKEN_PROGRAM_ID,
                        "Pool111",
                        "Authority111",
                        "BaseVault111",
                        "QuoteVault111",
                        "AmmOpenOrders111",
                        "UserBase111",
                        "UserQuote111"
                    ],
                    "instructions": []
                }
            }
        });
        let mut dto = crate::ChainTransactionDto::new(
            "sig-raydium-swap-1".to_string(),
            Some(999999),
            Some(1778000001),
            Some("helius_primary_http".to_string()),
            Some("0".to_string()),
            None,
            Some(meta.to_string()),
            transaction_json.to_string(),
        );
        dto.id = Some(100);
        return dto;
    }

    fn make_swap_instruction() -> crate::ChainInstructionDto {
        return make_swap_instruction_with_data("63SfuT4qF7xK35jRTGqxuUT");
    }

    fn make_swap_v2_instruction() -> crate::ChainInstructionDto {
        return make_swap_instruction_with_data("9rj8cBJgMm4L1xvzfy5AUsy");
    }

    fn make_swap_instruction_with_data(data_base58: &str) -> crate::ChainInstructionDto {
        let mut dto = crate::ChainInstructionDto::new(
            100,
            Some(55),
            4,
            Some(0),
            Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
            Some("raydium_amm_v4".to_string()),
            Some(2),
            serde_json::json!([
                crate::SPL_TOKEN_PROGRAM_ID,
                "Pool111",
                "Authority111",
                "BaseVault111",
                "QuoteVault111",
                "AmmOpenOrders111",
                "UserBase111",
                "UserQuote111"
            ])
            .to_string(),
            Some(serde_json::json!(data_base58).to_string()),
            None,
            None,
        );
        dto.id = Some(200);
        return dto;
    }

    #[test]
    fn raydium_amm_v4_initialize2_logs_are_detected() {
        let decoder = crate::RaydiumAmmV4Decoder::new();
        let transaction = make_transaction();
        let instructions = vec![make_instruction()];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(event) => {
                assert_eq!(event.transaction_id, 42);
                assert_eq!(event.instruction_id, 7);
                assert_eq!(event.pool_account, Some("Pool111".to_string()));
                assert_eq!(event.lp_mint, Some("LpMint111".to_string()));
                assert_eq!(event.token_a_mint, Some("TokenA111".to_string()));
                assert_eq!(event.token_b_mint, Some("TokenB111".to_string()));
                assert_eq!(event.market_account, Some("Market111".to_string()));
            },
            _ => panic!("expected initialize2 event"),
        }
    }

    #[test]
    fn raydium_amm_v4_initialize2_returns_none_without_expected_log() {
        let decoder = crate::RaydiumAmmV4Decoder::new();
        let mut transaction = make_transaction();
        transaction.transaction_json = serde_json::json!({
            "slot": 888888,
            "meta": {
                "logMessages": [
                    "Program log: swap"
                ]
            },
            "transaction": {
                "message": {
                    "instructions": [],
                    "accountKeys": []
                }
            }
        })
        .to_string();
        let instructions = vec![make_instruction()];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 0);
    }

    #[test]
    fn raydium_amm_v4_swap_is_decoded_from_inner_instruction_and_vault_deltas() {
        let decoder = crate::RaydiumAmmV4Decoder::new();
        let transaction = make_swap_transaction();
        let instructions = vec![make_swap_instruction()];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::RaydiumAmmV4DecodedEvent::Swap(event) => {
                assert_eq!(event.transaction_id, 100);
                assert_eq!(event.instruction_id, 200);
                assert_eq!(event.pool_account, "Pool111".to_string());
                assert_eq!(event.event_kind, "raydium_amm_v4.swap_base_in".to_string());
                assert_eq!(event.instruction_name, "swap_base_in".to_string());
                assert_eq!(event.discriminator_hex, "09".to_string());
                assert_eq!(event.token_a_mint, "BaseMint111".to_string());
                assert_eq!(event.token_b_mint, crate::WSOL_MINT_ID.to_string());
                assert_eq!(event.base_vault, "BaseVault111".to_string());
                assert_eq!(event.quote_vault, "QuoteVault111".to_string());
                assert_eq!(event.base_amount_raw, Some("100".to_string()));
                assert_eq!(event.quote_amount_raw, Some("100".to_string()));
                assert_eq!(event.trade_side, Some("BuyBase".to_string()));
                assert_eq!(event.input_vault, Some("QuoteVault111".to_string()));
                assert_eq!(event.output_vault, Some("BaseVault111".to_string()));
                assert_eq!(event.input_token_account, Some("UserQuote111".to_string()));
                assert_eq!(event.output_token_account, Some("UserBase111".to_string()));
            },
            _ => panic!("expected swap event"),
        }
    }
    #[test]
    fn raydium_amm_v4_swap_base_in_v2_is_decoded_from_inner_instruction_and_vault_deltas() {
        let decoder = crate::RaydiumAmmV4Decoder::new();
        let transaction = make_swap_transaction();
        let instructions = vec![make_swap_v2_instruction()];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::RaydiumAmmV4DecodedEvent::Swap(event) => {
                assert_eq!(event.event_kind, "raydium_amm_v4.swap_base_in_v2".to_string());
                assert_eq!(event.instruction_name, "swap_base_in_v2".to_string());
                assert_eq!(event.discriminator_hex, "10".to_string());
                assert_eq!(event.pool_account, "Pool111".to_string());
                assert_eq!(event.base_amount_raw, Some("100".to_string()));
                assert_eq!(event.quote_amount_raw, Some("100".to_string()));
                assert_eq!(event.trade_side, Some("BuyBase".to_string()));
            },
            _ => panic!("expected swap event"),
        }
    }

}
