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

//! Meteora Dynamic Bonding Curve (DBC) transaction decoder.
//!
//! The local authority for this decoder is
//! `idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json`.
//! Instruction layouts are decoded conservatively from the IDL account order.
//! Business materialization is only enabled when the account and amount context
//! is strong enough for the existing local event model.

const DBC_ANCHOR_SELF_CPI_LOG_SELECTOR: [u8; 8] = [0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d];

const DBC_DISCRIMINATOR_LEGACY_CREATE_POOL: [u8; 8] =
    [0xe9, 0x92, 0xd1, 0x8e, 0xcf, 0x68, 0x40, 0xbc];
const DBC_DISCRIMINATOR_LEGACY_INITIALIZE_POOL: [u8; 8] =
    [0x5f, 0xb4, 0x0a, 0xac, 0x54, 0xae, 0xe8, 0x28];
const DBC_DISCRIMINATOR_LEGACY_LAUNCH_POOL: [u8; 8] =
    [0xa6, 0x77, 0xd1, 0xb6, 0xd6, 0x6d, 0x3a, 0xb5];

const DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_CREATOR_TRADING_FEE: [u8; 8] =
    [0x9a, 0xe4, 0xd7, 0xca, 0x85, 0x9b, 0xd6, 0x8a];
const DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_POOL_CREATION_FEE: [u8; 8] =
    [0x95, 0x6f, 0x95, 0x2c, 0x88, 0x40, 0xaf, 0x3e];
const DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_PROTOCOL_FEE: [u8; 8] =
    [0xba, 0xf4, 0x4b, 0xfb, 0xbc, 0x0d, 0x19, 0x21];
const DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_PROTOCOL_LIQUIDITY_MIGRATION_FEE: [u8; 8] =
    [0x51, 0xa8, 0x74, 0x1f, 0xa1, 0x56, 0x1b, 0x23];
const DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_TRADING_FEE: [u8; 8] =
    [0x1a, 0x53, 0x75, 0xf0, 0x5c, 0xca, 0x70, 0xfe];
const DBC_EVENT_DISCRIMINATOR_EVT_CLOSE_CLAIM_FEE_OPERATOR: [u8; 8] =
    [0x6f, 0x27, 0x25, 0x37, 0x6e, 0xd8, 0xc2, 0x17];
const DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CLAIM_FEE_OPERATOR: [u8; 8] =
    [0x15, 0x06, 0x99, 0x78, 0x44, 0x74, 0x1c, 0xb1];
const DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CONFIG: [u8; 8] =
    [0x83, 0xcf, 0xb4, 0xae, 0xb4, 0x49, 0xa5, 0x36];
const DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CONFIG_V2: [u8; 8] =
    [0xa3, 0x4a, 0x42, 0xbb, 0x77, 0xc3, 0x1a, 0x90];
const DBC_EVENT_DISCRIMINATOR_EVT_CREATE_METEORA_MIGRATION_METADATA: [u8; 8] =
    [0x63, 0xa7, 0x85, 0x3f, 0xd6, 0x8f, 0xaf, 0x8b];
const DBC_EVENT_DISCRIMINATOR_EVT_CREATOR_WITHDRAW_SURPLUS: [u8; 8] =
    [0x98, 0x49, 0x15, 0x0f, 0x42, 0x57, 0x35, 0x9d];
const DBC_EVENT_DISCRIMINATOR_EVT_CURVE_COMPLETE: [u8; 8] =
    [0xe5, 0xe7, 0x56, 0x54, 0x9c, 0x86, 0x4b, 0x18];
const DBC_EVENT_DISCRIMINATOR_EVT_INITIALIZE_POOL: [u8; 8] =
    [0xe4, 0x32, 0xf6, 0x55, 0xcb, 0x42, 0x86, 0x25];
const DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_CLAIM_POOL_CREATION_FEE: [u8; 8] =
    [0xae, 0xdf, 0x2c, 0x96, 0x91, 0x62, 0x59, 0xc3];
const DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_METADATA: [u8; 8] =
    [0xc8, 0x7f, 0x06, 0x37, 0x0d, 0x20, 0x08, 0x96];
const DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_WITHDRAW_MIGRATION_FEE: [u8; 8] =
    [0xb5, 0x69, 0x7f, 0x43, 0x08, 0xbb, 0x78, 0x39];
const DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_WITHDRAW_SURPLUS: [u8; 8] =
    [0xc3, 0x38, 0x98, 0x09, 0xe8, 0x48, 0x23, 0x16];
const DBC_EVENT_DISCRIMINATOR_EVT_SWAP: [u8; 8] = [0x1b, 0x3c, 0x15, 0xd5, 0x8a, 0xaa, 0xbb, 0x93];
const DBC_EVENT_DISCRIMINATOR_EVT_SWAP2: [u8; 8] = [0xbd, 0x42, 0x33, 0xa8, 0x26, 0x50, 0x75, 0x99];
const DBC_EVENT_DISCRIMINATOR_EVT_UPDATE_POOL_CREATOR: [u8; 8] =
    [0x6b, 0xe1, 0xa5, 0xed, 0x5b, 0x9e, 0xd5, 0xdc];
const DBC_EVENT_DISCRIMINATOR_EVT_VIRTUAL_POOL_METADATA: [u8; 8] =
    [0xbc, 0x12, 0x48, 0x4c, 0xc3, 0x5b, 0x26, 0x4a];
const DBC_EVENT_DISCRIMINATOR_EVT_WITHDRAW_LEFTOVER: [u8; 8] =
    [0xbf, 0xbd, 0x68, 0x8f, 0x6f, 0x9c, 0x5e, 0xe5];
const DBC_EVENT_DISCRIMINATOR_EVT_WITHDRAW_MIGRATION_FEE: [u8; 8] =
    [0x1a, 0xcb, 0x54, 0x55, 0xa1, 0x17, 0x64, 0xd6];

/// Decoded Meteora DBC create-pool event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDbcCreatePoolDecoded {
    /// 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,
    /// Fully qualified local event kind.
    pub event_kind: std::string::String,
    /// Optional pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional base mint.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional quote mint.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Optional config account.
    pub config_account: std::option::Option<std::string::String>,
    /// Optional creator.
    pub creator: std::option::Option<std::string::String>,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Meteora DBC swap event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDbcSwapDecoded {
    /// 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,
    /// Fully qualified local event kind.
    pub event_kind: std::string::String,
    /// Trade side relative to normalized base.
    pub trade_side: crate::SwapTradeSide,
    /// Optional pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional base mint.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional quote mint.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Meteora DBC instruction or Anchor event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDbcInstructionDecoded {
    /// 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,
    /// Fully qualified local event kind.
    pub event_kind: std::string::String,
    /// Optional pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional base mint.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional quote mint.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Optional related account.
    pub related_account: std::option::Option<std::string::String>,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Meteora DBC event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum MeteoraDbcDecodedEvent {
    /// Create pool / launch pool.
    CreatePool(MeteoraDbcCreatePoolDecoded),
    /// Swap / swap2.
    Swap(MeteoraDbcSwapDecoded),
    /// Generic instruction or Anchor event covered by the DBC IDL.
    Instruction(MeteoraDbcInstructionDecoded),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MeteoraDbcInstructionKind {
    CreatePool,
    Swap,
    PoolLifecycle,
    Fee,
    Admin,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MeteoraDbcArgLayout {
    None,
    FlagU8(&'static str),
    OneU64(&'static str),
    TwoU64(&'static str, &'static str),
    PermissionU128(&'static str),
    Swap,
    Swap2,
}

#[derive(Debug, Clone, Copy)]
struct MeteoraDbcInstructionSpec {
    name: &'static str,
    discriminator: [u8; 8],
    kind: MeteoraDbcInstructionKind,
    pool_index: std::option::Option<usize>,
    token_a_mint_index: std::option::Option<usize>,
    token_b_mint_index: std::option::Option<usize>,
    related_account_index: std::option::Option<usize>,
    actor_index: std::option::Option<usize>,
    arg_layout: MeteoraDbcArgLayout,
    skip_reason: std::option::Option<&'static str>,
}

const DBC_INSTRUCTION_SPECS: &[MeteoraDbcInstructionSpec] = &[
    MeteoraDbcInstructionSpec {
        name: "claim_creator_trading_fee",
        discriminator: [0x52, 0xdc, 0xfa, 0xbd, 0x03, 0x55, 0x6b, 0x2d],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(1),
        token_a_mint_index: Some(6),
        token_b_mint_index: Some(7),
        related_account_index: Some(2),
        actor_index: Some(8),
        arg_layout: MeteoraDbcArgLayout::TwoU64("maxBaseAmountRaw", "maxQuoteAmountRaw"),
        skip_reason: Some("instruction_args_are_claim_maximums_not_actual_fee_amounts"),
    },
    MeteoraDbcInstructionSpec {
        name: "claim_partner_pool_creation_fee",
        discriminator: [0xfa, 0xee, 0x1a, 0x04, 0x8b, 0x0a, 0x65, 0xf8],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(1),
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(2),
        actor_index: Some(3),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: Some("pool_creation_fee_instruction_without_amount_payload"),
    },
    MeteoraDbcInstructionSpec {
        name: "claim_protocol_fee",
        discriminator: [0xa5, 0xe4, 0x85, 0x30, 0x63, 0xf9, 0xff, 0x21],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(1),
        token_a_mint_index: Some(6),
        token_b_mint_index: Some(7),
        related_account_index: Some(2),
        actor_index: Some(8),
        arg_layout: MeteoraDbcArgLayout::TwoU64("maxBaseAmountRaw", "maxQuoteAmountRaw"),
        skip_reason: Some("instruction_args_are_claim_maximums_not_actual_fee_amounts"),
    },
    MeteoraDbcInstructionSpec {
        name: "claim_protocol_pool_creation_fee",
        discriminator: [0x72, 0xcd, 0x53, 0xbc, 0xf0, 0x99, 0x19, 0x36],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(1),
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(2),
        actor_index: Some(3),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: Some("pool_creation_fee_instruction_without_amount_payload"),
    },
    MeteoraDbcInstructionSpec {
        name: "claim_trading_fee",
        discriminator: [0x08, 0xec, 0x59, 0x31, 0x98, 0x7d, 0xb1, 0x51],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(1),
        token_a_mint_index: Some(6),
        token_b_mint_index: Some(7),
        related_account_index: Some(2),
        actor_index: Some(8),
        arg_layout: MeteoraDbcArgLayout::TwoU64("maxAmountARaw", "maxAmountBRaw"),
        skip_reason: Some("instruction_args_are_claim_maximums_not_actual_fee_amounts"),
    },
    MeteoraDbcInstructionSpec {
        name: "close_claim_protocol_fee_operator",
        discriminator: [0x08, 0x29, 0x57, 0x23, 0x50, 0x30, 0x79, 0x1a],
        kind: MeteoraDbcInstructionKind::Admin,
        pool_index: None,
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(0),
        actor_index: Some(1),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "close_operator_account",
        discriminator: [0xab, 0x09, 0xd5, 0x4a, 0x78, 0x17, 0x03, 0x1d],
        kind: MeteoraDbcInstructionKind::Admin,
        pool_index: None,
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(0),
        actor_index: Some(1),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "create_config",
        discriminator: [0xc9, 0xcf, 0xf3, 0x72, 0x4b, 0x6f, 0x2f, 0xbd],
        kind: MeteoraDbcInstructionKind::Admin,
        pool_index: None,
        token_a_mint_index: None,
        token_b_mint_index: Some(3),
        related_account_index: Some(0),
        actor_index: Some(4),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "create_locker",
        discriminator: [0xa7, 0x5a, 0x89, 0x9a, 0x4b, 0x2f, 0x11, 0x54],
        kind: MeteoraDbcInstructionKind::PoolLifecycle,
        pool_index: Some(1),
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(2),
        actor_index: Some(5),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "create_operator_account",
        discriminator: [0xdd, 0x40, 0xf6, 0x95, 0xf0, 0x99, 0xe5, 0xa3],
        kind: MeteoraDbcInstructionKind::Admin,
        pool_index: None,
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(0),
        actor_index: Some(1),
        arg_layout: MeteoraDbcArgLayout::PermissionU128("permissionRaw"),
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "create_partner_metadata",
        discriminator: [0xc0, 0xa8, 0xea, 0xbf, 0xbc, 0xe2, 0xe3, 0xff],
        kind: MeteoraDbcInstructionKind::Admin,
        pool_index: None,
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(0),
        actor_index: Some(1),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "create_virtual_pool_metadata",
        discriminator: [0x2d, 0x61, 0xbb, 0x67, 0xfe, 0x6d, 0x7c, 0x86],
        kind: MeteoraDbcInstructionKind::Admin,
        pool_index: Some(0),
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(1),
        actor_index: Some(2),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "creator_withdraw_surplus",
        discriminator: [0xa5, 0x03, 0x89, 0x07, 0x1c, 0x86, 0x4c, 0x50],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(1),
        token_a_mint_index: Some(4),
        token_b_mint_index: None,
        related_account_index: Some(2),
        actor_index: Some(5),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: Some("surplus_instruction_without_actual_amount_payload"),
    },
    MeteoraDbcInstructionSpec {
        name: "initialize_virtual_pool_with_spl_token",
        discriminator: [0x8c, 0x55, 0xd7, 0xb0, 0x66, 0x36, 0x68, 0x4f],
        kind: MeteoraDbcInstructionKind::CreatePool,
        pool_index: Some(5),
        token_a_mint_index: Some(3),
        token_b_mint_index: Some(4),
        related_account_index: Some(0),
        actor_index: Some(2),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "initialize_virtual_pool_with_token2022",
        discriminator: [0xa9, 0x76, 0x33, 0x4e, 0x91, 0x6e, 0xdc, 0x9b],
        kind: MeteoraDbcInstructionKind::CreatePool,
        pool_index: Some(5),
        token_a_mint_index: Some(3),
        token_b_mint_index: Some(4),
        related_account_index: Some(0),
        actor_index: Some(2),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "migrate_meteora_damm",
        discriminator: [0x1b, 0x01, 0x30, 0x16, 0xb4, 0x3f, 0x76, 0xd9],
        kind: MeteoraDbcInstructionKind::PoolLifecycle,
        pool_index: Some(0),
        token_a_mint_index: Some(7),
        token_b_mint_index: Some(8),
        related_account_index: Some(4),
        actor_index: Some(3),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "migrate_meteora_damm_claim_lp_token",
        discriminator: [0x8b, 0x85, 0x02, 0x1e, 0x5b, 0x91, 0x7f, 0x9a],
        kind: MeteoraDbcInstructionKind::PoolLifecycle,
        pool_index: Some(0),
        token_a_mint_index: Some(4),
        token_b_mint_index: Some(5),
        related_account_index: Some(3),
        actor_index: Some(2),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "migrate_meteora_damm_lock_lp_token",
        discriminator: [0xb1, 0x37, 0xee, 0x9d, 0xfb, 0x58, 0xa5, 0x2a],
        kind: MeteoraDbcInstructionKind::PoolLifecycle,
        pool_index: Some(0),
        token_a_mint_index: Some(4),
        token_b_mint_index: Some(5),
        related_account_index: Some(3),
        actor_index: Some(2),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "migration_damm_v2",
        discriminator: [0x9c, 0xa9, 0xe6, 0x67, 0x35, 0xe4, 0x50, 0x40],
        kind: MeteoraDbcInstructionKind::PoolLifecycle,
        pool_index: Some(0),
        token_a_mint_index: Some(13),
        token_b_mint_index: Some(14),
        related_account_index: Some(4),
        actor_index: Some(3),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "migration_damm_v2_create_metadata",
        discriminator: [0x6d, 0xbd, 0x13, 0x24, 0xc3, 0xb7, 0xde, 0x52],
        kind: MeteoraDbcInstructionKind::Admin,
        pool_index: Some(0),
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(1),
        actor_index: Some(2),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "migration_meteora_damm_create_metadata",
        discriminator: [0x2f, 0x5e, 0x7e, 0x73, 0xdd, 0xe2, 0xc2, 0x85],
        kind: MeteoraDbcInstructionKind::Admin,
        pool_index: Some(0),
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(1),
        actor_index: Some(2),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "partner_withdraw_surplus",
        discriminator: [0xa8, 0xad, 0x48, 0x64, 0xc9, 0x62, 0x26, 0x5c],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(1),
        token_a_mint_index: Some(4),
        token_b_mint_index: None,
        related_account_index: Some(2),
        actor_index: Some(5),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: Some("surplus_instruction_without_actual_amount_payload"),
    },
    MeteoraDbcInstructionSpec {
        name: "swap",
        discriminator: [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8],
        kind: MeteoraDbcInstructionKind::Swap,
        pool_index: Some(2),
        token_a_mint_index: Some(7),
        token_b_mint_index: Some(8),
        related_account_index: Some(1),
        actor_index: Some(9),
        arg_layout: MeteoraDbcArgLayout::Swap,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "swap2",
        discriminator: [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88],
        kind: MeteoraDbcInstructionKind::Swap,
        pool_index: Some(2),
        token_a_mint_index: Some(7),
        token_b_mint_index: Some(8),
        related_account_index: Some(1),
        actor_index: Some(9),
        arg_layout: MeteoraDbcArgLayout::Swap2,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "transfer_pool_creator",
        discriminator: [0x14, 0x07, 0xa9, 0x21, 0x3a, 0x93, 0xa6, 0x21],
        kind: MeteoraDbcInstructionKind::Admin,
        pool_index: Some(0),
        token_a_mint_index: None,
        token_b_mint_index: None,
        related_account_index: Some(3),
        actor_index: Some(2),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: None,
    },
    MeteoraDbcInstructionSpec {
        name: "withdraw_leftover",
        discriminator: [0x14, 0xc6, 0xca, 0xed, 0xeb, 0xf3, 0xb7, 0x42],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(2),
        token_a_mint_index: Some(5),
        token_b_mint_index: None,
        related_account_index: Some(6),
        actor_index: Some(7),
        arg_layout: MeteoraDbcArgLayout::None,
        skip_reason: Some("leftover_instruction_without_actual_amount_payload"),
    },
    MeteoraDbcInstructionSpec {
        name: "withdraw_migration_fee",
        discriminator: [0xed, 0x8e, 0x2d, 0x17, 0x81, 0x06, 0xde, 0xa2],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(2),
        token_a_mint_index: None,
        token_b_mint_index: Some(5),
        related_account_index: Some(6),
        actor_index: Some(7),
        arg_layout: MeteoraDbcArgLayout::FlagU8("flag"),
        skip_reason: Some("migration_fee_instruction_without_actual_amount_payload"),
    },
    MeteoraDbcInstructionSpec {
        name: "zap_protocol_fee",
        discriminator: [0xd5, 0x9b, 0xbb, 0x22, 0x38, 0xb6, 0x5b, 0xf0],
        kind: MeteoraDbcInstructionKind::Fee,
        pool_index: Some(2),
        token_a_mint_index: Some(4),
        token_b_mint_index: None,
        related_account_index: Some(5),
        actor_index: Some(6),
        arg_layout: MeteoraDbcArgLayout::OneU64("maxAmountRaw"),
        skip_reason: Some("instruction_arg_is_maximum_not_actual_fee_amount"),
    },
];

/// Meteora DBC decoder.
#[derive(Debug, Clone, Default)]
pub struct MeteoraDbcDecoder;

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

    /// Decodes one projected transaction into zero or more Meteora DBC events.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::MeteoraDbcDecodedEvent>, crate::Error> {
        let transaction_id_option = transaction.id;
        let transaction_id = match transaction_id_option {
            Some(transaction_id) => transaction_id,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "chain transaction '{}' has no internal id",
                    transaction.signature
                )));
            },
        };
        let transaction_json_result =
            serde_json::from_str::<serde_json::Value>(transaction.transaction_json.as_str());
        let transaction_json = match transaction_json_result {
            Ok(transaction_json) => transaction_json,
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot parse transaction_json for signature '{}': {}",
                    transaction.signature, error
                )));
            },
        };
        let log_messages = extract_log_messages(&transaction_json);
        let mut decoded_events = std::vec::Vec::new();
        for instruction in instructions {
            let program_id_option = &instruction.program_id;
            let program_id = match program_id_option {
                Some(program_id) => program_id,
                None => continue,
            };
            if program_id.as_str() != crate::METEORA_DBC_PROGRAM_ID {
                continue;
            }
            let instruction_id_option = instruction.id;
            let instruction_id = match instruction_id_option {
                Some(instruction_id) => instruction_id,
                None => continue,
            };
            let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
            let accounts = match accounts_result {
                Ok(accounts) => accounts,
                Err(error) => return Err(error),
            };
            let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
            let parsed_json = match parsed_json_result {
                Ok(parsed_json) => parsed_json,
                Err(error) => return Err(error),
            };
            let instruction_data_result =
                decode_instruction_data_json(instruction.data_json.as_ref());
            let instruction_data = match instruction_data_result {
                Ok(instruction_data) => instruction_data,
                Err(error) => return Err(error),
            };
            if instruction.parent_instruction_id.is_some() && instruction_data.is_none() {
                continue;
            }
            if let Some(anchor_event) = decode_anchor_self_cpi_event(
                transaction_id,
                instruction_id,
                transaction.signature.as_str(),
                program_id.as_str(),
                instruction,
                instruction_data.as_deref(),
            ) {
                decoded_events.push(anchor_event);
                continue;
            }
            let instruction_kind = classify_instruction_kind(
                parsed_json.as_ref(),
                instruction_data.as_deref(),
                &log_messages,
            );
            let spec = resolve_instruction_spec(
                parsed_json.as_ref(),
                instruction_data.as_deref(),
                &log_messages,
            );
            let spec = match spec {
                Some(spec) => spec,
                None => continue,
            };
            if instruction_kind == MeteoraDbcInstructionKind::CreatePool {
                let event = build_create_pool_event(
                    transaction_id,
                    instruction_id,
                    transaction,
                    instruction,
                    program_id.as_str(),
                    spec,
                    &accounts,
                    parsed_json.as_ref(),
                    instruction_data.as_deref(),
                    &log_messages,
                );
                decoded_events.push(event);
                continue;
            }
            if instruction_kind == MeteoraDbcInstructionKind::Swap {
                let decoded_amounts_result = crate::meteora_swap_amount_inference::infer_meteora_swap_amounts_from_inner_transfers(
                    transaction,
                    instructions,
                    instruction,
                    resolve_pool_account(parsed_json.as_ref(), &accounts, spec).as_deref(),
                );
                let decoded_amounts = match decoded_amounts_result {
                    Ok(decoded_amounts) => decoded_amounts,
                    Err(error) => return Err(error),
                };
                let event = build_swap_event(
                    transaction_id,
                    instruction_id,
                    transaction,
                    instruction,
                    program_id.as_str(),
                    spec,
                    &accounts,
                    parsed_json.as_ref(),
                    instruction_data.as_deref(),
                    &log_messages,
                    decoded_amounts,
                );
                decoded_events.push(event);
                continue;
            }
            let event = build_generic_instruction_event(
                transaction_id,
                instruction_id,
                transaction,
                instruction,
                program_id.as_str(),
                spec,
                &accounts,
                parsed_json.as_ref(),
                instruction_data.as_deref(),
                &log_messages,
            );
            decoded_events.push(event);
        }
        return Ok(decoded_events);
    }
}

fn build_create_pool_event(
    transaction_id: i64,
    instruction_id: i64,
    transaction: &crate::ChainTransactionDto,
    instruction: &crate::ChainInstructionDto,
    program_id: &str,
    spec: MeteoraDbcInstructionSpec,
    accounts: &[std::string::String],
    parsed_json: std::option::Option<&serde_json::Value>,
    instruction_data: std::option::Option<&[u8]>,
    log_messages: &[std::string::String],
) -> crate::MeteoraDbcDecodedEvent {
    let pool_account = resolve_pool_account(parsed_json, accounts, spec);
    let token_a_mint = resolve_token_a_mint(parsed_json, accounts, spec);
    let token_b_mint = resolve_token_b_mint(parsed_json, accounts, spec);
    let config_account = resolve_related_account(parsed_json, accounts, spec);
    let creator = resolve_actor(parsed_json, accounts, spec);
    let payload_json = serde_json::json!({
        "decoder": "meteora_dbc",
        "eventKind": "create_pool",
        "dataDiscriminatorHex": instruction_data.and_then(first_8_bytes_hex),
        "decodedInstructionName": spec.name,
        "sourceInstructionName": spec.name,
        "classifiedInstructionKind": "pool_create",
        "eventCategory": "pool_lifecycle",
        "eventLifecycleKind": "pool_create",
        "eventActionability": "catalog_candidate",
        "tradeCandidate": false,
        "candleCandidate": false,
        "nonTradeUseful": true,
        "skipTradeReason": "non_swap_instruction",
        "skipCandleReason": "non_swap_instruction",
        "signature": transaction.signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "parentInstructionId": instruction.parent_instruction_id,
        "stackHeight": instruction.stack_height,
        "accounts": accounts,
        "parsed": parsed_json,
        "logMessages": log_messages,
        "poolAccount": pool_account,
        "tokenAMint": token_a_mint,
        "tokenBMint": token_b_mint,
        "configAccount": config_account,
        "creator": creator,
        "actorWallet": creator
    });
    return crate::MeteoraDbcDecodedEvent::CreatePool(crate::MeteoraDbcCreatePoolDecoded {
        transaction_id,
        instruction_id,
        signature: transaction.signature.clone(),
        program_id: program_id.to_string(),
        event_kind: "meteora_dbc.create_pool".to_string(),
        pool_account,
        token_a_mint,
        token_b_mint,
        config_account,
        creator,
        payload_json,
    });
}

fn build_swap_event(
    transaction_id: i64,
    instruction_id: i64,
    transaction: &crate::ChainTransactionDto,
    instruction: &crate::ChainInstructionDto,
    program_id: &str,
    spec: MeteoraDbcInstructionSpec,
    accounts: &[std::string::String],
    parsed_json: std::option::Option<&serde_json::Value>,
    instruction_data: std::option::Option<&[u8]>,
    log_messages: &[std::string::String],
    decoded_amounts: std::option::Option<
        crate::meteora_swap_amount_inference::MeteoraSwapAmountInference,
    >,
) -> crate::MeteoraDbcDecodedEvent {
    let pool_account = resolve_pool_account(parsed_json, accounts, spec);
    let token_a_mint = resolve_token_a_mint(parsed_json, accounts, spec);
    let token_b_mint = resolve_token_b_mint(parsed_json, accounts, spec);
    let fallback_trade_side = infer_trade_side(log_messages);
    let trade_side = match decoded_amounts.as_ref() {
        Some(decoded_amounts) => decoded_amounts.trade_side,
        None => fallback_trade_side,
    };
    let has_direct_amount_payload = parsed_json_has_trade_amount_or_price_payload(parsed_json);
    let has_inferred_amount_payload = decoded_amounts.is_some();
    let fallback_base_vault = extract_account(accounts, 5);
    let fallback_quote_vault = extract_account(accounts, 6);
    let transfer_fallback_candidate = pool_account.is_some()
        && token_a_mint.is_some()
        && token_b_mint.is_some()
        && fallback_base_vault.is_some()
        && fallback_quote_vault.is_some();
    let has_trade_amount_payload =
        has_direct_amount_payload || has_inferred_amount_payload || transfer_fallback_candidate;
    let event_actionability = if has_trade_amount_payload {
        "trade_candidate"
    } else {
        "non_actionable_trade"
    };
    let effective_token_a_mint = match decoded_amounts.as_ref() {
        Some(decoded_amounts) => Some(decoded_amounts.base_token_mint.clone()),
        None => token_a_mint.clone(),
    };
    let effective_token_b_mint = match decoded_amounts.as_ref() {
        Some(decoded_amounts) => Some(decoded_amounts.quote_token_mint.clone()),
        None => token_b_mint.clone(),
    };
    let base_vault = match decoded_amounts.as_ref() {
        Some(decoded_amounts) => decoded_amounts.base_vault_address.clone(),
        None => fallback_base_vault.clone(),
    };
    let quote_vault = match decoded_amounts.as_ref() {
        Some(decoded_amounts) => decoded_amounts.quote_vault_address.clone(),
        None => fallback_quote_vault.clone(),
    };
    let base_amount_raw = match decoded_amounts.as_ref() {
        Some(decoded_amounts) => serde_json::Value::String(decoded_amounts.base_amount_raw.clone()),
        None => serde_json::Value::Null,
    };
    let quote_amount_raw = match decoded_amounts.as_ref() {
        Some(decoded_amounts) => {
            serde_json::Value::String(decoded_amounts.quote_amount_raw.clone())
        },
        None => serde_json::Value::Null,
    };
    let amount_resolution_source = if has_inferred_amount_payload {
        "flattened_cpi_pool_transfer_window"
    } else if has_direct_amount_payload {
        "parsed_payload"
    } else if transfer_fallback_candidate {
        "trade_aggregation_transfer_or_vault_delta_fallback"
    } else {
        "none"
    };
    let skip_reason = if has_trade_amount_payload {
        serde_json::Value::Null
    } else {
        serde_json::Value::String("swap_without_amount_or_transfer_context".to_string())
    };
    let mut instruction_args = decode_instruction_args(spec, instruction_data);
    if let Some(object) = instruction_args.as_object_mut() {
        object.insert(
            "amountResolutionSource".to_string(),
            serde_json::Value::String(amount_resolution_source.to_string()),
        );
    }
    let payload_json = serde_json::json!({
        "decoder": "meteora_dbc",
        "eventKind": spec.name,
        "dataDiscriminatorHex": instruction_data.and_then(first_8_bytes_hex),
        "decodedInstructionName": spec.name,
        "classifiedInstructionKind": "swap",
        "eventCategory": "trade",
        "eventLifecycleKind": "trade_swap",
        "eventActionability": event_actionability,
        "tradeCandidate": has_trade_amount_payload,
        "candleCandidate": has_trade_amount_payload,
        "nonTradeUseful": false,
        "materializationSkipReason": skip_reason,
        "skipTradeReason": skip_reason,
        "skipCandleReason": skip_reason,
        "signature": transaction.signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "parentInstructionId": instruction.parent_instruction_id,
        "stackHeight": instruction.stack_height,
        "accounts": accounts,
        "parsed": parsed_json,
        "logMessages": log_messages,
        "poolAccount": pool_account,
        "tokenAMint": effective_token_a_mint.clone(),
        "tokenBMint": effective_token_b_mint.clone(),
        "baseVault": base_vault,
        "quoteVault": quote_vault,
        "baseAmountRaw": base_amount_raw,
        "quoteAmountRaw": quote_amount_raw,
        "tradeSide": format!("{:?}", trade_side),
        "instructionArgs": instruction_args
    });
    return crate::MeteoraDbcDecodedEvent::Swap(crate::MeteoraDbcSwapDecoded {
        transaction_id,
        instruction_id,
        signature: transaction.signature.clone(),
        program_id: program_id.to_string(),
        event_kind: format!("meteora_dbc.{}", spec.name),
        trade_side,
        pool_account,
        token_a_mint: effective_token_a_mint,
        token_b_mint: effective_token_b_mint,
        payload_json,
    });
}

fn build_generic_instruction_event(
    transaction_id: i64,
    instruction_id: i64,
    transaction: &crate::ChainTransactionDto,
    instruction: &crate::ChainInstructionDto,
    program_id: &str,
    spec: MeteoraDbcInstructionSpec,
    accounts: &[std::string::String],
    parsed_json: std::option::Option<&serde_json::Value>,
    instruction_data: std::option::Option<&[u8]>,
    log_messages: &[std::string::String],
) -> crate::MeteoraDbcDecodedEvent {
    let pool_account = resolve_pool_account(parsed_json, accounts, spec);
    let token_a_mint = resolve_token_a_mint(parsed_json, accounts, spec);
    let token_b_mint = resolve_token_b_mint(parsed_json, accounts, spec);
    let related_account = resolve_related_account(parsed_json, accounts, spec);
    let actor = resolve_actor(parsed_json, accounts, spec);
    let event_kind = format!("meteora_dbc.{}", spec.name);
    let skip_key = skip_reason_key_for_kind(spec.kind);
    let skip_reason_value = match spec.skip_reason {
        Some(skip_reason) => serde_json::Value::String(skip_reason.to_string()),
        None => serde_json::Value::Null,
    };
    let instruction_args = decode_instruction_args(spec, instruction_data);
    let mut payload_json = serde_json::json!({
        "decoder": "meteora_dbc",
        "eventKind": spec.name,
        "dataDiscriminatorHex": instruction_data.and_then(first_8_bytes_hex),
        "decodedInstructionName": spec.name,
        "classifiedInstructionKind": crate::classify_dex_event_lifecycle_kind_code(event_kind.as_str()),
        "eventCategory": event_category_for_kind(spec.kind),
        "eventActionability": event_actionability_for_kind(spec.kind, spec.skip_reason),
        "tradeCandidate": false,
        "candleCandidate": false,
        "nonTradeUseful": spec.skip_reason.is_none(),
        "skipTradeReason": "non_swap_instruction",
        "skipCandleReason": "non_swap_instruction",
        "signature": transaction.signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "parentInstructionId": instruction.parent_instruction_id,
        "stackHeight": instruction.stack_height,
        "accounts": accounts,
        "parsed": parsed_json,
        "logMessages": log_messages,
        "poolAccount": pool_account,
        "tokenAMint": token_a_mint,
        "tokenBMint": token_b_mint,
        "relatedAccount": related_account,
        "configAccount": related_account,
        "actorWallet": actor,
        "adminAction": spec.name,
        "instructionArgs": instruction_args
    });
    if let Some(object) = payload_json.as_object_mut() {
        object.insert(skip_key.to_string(), skip_reason_value);
    }
    return crate::MeteoraDbcDecodedEvent::Instruction(crate::MeteoraDbcInstructionDecoded {
        transaction_id,
        instruction_id,
        signature: transaction.signature.clone(),
        program_id: program_id.to_string(),
        event_kind,
        pool_account,
        token_a_mint,
        token_b_mint,
        related_account,
        payload_json,
    });
}

fn resolve_instruction_spec(
    parsed_json: std::option::Option<&serde_json::Value>,
    instruction_data: std::option::Option<&[u8]>,
    log_messages: &[std::string::String],
) -> std::option::Option<MeteoraDbcInstructionSpec> {
    let data_spec = resolve_instruction_spec_from_data(instruction_data);
    if data_spec.is_some() {
        return data_spec;
    }
    if instruction_data_has_full_discriminator(instruction_data) {
        return None;
    }
    let parsed_instruction_name = extract_string_by_candidate_keys(
        parsed_json,
        &["instruction", "instructionName", "type", "name"],
    );
    if let Some(parsed_instruction_name) = parsed_instruction_name {
        let normalized = normalize_log_text(parsed_instruction_name.as_str());
        if let Some(spec) = resolve_instruction_spec_by_normalized_name(normalized.as_str()) {
            return Some(spec);
        }
    }
    let has_create_config = value_contains_any_key(
        parsed_json,
        &["poolConfig", "migrationQuoteThreshold", "curveConfig", "dbcConfig"],
    );
    if has_create_config {
        return resolve_instruction_spec_by_name("initialize_virtual_pool_with_spl_token");
    }
    if log_messages_contain_any_keyword(
        log_messages,
        &["create_pool", "createpool", "initialize_pool", "initializepool", "launch_pool"],
    ) {
        return resolve_instruction_spec_by_name("initialize_virtual_pool_with_spl_token");
    }
    if log_messages_contain_any_keyword(log_messages, &["swap2"]) {
        return resolve_instruction_spec_by_name("swap2");
    }
    if log_messages_contain_any_keyword(log_messages, &["swap"]) {
        return resolve_instruction_spec_by_name("swap");
    }
    return None;
}

fn resolve_instruction_spec_from_data(
    instruction_data: std::option::Option<&[u8]>,
) -> std::option::Option<MeteoraDbcInstructionSpec> {
    let instruction_data = match instruction_data {
        Some(instruction_data) => instruction_data,
        None => return None,
    };
    if instruction_data.len() < 8 {
        return None;
    }
    let discriminator = read_8_bytes(instruction_data, 0);
    if discriminator == DBC_DISCRIMINATOR_LEGACY_CREATE_POOL
        || discriminator == DBC_DISCRIMINATOR_LEGACY_INITIALIZE_POOL
        || discriminator == DBC_DISCRIMINATOR_LEGACY_LAUNCH_POOL
    {
        return resolve_instruction_spec_by_name("initialize_virtual_pool_with_spl_token");
    }
    for spec in DBC_INSTRUCTION_SPECS {
        if spec.discriminator == discriminator {
            return Some(*spec);
        }
    }
    return None;
}

fn resolve_instruction_spec_by_name(name: &str) -> std::option::Option<MeteoraDbcInstructionSpec> {
    for spec in DBC_INSTRUCTION_SPECS {
        if spec.name == name {
            return Some(*spec);
        }
    }
    return None;
}

fn resolve_instruction_spec_by_normalized_name(
    normalized_name: &str,
) -> std::option::Option<MeteoraDbcInstructionSpec> {
    for spec in DBC_INSTRUCTION_SPECS {
        let normalized_spec_name = normalize_log_text(spec.name);
        if normalized_spec_name == normalized_name {
            return Some(*spec);
        }
    }
    if normalized_name.contains("createpool")
        || normalized_name.contains("initializepool")
        || normalized_name.contains("launchpool")
    {
        return resolve_instruction_spec_by_name("initialize_virtual_pool_with_spl_token");
    }
    return None;
}

fn classify_instruction_kind(
    parsed_json: std::option::Option<&serde_json::Value>,
    instruction_data: std::option::Option<&[u8]>,
    log_messages: &[std::string::String],
) -> MeteoraDbcInstructionKind {
    let data_kind = classify_instruction_kind_from_data(instruction_data);
    if data_kind != MeteoraDbcInstructionKind::Unknown {
        return data_kind;
    }
    let spec = resolve_instruction_spec(parsed_json, instruction_data, log_messages);
    match spec {
        Some(spec) => return spec.kind,
        None => return MeteoraDbcInstructionKind::Unknown,
    }
}

fn classify_instruction_kind_from_data(
    instruction_data: std::option::Option<&[u8]>,
) -> MeteoraDbcInstructionKind {
    let spec = resolve_instruction_spec_from_data(instruction_data);
    match spec {
        Some(spec) => return spec.kind,
        None => return MeteoraDbcInstructionKind::Unknown,
    }
}

fn decode_anchor_self_cpi_event(
    transaction_id: i64,
    instruction_id: i64,
    signature: &str,
    program_id: &str,
    instruction: &crate::ChainInstructionDto,
    instruction_data: std::option::Option<&[u8]>,
) -> std::option::Option<crate::MeteoraDbcDecodedEvent> {
    let data = match instruction_data {
        Some(data) => data,
        None => return None,
    };
    if data.len() < 16 {
        return None;
    }
    let selector = read_8_bytes(data, 0);
    if selector != DBC_ANCHOR_SELF_CPI_LOG_SELECTOR {
        return None;
    }
    let discriminator = read_8_bytes(data, 8);
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_SWAP {
        return decode_anchor_swap_event(
            transaction_id,
            instruction_id,
            signature,
            program_id,
            instruction,
            data,
            false,
        );
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_SWAP2 {
        return decode_anchor_swap_event(
            transaction_id,
            instruction_id,
            signature,
            program_id,
            instruction,
            data,
            true,
        );
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_INITIALIZE_POOL {
        return decode_anchor_initialize_pool_event(
            transaction_id,
            instruction_id,
            signature,
            program_id,
            instruction,
            data,
        );
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CURVE_COMPLETE {
        return decode_anchor_curve_complete_event(
            transaction_id,
            instruction_id,
            signature,
            program_id,
            instruction,
            data,
        );
    }
    return decode_anchor_generic_event(
        transaction_id,
        instruction_id,
        signature,
        program_id,
        instruction,
        data,
        discriminator,
    );
}

fn decode_anchor_swap_event(
    transaction_id: i64,
    instruction_id: i64,
    signature: &str,
    program_id: &str,
    instruction: &crate::ChainInstructionDto,
    data: &[u8],
    is_swap2: bool,
) -> std::option::Option<crate::MeteoraDbcDecodedEvent> {
    if !is_swap2 && data.len() < 170 {
        return None;
    }
    if is_swap2 && data.len() < 195 {
        return None;
    }
    let pool_account = read_pubkey_string(data, 16);
    let config_account = read_pubkey_string(data, 48);
    let trade_direction = read_u8_string(data, 80);
    let has_referral = read_bool(data, 81);
    let event_kind = if is_swap2 {
        "meteora_dbc.evt_swap2_event"
    } else {
        "meteora_dbc.evt_swap_event"
    };
    let anchor_event_name = if is_swap2 { "EvtSwap2" } else { "EvtSwap" };
    let event_discriminator_hex = if is_swap2 { "bd4233a826507599" } else { "1b3c15d58aaabb93" };
    let input_amount = if is_swap2 { read_u64_string(data, 99) } else { read_u64_string(data, 98) };
    let output_amount =
        if is_swap2 { read_u64_string(data, 123) } else { read_u64_string(data, 106) };
    let trading_fee =
        if is_swap2 { read_u64_string(data, 147) } else { read_u64_string(data, 130) };
    let protocol_fee =
        if is_swap2 { read_u64_string(data, 155) } else { read_u64_string(data, 138) };
    let referral_fee =
        if is_swap2 { read_u64_string(data, 163) } else { read_u64_string(data, 146) };
    let payload_json = serde_json::json!({
        "decoder": "meteora_dbc",
        "eventKind": if is_swap2 { "evt_swap2_event" } else { "evt_swap_event" },
        "decodedInstructionName": "anchor_self_cpi_log",
        "classifiedInstructionKind": "swap",
        "eventCategory": "trade",
        "eventActionability": "decoded_only_missing_mint_context",
        "tradeCandidate": false,
        "candleCandidate": false,
        "nonTradeUseful": false,
        "skipTradeReason": "anchor_swap_event_without_mint_context",
        "skipCandleReason": "anchor_swap_event_without_mint_context",
        "materializationSkipReason": "anchor_swap_event_without_mint_context",
        "signature": signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "parentInstructionId": instruction.parent_instruction_id,
        "stackHeight": instruction.stack_height,
        "anchorSelfCpiLog": true,
        "anchorSelfCpiLogSelectorHex": "e445a52e51cb9a1d",
        "anchorEventName": anchor_event_name,
        "anchorEventDiscriminatorHex": event_discriminator_hex,
        "anchorEventPayloadSize": data.len().saturating_sub(16),
        "proofStatus": "local_idl_layout",
        "poolAccount": pool_account,
        "configAccount": config_account,
        "tradeDirectionRaw": trade_direction,
        "hasReferral": has_referral,
        "amountInRaw": input_amount,
        "amountOutRaw": output_amount,
        "tradingFeeRaw": trading_fee,
        "protocolFeeRaw": protocol_fee,
        "referralFeeRaw": referral_fee
    });
    return Some(crate::MeteoraDbcDecodedEvent::Instruction(
        crate::MeteoraDbcInstructionDecoded {
            transaction_id,
            instruction_id,
            signature: signature.to_string(),
            program_id: program_id.to_string(),
            event_kind: event_kind.to_string(),
            pool_account,
            token_a_mint: None,
            token_b_mint: None,
            related_account: config_account,
            payload_json,
        },
    ));
}

fn decode_anchor_initialize_pool_event(
    transaction_id: i64,
    instruction_id: i64,
    signature: &str,
    program_id: &str,
    instruction: &crate::ChainInstructionDto,
    data: &[u8],
) -> std::option::Option<crate::MeteoraDbcDecodedEvent> {
    if data.len() < 153 {
        return None;
    }
    let pool_account = read_pubkey_string(data, 16);
    let config_account = read_pubkey_string(data, 48);
    let creator = read_pubkey_string(data, 80);
    let base_mint = read_pubkey_string(data, 112);
    let pool_type = read_u8_string(data, 144);
    let activation_point = read_u64_string(data, 145);
    let payload_json = serde_json::json!({
        "decoder": "meteora_dbc",
        "eventKind": "evt_initialize_pool_event",
        "decodedInstructionName": "anchor_self_cpi_log",
        "classifiedInstructionKind": "pool_create",
        "eventCategory": "pool_lifecycle",
        "eventActionability": "catalog_candidate",
        "tradeCandidate": false,
        "candleCandidate": false,
        "nonTradeUseful": true,
        "skipTradeReason": "non_swap_anchor_event",
        "skipCandleReason": "non_swap_anchor_event",
        "signature": signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "parentInstructionId": instruction.parent_instruction_id,
        "stackHeight": instruction.stack_height,
        "anchorSelfCpiLog": true,
        "anchorSelfCpiLogSelectorHex": "e445a52e51cb9a1d",
        "anchorEventName": "EvtInitializePool",
        "anchorEventDiscriminatorHex": "e432f655cb428625",
        "anchorEventPayloadSize": data.len().saturating_sub(16),
        "proofStatus": "local_idl_layout",
        "poolAccount": pool_account,
        "configAccount": config_account,
        "creator": creator,
        "actorWallet": creator,
        "baseMint": base_mint,
        "tokenAMint": base_mint,
        "poolType": pool_type,
        "activationPointRaw": activation_point,
        "skipCatalogReason": "anchor_initialize_pool_event_without_quote_mint_context"
    });
    return Some(crate::MeteoraDbcDecodedEvent::Instruction(
        crate::MeteoraDbcInstructionDecoded {
            transaction_id,
            instruction_id,
            signature: signature.to_string(),
            program_id: program_id.to_string(),
            event_kind: "meteora_dbc.evt_initialize_pool_event".to_string(),
            pool_account,
            token_a_mint: base_mint,
            token_b_mint: None,
            related_account: config_account,
            payload_json,
        },
    ));
}

fn decode_anchor_curve_complete_event(
    transaction_id: i64,
    instruction_id: i64,
    signature: &str,
    program_id: &str,
    instruction: &crate::ChainInstructionDto,
    data: &[u8],
) -> std::option::Option<crate::MeteoraDbcDecodedEvent> {
    if data.len() < 96 {
        return None;
    }
    let pool_account = read_pubkey_string(data, 16);
    let config_account = read_pubkey_string(data, 48);
    let base_reserve = read_u64_string(data, 80);
    let quote_reserve = read_u64_string(data, 88);
    let payload_json = serde_json::json!({
        "decoder": "meteora_dbc",
        "eventKind": "evt_curve_complete_event",
        "decodedInstructionName": "anchor_self_cpi_log",
        "classifiedInstructionKind": "migration",
        "eventCategory": "pool_lifecycle",
        "eventActionability": "lifecycle_candidate",
        "tradeCandidate": false,
        "candleCandidate": false,
        "nonTradeUseful": true,
        "skipTradeReason": "non_swap_anchor_event",
        "skipCandleReason": "non_swap_anchor_event",
        "signature": signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "parentInstructionId": instruction.parent_instruction_id,
        "stackHeight": instruction.stack_height,
        "anchorSelfCpiLog": true,
        "anchorSelfCpiLogSelectorHex": "e445a52e51cb9a1d",
        "anchorEventName": "EvtCurveComplete",
        "anchorEventDiscriminatorHex": "e5e756549c864b18",
        "anchorEventPayloadSize": data.len().saturating_sub(16),
        "proofStatus": "local_idl_layout",
        "poolAccount": pool_account,
        "configAccount": config_account,
        "baseReserveRaw": base_reserve,
        "quoteReserveRaw": quote_reserve
    });
    return Some(crate::MeteoraDbcDecodedEvent::Instruction(
        crate::MeteoraDbcInstructionDecoded {
            transaction_id,
            instruction_id,
            signature: signature.to_string(),
            program_id: program_id.to_string(),
            event_kind: "meteora_dbc.evt_curve_complete_event".to_string(),
            pool_account,
            token_a_mint: None,
            token_b_mint: None,
            related_account: config_account,
            payload_json,
        },
    ));
}

fn decode_anchor_generic_event(
    transaction_id: i64,
    instruction_id: i64,
    signature: &str,
    program_id: &str,
    instruction: &crate::ChainInstructionDto,
    data: &[u8],
    discriminator: [u8; 8],
) -> std::option::Option<crate::MeteoraDbcDecodedEvent> {
    let metadata = resolve_anchor_event_metadata(discriminator);
    let metadata = match metadata {
        Some(metadata) => metadata,
        None => return None,
    };
    let pool_account = read_pubkey_string(data, 16);
    let related_account = read_pubkey_string(data, 48);
    let mut payload_json = serde_json::json!({
        "decoder": "meteora_dbc",
        "eventKind": metadata.0,
        "decodedInstructionName": "anchor_self_cpi_log",
        "classifiedInstructionKind": metadata.3,
        "eventCategory": metadata.4,
        "eventActionability": "decoded_only_anchor_event",
        "tradeCandidate": false,
        "candleCandidate": false,
        "nonTradeUseful": false,
        "skipTradeReason": "non_swap_anchor_event",
        "skipCandleReason": "non_swap_anchor_event",
        "signature": signature,
        "instructionId": instruction_id,
        "instructionIndex": instruction.instruction_index,
        "innerInstructionIndex": instruction.inner_instruction_index,
        "parentInstructionId": instruction.parent_instruction_id,
        "stackHeight": instruction.stack_height,
        "anchorSelfCpiLog": true,
        "anchorSelfCpiLogSelectorHex": "e445a52e51cb9a1d",
        "anchorEventName": metadata.1,
        "anchorEventDiscriminatorHex": metadata.2,
        "anchorEventPayloadSize": data.len().saturating_sub(16),
        "proofStatus": "local_idl_discriminator_generic_payload",
        "poolAccount": pool_account,
        "relatedAccount": related_account
    });
    if let Some(object) = payload_json.as_object_mut() {
        object.insert(metadata.5.to_string(), serde_json::Value::String(metadata.6.to_string()));
    }
    return Some(crate::MeteoraDbcDecodedEvent::Instruction(
        crate::MeteoraDbcInstructionDecoded {
            transaction_id,
            instruction_id,
            signature: signature.to_string(),
            program_id: program_id.to_string(),
            event_kind: format!("meteora_dbc.{}", metadata.0),
            pool_account,
            token_a_mint: None,
            token_b_mint: None,
            related_account,
            payload_json,
        },
    ));
}

fn resolve_anchor_event_metadata(
    discriminator: [u8; 8],
) -> std::option::Option<(
    &'static str,
    &'static str,
    &'static str,
    &'static str,
    &'static str,
    &'static str,
    &'static str,
)> {
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_CREATOR_TRADING_FEE {
        return Some((
            "evt_claim_creator_trading_fee_event",
            "EvtClaimCreatorTradingFee",
            "9ae4d7ca859bd68a",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_fee_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_POOL_CREATION_FEE {
        return Some((
            "evt_claim_pool_creation_fee_event",
            "EvtClaimPoolCreationFee",
            "956f952c8840af3e",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_fee_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_PROTOCOL_FEE {
        return Some((
            "evt_claim_protocol_fee_event",
            "EvtClaimProtocolFee",
            "baf44bfbbc0d1921",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_fee_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_PROTOCOL_LIQUIDITY_MIGRATION_FEE {
        return Some((
            "evt_claim_protocol_liquidity_migration_fee_event",
            "EvtClaimProtocolLiquidityMigrationFee",
            "51a8741fa1561b23",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_fee_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_TRADING_FEE {
        return Some((
            "evt_claim_trading_fee_event",
            "EvtClaimTradingFee",
            "1a5375f05cca70fe",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_fee_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CLOSE_CLAIM_FEE_OPERATOR {
        return Some((
            "evt_close_claim_fee_operator_event",
            "EvtCloseClaimFeeOperator",
            "6f2725376ed8c217",
            "admin",
            "admin",
            "skipAdminReason",
            "anchor_operator_event_decoded_only",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CLAIM_FEE_OPERATOR {
        return Some((
            "evt_create_claim_fee_operator_event",
            "EvtCreateClaimFeeOperator",
            "1506997844741cb1",
            "admin",
            "admin",
            "skipAdminReason",
            "anchor_operator_event_decoded_only",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CONFIG {
        return Some((
            "evt_create_config_event",
            "EvtCreateConfig",
            "83cfb4aeb449a536",
            "admin",
            "admin",
            "skipAdminReason",
            "anchor_config_event_generic_payload",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CONFIG_V2 {
        return Some((
            "evt_create_config_v2_event",
            "EvtCreateConfigV2",
            "a34a42bb77c31a90",
            "admin",
            "admin",
            "skipAdminReason",
            "anchor_config_v2_event_generic_payload",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CREATE_METEORA_MIGRATION_METADATA {
        return Some((
            "evt_create_meteora_migration_metadata_event",
            "EvtCreateMeteoraMigrationMetadata",
            "63a7853fd68faf8b",
            "migration",
            "pool_lifecycle",
            "skipLifecycleReason",
            "anchor_migration_metadata_event_decoded_only",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_CREATOR_WITHDRAW_SURPLUS {
        return Some((
            "evt_creator_withdraw_surplus_event",
            "EvtCreatorWithdrawSurplus",
            "9849150f4257359d",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_surplus_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_CLAIM_POOL_CREATION_FEE {
        return Some((
            "evt_partner_claim_pool_creation_fee_event",
            "EvtPartnerClaimPoolCreationFee",
            "aedf2c96916259c3",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_fee_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_METADATA {
        return Some((
            "evt_partner_metadata_event",
            "EvtPartnerMetadata",
            "c87f06370d200896",
            "admin",
            "admin",
            "skipAdminReason",
            "anchor_metadata_event_decoded_only",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_WITHDRAW_MIGRATION_FEE {
        return Some((
            "evt_partner_withdraw_migration_fee_event",
            "EvtPartnerWithdrawMigrationFee",
            "b5697f4308bb7839",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_fee_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_WITHDRAW_SURPLUS {
        return Some((
            "evt_partner_withdraw_surplus_event",
            "EvtPartnerWithdrawSurplus",
            "c3389809e8482316",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_surplus_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_UPDATE_POOL_CREATOR {
        return Some((
            "evt_update_pool_creator_event",
            "EvtUpdatePoolCreator",
            "6be1a5ed5b9ed5dc",
            "admin",
            "admin",
            "skipAdminReason",
            "anchor_update_pool_creator_event_decoded_only",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_VIRTUAL_POOL_METADATA {
        return Some((
            "evt_virtual_pool_metadata_event",
            "EvtVirtualPoolMetadata",
            "bc12484cc35b264a",
            "admin",
            "admin",
            "skipAdminReason",
            "anchor_metadata_event_decoded_only",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_WITHDRAW_LEFTOVER {
        return Some((
            "evt_withdraw_leftover_event",
            "EvtWithdrawLeftover",
            "bfbd688f6f9c5ee5",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_leftover_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    if discriminator == DBC_EVENT_DISCRIMINATOR_EVT_WITHDRAW_MIGRATION_FEE {
        return Some((
            "evt_withdraw_migration_fee_event",
            "EvtWithdrawMigrationFee",
            "1acb5455a11764d6",
            "fee",
            "fee",
            "skipFeeReason",
            "anchor_fee_event_decoded_only_sibling_instruction_materializes_or_explains_amount",
        ));
    }
    return None;
}

fn resolve_pool_account(
    parsed_json: std::option::Option<&serde_json::Value>,
    accounts: &[std::string::String],
    spec: MeteoraDbcInstructionSpec,
) -> std::option::Option<std::string::String> {
    return extract_string_by_candidate_keys(
        parsed_json,
        &["pool", "poolAccount", "poolState", "virtualPool", "poolKey"],
    )
    .or_else(|| return extract_account_option(accounts, spec.pool_index));
}

fn resolve_token_a_mint(
    parsed_json: std::option::Option<&serde_json::Value>,
    accounts: &[std::string::String],
    spec: MeteoraDbcInstructionSpec,
) -> std::option::Option<std::string::String> {
    return extract_string_by_candidate_keys(
        parsed_json,
        &["baseMint", "tokenAMint", "mintA", "token0Mint", "mint0"],
    )
    .or_else(|| return extract_account_option(accounts, spec.token_a_mint_index));
}

fn resolve_token_b_mint(
    parsed_json: std::option::Option<&serde_json::Value>,
    accounts: &[std::string::String],
    spec: MeteoraDbcInstructionSpec,
) -> std::option::Option<std::string::String> {
    return extract_string_by_candidate_keys(
        parsed_json,
        &["quoteMint", "tokenBMint", "mintB", "token1Mint", "mint1"],
    )
    .or_else(|| return extract_account_option(accounts, spec.token_b_mint_index));
}

fn resolve_related_account(
    parsed_json: std::option::Option<&serde_json::Value>,
    accounts: &[std::string::String],
    spec: MeteoraDbcInstructionSpec,
) -> std::option::Option<std::string::String> {
    return extract_string_by_candidate_keys(
        parsed_json,
        &["poolConfig", "config", "dbcConfig", "curveConfig", "metadata", "operator"],
    )
    .or_else(|| return extract_account_option(accounts, spec.related_account_index));
}

fn resolve_actor(
    parsed_json: std::option::Option<&serde_json::Value>,
    accounts: &[std::string::String],
    spec: MeteoraDbcInstructionSpec,
) -> std::option::Option<std::string::String> {
    return extract_string_by_candidate_keys(
        parsed_json,
        &["creator", "poolCreator", "owner", "user", "payer", "operator"],
    )
    .or_else(|| return extract_account_option(accounts, spec.actor_index));
}

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

fn decode_instruction_args(
    spec: MeteoraDbcInstructionSpec,
    instruction_data: std::option::Option<&[u8]>,
) -> serde_json::Value {
    let data = match instruction_data {
        Some(data) => data,
        None => return serde_json::json!({}),
    };
    let payload = if data.len() >= 8 { &data[8..] } else { &[] };
    match spec.arg_layout {
        MeteoraDbcArgLayout::None => return serde_json::json!({}),
        MeteoraDbcArgLayout::FlagU8(name) => {
            return json_object_with_one_optional_string(name, read_u8_string(payload, 0));
        },
        MeteoraDbcArgLayout::OneU64(name) => {
            return json_object_with_one_optional_string(name, read_u64_string(payload, 0));
        },
        MeteoraDbcArgLayout::TwoU64(name_a, name_b) => {
            let mut value = serde_json::json!({});
            if let Some(object) = value.as_object_mut() {
                object.insert(
                    name_a.to_string(),
                    optional_string_to_json(read_u64_string(payload, 0)),
                );
                object.insert(
                    name_b.to_string(),
                    optional_string_to_json(read_u64_string(payload, 8)),
                );
            }
            return value;
        },
        MeteoraDbcArgLayout::PermissionU128(name) => {
            return json_object_with_one_optional_string(name, read_u128_string(payload, 0));
        },
        MeteoraDbcArgLayout::Swap => {
            return serde_json::json!({
                "amountInRaw": read_u64_string(payload, 0),
                "minimumAmountOutRaw": read_u64_string(payload, 8)
            });
        },
        MeteoraDbcArgLayout::Swap2 => {
            return serde_json::json!({
                "amount0Raw": read_u64_string(payload, 0),
                "amount1Raw": read_u64_string(payload, 8),
                "swapMode": read_u8_string(payload, 16)
            });
        },
    }
}

fn json_object_with_one_optional_string(
    key: &str,
    value: std::option::Option<std::string::String>,
) -> serde_json::Value {
    let mut json_value = serde_json::json!({});
    if let Some(object) = json_value.as_object_mut() {
        object.insert(key.to_string(), optional_string_to_json(value));
    }
    return json_value;
}

fn optional_string_to_json(value: std::option::Option<std::string::String>) -> serde_json::Value {
    match value {
        Some(value) => return serde_json::Value::String(value),
        None => return serde_json::Value::Null,
    }
}

fn event_category_for_kind(kind: MeteoraDbcInstructionKind) -> &'static str {
    match kind {
        MeteoraDbcInstructionKind::CreatePool => return "pool_lifecycle",
        MeteoraDbcInstructionKind::Swap => return "trade",
        MeteoraDbcInstructionKind::PoolLifecycle => return "pool_lifecycle",
        MeteoraDbcInstructionKind::Fee => return "fee",
        MeteoraDbcInstructionKind::Admin => return "admin",
        MeteoraDbcInstructionKind::Unknown => return "unknown",
    }
}

fn event_actionability_for_kind(
    kind: MeteoraDbcInstructionKind,
    skip_reason: std::option::Option<&str>,
) -> &'static str {
    if skip_reason.is_some() {
        return "decoded_only_with_explicit_skip_reason";
    }
    match kind {
        MeteoraDbcInstructionKind::PoolLifecycle => return "lifecycle_candidate",
        MeteoraDbcInstructionKind::Fee => return "fee_candidate",
        MeteoraDbcInstructionKind::Admin => return "admin_candidate",
        _ => return "decoded_only",
    }
}

fn skip_reason_key_for_kind(kind: MeteoraDbcInstructionKind) -> &'static str {
    match kind {
        MeteoraDbcInstructionKind::Fee => return "skipFeeReason",
        MeteoraDbcInstructionKind::Admin => return "skipAdminReason",
        MeteoraDbcInstructionKind::PoolLifecycle => return "skipLifecycleReason",
        _ => return "materializationSkipReason",
    }
}

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

fn log_messages_contain_any_keyword(
    log_messages: &[std::string::String],
    keywords: &[&str],
) -> bool {
    for keyword in keywords {
        if log_messages_contain_keyword(log_messages, keyword) {
            return true;
        }
    }
    return false;
}

fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
    let keyword_normalized = normalize_log_text(keyword);
    for log_message in log_messages {
        let log_normalized = normalize_log_text(log_message.as_str());
        if log_normalized.contains(keyword_normalized.as_str()) {
            return true;
        }
    }
    return false;
}

fn normalize_log_text(value: &str) -> std::string::String {
    let mut normalized = std::string::String::new();
    for character in value.chars() {
        if character.is_ascii_alphanumeric() {
            normalized.push(character.to_ascii_lowercase());
        }
    }
    return normalized;
}

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

fn parse_optional_parsed_json(
    parsed_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
    let parsed_json = match parsed_json {
        Some(parsed_json) => parsed_json,
        None => return Ok(None),
    };
    let value_result = serde_json::from_str::<serde_json::Value>(parsed_json.as_str());
    match value_result {
        Ok(value) => return Ok(Some(value)),
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse instruction parsed_json '{}': {}",
                parsed_json, error
            )));
        },
    }
}

fn extract_string_by_candidate_keys(
    value: std::option::Option<&serde_json::Value>,
    candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
    let value = match value {
        Some(value) => value,
        None => return None,
    };
    return extract_string_by_candidate_keys_inner(value, candidate_keys);
}

fn extract_string_by_candidate_keys_inner(
    value: &serde_json::Value,
    candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
    if let Some(object) = value.as_object() {
        for candidate_key in candidate_keys {
            let direct_option = object.get(*candidate_key);
            if let Some(direct) = direct_option {
                let direct_text_option = direct.as_str();
                if let Some(direct_text) = direct_text_option {
                    return Some(direct_text.to_string());
                }
            }
        }
        for nested_value in object.values() {
            let nested_result =
                extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
            if nested_result.is_some() {
                return nested_result;
            }
        }
        return None;
    }
    if let Some(array) = value.as_array() {
        for nested_value in array {
            let nested_result =
                extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
            if nested_result.is_some() {
                return nested_result;
            }
        }
    }
    return None;
}

fn value_contains_any_key(
    value: std::option::Option<&serde_json::Value>,
    candidate_keys: &[&str],
) -> bool {
    let value = match value {
        Some(value) => value,
        None => return false,
    };
    return value_contains_any_key_inner(value, candidate_keys);
}

fn value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
    if let Some(object) = value.as_object() {
        for candidate_key in candidate_keys {
            if object.contains_key(*candidate_key) {
                return true;
            }
        }
        for nested_value in object.values() {
            if value_contains_any_key_inner(nested_value, candidate_keys) {
                return true;
            }
        }
        return false;
    }
    if let Some(array) = value.as_array() {
        for nested_value in array {
            if value_contains_any_key_inner(nested_value, candidate_keys) {
                return true;
            }
        }
    }
    return false;
}

fn decode_instruction_data_json(
    data_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<std::vec::Vec<u8>>, crate::Error> {
    let data_json = match data_json {
        Some(data_json) => data_json,
        None => return Ok(None),
    };
    let parsed_result = serde_json::from_str::<serde_json::Value>(data_json.as_str());
    let parsed = match parsed_result {
        Ok(parsed) => parsed,
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse Meteora DBC data_json: {}",
                error
            )));
        },
    };
    if let serde_json::Value::String(base58_text) = parsed {
        let decoded_result = bs58::decode(base58_text.as_str()).into_vec();
        match decoded_result {
            Ok(decoded) => return Ok(Some(decoded)),
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot decode Meteora DBC instruction data from base58: {}",
                    error
                )));
            },
        }
    }
    return Ok(None);
}

fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
    if bytes.len() < 8 {
        return None;
    }
    return Some(format!(
        "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
    ));
}

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

fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSide {
    if log_messages_contain_keyword(log_messages, "buy") {
        return crate::SwapTradeSide::BuyBase;
    }
    if log_messages_contain_keyword(log_messages, "sell") {
        return crate::SwapTradeSide::SellBase;
    }
    return crate::SwapTradeSide::Unknown;
}

fn instruction_data_has_full_discriminator(instruction_data: std::option::Option<&[u8]>) -> bool {
    let instruction_data = match instruction_data {
        Some(instruction_data) => instruction_data,
        None => return false,
    };
    return instruction_data.len() >= 8;
}

fn parsed_json_has_trade_amount_or_price_payload(
    parsed_json: std::option::Option<&serde_json::Value>,
) -> bool {
    let parsed_json = match parsed_json {
        Some(parsed_json) => parsed_json,
        None => return false,
    };
    return json_value_contains_any_trade_amount_or_price_key(parsed_json);
}

fn json_value_contains_any_trade_amount_or_price_key(value: &serde_json::Value) -> bool {
    match value {
        serde_json::Value::Object(map) => {
            for key in map.keys() {
                let normalized = normalize_log_text(key.as_str());
                if is_trade_amount_or_price_key(normalized.as_str()) {
                    return true;
                }
            }
            for child in map.values() {
                if json_value_contains_any_trade_amount_or_price_key(child) {
                    return true;
                }
            }
            return false;
        },
        serde_json::Value::Array(values) => {
            for child in values {
                if json_value_contains_any_trade_amount_or_price_key(child) {
                    return true;
                }
            }
            return false;
        },
        _ => return false,
    }
}

fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
    return normalized_key == "baseamountraw"
        || normalized_key == "quoteamountraw"
        || normalized_key == "baseamount"
        || normalized_key == "quoteamount"
        || normalized_key == "amountin"
        || normalized_key == "amountout"
        || normalized_key == "tokenain"
        || normalized_key == "tokenaout"
        || normalized_key == "tokenbin"
        || normalized_key == "tokenbout"
        || normalized_key == "inputamount"
        || normalized_key == "outputamount"
        || normalized_key == "swapamount"
        || normalized_key == "price"
        || normalized_key == "pricequoteperbase";
}

fn read_8_bytes(data: &[u8], offset: usize) -> [u8; 8] {
    let mut bytes = [0_u8; 8];
    if data.len() < offset + 8 {
        return bytes;
    }
    let mut index = 0_usize;
    while index < 8 {
        bytes[index] = data[offset + index];
        index += 1;
    }
    return bytes;
}

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

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

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

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

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

#[cfg(test)]
mod tests {
    fn make_create_transaction() -> crate::ChainTransactionDto {
        let mut dto = crate::ChainTransactionDto::new(
            "sig-meteora_dbc-create-1".to_string(),
            Some(888001),
            Some(1779300001),
            Some("helius_primary_http".to_string()),
            Some("0".to_string()),
            None,
            None,
            serde_json::json!({
                "slot": 888001,
                "meta": {
                    "logMessages": [
                        "Program log: Instruction: InitializeVirtualPoolWithSplToken"
                    ]
                },
                "transaction": {
                    "message": {
                        "instructions": []
                    }
                }
            })
            .to_string(),
        );
        dto.id = Some(301);
        return dto;
    }

    fn make_create_instruction() -> crate::ChainInstructionDto {
        let mut dto = crate::ChainInstructionDto::new(
            301,
            None,
            0,
            None,
            Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
            Some("meteora_dbc".to_string()),
            Some(1),
            serde_json::json!([
                "DbcConfig111",
                "DbcAuthority111",
                "DbcCreator111",
                "DbcTokenA111",
                crate::WSOL_MINT_ID,
                "DbcPool111"
            ])
            .to_string(),
            None,
            None,
            Some(
                serde_json::json!({
                    "info": {
                        "pool": "DbcPool111",
                        "baseMint": "DbcTokenA111",
                        "quoteMint": crate::WSOL_MINT_ID,
                        "poolConfig": "DbcConfig111",
                        "creator": "DbcCreator111"
                    }
                })
                .to_string(),
            ),
        );
        dto.id = Some(302);
        return dto;
    }

    fn make_swap_transaction() -> crate::ChainTransactionDto {
        let mut dto = crate::ChainTransactionDto::new(
            "sig-meteora_dbc-swap-1".to_string(),
            Some(888002),
            Some(1779300002),
            Some("helius_primary_http".to_string()),
            Some("0".to_string()),
            None,
            None,
            serde_json::json!({
                "slot": 888002,
                "meta": {
                    "logMessages": [
                        "Program log: Instruction: Swap2"
                    ]
                },
                "transaction": {
                    "message": {
                        "instructions": []
                    }
                }
            })
            .to_string(),
        );
        dto.id = Some(303);
        return dto;
    }

    fn make_swap_instruction() -> crate::ChainInstructionDto {
        let mut dto = crate::ChainInstructionDto::new(
            303,
            None,
            0,
            None,
            Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
            Some("meteora_dbc".to_string()),
            Some(1),
            serde_json::json!([
                "DbcAuthority111",
                "DbcConfigSwap111",
                "DbcPoolSwap111",
                "InputTokenAccount111",
                "OutputTokenAccount111",
                "DbcBaseVault111",
                "DbcQuoteVault111",
                "DbcSwapTokenA111",
                crate::WSOL_MINT_ID,
                "DbcPayer111"
            ])
            .to_string(),
            None,
            None,
            Some(
                serde_json::json!({
                    "info": {
                        "pool": "DbcPoolSwap111",
                        "baseMint": "DbcSwapTokenA111",
                        "quoteMint": crate::WSOL_MINT_ID
                    }
                })
                .to_string(),
            ),
        );
        dto.id = Some(304);
        return dto;
    }

    #[test]
    fn meteora_dbc_create_pool_is_detected() {
        let decoder = crate::MeteoraDbcDecoder::new();
        let transaction = make_create_transaction();
        let instructions = vec![make_create_instruction()];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::MeteoraDbcDecodedEvent::CreatePool(event) => {
                assert_eq!(event.transaction_id, 301);
                assert_eq!(event.instruction_id, 302);
                assert_eq!(event.pool_account, Some("DbcPool111".to_string()));
                assert_eq!(event.token_a_mint, Some("DbcTokenA111".to_string()));
                assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
                assert_eq!(event.config_account, Some("DbcConfig111".to_string()));
            },
            crate::MeteoraDbcDecodedEvent::Swap(_) => {
                panic!("unexpected swap event")
            },
            crate::MeteoraDbcDecodedEvent::Instruction(_) => {
                panic!("unexpected generic event")
            },
        }
    }

    #[test]
    fn meteora_dbc_swap_is_detected() {
        let decoder = crate::MeteoraDbcDecoder::new();
        let transaction = make_swap_transaction();
        let instructions = vec![make_swap_instruction()];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::MeteoraDbcDecodedEvent::Swap(event) => {
                assert_eq!(event.transaction_id, 303);
                assert_eq!(event.instruction_id, 304);
                assert_eq!(event.pool_account, Some("DbcPoolSwap111".to_string()));
                assert_eq!(event.token_a_mint, Some("DbcSwapTokenA111".to_string()));
                assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
            },
            crate::MeteoraDbcDecodedEvent::CreatePool(_) => {
                panic!("unexpected create event")
            },
            crate::MeteoraDbcDecodedEvent::Instruction(_) => {
                panic!("unexpected generic event")
            },
        }
    }

    #[test]
    fn meteora_dbc_quote_mint_alone_does_not_trigger_create_pool() {
        let decoder = crate::MeteoraDbcDecoder::new();
        let transaction = make_swap_transaction();
        let mut instruction = make_swap_instruction();
        instruction.parsed_json = Some(
            serde_json::json!({
                "info": {
                    "quoteMint": crate::WSOL_MINT_ID
                }
            })
            .to_string(),
        );
        let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::MeteoraDbcDecodedEvent::Swap(_) => {},
            crate::MeteoraDbcDecodedEvent::CreatePool(_) => {
                panic!("unexpected create event")
            },
            crate::MeteoraDbcDecodedEvent::Instruction(_) => {
                panic!("unexpected generic event")
            },
        }
    }

    #[test]
    fn meteora_dbc_pool_config_triggers_create_pool() {
        let decoder = crate::MeteoraDbcDecoder::new();
        let transaction = make_create_transaction();
        let instruction = make_create_instruction();
        let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::MeteoraDbcDecodedEvent::CreatePool(_) => {},
            crate::MeteoraDbcDecodedEvent::Swap(_) => {
                panic!("unexpected swap event")
            },
            crate::MeteoraDbcDecodedEvent::Instruction(_) => {
                panic!("unexpected generic event")
            },
        }
    }

    #[test]
    fn meteora_dbc_swap2_discriminator_is_detected() {
        let data = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01];
        let kind = super::classify_instruction_kind_from_data(Some(&data));
        assert_eq!(kind, super::MeteoraDbcInstructionKind::Swap);
    }

    #[test]
    fn meteora_dbc_create_config_discriminator_is_decoded() {
        let data = [0xc9, 0xcf, 0xf3, 0x72, 0x4b, 0x6f, 0x2f, 0xbd, 0x01];
        let kind = super::classify_instruction_kind_from_data(Some(&data));
        assert_eq!(kind, super::MeteoraDbcInstructionKind::Admin);
    }

    #[test]
    fn meteora_dbc_unknown_data_discriminator_does_not_fallback_to_global_swap_logs() {
        let data = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x01];
        let logs = vec!["Program log: Instruction: Swap2".to_string()];
        let kind = super::classify_instruction_kind(None, Some(&data), &logs);
        assert_eq!(kind, super::MeteoraDbcInstructionKind::Unknown);
    }

    #[test]
    fn meteora_dbc_inner_swap2_instruction_with_data_is_not_skipped() {
        let decoder = crate::MeteoraDbcDecoder::new();
        let transaction = make_swap_transaction();
        let mut instruction = make_swap_instruction();
        instruction.parent_instruction_id = Some(300);
        instruction.data_json = Some(format!(
            "\"{}\"",
            bs58::encode(&[0x41_u8, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01]).into_string()
        ));
        let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::MeteoraDbcDecodedEvent::Swap(event) => {
                assert_eq!(event.pool_account, Some("DbcPoolSwap111".to_string()));
            },
            crate::MeteoraDbcDecodedEvent::CreatePool(_) => panic!("unexpected create event"),
            crate::MeteoraDbcDecodedEvent::Instruction(_) => panic!("unexpected generic event"),
        }
    }

    #[test]
    fn meteora_dbc_all_instruction_specs_resolve_from_discriminators() {
        for spec in super::DBC_INSTRUCTION_SPECS {
            let resolved = super::resolve_instruction_spec_from_data(Some(&spec.discriminator[..]));
            let resolved = match resolved {
                Some(resolved) => resolved,
                None => panic!("instruction spec discriminator must resolve: {}", spec.name),
            };
            assert_eq!(resolved.name, spec.name);
            assert_eq!(resolved.kind, spec.kind);
        }
    }

    #[test]
    fn meteora_dbc_all_generic_anchor_event_discriminators_are_mapped() {
        let discriminators = [
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_CREATOR_TRADING_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_POOL_CREATION_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_PROTOCOL_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_PROTOCOL_LIQUIDITY_MIGRATION_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_TRADING_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLOSE_CLAIM_FEE_OPERATOR,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CLAIM_FEE_OPERATOR,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CONFIG,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CONFIG_V2,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATE_METEORA_MIGRATION_METADATA,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATOR_WITHDRAW_SURPLUS,
            super::DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_CLAIM_POOL_CREATION_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_METADATA,
            super::DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_WITHDRAW_MIGRATION_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_WITHDRAW_SURPLUS,
            super::DBC_EVENT_DISCRIMINATOR_EVT_UPDATE_POOL_CREATOR,
            super::DBC_EVENT_DISCRIMINATOR_EVT_VIRTUAL_POOL_METADATA,
            super::DBC_EVENT_DISCRIMINATOR_EVT_WITHDRAW_LEFTOVER,
            super::DBC_EVENT_DISCRIMINATOR_EVT_WITHDRAW_MIGRATION_FEE,
        ];
        for discriminator in discriminators {
            let metadata = super::resolve_anchor_event_metadata(discriminator);
            assert!(metadata.is_some());
        }
    }

    #[test]
    fn meteora_dbc_swap2_with_complete_context_is_trade_fallback_candidate() {
        let decoder = crate::MeteoraDbcDecoder::new();
        let transaction = make_swap_transaction();
        let mut instruction = make_swap_instruction();
        instruction.data_json = Some(format!(
            "\"{}\"",
            bs58::encode(&[0x41_u8, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01]).into_string()
        ));
        let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::MeteoraDbcDecodedEvent::Swap(event) => {
                let payload = event.payload_json.as_object();
                let payload = match payload {
                    Some(payload) => payload,
                    None => panic!("payload must be an object"),
                };
                assert_eq!(
                    payload.get("tradeCandidate").and_then(|value| return value.as_bool()),
                    Some(true)
                );
                assert_eq!(
                    payload.get("candleCandidate").and_then(|value| return value.as_bool()),
                    Some(true)
                );
                assert_eq!(
                    payload
                        .get("instructionArgs")
                        .and_then(|value| return value.as_object())
                        .and_then(|object| return object.get("amountResolutionSource"))
                        .and_then(|value| return value.as_str()),
                    Some("trade_aggregation_transfer_or_vault_delta_fallback")
                );
            },
            crate::MeteoraDbcDecodedEvent::CreatePool(_) => panic!("unexpected create event"),
            crate::MeteoraDbcDecodedEvent::Instruction(_) => panic!("unexpected generic event"),
        }
    }

    fn make_synthetic_transaction(signature: &str, id: i64, instruction_name: &str) -> crate::ChainTransactionDto {
        let mut dto = crate::ChainTransactionDto::new(
            signature.to_string(),
            Some(888100),
            Some(1779300100),
            Some("helius_primary_http".to_string()),
            Some("0".to_string()),
            None,
            None,
            serde_json::json!({
                "slot": 888100,
                "meta": {
                    "logMessages": [
                        format!("Program log: Instruction: {}", instruction_name)
                    ]
                },
                "transaction": {
                    "message": {
                        "instructions": []
                    }
                }
            })
            .to_string(),
        );
        dto.id = Some(id);
        return dto;
    }

    fn synthetic_accounts() -> std::vec::Vec<std::string::String> {
        let mut accounts = std::vec::Vec::new();
        for index in 0..24 {
            accounts.push(format!("SyntheticDbcAccount{:02}", index));
        }
        return accounts;
    }

    fn synthetic_payload_for_arg_layout(layout: super::MeteoraDbcArgLayout) -> std::vec::Vec<u8> {
        let mut payload = std::vec::Vec::new();
        match layout {
            super::MeteoraDbcArgLayout::None => {},
            super::MeteoraDbcArgLayout::FlagU8(_) => payload.push(1),
            super::MeteoraDbcArgLayout::OneU64(_) => {
                payload.extend_from_slice(&123_u64.to_le_bytes());
            },
            super::MeteoraDbcArgLayout::TwoU64(_, _) => {
                payload.extend_from_slice(&123_u64.to_le_bytes());
                payload.extend_from_slice(&456_u64.to_le_bytes());
            },
            super::MeteoraDbcArgLayout::PermissionU128(_) => {
                payload.extend_from_slice(&789_u128.to_le_bytes());
            },
            super::MeteoraDbcArgLayout::Swap => {
                payload.extend_from_slice(&1000_u64.to_le_bytes());
                payload.extend_from_slice(&900_u64.to_le_bytes());
            },
            super::MeteoraDbcArgLayout::Swap2 => {
                payload.extend_from_slice(&1000_u64.to_le_bytes());
                payload.extend_from_slice(&900_u64.to_le_bytes());
                payload.push(1);
            },
        }
        return payload;
    }

    fn make_synthetic_instruction_for_spec(
        transaction_id: i64,
        instruction_id: i64,
        spec: super::MeteoraDbcInstructionSpec,
    ) -> crate::ChainInstructionDto {
        let mut data = std::vec::Vec::new();
        data.extend_from_slice(&spec.discriminator);
        data.extend_from_slice(&synthetic_payload_for_arg_layout(spec.arg_layout));
        let mut dto = crate::ChainInstructionDto::new(
            transaction_id,
            None,
            0,
            None,
            Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
            Some("meteora_dbc".to_string()),
            Some(1),
            serde_json::json!(synthetic_accounts()).to_string(),
            Some(format!("\"{}\"", bs58::encode(data).into_string())),
            None,
            None,
        );
        dto.id = Some(instruction_id);
        return dto;
    }

    fn synthetic_anchor_event_data(discriminator: [u8; 8]) -> std::vec::Vec<u8> {
        let mut data = std::vec::Vec::new();
        data.extend_from_slice(&super::DBC_ANCHOR_SELF_CPI_LOG_SELECTOR);
        data.extend_from_slice(&discriminator);
        data.extend_from_slice(&[1_u8; 32]);
        data.extend_from_slice(&[2_u8; 32]);
        data.extend_from_slice(&[0_u8; 32]);
        return data;
    }

    fn make_synthetic_anchor_instruction(
        transaction_id: i64,
        instruction_id: i64,
        discriminator: [u8; 8],
    ) -> crate::ChainInstructionDto {
        let data = synthetic_anchor_event_data(discriminator);
        let mut dto = crate::ChainInstructionDto::new(
            transaction_id,
            None,
            1,
            Some(0),
            Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
            Some("meteora_dbc".to_string()),
            Some(2),
            serde_json::json!(synthetic_accounts()).to_string(),
            Some(format!("\"{}\"", bs58::encode(data).into_string())),
            None,
            None,
        );
        dto.id = Some(instruction_id);
        return dto;
    }

    #[test]
    fn meteora_dbc_synthetic_non_trade_instruction_specs_decode_to_expected_event_kinds() {
        let decoder = crate::MeteoraDbcDecoder::new();
        for spec in super::DBC_INSTRUCTION_SPECS {
            if spec.kind == super::MeteoraDbcInstructionKind::Swap
                || spec.kind == super::MeteoraDbcInstructionKind::CreatePool
            {
                continue;
            }
            let transaction = make_synthetic_transaction(
                format!("sig-meteora-dbc-synthetic-{}", spec.name).as_str(),
                400,
                spec.name,
            );
            let instruction = make_synthetic_instruction_for_spec(400, 401, *spec);
            let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
            let decoded = match decoded_result {
                Ok(decoded) => decoded,
                Err(error) => panic!("decode must succeed for {}: {}", spec.name, error),
            };
            assert_eq!(decoded.len(), 1, "{} must decode once", spec.name);
            match &decoded[0] {
                crate::MeteoraDbcDecodedEvent::Instruction(event) => {
                    assert_eq!(event.event_kind, format!("meteora_dbc.{}", spec.name));
                    let payload = match event.payload_json.as_object() {
                        Some(payload) => payload,
                        None => panic!("{} payload must be object", spec.name),
                    };
                    assert_eq!(
                        payload.get("decodedInstructionName").and_then(|value| return value.as_str()),
                        Some(spec.name)
                    );
                    assert_eq!(
                        payload.get("eventCategory").and_then(|value| return value.as_str()),
                        Some(super::event_category_for_kind(spec.kind))
                    );
                    assert_eq!(
                        payload.get("tradeCandidate").and_then(|value| return value.as_bool()),
                        Some(false)
                    );
                    assert_eq!(
                        payload.get("candleCandidate").and_then(|value| return value.as_bool()),
                        Some(false)
                    );
                    if let Some(skip_reason) = spec.skip_reason {
                        let skip_key = super::skip_reason_key_for_kind(spec.kind);
                        assert_eq!(
                            payload.get(skip_key).and_then(|value| return value.as_str()),
                            Some(skip_reason)
                        );
                    }
                },
                crate::MeteoraDbcDecodedEvent::CreatePool(_) => {
                    panic!("{} unexpectedly decoded as create pool", spec.name)
                },
                crate::MeteoraDbcDecodedEvent::Swap(_) => {
                    panic!("{} unexpectedly decoded as swap", spec.name)
                },
            }
        }
    }

    #[test]
    fn meteora_dbc_synthetic_generic_anchor_events_decode_with_explicit_skip_reasons() {
        let decoder = crate::MeteoraDbcDecoder::new();
        let discriminators = [
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_CREATOR_TRADING_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_POOL_CREATION_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_PROTOCOL_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_PROTOCOL_LIQUIDITY_MIGRATION_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLAIM_TRADING_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CLOSE_CLAIM_FEE_OPERATOR,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CLAIM_FEE_OPERATOR,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CONFIG,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATE_CONFIG_V2,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATE_METEORA_MIGRATION_METADATA,
            super::DBC_EVENT_DISCRIMINATOR_EVT_CREATOR_WITHDRAW_SURPLUS,
            super::DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_CLAIM_POOL_CREATION_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_METADATA,
            super::DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_WITHDRAW_MIGRATION_FEE,
            super::DBC_EVENT_DISCRIMINATOR_EVT_PARTNER_WITHDRAW_SURPLUS,
            super::DBC_EVENT_DISCRIMINATOR_EVT_UPDATE_POOL_CREATOR,
            super::DBC_EVENT_DISCRIMINATOR_EVT_VIRTUAL_POOL_METADATA,
            super::DBC_EVENT_DISCRIMINATOR_EVT_WITHDRAW_LEFTOVER,
            super::DBC_EVENT_DISCRIMINATOR_EVT_WITHDRAW_MIGRATION_FEE,
        ];
        for discriminator in discriminators {
            let metadata = match super::resolve_anchor_event_metadata(discriminator) {
                Some(metadata) => metadata,
                None => panic!("anchor metadata must resolve"),
            };
            let transaction = make_synthetic_transaction(
                format!("sig-meteora-dbc-anchor-{}", metadata.0).as_str(),
                410,
                "anchor_self_cpi_log",
            );
            let instruction = make_synthetic_anchor_instruction(410, 411, discriminator);
            let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
            let decoded = match decoded_result {
                Ok(decoded) => decoded,
                Err(error) => panic!("anchor decode must succeed for {}: {}", metadata.0, error),
            };
            assert_eq!(decoded.len(), 1, "{} must decode once", metadata.0);
            match &decoded[0] {
                crate::MeteoraDbcDecodedEvent::Instruction(event) => {
                    assert_eq!(event.event_kind, format!("meteora_dbc.{}", metadata.0));
                    let payload = match event.payload_json.as_object() {
                        Some(payload) => payload,
                        None => panic!("{} payload must be object", metadata.0),
                    };
                    assert_eq!(
                        payload.get("anchorEventName").and_then(|value| return value.as_str()),
                        Some(metadata.1)
                    );
                    assert_eq!(
                        payload.get("eventActionability").and_then(|value| return value.as_str()),
                        Some("decoded_only_anchor_event")
                    );
                    assert_eq!(
                        payload.get(metadata.5).and_then(|value| return value.as_str()),
                        Some(metadata.6)
                    );
                },
                crate::MeteoraDbcDecodedEvent::CreatePool(_) => {
                    panic!("{} unexpectedly decoded as create pool", metadata.0)
                },
                crate::MeteoraDbcDecodedEvent::Swap(_) => {
                    panic!("{} unexpectedly decoded as swap", metadata.0)
                },
            }
        }
    }

}
