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

//! OpenBook v2 audit-only transaction decoder.
//!
//! This decoder promotes upstream-registry discriminator matches for OpenBook v2
//! into protocol-specific audit events. It intentionally does not create trades,
//! candles, pools or materialized orderbook rows.

/// Decoded OpenBook v2 audit event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OpenBookV2AuditDecoded {
    /// 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,
    /// Protocol-specific audit event kind.
    pub event_kind: std::string::String,
    /// Upstream instruction name.
    pub upstream_entry_name: std::string::String,
    /// Upstream discriminator hex.
    pub upstream_discriminator_hex: std::option::Option<std::string::String>,
    /// Optional market/state account candidate.
    pub market_account: std::option::Option<std::string::String>,
    /// Optional token A/base mint observed from transaction metadata.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional token B/quote mint observed from transaction metadata.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Decoded audit payload.
    pub payload_json: serde_json::Value,
}

/// Decoded OpenBook v2 event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum OpenBookV2DecodedEvent {
    /// Audit-only instruction event.
    Audit(OpenBookV2AuditDecoded),
}

/// OpenBook v2 audit-only decoder.
#[derive(Debug, Clone, Default)]
pub struct OpenBookV2Decoder;

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

    /// Decodes OpenBook v2 instructions into audit-only events.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::OpenBookV2DecodedEvent>, crate::Error> {
        let transaction_id = match transaction.id {
            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 log_messages = extract_log_messages(&transaction_json);
        let openbook_log_audit = extract_openbook_v2_log_audit(log_messages.as_slice());
        let token_mints = extract_distinct_token_mints(&transaction_json);
        let mut decoded_events = 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::OPENBOOK_V2_PROGRAM_ID {
                continue;
            }
            let instruction_id = match instruction.id {
                Some(instruction_id) => instruction_id,
                None => continue,
            };
            let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
            let registry_match =
                crate::upstream_registry_match::upstream_registry_match_instruction_data(
                    program_id.as_str(),
                    data_base58.as_deref(),
                );
            let registry_match = match registry_match {
                Some(registry_match) => registry_match,
                None => continue,
            };
            if registry_match.decoder_code.as_str() != "openbook_v2" {
                continue;
            }
            let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
            let event_kind = openbook_v2_audit_event_kind(registry_match.entry_name.as_str());
            let market_account =
                candidate_market_account(registry_match.entry_name.as_str(), accounts.as_slice());
            let token_a_mint = token_mints.get(0).cloned();
            let token_b_mint = token_mints.get(1).cloned();
            let payload_json = build_openbook_v2_audit_payload(
                transaction,
                instruction,
                &registry_match,
                data_base58.as_deref(),
                accounts,
                log_messages.as_slice(),
                &openbook_log_audit,
                token_mints.as_slice(),
                event_kind,
                market_account.as_deref(),
            );
            decoded_events.push(crate::OpenBookV2DecodedEvent::Audit(
                crate::OpenBookV2AuditDecoded {
                    transaction_id,
                    instruction_id,
                    signature: transaction.signature.clone(),
                    program_id: program_id.clone(),
                    event_kind: event_kind.to_string(),
                    upstream_entry_name: registry_match.entry_name.clone(),
                    upstream_discriminator_hex: registry_match.discriminator_hex.clone(),
                    market_account,
                    token_a_mint,
                    token_b_mint,
                    payload_json,
                },
            ));
        }
        return Ok(decoded_events);
    }
}

fn openbook_v2_audit_event_kind(entry_name: &str) -> &'static str {
    match entry_name {
        "place_order" => return "openbook_v2.order_place_audit",
        "place_take_order" => return "openbook_v2.order_place_audit",
        "cancel_order" => return "openbook_v2.order_cancel_audit",
        "cancel_order_by_client_order_id" => return "openbook_v2.order_cancel_audit",
        "cancel_all_orders" => return "openbook_v2.order_cancel_audit",
        "consume_events" => return "openbook_v2.consume_events_audit",
        "settle_funds" => return "openbook_v2.settle_funds_audit",
        "create_market" => return "openbook_v2.market_create_audit",
        "close_market" => return "openbook_v2.close_market_audit",
        "create_open_orders_account" => return "openbook_v2.open_orders_create_audit",
        "create_open_orders_indexer" => return "openbook_v2.open_orders_create_audit",
        "close_open_orders_account" => return "openbook_v2.open_orders_close_audit",
        _ => return "openbook_v2.instruction_audit",
    }
}

fn candidate_market_account(
    entry_name: &str,
    accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
    let index = match entry_name {
        "place_order" => 1_usize,
        "place_take_order" => 1_usize,
        "cancel_order" => 1_usize,
        "cancel_order_by_client_order_id" => 1_usize,
        "cancel_all_orders" => 1_usize,
        "consume_events" => 0_usize,
        "settle_funds" => 3_usize,
        "create_market" => 0_usize,
        "close_market" => 0_usize,
        "create_open_orders_account" => 1_usize,
        "create_open_orders_indexer" => 1_usize,
        "close_open_orders_account" => 1_usize,
        _ => return None,
    };
    return accounts.get(index).cloned();
}

fn build_openbook_v2_audit_payload(
    transaction: &crate::ChainTransactionDto,
    instruction: &crate::ChainInstructionDto,
    registry_match: &crate::UpstreamRegistryEntryDto,
    data_base58: std::option::Option<&str>,
    accounts: std::vec::Vec<std::string::String>,
    log_messages: &[std::string::String],
    openbook_log_audit: &OpenBookV2LogAudit,
    token_mints: &[std::string::String],
    event_kind: &str,
    market_account: std::option::Option<&str>,
) -> serde_json::Value {
    let transaction_error = nullable_optional_json_string(transaction.err_json.as_deref());
    let failed = !transaction_error.is_null();
    let account_count = accounts.len();
    let expected_instruction_log_name =
        openbook_v2_instruction_log_name(registry_match.entry_name.as_str());
    let scoped_program_return_records = openbook_program_return_records_for_instruction(
        openbook_log_audit,
        expected_instruction_log_name.as_str(),
    );
    let scoped_program_return_record_json =
        openbook_program_return_records_to_json(scoped_program_return_records.as_slice());
    let scoped_program_return_u64_values =
        openbook_program_return_u64_values(scoped_program_return_records.as_slice());
    let scoped_program_return_i64_values =
        openbook_program_return_i64_values(scoped_program_return_records.as_slice());
    let scoped_program_data_records = openbook_program_data_records_for_instruction(
        openbook_log_audit,
        expected_instruction_log_name.as_str(),
    );
    let scoped_program_data_record_json =
        openbook_program_data_records_to_json(scoped_program_data_records.as_slice());
    let scoped_program_data_discriminator_hex_values =
        openbook_program_data_discriminator_hex_values(scoped_program_data_records.as_slice());
    let scoped_program_data_event_log_names =
        openbook_program_data_event_log_names(scoped_program_data_records.as_slice());
    let transaction_program_return_record_json = openbook_program_return_records_to_json(
        openbook_log_audit.program_return_records.as_slice(),
    );
    let transaction_program_data_record_json =
        openbook_program_data_records_to_json(openbook_log_audit.program_data_records.as_slice());
    let consume_events_audit = build_consume_events_audit(
        event_kind,
        scoped_program_return_records.as_slice(),
        scoped_program_return_u64_values.as_slice(),
        scoped_program_return_i64_values.as_slice(),
        scoped_program_data_records.as_slice(),
        scoped_program_data_discriminator_hex_values.as_slice(),
        scoped_program_data_event_log_names.as_slice(),
    );

    let mut payload = serde_json::Map::new();
    payload.insert("decoder".to_string(), serde_json::Value::String("openbook_v2".to_string()));
    payload.insert("auditOnly".to_string(), serde_json::Value::Bool(true));
    payload.insert("eventKind".to_string(), serde_json::Value::String(event_kind.to_string()));
    payload.insert(
        "signature".to_string(),
        serde_json::Value::String(transaction.signature.clone()),
    );
    payload.insert("slot".to_string(), openbook_json_value(transaction.slot));
    payload.insert("transactionFailed".to_string(), serde_json::Value::Bool(failed));
    payload.insert("transactionError".to_string(), transaction_error);
    payload.insert("instructionId".to_string(), openbook_json_value(instruction.id));
    payload.insert(
        "instructionIndex".to_string(),
        openbook_json_value(instruction.instruction_index),
    );
    payload.insert(
        "innerInstructionIndex".to_string(),
        openbook_json_value(instruction.inner_instruction_index),
    );
    payload.insert(
        "parentInstructionId".to_string(),
        openbook_json_value(instruction.parent_instruction_id),
    );
    payload.insert("stackHeight".to_string(), openbook_json_value(instruction.stack_height));
    payload.insert("programId".to_string(), openbook_json_value(instruction.program_id.clone()));
    payload
        .insert("programName".to_string(), openbook_json_value(instruction.program_name.clone()));
    payload.insert("accounts".to_string(), openbook_json_value(accounts));
    payload.insert("accountCount".to_string(), openbook_json_value(account_count));
    payload.insert("dataBase58".to_string(), openbook_json_value(data_base58));
    payload.insert("marketAccountCandidate".to_string(), openbook_json_value(market_account));
    payload.insert("observedTokenMints".to_string(), openbook_json_value(token_mints));
    payload.insert(
        "openbookInstructionLogNames".to_string(),
        openbook_json_value(openbook_log_audit.instruction_names.clone()),
    );
    payload.insert(
        "openbookExpectedInstructionLogName".to_string(),
        serde_json::Value::String(expected_instruction_log_name),
    );
    payload.insert(
        "openbookProgramReturnRecords".to_string(),
        serde_json::Value::Array(scoped_program_return_record_json),
    );
    payload.insert(
        "openbookProgramReturnU64LeValues".to_string(),
        openbook_json_value(scoped_program_return_u64_values),
    );
    payload.insert(
        "openbookProgramReturnI64LeValues".to_string(),
        openbook_json_value(scoped_program_return_i64_values),
    );
    payload.insert(
        "openbookProgramReturnRecordCount".to_string(),
        openbook_json_value(scoped_program_return_records.len()),
    );
    payload.insert(
        "openbookProgramDataRecords".to_string(),
        serde_json::Value::Array(scoped_program_data_record_json),
    );
    payload.insert(
        "openbookProgramDataDiscriminatorHexValues".to_string(),
        openbook_json_value(scoped_program_data_discriminator_hex_values),
    );
    payload.insert(
        "openbookProgramDataEventLogNames".to_string(),
        openbook_json_value(scoped_program_data_event_log_names),
    );
    payload.insert(
        "openbookProgramDataRecordCount".to_string(),
        openbook_json_value(scoped_program_data_records.len()),
    );
    payload.insert(
        "openbookTransactionProgramReturnRecords".to_string(),
        serde_json::Value::Array(transaction_program_return_record_json),
    );
    payload.insert(
        "openbookTransactionProgramReturnRecordCount".to_string(),
        openbook_json_value(openbook_log_audit.program_return_records.len()),
    );
    payload.insert(
        "openbookTransactionProgramDataRecords".to_string(),
        serde_json::Value::Array(transaction_program_data_record_json),
    );
    payload.insert(
        "openbookTransactionProgramDataRecordCount".to_string(),
        openbook_json_value(openbook_log_audit.program_data_records.len()),
    );
    payload.insert("consumeEventsAudit".to_string(), consume_events_audit);
    payload.insert(
        "upstreamSourceRepo".to_string(),
        openbook_json_value(registry_match.source_repo.clone()),
    );
    payload.insert(
        "upstreamSourcePath".to_string(),
        openbook_json_value(registry_match.source_path.clone()),
    );
    payload.insert(
        "upstreamDecoderCode".to_string(),
        serde_json::Value::String(registry_match.decoder_code.clone()),
    );
    payload.insert(
        "upstreamProgramFamily".to_string(),
        serde_json::Value::String(registry_match.program_family.clone()),
    );
    payload.insert(
        "upstreamSurfaceKind".to_string(),
        serde_json::Value::String(registry_match.surface_kind.clone()),
    );
    payload.insert(
        "upstreamEntryKind".to_string(),
        serde_json::Value::String(registry_match.entry_kind.clone()),
    );
    payload.insert(
        "upstreamEntryName".to_string(),
        serde_json::Value::String(registry_match.entry_name.clone()),
    );
    payload.insert(
        "upstreamDiscriminatorHex".to_string(),
        openbook_json_value(registry_match.discriminator_hex.clone()),
    );
    payload.insert(
        "upstreamDiscriminatorLen".to_string(),
        openbook_json_value(registry_match.discriminator_len),
    );
    payload.insert(
        "upstreamProofStatus".to_string(),
        serde_json::Value::String(registry_match.proof_status.clone()),
    );
    payload.insert("upstreamNotes".to_string(), openbook_json_value(registry_match.notes.clone()));
    payload.insert("logMessages".to_string(), openbook_json_value(log_messages));
    payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
    payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
    payload.insert("nonTradeUseful".to_string(), serde_json::Value::Bool(true));
    payload.insert(
        "skipTradeReason".to_string(),
        serde_json::Value::String("openbook_v2_audit_only_orderbook_decoder".to_string()),
    );
    payload.insert(
        "skipCandleReason".to_string(),
        serde_json::Value::String("openbook_v2_audit_only_orderbook_decoder".to_string()),
    );
    return serde_json::Value::Object(payload);
}

fn openbook_json_value<T: serde::Serialize>(value: T) -> serde_json::Value {
    let serialized = serde_json::to_value(value);
    match serialized {
        Ok(serialized) => return serialized,
        Err(_) => return serde_json::Value::Null,
    }
}

#[derive(Debug, Clone, Default)]
struct OpenBookV2LogAudit {
    instruction_names: std::vec::Vec<std::string::String>,
    program_return_records: std::vec::Vec<OpenBookV2ProgramReturnRecord>,
    program_data_records: std::vec::Vec<OpenBookV2ProgramDataRecord>,
}

#[derive(Debug, Clone)]
struct OpenBookV2ProgramReturnRecord {
    instruction_log_name: std::option::Option<std::string::String>,
    program_id: std::string::String,
    data_base64: std::string::String,
    decoded_len: usize,
    u64_le: std::option::Option<u64>,
    i64_le: std::option::Option<i64>,
}

#[derive(Debug, Clone)]
struct OpenBookV2ProgramDataRecord {
    instruction_log_name: std::option::Option<std::string::String>,
    data_base64: std::string::String,
    decoded_len: usize,
    discriminator_hex: std::option::Option<std::string::String>,
    event_log_name: std::option::Option<std::string::String>,
    event_log_mapping_status: std::string::String,
    event_log_payload_status: std::string::String,
    event_log_payload_json: std::option::Option<serde_json::Value>,
    payload_len: usize,
}

fn extract_openbook_v2_log_audit(log_messages: &[std::string::String]) -> OpenBookV2LogAudit {
    let mut audit = OpenBookV2LogAudit::default();
    let mut current_instruction_log_name: std::option::Option<std::string::String> = None;
    for log_message in log_messages {
        let instruction_log_name =
            collect_openbook_v2_instruction_log_name(&mut audit, log_message.as_str());
        if let Some(instruction_log_name) = instruction_log_name {
            current_instruction_log_name = Some(instruction_log_name);
            continue;
        }
        collect_openbook_v2_program_return_record(
            &mut audit,
            log_message.as_str(),
            current_instruction_log_name.as_deref(),
        );
        collect_openbook_v2_program_data_record(
            &mut audit,
            log_message.as_str(),
            current_instruction_log_name.as_deref(),
        );
    }
    return audit;
}

fn collect_openbook_v2_instruction_log_name(
    audit: &mut OpenBookV2LogAudit,
    log_message: &str,
) -> std::option::Option<std::string::String> {
    let prefix = "Program log: Instruction: ";
    let name = match log_message.strip_prefix(prefix) {
        Some(name) => name.trim(),
        None => return None,
    };
    if name.is_empty() {
        return None;
    }
    if !audit.instruction_names.iter().any(|existing| return existing == name) {
        audit.instruction_names.push(name.to_string());
    }
    return Some(name.to_string());
}

fn collect_openbook_v2_program_return_record(
    audit: &mut OpenBookV2LogAudit,
    log_message: &str,
    current_instruction_log_name: std::option::Option<&str>,
) {
    let prefix = "Program return: ";
    let payload = match log_message.strip_prefix(prefix) {
        Some(payload) => payload.trim(),
        None => return,
    };
    let mut parts = payload.split_whitespace();
    let program_id = match parts.next() {
        Some(program_id) => program_id,
        None => return,
    };
    if program_id != crate::OPENBOOK_V2_PROGRAM_ID {
        return;
    }
    let data_base64 = match parts.next() {
        Some(data_base64) => data_base64,
        None => return,
    };
    let decoded_bytes = decode_base64_standard(data_base64);
    let decoded_len = decoded_bytes.as_ref().map_or(0_usize, |bytes| return bytes.len());
    let decoded_le = decoded_bytes
        .as_ref()
        .and_then(|bytes| return decode_le_prefix(bytes.as_slice()));
    let (u64_le, i64_le) = match decoded_le {
        Some((u64_le, i64_le)) => (Some(u64_le), Some(i64_le)),
        None => (None, None),
    };
    audit.program_return_records.push(OpenBookV2ProgramReturnRecord {
        instruction_log_name: current_instruction_log_name.map(|value| return value.to_string()),
        program_id: program_id.to_string(),
        data_base64: data_base64.to_string(),
        decoded_len,
        u64_le,
        i64_le,
    });
}

fn collect_openbook_v2_program_data_record(
    audit: &mut OpenBookV2LogAudit,
    log_message: &str,
    current_instruction_log_name: std::option::Option<&str>,
) {
    let prefix = "Program data: ";
    let data_base64 = match log_message.strip_prefix(prefix) {
        Some(data_base64) => data_base64.trim(),
        None => return,
    };
    if data_base64.is_empty() {
        return;
    }
    let decoded_bytes = decode_base64_standard(data_base64);
    let decoded_len = decoded_bytes.as_ref().map_or(0_usize, |bytes| return bytes.len());
    let discriminator_hex = decoded_bytes
        .as_ref()
        .and_then(|bytes| return decode_discriminator_hex(bytes.as_slice()));
    let event_log_name = discriminator_hex.as_deref().and_then(|discriminator_hex| {
        return openbook_program_data_event_log_name(discriminator_hex);
    });
    let event_log_mapping_status = match event_log_name.as_ref() {
        Some(_) => "upstream_git_source_mapped_unverified",
        None => "upstream_git_source_unmapped",
    };
    let payload_len = decoded_len.saturating_sub(8);
    let (event_log_payload_json, event_log_payload_status) =
        decode_openbook_program_data_event_payload(event_log_name, decoded_bytes.as_deref());
    audit.program_data_records.push(OpenBookV2ProgramDataRecord {
        instruction_log_name: current_instruction_log_name.map(|value| return value.to_string()),
        data_base64: data_base64.to_string(),
        decoded_len,
        discriminator_hex,
        event_log_name: event_log_name.map(|value| return value.to_string()),
        event_log_mapping_status: event_log_mapping_status.to_string(),
        event_log_payload_status: event_log_payload_status.to_string(),
        event_log_payload_json,
        payload_len,
    });
}

fn build_consume_events_audit(
    event_kind: &str,
    scoped_program_return_records: &[OpenBookV2ProgramReturnRecord],
    scoped_program_return_u64_values: &[u64],
    scoped_program_return_i64_values: &[i64],
    scoped_program_data_records: &[OpenBookV2ProgramDataRecord],
    scoped_program_data_discriminator_hex_values: &[std::string::String],
    scoped_program_data_event_log_names: &[std::string::String],
) -> serde_json::Value {
    if event_kind != "openbook_v2.consume_events_audit" {
        return serde_json::Value::Null;
    }
    let mut value = serde_json::Map::new();
    value.insert("auditOnly".to_string(), serde_json::Value::Bool(true));
    value.insert("instructionLogCount".to_string(), openbook_json_value(1_usize));
    value.insert(
        "programReturnRecordCount".to_string(),
        openbook_json_value(scoped_program_return_records.len()),
    );
    value.insert(
        "programReturnU64LeValues".to_string(),
        openbook_json_value(scoped_program_return_u64_values),
    );
    value.insert(
        "programReturnI64LeValues".to_string(),
        openbook_json_value(scoped_program_return_i64_values),
    );
    value.insert(
        "programDataRecordCount".to_string(),
        openbook_json_value(scoped_program_data_records.len()),
    );
    value.insert(
        "programDataDiscriminatorHexValues".to_string(),
        openbook_json_value(scoped_program_data_discriminator_hex_values),
    );
    value.insert(
        "programDataEventLogNames".to_string(),
        openbook_json_value(scoped_program_data_event_log_names),
    );
    value.insert(
        "programDataRecords".to_string(),
        serde_json::Value::Array(openbook_program_data_records_to_json(
            scoped_program_data_records,
        )),
    );
    value.insert(
        "interpretationStatus".to_string(),
        serde_json::Value::String("audit_only_unverified".to_string()),
    );
    value.insert(
        "notes".to_string(),
        serde_json::Value::String("OpenBook v2 consume_events return/log/program-data captured for corpus analysis only; not materialized as fills, trades, or candles.".to_string()),
    );
    return serde_json::Value::Object(value);
}

fn openbook_program_return_records_for_instruction(
    openbook_log_audit: &OpenBookV2LogAudit,
    expected_instruction_log_name: &str,
) -> std::vec::Vec<OpenBookV2ProgramReturnRecord> {
    let mut records = std::vec::Vec::new();
    for record in &openbook_log_audit.program_return_records {
        let instruction_log_name = match record.instruction_log_name.as_ref() {
            Some(instruction_log_name) => instruction_log_name,
            None => continue,
        };
        if instruction_log_name.as_str() != expected_instruction_log_name {
            continue;
        }
        records.push(record.clone());
    }
    return records;
}

fn openbook_program_data_records_for_instruction(
    openbook_log_audit: &OpenBookV2LogAudit,
    expected_instruction_log_name: &str,
) -> std::vec::Vec<OpenBookV2ProgramDataRecord> {
    let mut records = std::vec::Vec::new();
    for record in &openbook_log_audit.program_data_records {
        let instruction_log_name = match record.instruction_log_name.as_ref() {
            Some(instruction_log_name) => instruction_log_name,
            None => continue,
        };
        if instruction_log_name.as_str() != expected_instruction_log_name {
            continue;
        }
        records.push(record.clone());
    }
    return records;
}

fn openbook_program_data_records_to_json(
    records: &[OpenBookV2ProgramDataRecord],
) -> std::vec::Vec<serde_json::Value> {
    let mut values = std::vec::Vec::new();
    for record in records {
        let mut value = serde_json::Map::new();
        value.insert(
            "instructionLogName".to_string(),
            openbook_json_value(record.instruction_log_name.clone()),
        );
        value.insert(
            "dataBase64".to_string(),
            serde_json::Value::String(record.data_base64.clone()),
        );
        value.insert("decodedLen".to_string(), openbook_json_value(record.decoded_len));
        value.insert(
            "discriminatorHex".to_string(),
            openbook_json_value(record.discriminator_hex.clone()),
        );
        value
            .insert("eventLogName".to_string(), openbook_json_value(record.event_log_name.clone()));
        value.insert(
            "eventLogMappingStatus".to_string(),
            serde_json::Value::String(record.event_log_mapping_status.clone()),
        );
        value.insert(
            "eventLogPayloadStatus".to_string(),
            serde_json::Value::String(record.event_log_payload_status.clone()),
        );
        value.insert(
            "eventLogPayload".to_string(),
            openbook_json_value(record.event_log_payload_json.clone()),
        );
        value.insert("payloadLen".to_string(), openbook_json_value(record.payload_len));
        values.push(serde_json::Value::Object(value));
    }
    return values;
}

fn openbook_program_data_event_log_name(
    discriminator_hex: &str,
) -> std::option::Option<&'static str> {
    match discriminator_hex {
        "8dbaa8fc6c8d485e" => return Some("DepositLog"),
        "9617299498a2d740" => return Some("FillLog"),
        "7da7b6f6f903394d" => return Some("TakerSignatureLog"),
        "d157d4eca43a3c75" => return Some("MarketMetaDataLog"),
        "08eb303aae4c9c69" => return Some("TotalOrderFillEvent"),
        "3582975c6d399170" => return Some("SetDelegateLog"),
        "0a32f075ed43e6e9" => return Some("SettleFundsLog"),
        "d2f21a4d5e30ff3d" => return Some("SweepFeesLog"),
        "c4f99421a8e44906" => return Some("OpenOrdersPositionLog"),
        _ => return None,
    }
}

fn openbook_program_data_event_log_names(
    records: &[OpenBookV2ProgramDataRecord],
) -> std::vec::Vec<std::string::String> {
    let mut values = std::vec::Vec::new();
    for record in records {
        let event_log_name = match record.event_log_name.as_ref() {
            Some(event_log_name) => event_log_name,
            None => continue,
        };
        if values
            .iter()
            .any(|existing: &std::string::String| return existing == event_log_name)
        {
            continue;
        }
        values.push(event_log_name.clone());
    }
    return values;
}

macro_rules! openbook_read_or_none {
    ($value:expr) => {
        match $value {
            Some(value) => value,
            None => return None,
        }
    };
}

struct OpenBookV2BytesReader<'a> {
    bytes: &'a [u8],
    offset: usize,
}

impl<'a> OpenBookV2BytesReader<'a> {
    fn new(bytes: &'a [u8]) -> Self {
        return Self { bytes, offset: 0_usize };
    }

    fn read_u8(&mut self) -> std::option::Option<u8> {
        if self.offset >= self.bytes.len() {
            return None;
        }
        let value = self.bytes[self.offset];
        self.offset += 1;
        return Some(value);
    }

    fn read_bool(&mut self) -> std::option::Option<bool> {
        let value = match self.read_u8() {
            Some(value) => value,
            None => return None,
        };
        return Some(value != 0_u8);
    }

    fn read_u32_le(&mut self) -> std::option::Option<u32> {
        if self.offset + 4_usize > self.bytes.len() {
            return None;
        }
        let mut raw = [0_u8; 4];
        let mut index = 0_usize;
        while index < 4_usize {
            raw[index] = self.bytes[self.offset + index];
            index += 1;
        }
        self.offset += 4_usize;
        return Some(u32::from_le_bytes(raw));
    }

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

    fn read_i64_le(&mut self) -> std::option::Option<i64> {
        if self.offset + 8_usize > self.bytes.len() {
            return None;
        }
        let mut raw = [0_u8; 8];
        let mut index = 0_usize;
        while index < 8_usize {
            raw[index] = self.bytes[self.offset + index];
            index += 1;
        }
        self.offset += 8_usize;
        return Some(i64::from_le_bytes(raw));
    }

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

    fn read_pubkey_string(&mut self) -> std::option::Option<std::string::String> {
        if self.offset + 32_usize > self.bytes.len() {
            return None;
        }
        let mut raw = [0_u8; 32];
        let mut index = 0_usize;
        while index < 32_usize {
            raw[index] = self.bytes[self.offset + index];
            index += 1;
        }
        self.offset += 32_usize;
        return Some(solana_sdk::pubkey::Pubkey::new_from_array(raw).to_string());
    }

    fn read_option_pubkey_string(
        &mut self,
    ) -> std::option::Option<std::option::Option<std::string::String>> {
        let tag = match self.read_u8() {
            Some(tag) => tag,
            None => return None,
        };
        if tag == 0_u8 {
            return Some(None);
        }
        if tag != 1_u8 {
            return None;
        }
        let pubkey = match self.read_pubkey_string() {
            Some(pubkey) => pubkey,
            None => return None,
        };
        return Some(Some(pubkey));
    }
}

fn decode_openbook_program_data_event_payload(
    event_log_name: std::option::Option<&str>,
    decoded_bytes: std::option::Option<&[u8]>,
) -> (std::option::Option<serde_json::Value>, &'static str) {
    let event_log_name = match event_log_name {
        Some(event_log_name) => event_log_name,
        None => return (None, "upstream_git_source_unmapped"),
    };
    let decoded_bytes = match decoded_bytes {
        Some(decoded_bytes) => decoded_bytes,
        None => return (None, "base64_decode_failed"),
    };
    if decoded_bytes.len() < 8_usize {
        return (None, "decoded_len_too_short");
    }
    let payload = &decoded_bytes[8_usize..];
    let decoded_payload = match event_log_name {
        "FillLog" => decode_openbook_fill_log_payload(payload),
        "TotalOrderFillEvent" => decode_openbook_total_order_fill_event_payload(payload),
        "SettleFundsLog" => decode_openbook_settle_funds_log_payload(payload),
        "OpenOrdersPositionLog" => decode_openbook_open_orders_position_log_payload(payload),
        "DepositLog" => decode_openbook_deposit_log_payload(payload),
        "TakerSignatureLog" => decode_openbook_taker_signature_log_payload(payload),
        "SweepFeesLog" => decode_openbook_sweep_fees_log_payload(payload),
        _ => return (None, "unsupported_event_log_layout_unverified"),
    };
    match decoded_payload {
        Some(decoded_payload) => {
            return (Some(decoded_payload), "decoded_from_upstream_git_source_layout_unverified");
        },
        None => return (None, "layout_decode_failed_unverified"),
    }
}

fn decode_openbook_fill_log_payload(bytes: &[u8]) -> std::option::Option<serde_json::Value> {
    let mut reader = OpenBookV2BytesReader::new(bytes);
    let mut value = serde_json::Map::new();
    value.insert("eventLogName".to_string(), openbook_json_value("FillLog"));
    value.insert(
        "market".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "takerSide".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u8())),
    );
    value.insert(
        "makerSlot".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u8())),
    );
    value.insert(
        "makerOut".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_bool())),
    );
    value.insert(
        "timestamp".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "seqNum".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "maker".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "makerClientOrderId".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "makerFee".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "makerTimestamp".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "taker".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "takerClientOrderId".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "takerFeeCeil".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "priceLots".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_i64_le())),
    );
    value.insert(
        "quantityBaseLots".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_i64_le())),
    );
    value.insert(
        "interpretationStatus".to_string(),
        openbook_json_value("audit_only_layout_decoded_unverified"),
    );
    return Some(serde_json::Value::Object(value));
}

fn decode_openbook_total_order_fill_event_payload(
    bytes: &[u8],
) -> std::option::Option<serde_json::Value> {
    let mut reader = OpenBookV2BytesReader::new(bytes);
    let mut value = serde_json::Map::new();
    value.insert("eventLogName".to_string(), openbook_json_value("TotalOrderFillEvent"));
    value.insert(
        "side".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u8())),
    );
    value.insert(
        "taker".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "totalQuantityPaid".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "totalQuantityReceived".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "fees".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "interpretationStatus".to_string(),
        openbook_json_value("audit_only_layout_decoded_unverified"),
    );
    return Some(serde_json::Value::Object(value));
}

fn decode_openbook_settle_funds_log_payload(
    bytes: &[u8],
) -> std::option::Option<serde_json::Value> {
    let mut reader = OpenBookV2BytesReader::new(bytes);
    let mut value = serde_json::Map::new();
    value.insert("eventLogName".to_string(), openbook_json_value("SettleFundsLog"));
    value.insert(
        "openOrdersAccount".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "baseNative".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "quoteNative".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "referrerRebate".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    let referrer = match reader.read_option_pubkey_string() {
        Some(referrer) => referrer,
        None => return None,
    };
    value.insert("referrer".to_string(), openbook_json_value(referrer));
    value.insert(
        "interpretationStatus".to_string(),
        openbook_json_value("audit_only_layout_decoded_unverified"),
    );
    return Some(serde_json::Value::Object(value));
}

fn decode_openbook_open_orders_position_log_payload(
    bytes: &[u8],
) -> std::option::Option<serde_json::Value> {
    let mut reader = OpenBookV2BytesReader::new(bytes);
    let mut value = serde_json::Map::new();
    value.insert("eventLogName".to_string(), openbook_json_value("OpenOrdersPositionLog"));
    value.insert(
        "owner".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "openOrdersAccountNum".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u32_le())),
    );
    value.insert(
        "market".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "bidsBaseLots".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_i64_le())),
    );
    value.insert(
        "bidsQuoteLots".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_i64_le())),
    );
    value.insert(
        "asksBaseLots".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_i64_le())),
    );
    value.insert(
        "baseFreeNative".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "quoteFreeNative".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "lockedMakerFees".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "referrerRebatesAvailable".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    let maker_volume = openbook_read_or_none!(reader.read_u128_le());
    let taker_volume = openbook_read_or_none!(reader.read_u128_le());
    value.insert("makerVolumeRaw".to_string(), openbook_json_value(maker_volume.to_string()));
    value.insert("takerVolumeRaw".to_string(), openbook_json_value(taker_volume.to_string()));
    value.insert(
        "interpretationStatus".to_string(),
        openbook_json_value("audit_only_layout_decoded_unverified"),
    );
    return Some(serde_json::Value::Object(value));
}

fn decode_openbook_deposit_log_payload(bytes: &[u8]) -> std::option::Option<serde_json::Value> {
    let mut reader = OpenBookV2BytesReader::new(bytes);
    let mut value = serde_json::Map::new();
    value.insert("eventLogName".to_string(), openbook_json_value("DepositLog"));
    value.insert(
        "openOrdersAccount".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "signer".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "baseAmount".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "quoteAmount".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "interpretationStatus".to_string(),
        openbook_json_value("audit_only_layout_decoded_unverified"),
    );
    return Some(serde_json::Value::Object(value));
}

fn decode_openbook_taker_signature_log_payload(
    bytes: &[u8],
) -> std::option::Option<serde_json::Value> {
    let mut reader = OpenBookV2BytesReader::new(bytes);
    let mut value = serde_json::Map::new();
    value.insert("eventLogName".to_string(), openbook_json_value("TakerSignatureLog"));
    value.insert(
        "market".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "seqNum".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "interpretationStatus".to_string(),
        openbook_json_value("audit_only_layout_decoded_unverified"),
    );
    return Some(serde_json::Value::Object(value));
}

fn decode_openbook_sweep_fees_log_payload(bytes: &[u8]) -> std::option::Option<serde_json::Value> {
    let mut reader = OpenBookV2BytesReader::new(bytes);
    let mut value = serde_json::Map::new();
    value.insert("eventLogName".to_string(), openbook_json_value("SweepFeesLog"));
    value.insert(
        "market".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "amount".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_u64_le())),
    );
    value.insert(
        "receiver".to_string(),
        openbook_json_value(openbook_read_or_none!(reader.read_pubkey_string())),
    );
    value.insert(
        "interpretationStatus".to_string(),
        openbook_json_value("audit_only_layout_decoded_unverified"),
    );
    return Some(serde_json::Value::Object(value));
}

fn openbook_program_data_discriminator_hex_values(
    records: &[OpenBookV2ProgramDataRecord],
) -> std::vec::Vec<std::string::String> {
    let mut values = std::vec::Vec::new();
    for record in records {
        let discriminator_hex = match record.discriminator_hex.as_ref() {
            Some(discriminator_hex) => discriminator_hex,
            None => continue,
        };
        if values
            .iter()
            .any(|existing: &std::string::String| return existing == discriminator_hex)
        {
            continue;
        }
        values.push(discriminator_hex.clone());
    }
    return values;
}

fn openbook_program_return_records_to_json(
    records: &[OpenBookV2ProgramReturnRecord],
) -> std::vec::Vec<serde_json::Value> {
    let mut values = std::vec::Vec::new();
    for record in records {
        values.push(serde_json::json!({
            "instructionLogName": record.instruction_log_name.clone(),
            "programId": record.program_id.clone(),
            "dataBase64": record.data_base64.clone(),
            "decodedLen": record.decoded_len,
            "u64Le": record.u64_le,
            "i64Le": record.i64_le,
        }));
    }
    return values;
}

fn openbook_program_return_u64_values(
    records: &[OpenBookV2ProgramReturnRecord],
) -> std::vec::Vec<u64> {
    let mut values = std::vec::Vec::new();
    for record in records {
        if let Some(value) = record.u64_le {
            values.push(value);
        }
    }
    return values;
}

fn openbook_program_return_i64_values(
    records: &[OpenBookV2ProgramReturnRecord],
) -> std::vec::Vec<i64> {
    let mut values = std::vec::Vec::new();
    for record in records {
        if let Some(value) = record.i64_le {
            values.push(value);
        }
    }
    return values;
}

fn openbook_v2_instruction_log_name(entry_name: &str) -> std::string::String {
    let mut name = std::string::String::new();
    let mut capitalize_next = true;
    for character in entry_name.chars() {
        if character == '_' {
            capitalize_next = true;
            continue;
        }
        if capitalize_next {
            for upper in character.to_uppercase() {
                name.push(upper);
            }
            capitalize_next = false;
            continue;
        }
        name.push(character);
    }
    return name;
}

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 decode_discriminator_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
    if bytes.len() < 8 {
        return None;
    }
    let mut output = std::string::String::new();
    let mut index = 0_usize;
    while index < 8 {
        output.push_str(format!("{:02x}", bytes[index]).as_str());
        index += 1;
    }
    return Some(output);
}

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

fn nullable_optional_json_string(value: std::option::Option<&str>) -> serde_json::Value {
    let value = match value {
        Some(value) => value,
        None => return serde_json::Value::Null,
    };
    let trimmed = value.trim();
    if trimmed.is_empty() || trimmed == "null" {
        return serde_json::Value::Null;
    }
    let parsed = serde_json::from_str::<serde_json::Value>(trimmed);
    match parsed {
        Ok(parsed) => return parsed,
        Err(_) => return serde_json::Value::String(trimmed.to_string()),
    }
}

fn parse_instruction_accounts_vec(accounts_json: &str) -> std::vec::Vec<std::string::String> {
    let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
    match accounts_result {
        Ok(accounts) => return accounts,
        Err(_) => return std::vec::Vec::new(),
    }
}

fn parse_instruction_data_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 data_result = serde_json::from_str::<std::string::String>(data_json);
    match data_result {
        Ok(data) => return Some(data),
        Err(_) => {
            if data_json.trim().is_empty() {
                return None;
            }
            return Some(data_json.to_string());
        },
    }
}

fn extract_log_messages(
    transaction_json: &serde_json::Value,
) -> std::vec::Vec<std::string::String> {
    let mut logs = std::vec::Vec::new();
    let log_values = transaction_json
        .pointer("/meta/logMessages")
        .and_then(|value| return value.as_array());
    let log_values = match log_values {
        Some(log_values) => log_values,
        None => return logs,
    };
    for value in log_values {
        if let Some(text) = value.as_str() {
            logs.push(text.to_string());
        }
    }
    return logs;
}

fn extract_distinct_token_mints(
    transaction_json: &serde_json::Value,
) -> std::vec::Vec<std::string::String> {
    let mut mints = std::vec::Vec::new();
    collect_token_balance_mints(&mut mints, transaction_json.pointer("/meta/preTokenBalances"));
    collect_token_balance_mints(&mut mints, transaction_json.pointer("/meta/postTokenBalances"));
    return mints;
}

fn collect_token_balance_mints(
    mints: &mut std::vec::Vec<std::string::String>,
    value: std::option::Option<&serde_json::Value>,
) {
    let value = match value {
        Some(value) => value,
        None => return,
    };
    let items = match value.as_array() {
        Some(items) => items,
        None => return,
    };
    for item in items {
        let mint = item.get("mint").and_then(|value| return value.as_str());
        let mint = match mint {
            Some(mint) => mint,
            None => continue,
        };
        if mints.iter().any(|existing| return existing == mint) {
            continue;
        }
        mints.push(mint.to_string());
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn maps_openbook_v2_core_instruction_names_to_audit_event_kinds() {
        assert_eq!(
            super::openbook_v2_audit_event_kind("place_order"),
            "openbook_v2.order_place_audit"
        );
        assert_eq!(
            super::openbook_v2_audit_event_kind("consume_events"),
            "openbook_v2.consume_events_audit"
        );
        assert_eq!(
            super::openbook_v2_audit_event_kind("settle_funds"),
            "openbook_v2.settle_funds_audit"
        );
        assert_eq!(
            super::openbook_v2_audit_event_kind("close_open_orders_account"),
            "openbook_v2.open_orders_close_audit"
        );
    }

    #[test]
    fn unknown_openbook_v2_instruction_remains_instruction_audit() {
        assert_eq!(
            super::openbook_v2_audit_event_kind("unknown_future_instruction"),
            "openbook_v2.instruction_audit"
        );
    }

    #[test]
    fn openbook_log_audit_extracts_instruction_scoped_program_return_u64() {
        let logs = vec![
            "Program log: Instruction: ConsumeEvents".to_string(),
            "Program return: opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb TgAAAAAAAAA=".to_string(),
        ];
        let audit = super::extract_openbook_v2_log_audit(logs.as_slice());
        let consume_records =
            super::openbook_program_return_records_for_instruction(&audit, "ConsumeEvents");
        assert_eq!(audit.instruction_names, vec!["ConsumeEvents".to_string()]);
        assert_eq!(
            super::openbook_program_return_u64_values(consume_records.as_slice()),
            vec![78_u64]
        );
        assert_eq!(
            super::openbook_program_return_i64_values(consume_records.as_slice()),
            vec![78_i64]
        );
        assert_eq!(audit.program_return_records.len(), 1);
    }

    #[test]
    fn openbook_log_audit_filters_returns_by_instruction_name() {
        let logs = vec![
            "Program log: Instruction: PlaceOrder".to_string(),
            "Program return: opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb AQAAAAAAAAA=".to_string(),
            "Program log: Instruction: ConsumeEvents".to_string(),
            "Program return: opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb TgAAAAAAAAA=".to_string(),
        ];
        let audit = super::extract_openbook_v2_log_audit(logs.as_slice());
        let place_records =
            super::openbook_program_return_records_for_instruction(&audit, "PlaceOrder");
        let consume_records =
            super::openbook_program_return_records_for_instruction(&audit, "ConsumeEvents");
        assert_eq!(
            super::openbook_program_return_u64_values(place_records.as_slice()),
            vec![1_u64]
        );
        assert_eq!(
            super::openbook_program_return_u64_values(consume_records.as_slice()),
            vec![78_u64]
        );
    }

    #[test]
    fn consume_events_audit_is_null_for_non_consume_event() {
        let value = super::build_consume_events_audit(
            "openbook_v2.settle_funds_audit",
            &[],
            &[],
            &[],
            &[],
            &[],
            &[],
        );
        assert!(value.is_null());
    }

    #[test]
    fn openbook_v2_instruction_log_name_maps_snake_case_to_anchor_case() {
        assert_eq!(super::openbook_v2_instruction_log_name("consume_events"), "ConsumeEvents");
        assert_eq!(
            super::openbook_v2_instruction_log_name("cancel_order_by_client_order_id"),
            "CancelOrderByClientOrderId"
        );
    }

    #[test]
    fn openbook_log_audit_extracts_instruction_scoped_program_data() {
        let logs = vec![
            "Program log: Instruction: ConsumeEvents".to_string(),
            "Program data: AQIDBAUGBwg=".to_string(),
        ];
        let audit = super::extract_openbook_v2_log_audit(logs.as_slice());
        let records = super::openbook_program_data_records_for_instruction(&audit, "ConsumeEvents");
        assert_eq!(records.len(), 1);
        assert_eq!(records[0].decoded_len, 8);
        assert_eq!(records[0].payload_len, 0);
        assert_eq!(records[0].discriminator_hex.as_deref(), Some("0102030405060708"));
    }

    #[test]
    fn openbook_log_audit_filters_program_data_by_instruction_name() {
        let logs = vec![
            "Program log: Instruction: PlaceOrder".to_string(),
            "Program data: AQIDBAUGBwg=".to_string(),
            "Program log: Instruction: ConsumeEvents".to_string(),
            "Program data: CQgHBgUEAwI=".to_string(),
        ];
        let audit = super::extract_openbook_v2_log_audit(logs.as_slice());
        let place_records =
            super::openbook_program_data_records_for_instruction(&audit, "PlaceOrder");
        let consume_records =
            super::openbook_program_data_records_for_instruction(&audit, "ConsumeEvents");
        assert_eq!(
            super::openbook_program_data_discriminator_hex_values(place_records.as_slice()),
            vec!["0102030405060708".to_string()]
        );
        assert_eq!(
            super::openbook_program_data_discriminator_hex_values(consume_records.as_slice()),
            vec!["0908070605040302".to_string()]
        );
    }

    #[test]
    fn openbook_program_data_maps_known_log_discriminators() {
        assert_eq!(
            super::openbook_program_data_event_log_name("9617299498a2d740"),
            Some("FillLog")
        );
        assert_eq!(
            super::openbook_program_data_event_log_name("08eb303aae4c9c69"),
            Some("TotalOrderFillEvent")
        );
        assert_eq!(
            super::openbook_program_data_event_log_name("c4f99421a8e44906"),
            Some("OpenOrdersPositionLog")
        );
        assert_eq!(
            super::openbook_program_data_event_log_name("0a32f075ed43e6e9"),
            Some("SettleFundsLog")
        );
        assert_eq!(super::openbook_program_data_event_log_name("ffffffffffffffff"), None);
    }

    #[test]
    fn openbook_log_audit_program_data_records_include_event_log_name() {
        let logs = vec![
            "Program log: Instruction: ConsumeEvents".to_string(),
            "Program data: lhcplJii10AAAAAAAAAAAAAAAAA=".to_string(),
        ];
        let audit = super::extract_openbook_v2_log_audit(logs.as_slice());
        let records = super::openbook_program_data_records_for_instruction(&audit, "ConsumeEvents");
        assert_eq!(records.len(), 1);
        assert_eq!(records[0].discriminator_hex.as_deref(), Some("9617299498a2d740"));
        assert_eq!(records[0].event_log_name.as_deref(), Some("FillLog"));
        assert_eq!(
            records[0].event_log_mapping_status.as_str(),
            "upstream_git_source_mapped_unverified"
        );
        assert_eq!(
            super::openbook_program_data_event_log_names(records.as_slice()),
            vec!["FillLog".to_string()]
        );
    }

    #[test]
    fn openbook_program_data_payload_decodes_total_order_fill_event_layout() {
        let mut bytes = vec![0x08, 0xeb, 0x30, 0x3a, 0xae, 0x4c, 0x9c, 0x69];
        bytes.push(1_u8);
        let mut taker = [0_u8; 32];
        taker[0] = 7_u8;
        bytes.extend_from_slice(&taker);
        bytes.extend_from_slice(&11_u64.to_le_bytes());
        bytes.extend_from_slice(&22_u64.to_le_bytes());
        bytes.extend_from_slice(&3_u64.to_le_bytes());
        let (decoded, status) = super::decode_openbook_program_data_event_payload(
            Some("TotalOrderFillEvent"),
            Some(bytes.as_slice()),
        );
        assert_eq!(status, "decoded_from_upstream_git_source_layout_unverified");
        let decoded = match decoded {
            Some(decoded) => decoded,
            None => panic!("expected decoded TotalOrderFillEvent payload"),
        };
        assert_eq!(decoded.get("side").and_then(|value| return value.as_u64()), Some(1_u64));
        assert_eq!(
            decoded.get("totalQuantityPaid").and_then(|value| return value.as_u64()),
            Some(11_u64)
        );
        assert_eq!(
            decoded.get("totalQuantityReceived").and_then(|value| return value.as_u64()),
            Some(22_u64)
        );
        assert_eq!(decoded.get("fees").and_then(|value| return value.as_u64()), Some(3_u64));
    }

    #[test]
    fn openbook_program_data_record_json_includes_payload_status_and_payload() {
        let logs = vec![
            "Program log: Instruction: PlaceOrder".to_string(),
            "Program data: COswOq5MnGkBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAABYAAAAAAAAAAwAAAAAAAAA=".to_string(),
        ];
        let audit = super::extract_openbook_v2_log_audit(logs.as_slice());
        let records = super::openbook_program_data_records_for_instruction(&audit, "PlaceOrder");
        assert_eq!(records.len(), 1);
        let json_records = super::openbook_program_data_records_to_json(records.as_slice());
        let first_record = match json_records.get(0) {
            Some(first_record) => first_record,
            None => panic!("expected program data json record"),
        };
        assert_eq!(
            first_record.get("eventLogName").and_then(|value| return value.as_str()),
            Some("TotalOrderFillEvent")
        );
        assert_eq!(
            first_record
                .get("eventLogPayloadStatus")
                .and_then(|value| return value.as_str()),
            Some("decoded_from_upstream_git_source_layout_unverified")
        );
        assert_eq!(
            first_record
                .pointer("/eventLogPayload/totalQuantityPaid")
                .and_then(|value| return value.as_u64()),
            Some(11_u64)
        );
    }
}
