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

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

/// Decoded Phoenix v1 audit event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PhoenixV1AuditDecoded {
    /// 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 Phoenix v1 event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum PhoenixV1DecodedEvent {
    /// Audit-only instruction event.
    Audit(PhoenixV1AuditDecoded),
}

/// Phoenix v1 audit-only decoder.
#[derive(Debug, Clone, Default)]
pub struct PhoenixV1Decoder;

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

    /// Decodes Phoenix v1 instructions into audit-only events.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::PhoenixV1DecodedEvent>, 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 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::PHOENIX_V1_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() != "phoenix_v1" {
                continue;
            }
            let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
            let event_kind = phoenix_v1_audit_event_kind(registry_match.entry_name.as_str());
            let market_account = phoenix_v1_candidate_market_account(
                registry_match.entry_name.as_str(),
                accounts.as_slice(),
            );
            let token_a_mint = token_mints.first().cloned();
            let token_b_mint = token_mints.get(1).cloned();
            let payload_json = build_phoenix_v1_audit_payload(
                transaction,
                instruction,
                &registry_match,
                data_base58.as_deref(),
                accounts,
                log_messages.as_slice(),
                token_mints.as_slice(),
                event_kind,
                market_account.as_deref(),
            );
            decoded_events.push(crate::PhoenixV1DecodedEvent::Audit(
                crate::PhoenixV1AuditDecoded {
                    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 phoenix_v1_audit_event_kind(entry_name: &str) -> &'static str {
    match entry_name {
        "swap" => return "phoenix_v1.swap_audit",
        "swap_with_free_funds" => return "phoenix_v1.swap_audit",
        "place_limit_order" => return "phoenix_v1.order_place_audit",
        "place_limit_order_with_free_funds" => return "phoenix_v1.order_place_audit",
        "place_multiple_post_only_orders" => return "phoenix_v1.order_place_audit",
        "place_multiple_post_only_orders_with_free_funds" => {
            return "phoenix_v1.order_place_audit";
        },
        "reduce_order" => return "phoenix_v1.order_reduce_audit",
        "reduce_order_with_free_funds" => return "phoenix_v1.order_reduce_audit",
        "cancel_all_orders" => return "phoenix_v1.order_cancel_audit",
        "cancel_all_orders_with_free_funds" => return "phoenix_v1.order_cancel_audit",
        "cancel_up_to" => return "phoenix_v1.order_cancel_audit",
        "cancel_up_to_with_free_funds" => return "phoenix_v1.order_cancel_audit",
        "cancel_multiple_orders_by_id" => return "phoenix_v1.order_cancel_audit",
        "cancel_multiple_orders_by_id_with_free_funds" => {
            return "phoenix_v1.order_cancel_audit";
        },
        "deposit_funds" => return "phoenix_v1.funds_deposit_audit",
        "withdraw_funds" => return "phoenix_v1.funds_withdraw_audit",
        "request_seat" => return "phoenix_v1.seat_request_audit",
        "request_seat_authorized" => return "phoenix_v1.seat_request_audit",
        "log" => return "phoenix_v1.log_audit",
        "initialize_market" => return "phoenix_v1.market_create_audit",
        "collect_fees" => return "phoenix_v1.collect_fees_audit",
        "change_fee_recipient" => return "phoenix_v1.admin_audit",
        "change_market_status" => return "phoenix_v1.admin_audit",
        "change_seat_status" => return "phoenix_v1.admin_audit",
        "claim_authority" => return "phoenix_v1.admin_audit",
        "name_successor" => return "phoenix_v1.admin_audit",
        "evict_seat" => return "phoenix_v1.admin_audit",
        "force_cancel_orders" => return "phoenix_v1.admin_audit",
        _ => return "phoenix_v1.instruction_audit",
    }
}

fn phoenix_v1_candidate_market_account(
    entry_name: &str,
    accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
    let index = match entry_name {
        "swap" => 0_usize,
        "swap_with_free_funds" => 0_usize,
        "place_limit_order" => 0_usize,
        "place_limit_order_with_free_funds" => 0_usize,
        "place_multiple_post_only_orders" => 0_usize,
        "place_multiple_post_only_orders_with_free_funds" => 0_usize,
        "reduce_order" => 0_usize,
        "reduce_order_with_free_funds" => 0_usize,
        "cancel_all_orders" => 0_usize,
        "cancel_all_orders_with_free_funds" => 0_usize,
        "cancel_up_to" => 0_usize,
        "cancel_up_to_with_free_funds" => 0_usize,
        "cancel_multiple_orders_by_id" => 0_usize,
        "cancel_multiple_orders_by_id_with_free_funds" => 0_usize,
        "deposit_funds" => 0_usize,
        "withdraw_funds" => 0_usize,
        "request_seat" => 0_usize,
        "request_seat_authorized" => 0_usize,
        "log" => 0_usize,
        "initialize_market" => 0_usize,
        "collect_fees" => 0_usize,
        _ => return accounts.get(0).cloned(),
    };
    return accounts.get(index).cloned();
}

fn build_phoenix_v1_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],
    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 instruction_data_prefix = phoenix_instruction_data_prefix(data_base58);
    let phoenix_log_instruction_audit = decode_phoenix_log_instruction_data(data_base58);
    let phoenix_log_audit = extract_phoenix_log_audit(log_messages);
    let mut payload = serde_json::Map::new();
    payload.insert("decoder".to_string(), phoenix_json_value("phoenix_v1"));
    payload.insert("auditOnly".to_string(), serde_json::Value::Bool(true));
    payload.insert("eventKind".to_string(), phoenix_json_value(event_kind));
    payload.insert("signature".to_string(), phoenix_json_value(transaction.signature.clone()));
    payload.insert("slot".to_string(), phoenix_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(), phoenix_json_value(instruction.id));
    payload.insert(
        "instructionIndex".to_string(),
        phoenix_json_value(instruction.instruction_index),
    );
    payload.insert(
        "innerInstructionIndex".to_string(),
        phoenix_json_value(instruction.inner_instruction_index),
    );
    payload.insert(
        "parentInstructionId".to_string(),
        phoenix_json_value(instruction.parent_instruction_id),
    );
    payload.insert("stackHeight".to_string(), phoenix_json_value(instruction.stack_height));
    payload.insert("programId".to_string(), phoenix_json_value(instruction.program_id.clone()));
    payload.insert("programName".to_string(), phoenix_json_value(instruction.program_name.clone()));
    payload.insert("accounts".to_string(), phoenix_json_value(accounts));
    payload.insert("accountCount".to_string(), phoenix_json_value(account_count));
    payload.insert("dataBase58".to_string(), phoenix_json_value(data_base58));
    payload
        .insert("instructionDataPrefix".to_string(), phoenix_json_value(instruction_data_prefix));
    payload.insert(
        "phoenixLogInstructionAudit".to_string(),
        phoenix_json_value(phoenix_log_instruction_audit),
    );
    payload.insert("marketAccountCandidate".to_string(), phoenix_json_value(market_account));
    payload.insert("observedTokenMints".to_string(), phoenix_json_value(token_mints));
    payload.insert(
        "phoenixInstructionLogNames".to_string(),
        phoenix_json_value(phoenix_log_audit.instruction_log_names),
    );
    payload.insert(
        "phoenixLogProgramDataRecords".to_string(),
        phoenix_json_value(phoenix_log_audit.program_data_records),
    );
    payload.insert(
        "phoenixLogProgramDataRecordCount".to_string(),
        phoenix_json_value(phoenix_log_audit.program_data_record_count),
    );
    payload.insert(
        "upstreamSourceRepo".to_string(),
        phoenix_json_value(registry_match.source_repo.clone()),
    );
    payload.insert(
        "upstreamSourcePath".to_string(),
        phoenix_json_value(registry_match.source_path.clone()),
    );
    payload.insert(
        "upstreamDecoderCode".to_string(),
        phoenix_json_value(registry_match.decoder_code.clone()),
    );
    payload.insert(
        "upstreamProgramFamily".to_string(),
        phoenix_json_value(registry_match.program_family.clone()),
    );
    payload.insert(
        "upstreamSurfaceKind".to_string(),
        phoenix_json_value(registry_match.surface_kind.clone()),
    );
    payload.insert(
        "upstreamEntryKind".to_string(),
        phoenix_json_value(registry_match.entry_kind.clone()),
    );
    payload.insert(
        "upstreamEntryName".to_string(),
        phoenix_json_value(registry_match.entry_name.clone()),
    );
    payload.insert(
        "upstreamDiscriminatorHex".to_string(),
        phoenix_json_value(registry_match.discriminator_hex.clone()),
    );
    payload.insert(
        "upstreamDiscriminatorLen".to_string(),
        phoenix_json_value(registry_match.discriminator_len),
    );
    payload.insert(
        "upstreamProofStatus".to_string(),
        phoenix_json_value(registry_match.proof_status.clone()),
    );
    payload.insert("upstreamNotes".to_string(), phoenix_json_value(registry_match.notes.clone()));
    payload.insert("logMessages".to_string(), phoenix_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("interpretationStatus".to_string(), phoenix_json_value("audit_only_unverified"));
    payload.insert(
        "skipTradeReason".to_string(),
        phoenix_json_value("phoenix_v1_audit_only_orderbook_decoder"),
    );
    payload.insert(
        "skipCandleReason".to_string(),
        phoenix_json_value("phoenix_v1_audit_only_orderbook_decoder"),
    );
    return serde_json::Value::Object(payload);
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct PhoenixLogInstructionAudit {
    status: std::string::String,
    decoded_len: std::option::Option<usize>,
    log_instruction_tag: std::option::Option<u8>,
    header_event_tag: std::option::Option<u8>,
    current_instruction_tag: std::option::Option<u8>,
    current_instruction_tag_hex: std::option::Option<std::string::String>,
    current_instruction_name: std::option::Option<std::string::String>,
    unknown_current_instruction_tag: bool,
    sequence_number: std::option::Option<u64>,
    timestamp: std::option::Option<i64>,
    slot: std::option::Option<u64>,
    market: std::option::Option<std::string::String>,
    signer: std::option::Option<std::string::String>,
    total_events: std::option::Option<u16>,
    parsed_event_count: usize,
    event_type_names: std::vec::Vec<std::string::String>,
    events: std::vec::Vec<PhoenixLogInstructionEventAudit>,
    interpretation_status: std::string::String,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct PhoenixLogInstructionEventAudit {
    offset: usize,
    event_tag: u8,
    event_name: std::option::Option<std::string::String>,
    decoded_len: usize,
    payload_status: std::string::String,
    payload: serde_json::Value,
}

fn decode_phoenix_log_instruction_data(
    data_base58: std::option::Option<&str>,
) -> std::option::Option<PhoenixLogInstructionAudit> {
    let data_base58 = match data_base58 {
        Some(data_base58) => data_base58,
        None => return None,
    };
    let decoded_result = bs58::decode(data_base58).into_vec();
    let decoded = match decoded_result {
        Ok(decoded) => decoded,
        Err(_) => {
            return Some(PhoenixLogInstructionAudit {
                status: "base58_decode_failed".to_string(),
                decoded_len: None,
                log_instruction_tag: None,
                header_event_tag: None,
                current_instruction_tag: None,
                current_instruction_tag_hex: None,
                current_instruction_name: None,
                unknown_current_instruction_tag: false,
                sequence_number: None,
                timestamp: None,
                slot: None,
                market: None,
                signer: None,
                total_events: None,
                parsed_event_count: 0_usize,
                event_type_names: std::vec::Vec::new(),
                events: std::vec::Vec::new(),
                interpretation_status: "audit_only_unverified".to_string(),
            });
        },
    };
    if decoded.first().copied() != Some(15_u8) {
        return Some(PhoenixLogInstructionAudit {
            status: "not_phoenix_log_instruction".to_string(),
            decoded_len: Some(decoded.len()),
            log_instruction_tag: decoded.first().copied(),
            header_event_tag: None,
            current_instruction_tag: None,
            current_instruction_tag_hex: None,
            current_instruction_name: None,
            unknown_current_instruction_tag: false,
            sequence_number: None,
            timestamp: None,
            slot: None,
            market: None,
            signer: None,
            total_events: None,
            parsed_event_count: 0_usize,
            event_type_names: std::vec::Vec::new(),
            events: std::vec::Vec::new(),
            interpretation_status: "audit_only_unverified".to_string(),
        });
    }
    if decoded.len() < 93_usize {
        return Some(PhoenixLogInstructionAudit {
            status: "phoenix_log_instruction_truncated".to_string(),
            decoded_len: Some(decoded.len()),
            log_instruction_tag: decoded.first().copied(),
            header_event_tag: decoded.get(1_usize).copied(),
            current_instruction_tag: decoded.get(2_usize).copied(),
            current_instruction_tag_hex: decoded.get(2_usize).copied().map(phoenix_byte_hex),
            current_instruction_name: decoded
                .get(2_usize)
                .copied()
                .and_then(phoenix_instruction_tag_name)
                .map(std::string::ToString::to_string),
            unknown_current_instruction_tag: decoded
                .get(2_usize)
                .copied()
                .map(|tag| return phoenix_instruction_tag_name(tag).is_none())
                .unwrap_or(false),
            sequence_number: None,
            timestamp: None,
            slot: None,
            market: None,
            signer: None,
            total_events: None,
            parsed_event_count: 0_usize,
            event_type_names: std::vec::Vec::new(),
            events: std::vec::Vec::new(),
            interpretation_status: "audit_only_unverified".to_string(),
        });
    }
    let current_instruction_tag = decoded.get(2_usize).copied();
    let current_instruction_name = current_instruction_tag
        .and_then(phoenix_instruction_tag_name)
        .map(std::string::ToString::to_string);
    let unknown_current_instruction_tag =
        current_instruction_tag.is_some() && current_instruction_name.is_none();
    let mut offset = 93_usize;
    let mut events = std::vec::Vec::new();
    let mut event_type_names = std::vec::Vec::new();
    while offset < decoded.len() {
        let parsed = parse_phoenix_log_instruction_event(decoded.as_slice(), offset);
        let parsed = match parsed {
            Some(parsed) => parsed,
            None => {
                events.push(PhoenixLogInstructionEventAudit {
                    offset,
                    event_tag: decoded[offset],
                    event_name: phoenix_market_event_tag_name(decoded[offset])
                        .map(std::string::ToString::to_string),
                    decoded_len: decoded.len() - offset,
                    payload_status: "truncated_or_unmapped_market_event".to_string(),
                    payload: phoenix_json_value(serde_json::Map::<
                        std::string::String,
                        serde_json::Value,
                    >::new()),
                });
                break;
            },
        };
        if let Some(event_name) = parsed.event_name.as_ref() {
            if !event_type_names.iter().any(|known| return known == event_name) {
                event_type_names.push(event_name.clone());
            }
        }
        offset += parsed.decoded_len;
        events.push(parsed);
    }
    return Some(PhoenixLogInstructionAudit {
        status: "decoded_phoenix_log_instruction_header_unverified".to_string(),
        decoded_len: Some(decoded.len()),
        log_instruction_tag: decoded.get(0_usize).copied(),
        header_event_tag: decoded.get(1_usize).copied(),
        current_instruction_tag,
        current_instruction_tag_hex: current_instruction_tag.map(phoenix_byte_hex),
        current_instruction_name,
        unknown_current_instruction_tag,
        sequence_number: read_u64_le(decoded.as_slice(), 3_usize),
        timestamp: read_i64_le(decoded.as_slice(), 11_usize),
        slot: read_u64_le(decoded.as_slice(), 19_usize),
        market: read_pubkey_base58(decoded.as_slice(), 27_usize),
        signer: read_pubkey_base58(decoded.as_slice(), 59_usize),
        total_events: read_u16_le(decoded.as_slice(), 91_usize),
        parsed_event_count: events.len(),
        event_type_names,
        events,
        interpretation_status: "audit_only_layout_decoded_unverified".to_string(),
    });
}

fn parse_phoenix_log_instruction_event(
    bytes: &[u8],
    offset: usize,
) -> std::option::Option<PhoenixLogInstructionEventAudit> {
    let tag = match bytes.get(offset) {
        Some(tag) => *tag,
        None => return None,
    };
    let event_name = phoenix_market_event_tag_name(tag).map(std::string::ToString::to_string);
    let event_len = match phoenix_market_event_serialized_len(tag) {
        Some(event_len) => event_len,
        None => {
            return Some(PhoenixLogInstructionEventAudit {
                offset,
                event_tag: tag,
                event_name,
                decoded_len: 1_usize,
                payload_status: "unmapped_market_event_tag".to_string(),
                payload: phoenix_json_value(serde_json::Map::<
                    std::string::String,
                    serde_json::Value,
                >::new()),
            });
        },
    };
    if bytes.len() < offset + event_len {
        return None;
    }
    let payload = decode_phoenix_market_event_payload(tag, bytes, offset);
    return Some(PhoenixLogInstructionEventAudit {
        offset,
        event_tag: tag,
        event_name,
        decoded_len: event_len,
        payload_status: "decoded_from_upstream_source_layout_unverified".to_string(),
        payload,
    });
}

fn decode_phoenix_market_event_payload(tag: u8, bytes: &[u8], offset: usize) -> serde_json::Value {
    let mut payload = serde_json::Map::new();
    payload.insert(
        "interpretationStatus".to_string(),
        phoenix_json_value("audit_only_layout_decoded_unverified"),
    );
    payload.insert("eventName".to_string(), phoenix_json_value(phoenix_market_event_tag_name(tag)));
    match tag {
        2_u8 => {
            payload.insert(
                "index".to_string(),
                phoenix_json_value(read_u16_le(bytes, offset + 1_usize)),
            );
            payload.insert(
                "makerId".to_string(),
                phoenix_json_value(read_pubkey_base58(bytes, offset + 3_usize)),
            );
            payload.insert(
                "orderSequenceNumber".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 35_usize)),
            );
            payload.insert(
                "priceInTicks".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 43_usize)),
            );
            payload.insert(
                "baseLotsFilled".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 51_usize)),
            );
            payload.insert(
                "baseLotsRemaining".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 59_usize)),
            );
        },
        3_u8 => {
            payload.insert(
                "index".to_string(),
                phoenix_json_value(read_u16_le(bytes, offset + 1_usize)),
            );
            payload.insert(
                "orderSequenceNumber".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 3_usize)),
            );
            payload.insert(
                "clientOrderId".to_string(),
                phoenix_json_value(read_u128_le_string(bytes, offset + 11_usize)),
            );
            payload.insert(
                "priceInTicks".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 27_usize)),
            );
            payload.insert(
                "baseLotsPlaced".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 35_usize)),
            );
        },
        4_u8 => {
            payload.insert(
                "index".to_string(),
                phoenix_json_value(read_u16_le(bytes, offset + 1_usize)),
            );
            payload.insert(
                "orderSequenceNumber".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 3_usize)),
            );
            payload.insert(
                "priceInTicks".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 11_usize)),
            );
            payload.insert(
                "baseLotsRemoved".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 19_usize)),
            );
            payload.insert(
                "baseLotsRemaining".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 27_usize)),
            );
        },
        5_u8 => {
            payload.insert(
                "index".to_string(),
                phoenix_json_value(read_u16_le(bytes, offset + 1_usize)),
            );
            payload.insert(
                "makerId".to_string(),
                phoenix_json_value(read_pubkey_base58(bytes, offset + 3_usize)),
            );
            payload.insert(
                "orderSequenceNumber".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 35_usize)),
            );
            payload.insert(
                "priceInTicks".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 43_usize)),
            );
            payload.insert(
                "baseLotsEvicted".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 51_usize)),
            );
        },
        6_u8 => {
            payload.insert(
                "index".to_string(),
                phoenix_json_value(read_u16_le(bytes, offset + 1_usize)),
            );
            payload.insert(
                "clientOrderId".to_string(),
                phoenix_json_value(read_u128_le_string(bytes, offset + 3_usize)),
            );
            payload.insert(
                "totalBaseLotsFilled".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 19_usize)),
            );
            payload.insert(
                "totalQuoteLotsFilled".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 27_usize)),
            );
            payload.insert(
                "totalFeeInQuoteLots".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 35_usize)),
            );
        },
        7_u8 => {
            payload.insert(
                "index".to_string(),
                phoenix_json_value(read_u16_le(bytes, offset + 1_usize)),
            );
            payload.insert(
                "feesCollectedInQuoteLots".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 3_usize)),
            );
        },
        8_u8 => {
            payload.insert(
                "index".to_string(),
                phoenix_json_value(read_u16_le(bytes, offset + 1_usize)),
            );
            payload.insert(
                "orderSequenceNumber".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 3_usize)),
            );
            payload.insert(
                "lastValidSlot".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 11_usize)),
            );
            payload.insert(
                "lastValidUnixTimestampInSeconds".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 19_usize)),
            );
        },
        9_u8 => {
            payload.insert(
                "index".to_string(),
                phoenix_json_value(read_u16_le(bytes, offset + 1_usize)),
            );
            payload.insert(
                "makerId".to_string(),
                phoenix_json_value(read_pubkey_base58(bytes, offset + 3_usize)),
            );
            payload.insert(
                "orderSequenceNumber".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 35_usize)),
            );
            payload.insert(
                "priceInTicks".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 43_usize)),
            );
            payload.insert(
                "baseLotsRemoved".to_string(),
                phoenix_json_value(read_u64_le(bytes, offset + 51_usize)),
            );
        },
        _ => {},
    }
    return serde_json::Value::Object(payload);
}

fn phoenix_market_event_serialized_len(tag: u8) -> std::option::Option<usize> {
    match tag {
        2_u8 => return Some(67_usize),
        3_u8 => return Some(43_usize),
        4_u8 => return Some(35_usize),
        5_u8 => return Some(59_usize),
        6_u8 => return Some(43_usize),
        7_u8 => return Some(11_usize),
        8_u8 => return Some(27_usize),
        9_u8 => return Some(59_usize),
        _ => return None,
    }
}

fn phoenix_market_event_tag_name(tag: u8) -> std::option::Option<&'static str> {
    match tag {
        0_u8 => return Some("Uninitialized"),
        1_u8 => return Some("Header"),
        2_u8 => return Some("Fill"),
        3_u8 => return Some("Place"),
        4_u8 => return Some("Reduce"),
        5_u8 => return Some("Evict"),
        6_u8 => return Some("FillSummary"),
        7_u8 => return Some("Fee"),
        8_u8 => return Some("TimeInForce"),
        9_u8 => return Some("ExpiredOrder"),
        _ => return None,
    }
}

fn phoenix_instruction_tag_name(tag: u8) -> std::option::Option<&'static str> {
    match tag {
        0_u8 => return Some("Swap"),
        1_u8 => return Some("SwapWithFreeFunds"),
        2_u8 => return Some("PlaceLimitOrder"),
        3_u8 => return Some("PlaceLimitOrderWithFreeFunds"),
        4_u8 => return Some("ReduceOrder"),
        5_u8 => return Some("ReduceOrderWithFreeFunds"),
        6_u8 => return Some("CancelAllOrders"),
        7_u8 => return Some("CancelAllOrdersWithFreeFunds"),
        8_u8 => return Some("CancelUpTo"),
        9_u8 => return Some("CancelUpToWithFreeFunds"),
        10_u8 => return Some("CancelMultipleOrdersById"),
        11_u8 => return Some("CancelMultipleOrdersByIdWithFreeFunds"),
        12_u8 => return Some("WithdrawFunds"),
        13_u8 => return Some("DepositFunds"),
        14_u8 => return Some("RequestSeat"),
        15_u8 => return Some("Log"),
        16_u8 => return Some("PlaceMultiplePostOnlyOrders"),
        17_u8 => return Some("PlaceMultiplePostOnlyOrdersWithFreeFunds"),
        100_u8 => return Some("InitializeMarket"),
        101_u8 => return Some("ClaimAuthority"),
        102_u8 => return Some("NameSuccessor"),
        103_u8 => return Some("ChangeMarketStatus"),
        104_u8 => return Some("ChangeSeatStatus"),
        105_u8 => return Some("RequestSeatAuthorized"),
        106_u8 => return Some("EvictSeat"),
        107_u8 => return Some("ForceCancelOrders"),
        108_u8 => return Some("CollectFees"),
        109_u8 => return Some("ChangeFeeRecipient"),
        _ => return None,
    }
}

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

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

fn read_i64_le(bytes: &[u8], offset: usize) -> std::option::Option<i64> {
    if bytes.len() < offset + 8_usize {
        return None;
    }
    let mut buffer = [0_u8; 8];
    buffer.copy_from_slice(&bytes[offset..offset + 8_usize]);
    return Some(i64::from_le_bytes(buffer));
}

fn read_u128_le_string(bytes: &[u8], offset: usize) -> std::option::Option<std::string::String> {
    if bytes.len() < offset + 16_usize {
        return None;
    }
    let mut buffer = [0_u8; 16];
    buffer.copy_from_slice(&bytes[offset..offset + 16_usize]);
    return Some(u128::from_le_bytes(buffer).to_string());
}

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

#[derive(Debug, Clone, Default, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct PhoenixLogAudit {
    instruction_log_names: std::vec::Vec<std::string::String>,
    program_data_records: std::vec::Vec<PhoenixProgramDataRecord>,
    program_data_record_count: usize,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct PhoenixProgramDataRecord {
    instruction_log_name: std::option::Option<std::string::String>,
    data_base64: std::string::String,
    decoded_len: std::option::Option<usize>,
    discriminator_hex: std::option::Option<std::string::String>,
    payload_len: std::option::Option<usize>,
    interpretation_status: std::string::String,
}

fn extract_phoenix_log_audit(log_messages: &[std::string::String]) -> PhoenixLogAudit {
    let mut audit = PhoenixLogAudit::default();
    let mut current_instruction_log_name: std::option::Option<std::string::String> = None;
    for log_message in log_messages {
        let maybe_name = collect_phoenix_instruction_log_name(log_message.as_str());
        if let Some(name) = maybe_name {
            current_instruction_log_name = Some(name.clone());
            audit.instruction_log_names.push(name);
            continue;
        }
        let maybe_record = parse_phoenix_program_data_record(
            log_message.as_str(),
            current_instruction_log_name.as_deref(),
        );
        if let Some(record) = maybe_record {
            audit.program_data_records.push(record);
        }
    }
    audit.program_data_record_count = audit.program_data_records.len();
    return audit;
}

fn collect_phoenix_instruction_log_name(
    log_message: &str,
) -> std::option::Option<std::string::String> {
    let prefix = "Program log: Instruction: ";
    if !log_message.starts_with(prefix) {
        return None;
    }
    let name = &log_message[prefix.len()..];
    if name.is_empty() {
        return None;
    }
    return Some(name.to_string());
}

fn parse_phoenix_program_data_record(
    log_message: &str,
    instruction_log_name: std::option::Option<&str>,
) -> std::option::Option<PhoenixProgramDataRecord> {
    let prefix = "Program data: ";
    if !log_message.starts_with(prefix) {
        return None;
    }
    let data_base64 = log_message[prefix.len()..].trim();
    if data_base64.is_empty() {
        return None;
    }
    let decoded_result = base64_decode_standard(data_base64);
    let (decoded_len, discriminator_hex, payload_len, status) = match decoded_result {
        Some(decoded) => {
            let discriminator = if decoded.len() >= 8_usize {
                Some(hex_encode_lower(&decoded[0_usize..8_usize]))
            } else {
                None
            };
            let payload_len =
                if decoded.len() >= 8_usize { Some(decoded.len() - 8_usize) } else { None };
            (
                Some(decoded.len()),
                discriminator,
                payload_len,
                "audit_only_unmapped_program_data",
            )
        },
        None => (None, None, None, "base64_decode_failed"),
    };
    return Some(PhoenixProgramDataRecord {
        instruction_log_name: instruction_log_name.map(std::string::ToString::to_string),
        data_base64: data_base64.to_string(),
        decoded_len,
        discriminator_hex,
        payload_len,
        interpretation_status: status.to_string(),
    });
}

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

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

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 value_result = serde_json::from_str::<serde_json::Value>(data_json);
    let value = match value_result {
        Ok(value) => value,
        Err(_) => return None,
    };
    if let Some(text) = value.as_str() {
        return Some(text.to_string());
    }
    if let Some(text) = value.get("data").and_then(serde_json::Value::as_str) {
        return Some(text.to_string());
    }
    return None;
}

fn parse_instruction_accounts_vec(data: &str) -> std::vec::Vec<std::string::String> {
    let value_result = serde_json::from_str::<serde_json::Value>(data);
    let value = match value_result {
        Ok(value) => value,
        Err(_) => return std::vec::Vec::new(),
    };
    let array = match value.as_array() {
        Some(array) => array,
        None => return std::vec::Vec::new(),
    };
    let mut accounts = std::vec::Vec::new();
    for account in array {
        if let Some(text) = account.as_str() {
            accounts.push(text.to_string());
            continue;
        }
        if let Some(pubkey) = account.get("pubkey").and_then(serde_json::Value::as_str) {
            accounts.push(pubkey.to_string());
            continue;
        }
    }
    return accounts;
}

fn phoenix_instruction_data_prefix(
    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 decoded_result = bs58::decode(data_base58).into_vec();
    let decoded = match decoded_result {
        Ok(decoded) => decoded,
        Err(_) => return None,
    };
    let end = std::cmp::min(decoded.len(), 8_usize);
    return Some(hex_encode_lower(&decoded[0_usize..end]));
}

fn extract_log_messages(value: &serde_json::Value) -> std::vec::Vec<std::string::String> {
    let mut logs = std::vec::Vec::new();
    collect_log_messages_from_value(value, &mut logs);
    return logs;
}

fn collect_log_messages_from_value(
    value: &serde_json::Value,
    logs: &mut std::vec::Vec<std::string::String>,
) {
    match value {
        serde_json::Value::Array(array) => {
            for item in array {
                collect_log_messages_from_value(item, logs);
            }
        },
        serde_json::Value::Object(object) => {
            for (key, child) in object {
                if key == "logMessages" {
                    if let Some(array) = child.as_array() {
                        for item in array {
                            if let Some(text) = item.as_str() {
                                logs.push(text.to_string());
                            }
                        }
                    }
                }
                collect_log_messages_from_value(child, logs);
            }
        },
        _ => {},
    }
}

fn extract_distinct_token_mints(value: &serde_json::Value) -> std::vec::Vec<std::string::String> {
    let mut mints = std::vec::Vec::new();
    collect_token_mints(value, &mut mints);
    return mints;
}

fn collect_token_mints(value: &serde_json::Value, mints: &mut std::vec::Vec<std::string::String>) {
    match value {
        serde_json::Value::Array(array) => {
            for item in array {
                collect_token_mints(item, mints);
            }
        },
        serde_json::Value::Object(object) => {
            for (key, child) in object {
                if key == "mint" {
                    if let Some(mint) = child.as_str() {
                        if !mints.iter().any(|known| return known == mint) {
                            mints.push(mint.to_string());
                        }
                    }
                }
                collect_token_mints(child, mints);
            }
        },
        _ => {},
    }
}

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

fn hex_encode_lower(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;
}

fn phoenix_byte_hex(value: u8) -> std::string::String {
    return format!("{value:02x}");
}

#[cfg(test)]
mod tests {
    fn phoenix_instruction_with_data(data: &[u8]) -> std::string::String {
        return bs58::encode(data).into_string();
    }

    #[test]
    fn classifies_phoenix_v1_core_orderbook_entries_as_audit_only() {
        assert_eq!(super::phoenix_v1_audit_event_kind("swap"), "phoenix_v1.swap_audit");
        assert_eq!(
            super::phoenix_v1_audit_event_kind("place_limit_order"),
            "phoenix_v1.order_place_audit"
        );
        assert_eq!(
            super::phoenix_v1_audit_event_kind("cancel_up_to_with_free_funds"),
            "phoenix_v1.order_cancel_audit"
        );
        assert_eq!(
            super::phoenix_v1_audit_event_kind("deposit_funds"),
            "phoenix_v1.funds_deposit_audit"
        );
    }

    #[test]
    fn extracts_short_phoenix_instruction_data_prefix() {
        let data = phoenix_instruction_with_data(&[0x02_u8, 0xaa, 0xbb]);
        let prefix = super::phoenix_instruction_data_prefix(Some(data.as_str()));
        assert_eq!(prefix.as_deref(), Some("02aabb"));
    }

    #[test]
    fn decodes_phoenix_log_instruction_header_and_market_events() {
        let mut data = std::vec::Vec::new();
        data.push(15_u8);
        data.push(1_u8);
        data.push(2_u8);
        data.extend_from_slice(&11_u64.to_le_bytes());
        data.extend_from_slice(&(-2_i64).to_le_bytes());
        data.extend_from_slice(&33_u64.to_le_bytes());
        data.extend_from_slice(&[1_u8; 32]);
        data.extend_from_slice(&[2_u8; 32]);
        data.extend_from_slice(&2_u16.to_le_bytes());
        data.push(3_u8);
        data.extend_from_slice(&0_u16.to_le_bytes());
        data.extend_from_slice(&44_u64.to_le_bytes());
        data.extend_from_slice(&55_u128.to_le_bytes());
        data.extend_from_slice(&66_u64.to_le_bytes());
        data.extend_from_slice(&77_u64.to_le_bytes());
        data.push(6_u8);
        data.extend_from_slice(&1_u16.to_le_bytes());
        data.extend_from_slice(&88_u128.to_le_bytes());
        data.extend_from_slice(&99_u64.to_le_bytes());
        data.extend_from_slice(&111_u64.to_le_bytes());
        data.extend_from_slice(&7_u64.to_le_bytes());
        let encoded = phoenix_instruction_with_data(data.as_slice());
        let audit = super::decode_phoenix_log_instruction_data(Some(encoded.as_str()));
        let audit = match audit {
            Some(audit) => audit,
            None => panic!("expected phoenix log instruction audit"),
        };
        assert_eq!(audit.status.as_str(), "decoded_phoenix_log_instruction_header_unverified");
        assert_eq!(audit.log_instruction_tag, Some(15));
        assert_eq!(audit.header_event_tag, Some(1));
        assert_eq!(audit.current_instruction_tag, Some(2));
        assert_eq!(audit.current_instruction_tag_hex.as_deref(), Some("02"));
        assert_eq!(audit.current_instruction_name.as_deref(), Some("PlaceLimitOrder"));
        assert!(!audit.unknown_current_instruction_tag);
        assert_eq!(audit.sequence_number, Some(11));
        assert_eq!(audit.timestamp, Some(-2));
        assert_eq!(audit.slot, Some(33));
        assert_eq!(audit.total_events, Some(2));
        assert_eq!(audit.parsed_event_count, 2);
        assert_eq!(audit.event_type_names, vec!["Place".to_string(), "FillSummary".to_string()]);
        assert_eq!(audit.events[0].event_name.as_deref(), Some("Place"));
        assert_eq!(audit.events[1].event_name.as_deref(), Some("FillSummary"));
    }

    #[test]
    fn serializes_phoenix_log_instruction_audit_with_camel_case_fields() {
        let mut data = std::vec::Vec::new();
        data.push(15_u8);
        data.push(1_u8);
        data.push(2_u8);
        data.extend_from_slice(&11_u64.to_le_bytes());
        data.extend_from_slice(&22_i64.to_le_bytes());
        data.extend_from_slice(&33_u64.to_le_bytes());
        data.extend_from_slice(&[1_u8; 32]);
        data.extend_from_slice(&[2_u8; 32]);
        data.extend_from_slice(&0_u16.to_le_bytes());
        let encoded = phoenix_instruction_with_data(data.as_slice());
        let audit = super::decode_phoenix_log_instruction_data(Some(encoded.as_str()));
        let audit = match audit {
            Some(audit) => audit,
            None => panic!("expected phoenix log instruction audit"),
        };
        let value = super::phoenix_json_value(audit);
        assert!(value.get("currentInstructionName").is_some());
        assert!(value.get("current_instruction_name").is_none());
        assert_eq!(
            value.get("currentInstructionName").and_then(serde_json::Value::as_str),
            Some("PlaceLimitOrder")
        );
        assert_eq!(value.get("totalEvents").and_then(serde_json::Value::as_u64), Some(0_u64));
    }

    #[test]
    fn non_log_instruction_data_is_not_decoded_as_phoenix_log_header() {
        let data = phoenix_instruction_with_data(&[2_u8, 0_u8, 0_u8, 0_u8, 0_u8]);
        let audit = super::decode_phoenix_log_instruction_data(Some(data.as_str()));
        let audit = match audit {
            Some(audit) => audit,
            None => panic!("expected phoenix audit status"),
        };
        assert_eq!(audit.status.as_str(), "not_phoenix_log_instruction");
        assert_eq!(audit.log_instruction_tag, Some(2_u8));
        assert_eq!(audit.current_instruction_tag, None);
        assert_eq!(audit.current_instruction_tag_hex, None);
        assert_eq!(audit.current_instruction_name, None);
        assert!(!audit.unknown_current_instruction_tag);
        assert_eq!(audit.total_events, None);
        assert_eq!(audit.parsed_event_count, 0_usize);
        assert_eq!(audit.event_type_names.len(), 0_usize);
    }

    #[test]
    fn truncated_log_instruction_is_reported_separately() {
        let data = phoenix_instruction_with_data(&[15_u8, 1_u8, 2_u8]);
        let audit = super::decode_phoenix_log_instruction_data(Some(data.as_str()));
        let audit = match audit {
            Some(audit) => audit,
            None => panic!("expected phoenix truncated log status"),
        };
        assert_eq!(audit.status.as_str(), "phoenix_log_instruction_truncated");
        assert_eq!(audit.log_instruction_tag, Some(15_u8));
        assert_eq!(audit.current_instruction_tag, Some(2_u8));
        assert_eq!(audit.current_instruction_tag_hex.as_deref(), Some("02"));
        assert_eq!(audit.current_instruction_name.as_deref(), Some("PlaceLimitOrder"));
        assert!(!audit.unknown_current_instruction_tag);
        assert_eq!(audit.total_events, None);
        assert_eq!(audit.parsed_event_count, 0_usize);
    }

    #[test]
    fn maps_known_phoenix_current_instruction_tags_from_source_enum() {
        assert_eq!(super::phoenix_instruction_tag_name(14_u8), Some("RequestSeat"));
        assert_eq!(super::phoenix_instruction_tag_name(16_u8), Some("PlaceMultiplePostOnlyOrders"));
        assert_eq!(
            super::phoenix_instruction_tag_name(17_u8),
            Some("PlaceMultiplePostOnlyOrdersWithFreeFunds")
        );
        assert_eq!(super::phoenix_instruction_tag_name(106_u8), Some("EvictSeat"));
        assert_eq!(super::phoenix_instruction_tag_name(107_u8), Some("ForceCancelOrders"));
        assert_eq!(super::phoenix_instruction_tag_name(108_u8), Some("CollectFees"));
        assert_eq!(super::phoenix_instruction_tag_name(109_u8), Some("ChangeFeeRecipient"));
    }

    #[test]
    fn maps_phoenix_log_current_instruction_tag_16() {
        let mut data = std::vec::Vec::new();
        data.push(15_u8);
        data.push(1_u8);
        data.push(16_u8);
        data.extend_from_slice(&11_u64.to_le_bytes());
        data.extend_from_slice(&22_i64.to_le_bytes());
        data.extend_from_slice(&33_u64.to_le_bytes());
        data.extend_from_slice(&[1_u8; 32]);
        data.extend_from_slice(&[2_u8; 32]);
        data.extend_from_slice(&0_u16.to_le_bytes());
        let encoded = phoenix_instruction_with_data(data.as_slice());
        let audit = super::decode_phoenix_log_instruction_data(Some(encoded.as_str()));
        let audit = match audit {
            Some(audit) => audit,
            None => panic!("expected phoenix log instruction audit"),
        };
        assert_eq!(audit.current_instruction_tag, Some(16_u8));
        assert_eq!(audit.current_instruction_tag_hex.as_deref(), Some("10"));
        assert_eq!(audit.current_instruction_name.as_deref(), Some("PlaceMultiplePostOnlyOrders"));
        assert!(!audit.unknown_current_instruction_tag);
    }

    #[test]
    fn exposes_unknown_phoenix_current_instruction_tag_without_guessing_name() {
        let mut data = std::vec::Vec::new();
        data.push(15_u8);
        data.push(1_u8);
        data.push(250_u8);
        data.extend_from_slice(&11_u64.to_le_bytes());
        data.extend_from_slice(&22_i64.to_le_bytes());
        data.extend_from_slice(&33_u64.to_le_bytes());
        data.extend_from_slice(&[1_u8; 32]);
        data.extend_from_slice(&[2_u8; 32]);
        data.extend_from_slice(&0_u16.to_le_bytes());
        let encoded = phoenix_instruction_with_data(data.as_slice());
        let audit = super::decode_phoenix_log_instruction_data(Some(encoded.as_str()));
        let audit = match audit {
            Some(audit) => audit,
            None => panic!("expected phoenix log instruction audit"),
        };
        assert_eq!(audit.status.as_str(), "decoded_phoenix_log_instruction_header_unverified");
        assert_eq!(audit.current_instruction_tag, Some(250_u8));
        assert_eq!(audit.current_instruction_tag_hex.as_deref(), Some("fa"));
        assert_eq!(audit.current_instruction_name, None);
        assert!(audit.unknown_current_instruction_tag);
        let value = super::phoenix_json_value(audit);
        assert_eq!(
            value.get("currentInstructionTagHex").and_then(serde_json::Value::as_str),
            Some("fa")
        );
        assert_eq!(
            value.get("unknownCurrentInstructionTag").and_then(serde_json::Value::as_bool),
            Some(true)
        );
    }

    #[test]
    fn parses_phoenix_program_data_audit_record() {
        let record =
            super::parse_phoenix_program_data_record("Program data: AQIDBAUGBwgJCg==", Some("Log"));
        let record = match record {
            Some(record) => record,
            None => panic!("expected program data record"),
        };
        assert_eq!(record.instruction_log_name.as_deref(), Some("Log"));
        assert_eq!(record.decoded_len, Some(10));
        assert_eq!(record.discriminator_hex.as_deref(), Some("0102030405060708"));
        assert_eq!(record.payload_len, Some(2));
    }
}
