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

//! PumpSwap AMM transaction decoder.

const PUMP_SWAP_BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
const PUMP_SWAP_SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
const PUMP_SWAP_BUY_EXACT_QUOTE_IN_DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
const PUMP_SWAP_ADMIN_SET_COIN_CREATOR_DISCRIMINATOR: [u8; 8] =
    [242, 40, 117, 145, 73, 96, 105, 104];
const PUMP_SWAP_ADMIN_UPDATE_TOKEN_INCENTIVES_DISCRIMINATOR: [u8; 8] =
    [209, 11, 115, 87, 213, 23, 124, 204];
const PUMP_SWAP_CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
const PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_DISCRIMINATOR: [u8; 8] = [16, 4, 71, 28, 204, 1, 40, 27];
const PUMP_SWAP_CLOSE_USER_VOLUME_ACCUMULATOR_DISCRIMINATOR: [u8; 8] =
    [249, 69, 164, 218, 150, 103, 84, 138];
const PUMP_SWAP_COLLECT_COIN_CREATOR_FEE_DISCRIMINATOR: [u8; 8] =
    [160, 57, 89, 42, 181, 139, 43, 66];
const PUMP_SWAP_CREATE_CONFIG_DISCRIMINATOR: [u8; 8] = [201, 207, 243, 114, 75, 111, 47, 189];
const PUMP_SWAP_CREATE_POOL_DISCRIMINATOR: [u8; 8] = [233, 146, 209, 142, 207, 104, 64, 188];
const PUMP_SWAP_DEPOSIT_DISCRIMINATOR: [u8; 8] = [242, 35, 198, 137, 82, 225, 242, 182];
const PUMP_SWAP_DISABLE_DISCRIMINATOR: [u8; 8] = [185, 173, 187, 90, 216, 15, 238, 233];
const PUMP_SWAP_EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
const PUMP_SWAP_INIT_USER_VOLUME_ACCUMULATOR_DISCRIMINATOR: [u8; 8] =
    [94, 6, 202, 115, 255, 96, 232, 183];
const PUMP_SWAP_MIGRATE_POOL_COIN_CREATOR_DISCRIMINATOR: [u8; 8] =
    [208, 8, 159, 4, 74, 175, 16, 58];
const PUMP_SWAP_SET_COIN_CREATOR_DISCRIMINATOR: [u8; 8] = [210, 149, 128, 45, 188, 58, 78, 175];
const PUMP_SWAP_SET_RESERVED_FEE_RECIPIENTS_DISCRIMINATOR: [u8; 8] =
    [111, 172, 162, 232, 114, 89, 213, 142];
const PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_DISCRIMINATOR: [u8; 8] =
    [86, 31, 192, 87, 163, 87, 79, 238];
const PUMP_SWAP_TOGGLE_CASHBACK_ENABLED_DISCRIMINATOR: [u8; 8] =
    [115, 103, 224, 255, 189, 89, 86, 195];
const PUMP_SWAP_TOGGLE_MAYHEM_MODE_DISCRIMINATOR: [u8; 8] = [1, 9, 111, 208, 100, 31, 255, 163];
const PUMP_SWAP_TRANSFER_CREATOR_FEES_TO_PUMP_DISCRIMINATOR: [u8; 8] =
    [139, 52, 134, 85, 228, 229, 108, 241];
const PUMP_SWAP_UPDATE_ADMIN_DISCRIMINATOR: [u8; 8] = [161, 176, 40, 213, 60, 184, 179, 228];
const PUMP_SWAP_UPDATE_FEE_CONFIG_DISCRIMINATOR: [u8; 8] = [104, 184, 103, 242, 88, 151, 107, 20];
const PUMP_SWAP_WITHDRAW_DISCRIMINATOR: [u8; 8] = [183, 18, 70, 156, 148, 109, 161, 34];
const PUMP_SWAP_TRANSFER_CREATOR_FEES_TO_PUMP_V2_DISCRIMINATOR: [u8; 8] =
    [1, 33, 78, 185, 33, 67, 44, 92];
const PUMP_SWAP_UPDATE_BUYBACK_CONFIG_DISCRIMINATOR: [u8; 8] =
    [251, 224, 171, 146, 160, 26, 113, 233];
const PUMP_SWAP_SET_RESERVED_FEE_RECIPIENT_DISCRIMINATOR: [u8; 8] =
    [207, 189, 178, 71, 167, 122, 68, 180];
const PUMP_SWAP_ANCHOR_SELF_CPI_LOG_DISCRIMINATOR: [u8; 8] = [228, 69, 165, 46, 81, 203, 154, 29];
const PUMP_SWAP_BUY_EVENT_DISCRIMINATOR: [u8; 8] = [103, 244, 82, 31, 44, 245, 119, 119];
const PUMP_SWAP_ADMIN_SET_COIN_CREATOR_EVENT_DISCRIMINATOR: [u8; 8] =
    [45, 220, 93, 24, 25, 97, 172, 104];
const PUMP_SWAP_ADMIN_UPDATE_TOKEN_INCENTIVES_EVENT_DISCRIMINATOR: [u8; 8] =
    [147, 250, 108, 120, 247, 29, 67, 222];
const PUMP_SWAP_CLAIM_CASHBACK_EVENT_DISCRIMINATOR: [u8; 8] = [226, 214, 246, 33, 7, 242, 147, 229];
const PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_EVENT_DISCRIMINATOR: [u8; 8] =
    [79, 172, 246, 49, 205, 91, 206, 232];
const PUMP_SWAP_CLOSE_USER_VOLUME_ACCUMULATOR_EVENT_DISCRIMINATOR: [u8; 8] =
    [146, 159, 189, 172, 146, 88, 56, 244];
const PUMP_SWAP_COLLECT_COIN_CREATOR_FEE_EVENT_DISCRIMINATOR: [u8; 8] =
    [232, 245, 194, 238, 234, 218, 58, 89];
const PUMP_SWAP_CREATE_CONFIG_EVENT_DISCRIMINATOR: [u8; 8] = [107, 52, 89, 129, 55, 226, 81, 22];
const PUMP_SWAP_CREATE_POOL_EVENT_DISCRIMINATOR: [u8; 8] = [177, 49, 12, 210, 160, 118, 167, 116];
const PUMP_SWAP_DEPOSIT_EVENT_DISCRIMINATOR: [u8; 8] = [120, 248, 61, 83, 31, 142, 107, 144];
const PUMP_SWAP_DISABLE_EVENT_DISCRIMINATOR: [u8; 8] = [107, 253, 193, 76, 228, 202, 27, 104];
const PUMP_SWAP_EXTEND_ACCOUNT_EVENT_DISCRIMINATOR: [u8; 8] = [97, 97, 215, 144, 93, 146, 22, 124];
const PUMP_SWAP_INIT_USER_VOLUME_ACCUMULATOR_EVENT_DISCRIMINATOR: [u8; 8] =
    [134, 36, 13, 72, 232, 101, 130, 216];
const PUMP_SWAP_MIGRATE_POOL_COIN_CREATOR_EVENT_DISCRIMINATOR: [u8; 8] =
    [170, 221, 82, 199, 147, 165, 247, 46];
const PUMP_SWAP_RESERVED_FEE_RECIPIENTS_EVENT_DISCRIMINATOR: [u8; 8] =
    [43, 188, 250, 18, 221, 75, 187, 95];
const PUMP_SWAP_SELL_EVENT_DISCRIMINATOR: [u8; 8] = [62, 47, 55, 10, 165, 3, 220, 42];
const PUMP_SWAP_SET_BONDING_CURVE_COIN_CREATOR_EVENT_DISCRIMINATOR: [u8; 8] =
    [242, 231, 235, 102, 65, 99, 189, 211];
const PUMP_SWAP_SET_METAPLEX_COIN_CREATOR_EVENT_DISCRIMINATOR: [u8; 8] =
    [150, 107, 199, 123, 124, 207, 102, 228];
const PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_DISCRIMINATOR: [u8; 8] =
    [197, 122, 167, 124, 116, 81, 91, 255];
const PUMP_SWAP_UPDATE_ADMIN_EVENT_DISCRIMINATOR: [u8; 8] = [225, 152, 171, 87, 246, 63, 66, 234];
const PUMP_SWAP_UPDATE_FEE_CONFIG_EVENT_DISCRIMINATOR: [u8; 8] =
    [90, 23, 65, 35, 62, 244, 188, 208];
const PUMP_SWAP_WITHDRAW_EVENT_DISCRIMINATOR: [u8; 8] = [22, 9, 133, 26, 160, 44, 71, 192];

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PumpSwapInstructionKind {
    AdminSetCoinCreator,
    AdminUpdateTokenIncentives,
    Buy,
    BuyExactQuoteIn,
    ClaimCashback,
    ClaimTokenIncentives,
    CloseUserVolumeAccumulator,
    CollectCoinCreatorFee,
    CreateConfig,
    CreatePool,
    Deposit,
    Disable,
    ExtendAccount,
    InitUserVolumeAccumulator,
    MigratePoolCoinCreator,
    Sell,
    SetCoinCreator,
    SetReservedFeeRecipients,
    SyncUserVolumeAccumulator,
    ToggleCashbackEnabled,
    ToggleMayhemMode,
    TransferCreatorFeesToPump,
    UpdateAdmin,
    UpdateFeeConfig,
    Withdraw,
    TransferCreatorFeesToPumpV2,
    UpdateBuybackConfig,
    SetReservedFeeRecipient,
}

impl PumpSwapInstructionKind {
    fn instruction_name(self) -> &'static str {
        match self {
            Self::AdminSetCoinCreator => return "admin_set_coin_creator",
            Self::AdminUpdateTokenIncentives => return "admin_update_token_incentives",
            Self::Buy => return "buy",
            Self::BuyExactQuoteIn => return "buy_exact_quote_in",
            Self::ClaimCashback => return "claim_cashback",
            Self::ClaimTokenIncentives => return "claim_token_incentives",
            Self::CloseUserVolumeAccumulator => return "close_user_volume_accumulator",
            Self::CollectCoinCreatorFee => return "collect_coin_creator_fee",
            Self::CreateConfig => return "create_config",
            Self::CreatePool => return "create_pool",
            Self::Deposit => return "deposit",
            Self::Disable => return "disable",
            Self::ExtendAccount => return "extend_account",
            Self::InitUserVolumeAccumulator => return "init_user_volume_accumulator",
            Self::MigratePoolCoinCreator => return "migrate_pool_coin_creator",
            Self::Sell => return "sell",
            Self::SetCoinCreator => return "set_coin_creator",
            Self::SetReservedFeeRecipients => return "set_reserved_fee_recipients",
            Self::SyncUserVolumeAccumulator => return "sync_user_volume_accumulator",
            Self::ToggleCashbackEnabled => return "toggle_cashback_enabled",
            Self::ToggleMayhemMode => return "toggle_mayhem_mode",
            Self::TransferCreatorFeesToPump => return "transfer_creator_fees_to_pump",
            Self::UpdateAdmin => return "update_admin",
            Self::UpdateFeeConfig => return "update_fee_config",
            Self::Withdraw => return "withdraw",
            Self::TransferCreatorFeesToPumpV2 => return "transfer_creator_fees_to_pump_v2",
            Self::UpdateBuybackConfig => return "update_buyback_config",
            Self::SetReservedFeeRecipient => return "set_reserved_fee_recipient",
        }
    }

    fn event_kind(self) -> std::string::String {
        return format!("pump_swap.{}", self.instruction_name());
    }

    fn is_sell_like_trade(self) -> bool {
        return self == Self::Sell;
    }

    fn is_materializable_trade_candidate(self) -> bool {
        return self == Self::Buy || self == Self::Sell;
    }

    fn requires_exact_amount_skip(self) -> bool {
        return self == Self::BuyExactQuoteIn;
    }
}

#[derive(Debug, Clone)]
struct PumpSwapInstructionData {
    instruction_kind: PumpSwapInstructionKind,
    discriminator_hex: std::string::String,
    args_json: serde_json::Value,
}

#[derive(Debug, Clone)]
struct PumpSwapAnchorBuyEvent {
    payload_size: usize,
    timestamp_raw: std::string::String,
    base_amount_out_raw: std::string::String,
    max_quote_amount_in_raw: std::string::String,
    user_base_token_reserves_raw: std::string::String,
    user_quote_token_reserves_raw: std::string::String,
    pool_base_token_reserves_raw: std::string::String,
    pool_quote_token_reserves_raw: std::string::String,
    quote_amount_in_raw: std::string::String,
    lp_fee_basis_points_raw: std::string::String,
    lp_fee_raw: std::string::String,
    protocol_fee_basis_points_raw: std::string::String,
    protocol_fee_raw: std::string::String,
    quote_amount_in_with_lp_fee_raw: std::string::String,
    user_quote_amount_in_raw: std::string::String,
    pool: std::string::String,
    user: std::string::String,
    user_base_token_account: std::string::String,
    user_quote_token_account: std::string::String,
    protocol_fee_recipient: std::string::String,
    protocol_fee_recipient_token_account: std::string::String,
    coin_creator: std::string::String,
    coin_creator_fee_basis_points_raw: std::string::String,
    coin_creator_fee_raw: std::string::String,
    track_volume: bool,
    total_unclaimed_tokens_raw: std::string::String,
    total_claimed_tokens_raw: std::string::String,
    current_sol_volume_raw: std::string::String,
    last_update_timestamp_raw: std::string::String,
    min_base_amount_out_raw: std::string::String,
    ix_name: std::string::String,
    cashback_fee_basis_points_raw: std::option::Option<std::string::String>,
    cashback_raw: std::option::Option<std::string::String>,
    buyback_fee_basis_points_raw: std::option::Option<std::string::String>,
    buyback_fee_raw: std::option::Option<std::string::String>,
}

#[derive(Debug, Clone)]
struct PumpSwapAnchorEventDecoded {
    event_name: &'static str,
    event_kind: &'static str,
    discriminator_hex: std::string::String,
    payload_size: usize,
    payload_json: serde_json::Value,
    pool_account: std::option::Option<std::string::String>,
    token_a_mint: std::option::Option<std::string::String>,
    token_b_mint: std::option::Option<std::string::String>,
    lp_mint: std::option::Option<std::string::String>,
}

#[derive(Debug, Clone)]
struct PumpSwapInstructionLink {
    instruction_id: i64,
    instruction_kind: PumpSwapInstructionKind,
    pool_account: std::option::Option<std::string::String>,
}

#[derive(Debug, Clone, Copy)]
enum PumpSwapAnchorFieldKind {
    U8,
    U16,
    U64,
    I64,
    Bool,
    Pubkey,
    PubkeyArray(usize),
}

#[derive(Debug, Clone, Copy)]
struct PumpSwapAnchorFieldDescriptor {
    name: &'static str,
    kind: PumpSwapAnchorFieldKind,
}

#[derive(Debug, Clone, Copy)]
struct PumpSwapAnchorEventDescriptor {
    event_name: &'static str,
    event_kind: &'static str,
    discriminator: [u8; 8],
    fields: &'static [PumpSwapAnchorFieldDescriptor],
}

const PUMP_SWAP_ADMIN_SET_COIN_CREATOR_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "admin_set_coin_creator_authority",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_mint",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "old_coin_creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "new_coin_creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];
const PUMP_SWAP_ADMIN_UPDATE_TOKEN_INCENTIVES_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "start_time",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "end_time",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "day_number",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "token_supply_per_day",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "mint",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "seconds_in_a_day",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
];
const PUMP_SWAP_CLAIM_CASHBACK_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "user",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "amount",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "total_claimed",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "total_cashback_earned",
        kind: PumpSwapAnchorFieldKind::U64,
    },
];
const PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "user",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "mint",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "amount",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "total_claimed_tokens",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "current_sol_volume",
        kind: PumpSwapAnchorFieldKind::U64,
    },
];
const PUMP_SWAP_CLOSE_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "user",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "total_unclaimed_tokens",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "total_claimed_tokens",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "current_sol_volume",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "last_update_timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
];
const PUMP_SWAP_COLLECT_COIN_CREATOR_FEE_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator_fee",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator_vault_ata",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];
const PUMP_SWAP_CREATE_CONFIG_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "admin",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "protocol_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "protocol_fee_recipients",
        kind: PumpSwapAnchorFieldKind::PubkeyArray(8),
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "admin_set_coin_creator_authority",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];
const PUMP_SWAP_CREATE_POOL_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "index",
        kind: PumpSwapAnchorFieldKind::U16,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_mint",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "quote_mint",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_mint_decimals",
        kind: PumpSwapAnchorFieldKind::U8,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "quote_mint_decimals",
        kind: PumpSwapAnchorFieldKind::U8,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_amount_in",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "quote_amount_in",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool_base_amount",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool_quote_amount",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "minimum_liquidity",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "initial_liquidity",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_token_amount_out",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool_bump",
        kind: PumpSwapAnchorFieldKind::U8,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_mint",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_base_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_quote_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "is_mayhem_mode",
        kind: PumpSwapAnchorFieldKind::Bool,
    },
];
const PUMP_SWAP_DEPOSIT_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_token_amount_out",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "max_base_amount_in",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "max_quote_amount_in",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_base_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_quote_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool_base_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool_quote_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_amount_in",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "quote_amount_in",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_mint_supply",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_base_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_quote_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_pool_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];
const PUMP_SWAP_DISABLE_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "admin",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "disable_create_pool",
        kind: PumpSwapAnchorFieldKind::Bool,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "disable_deposit",
        kind: PumpSwapAnchorFieldKind::Bool,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "disable_withdraw",
        kind: PumpSwapAnchorFieldKind::Bool,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "disable_buy",
        kind: PumpSwapAnchorFieldKind::Bool,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "disable_sell",
        kind: PumpSwapAnchorFieldKind::Bool,
    },
];
const PUMP_SWAP_EXTEND_ACCOUNT_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "current_size",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "new_size",
        kind: PumpSwapAnchorFieldKind::U64,
    },
];
const PUMP_SWAP_INIT_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "payer",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
];
const PUMP_SWAP_MIGRATE_POOL_COIN_CREATOR_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_mint",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "sharing_config",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "old_coin_creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "new_coin_creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];
const PUMP_SWAP_RESERVED_FEE_RECIPIENTS_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "reserved_fee_recipient",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "reserved_fee_recipients",
        kind: PumpSwapAnchorFieldKind::PubkeyArray(7),
    },
];
const PUMP_SWAP_SELL_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_amount_in",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "min_quote_amount_out",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_base_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_quote_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool_base_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool_quote_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "quote_amount_out",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_fee",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "protocol_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "protocol_fee",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "quote_amount_out_without_lp_fee",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_quote_amount_out",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_base_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_quote_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "protocol_fee_recipient",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "protocol_fee_recipient_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator_fee",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "cashback_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "cashback",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "buyback_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "buyback_fee",
        kind: PumpSwapAnchorFieldKind::U64,
    },
];
const PUMP_SWAP_SET_BONDING_CURVE_COIN_CREATOR_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_mint",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "bonding_curve",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];
const PUMP_SWAP_SET_METAPLEX_COIN_CREATOR_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_mint",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "metadata",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];
const PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "user",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "total_claimed_tokens_before",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "total_claimed_tokens_after",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
];
const PUMP_SWAP_UPDATE_ADMIN_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "admin",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "new_admin",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];
const PUMP_SWAP_UPDATE_FEE_CONFIG_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "admin",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "protocol_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "protocol_fee_recipients",
        kind: PumpSwapAnchorFieldKind::PubkeyArray(8),
    },
    PumpSwapAnchorFieldDescriptor {
        name: "coin_creator_fee_basis_points",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "admin_set_coin_creator_authority",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];
const PUMP_SWAP_WITHDRAW_EVENT_FIELDS: &[PumpSwapAnchorFieldDescriptor] = &[
    PumpSwapAnchorFieldDescriptor {
        name: "timestamp",
        kind: PumpSwapAnchorFieldKind::I64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_token_amount_in",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "min_base_amount_out",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "min_quote_amount_out",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_base_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_quote_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool_base_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool_quote_token_reserves",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "base_amount_out",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "quote_amount_out",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "lp_mint_supply",
        kind: PumpSwapAnchorFieldKind::U64,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "pool",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_base_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_quote_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
    PumpSwapAnchorFieldDescriptor {
        name: "user_pool_token_account",
        kind: PumpSwapAnchorFieldKind::Pubkey,
    },
];

#[derive(Debug, Clone, Copy)]
struct PumpSwapAccountLayout {
    pool_index: std::option::Option<usize>,
    token_a_mint_index: std::option::Option<usize>,
    token_b_mint_index: std::option::Option<usize>,
    lp_mint_index: std::option::Option<usize>,
    user_base_token_account_index: std::option::Option<usize>,
    user_quote_token_account_index: std::option::Option<usize>,
    pool_base_token_account_index: std::option::Option<usize>,
    pool_quote_token_account_index: std::option::Option<usize>,
    global_volume_accumulator_index: std::option::Option<usize>,
    user_volume_accumulator_index: std::option::Option<usize>,
}

/// Decoded PumpSwap trade event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PumpSwapTradeDecoded {
    /// 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,
    /// Trade side.
    pub trade_side: crate::SwapTradeSide,
    /// Optional heuristic pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional token A mint.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional token B mint.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Optional pool_v2 account or upgraded pool account.
    pub pool_v2: std::option::Option<std::string::String>,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded PumpSwap non-trade instruction event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PumpSwapInstructionDecoded {
    /// Parent transaction id.
    pub transaction_id: i64,
    /// Parent instruction id.
    pub instruction_id: i64,
    /// Transaction signature.
    pub signature: std::string::String,
    /// Program id.
    pub program_id: std::string::String,
    /// Local decoded event kind.
    pub event_kind: std::string::String,
    /// Optional heuristic pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional token A mint.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional token B mint.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Optional LP mint or pool token mint.
    pub lp_mint: std::option::Option<std::string::String>,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded PumpSwap event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum PumpSwapDecodedEvent {
    /// Buy trade.
    BuyTrade(PumpSwapTradeDecoded),
    /// Buy-exact-quote-in trade.
    BuyExactQuoteInTrade(PumpSwapTradeDecoded),
    /// Sell trade.
    SellTrade(PumpSwapTradeDecoded),
    /// Specialized non-trade instruction retained for coverage and diagnostics.
    Instruction(PumpSwapInstructionDecoded),
}

/// PumpSwap decoder.
#[derive(Debug, Clone, Default)]
pub struct PumpSwapDecoder;

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

    /// Decodes one projected transaction into zero or more PumpSwap events.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::PumpSwapDecodedEvent>, 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();
        let mut instruction_links = 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::PUMP_SWAP_PROGRAM_ID {
                continue;
            }
            let instruction_id_option = instruction.id;
            let instruction_id = match instruction_id_option {
                Some(instruction_id) => instruction_id,
                None => continue,
            };
            let instruction_data_result =
                parse_pump_swap_instruction_data(instruction.data_json.as_ref());
            let instruction_data_option = match instruction_data_result {
                Ok(instruction_data_option) => instruction_data_option,
                Err(error) => return Err(error),
            };
            let instruction_data = match instruction_data_option {
                Some(instruction_data) => instruction_data,
                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 layout = account_layout_for_instruction(instruction_data.instruction_kind);
            let pool_account =
                resolve_pool_account(parsed_json.as_ref(), accounts.as_slice(), layout);
            let token_a_mint =
                resolve_token_a_mint(parsed_json.as_ref(), accounts.as_slice(), layout);
            let token_b_mint =
                resolve_token_b_mint(parsed_json.as_ref(), accounts.as_slice(), layout);
            let lp_mint = resolve_lp_mint(parsed_json.as_ref(), accounts.as_slice(), layout);
            let user_base_token_account =
                account_at(accounts.as_slice(), layout.user_base_token_account_index);
            let user_quote_token_account =
                account_at(accounts.as_slice(), layout.user_quote_token_account_index);
            let pool_base_token_account =
                account_at(accounts.as_slice(), layout.pool_base_token_account_index);
            let pool_quote_token_account =
                account_at(accounts.as_slice(), layout.pool_quote_token_account_index);
            let global_volume_accumulator =
                account_at(accounts.as_slice(), layout.global_volume_accumulator_index);
            let user_volume_accumulator =
                account_at(accounts.as_slice(), layout.user_volume_accumulator_index);
            instruction_links.push(PumpSwapInstructionLink {
                instruction_id,
                instruction_kind: instruction_data.instruction_kind,
                pool_account: pool_account.clone(),
            });
            let mut payload_json = build_instruction_payload(
                transaction,
                instruction,
                &accounts,
                parsed_json,
                log_messages.clone(),
                &instruction_data,
                pool_account.clone(),
                token_a_mint.clone(),
                token_b_mint.clone(),
                lp_mint.clone(),
                user_base_token_account,
                user_quote_token_account,
                pool_base_token_account,
                pool_quote_token_account,
                global_volume_accumulator,
                user_volume_accumulator,
            );
            if instruction_data.instruction_kind == PumpSwapInstructionKind::BuyExactQuoteIn {
                let anchor_buy_event = find_pump_swap_anchor_buy_event_for_instruction(
                    log_messages.as_slice(),
                    pool_account.as_deref(),
                    "buy_exact_quote_in",
                );
                if let Some(anchor_buy_event) = anchor_buy_event {
                    payload_json =
                        enrich_payload_with_anchor_buy_event(payload_json, &anchor_buy_event);
                    decoded_events.push(crate::PumpSwapDecodedEvent::BuyExactQuoteInTrade(
                        crate::PumpSwapTradeDecoded {
                            transaction_id,
                            instruction_id,
                            signature: transaction.signature.clone(),
                            program_id: program_id.clone(),
                            trade_side: crate::SwapTradeSide::BuyBase,
                            pool_account,
                            token_a_mint,
                            token_b_mint,
                            pool_v2: lp_mint,
                            payload_json,
                        },
                    ));
                    continue;
                }
                decoded_events.push(crate::PumpSwapDecodedEvent::Instruction(
                    crate::PumpSwapInstructionDecoded {
                        transaction_id,
                        instruction_id,
                        signature: transaction.signature.clone(),
                        program_id: program_id.clone(),
                        event_kind: instruction_data.instruction_kind.event_kind(),
                        pool_account,
                        token_a_mint,
                        token_b_mint,
                        lp_mint,
                        payload_json,
                    },
                ));
                continue;
            }
            if instruction_data.instruction_kind == PumpSwapInstructionKind::Buy {
                decoded_events.push(crate::PumpSwapDecodedEvent::BuyTrade(
                    crate::PumpSwapTradeDecoded {
                        transaction_id,
                        instruction_id,
                        signature: transaction.signature.clone(),
                        program_id: program_id.clone(),
                        trade_side: crate::SwapTradeSide::BuyBase,
                        pool_account,
                        token_a_mint,
                        token_b_mint,
                        pool_v2: lp_mint,
                        payload_json,
                    },
                ));
                continue;
            }
            if instruction_data.instruction_kind.is_sell_like_trade() {
                decoded_events.push(crate::PumpSwapDecodedEvent::SellTrade(
                    crate::PumpSwapTradeDecoded {
                        transaction_id,
                        instruction_id,
                        signature: transaction.signature.clone(),
                        program_id: program_id.clone(),
                        trade_side: crate::SwapTradeSide::SellBase,
                        pool_account,
                        token_a_mint,
                        token_b_mint,
                        pool_v2: lp_mint,
                        payload_json,
                    },
                ));
                continue;
            }
            decoded_events.push(crate::PumpSwapDecodedEvent::Instruction(
                crate::PumpSwapInstructionDecoded {
                    transaction_id,
                    instruction_id,
                    signature: transaction.signature.clone(),
                    program_id: program_id.clone(),
                    event_kind: instruction_data.instruction_kind.event_kind(),
                    pool_account,
                    token_a_mint,
                    token_b_mint,
                    lp_mint,
                    payload_json,
                },
            ));
        }
        let anchor_event_decoded = decode_pump_swap_anchor_event_instruction_events(
            transaction,
            instruction_links.as_slice(),
            log_messages.as_slice(),
        );
        decoded_events.extend(anchor_event_decoded);
        return Ok(decoded_events);
    }
}

fn instruction_id_for_payload(instruction: &crate::ChainInstructionDto) -> i64 {
    match instruction.id {
        Some(instruction_id) => return instruction_id,
        None => return 0,
    }
}

fn account_layout_for_instruction(kind: PumpSwapInstructionKind) -> PumpSwapAccountLayout {
    match kind {
        PumpSwapInstructionKind::Buy
        | PumpSwapInstructionKind::Sell
        | PumpSwapInstructionKind::BuyExactQuoteIn => {
            return PumpSwapAccountLayout {
                pool_index: Some(0),
                token_a_mint_index: Some(3),
                token_b_mint_index: Some(4),
                lp_mint_index: None,
                user_base_token_account_index: Some(5),
                user_quote_token_account_index: Some(6),
                pool_base_token_account_index: Some(7),
                pool_quote_token_account_index: Some(8),
                global_volume_accumulator_index: Some(19),
                user_volume_accumulator_index: Some(20),
            };
        },
        PumpSwapInstructionKind::CreatePool
        | PumpSwapInstructionKind::Deposit
        | PumpSwapInstructionKind::Withdraw => {
            return PumpSwapAccountLayout {
                pool_index: Some(0),
                token_a_mint_index: Some(3),
                token_b_mint_index: Some(4),
                lp_mint_index: Some(5),
                user_base_token_account_index: Some(6),
                user_quote_token_account_index: Some(7),
                pool_base_token_account_index: Some(9),
                pool_quote_token_account_index: Some(10),
                global_volume_accumulator_index: None,
                user_volume_accumulator_index: None,
            };
        },
        PumpSwapInstructionKind::CollectCoinCreatorFee
        | PumpSwapInstructionKind::AdminSetCoinCreator
        | PumpSwapInstructionKind::MigratePoolCoinCreator
        | PumpSwapInstructionKind::SetCoinCreator => {
            return PumpSwapAccountLayout {
                pool_index: Some(0),
                token_a_mint_index: None,
                token_b_mint_index: None,
                lp_mint_index: None,
                user_base_token_account_index: None,
                user_quote_token_account_index: None,
                pool_base_token_account_index: None,
                pool_quote_token_account_index: None,
                global_volume_accumulator_index: None,
                user_volume_accumulator_index: None,
            };
        },
        _ => {
            return PumpSwapAccountLayout {
                pool_index: None,
                token_a_mint_index: None,
                token_b_mint_index: None,
                lp_mint_index: None,
                user_base_token_account_index: None,
                user_quote_token_account_index: None,
                pool_base_token_account_index: None,
                pool_quote_token_account_index: None,
                global_volume_accumulator_index: None,
                user_volume_accumulator_index: None,
            };
        },
    }
}

#[allow(clippy::too_many_arguments)]
fn build_instruction_payload(
    transaction: &crate::ChainTransactionDto,
    instruction: &crate::ChainInstructionDto,
    accounts: &[std::string::String],
    parsed_json: std::option::Option<serde_json::Value>,
    log_messages: std::vec::Vec<std::string::String>,
    instruction_data: &PumpSwapInstructionData,
    pool_account: std::option::Option<std::string::String>,
    token_a_mint: std::option::Option<std::string::String>,
    token_b_mint: std::option::Option<std::string::String>,
    lp_mint: std::option::Option<std::string::String>,
    user_base_token_account: std::option::Option<std::string::String>,
    user_quote_token_account: std::option::Option<std::string::String>,
    pool_base_token_account: std::option::Option<std::string::String>,
    pool_quote_token_account: std::option::Option<std::string::String>,
    global_volume_accumulator: std::option::Option<std::string::String>,
    user_volume_accumulator: std::option::Option<std::string::String>,
) -> serde_json::Value {
    let trade_candidate = instruction_data.instruction_kind.is_materializable_trade_candidate()
        && pool_account.is_some()
        && token_a_mint.is_some()
        && token_b_mint.is_some();
    let mut object = serde_json::Map::new();
    object.insert("decoder".to_string(), serde_json::Value::String("pump_swap".to_string()));
    object.insert(
        "signature".to_string(),
        serde_json::Value::String(transaction.signature.clone()),
    );
    object.insert(
        "instructionId".to_string(),
        serde_json::Value::Number(serde_json::Number::from(instruction_id_for_payload(
            instruction,
        ))),
    );
    insert_optional_i64(&mut object, "parentInstructionId", instruction.parent_instruction_id);
    object.insert(
        "instructionIndex".to_string(),
        serde_json::Value::Number(serde_json::Number::from(instruction.instruction_index)),
    );
    insert_optional_u32(&mut object, "innerInstructionIndex", instruction.inner_instruction_index);
    object.insert(
        "instructionKind".to_string(),
        serde_json::Value::String(instruction_data.instruction_kind.instruction_name().to_string()),
    );
    object.insert(
        "eventKind".to_string(),
        serde_json::Value::String(instruction_data.instruction_kind.event_kind()),
    );
    object.insert(
        "instructionDiscriminatorHex".to_string(),
        serde_json::Value::String(instruction_data.discriminator_hex.clone()),
    );
    object.insert("args".to_string(), instruction_data.args_json.clone());
    insert_optional_string(&mut object, "poolAccount", pool_account.clone());
    insert_optional_string(&mut object, "pool", pool_account);
    insert_optional_string(&mut object, "tokenAMint", token_a_mint.clone());
    insert_optional_string(&mut object, "baseMint", token_a_mint);
    insert_optional_string(&mut object, "tokenBMint", token_b_mint.clone());
    insert_optional_string(&mut object, "quoteMint", token_b_mint);
    insert_optional_string(&mut object, "lpMint", lp_mint);
    insert_optional_string(&mut object, "poolV2", None);
    insert_optional_string(&mut object, "userBaseTokenAccount", user_base_token_account);
    insert_optional_string(&mut object, "userQuoteTokenAccount", user_quote_token_account);
    insert_optional_string(&mut object, "poolBaseTokenAccount", pool_base_token_account);
    insert_optional_string(&mut object, "poolQuoteTokenAccount", pool_quote_token_account);
    insert_optional_string(&mut object, "globalVolumeAccumulator", global_volume_accumulator);
    insert_optional_string(&mut object, "userVolumeAccumulator", user_volume_accumulator);
    object.insert("accounts".to_string(), serde_json::json!(accounts));
    insert_optional_value(&mut object, "parsed", parsed_json);
    object.insert("logMessages".to_string(), serde_json::json!(log_messages));
    object.insert("accountCount".to_string(), serde_json::json!(accounts.len()));
    object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(trade_candidate));
    object.insert("candleCandidate".to_string(), serde_json::Value::Bool(trade_candidate));
    if instruction_data.instruction_kind.requires_exact_amount_skip() {
        object.insert(
            "amountSource".to_string(),
            serde_json::Value::String("instruction_bounds_only".to_string()),
        );
        object.insert(
            "amountResolutionStatus".to_string(),
            serde_json::Value::String("exact_amounts_not_proven".to_string()),
        );
        object.insert(
            "skipTradeReason".to_string(),
            serde_json::Value::String(
                "pump_swap_buy_exact_quote_in_exact_amounts_not_proven".to_string(),
            ),
        );
        object.insert(
            "skipCandleReason".to_string(),
            serde_json::Value::String(
                "pump_swap_buy_exact_quote_in_exact_amounts_not_proven".to_string(),
            ),
        );
    } else if trade_candidate {
        object.insert(
            "amountSource".to_string(),
            serde_json::Value::String("pump_swap_post_decode_balance_delta_resolution".to_string()),
        );
    } else {
        object.insert(
            "skipTradeReason".to_string(),
            serde_json::Value::String(
                "pump_swap_non_trade_or_incomplete_trade_instruction".to_string(),
            ),
        );
        object.insert(
            "skipCandleReason".to_string(),
            serde_json::Value::String(
                "pump_swap_non_trade_or_incomplete_trade_instruction".to_string(),
            ),
        );
    }
    return serde_json::Value::Object(object);
}

fn enrich_payload_with_anchor_buy_event(
    payload_json: serde_json::Value,
    anchor_event: &PumpSwapAnchorBuyEvent,
) -> serde_json::Value {
    let mut object = match payload_json {
        serde_json::Value::Object(object) => object,
        other => {
            let mut object = serde_json::Map::new();
            object.insert("rawPayload".to_string(), other);
            object
        },
    };
    object.remove("skipTradeReason");
    object.remove("skipCandleReason");
    object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(true));
    object.insert("candleCandidate".to_string(), serde_json::Value::Bool(true));
    object.insert("tradeSide".to_string(), serde_json::Value::String("BuyBase".to_string()));
    object.insert(
        "amountSource".to_string(),
        serde_json::Value::String("pump_swap_anchor_buy_event".to_string()),
    );
    object.insert(
        "amountResolutionStatus".to_string(),
        serde_json::Value::String("exact_anchor_buy_event".to_string()),
    );
    object.insert(
        "baseAmountRaw".to_string(),
        serde_json::Value::String(anchor_event.base_amount_out_raw.clone()),
    );
    object.insert(
        "quoteAmountRaw".to_string(),
        serde_json::Value::String(anchor_event.user_quote_amount_in_raw.clone()),
    );
    object.insert("user".to_string(), serde_json::Value::String(anchor_event.user.clone()));
    object.insert("anchorBuyEvent".to_string(), anchor_buy_event_to_json(anchor_event));
    return serde_json::Value::Object(object);
}

fn anchor_buy_event_to_json(anchor_event: &PumpSwapAnchorBuyEvent) -> serde_json::Value {
    let mut object = serde_json::Map::new();
    object.insert(
        "anchorEventName".to_string(),
        serde_json::Value::String("buy_event".to_string()),
    );
    object.insert(
        "anchorEventDiscriminatorHex".to_string(),
        serde_json::Value::String("67f4521f2cf57777".to_string()),
    );
    object.insert(
        "anchorEventPayloadSize".to_string(),
        serde_json::json!(anchor_event.payload_size),
    );
    object.insert("ixName".to_string(), serde_json::Value::String(anchor_event.ix_name.clone()));
    object.insert(
        "timestampRaw".to_string(),
        serde_json::Value::String(anchor_event.timestamp_raw.clone()),
    );
    object.insert(
        "baseAmountOutRaw".to_string(),
        serde_json::Value::String(anchor_event.base_amount_out_raw.clone()),
    );
    object.insert(
        "maxQuoteAmountInRaw".to_string(),
        serde_json::Value::String(anchor_event.max_quote_amount_in_raw.clone()),
    );
    object.insert(
        "userBaseTokenReservesRaw".to_string(),
        serde_json::Value::String(anchor_event.user_base_token_reserves_raw.clone()),
    );
    object.insert(
        "userQuoteTokenReservesRaw".to_string(),
        serde_json::Value::String(anchor_event.user_quote_token_reserves_raw.clone()),
    );
    object.insert(
        "poolBaseTokenReservesRaw".to_string(),
        serde_json::Value::String(anchor_event.pool_base_token_reserves_raw.clone()),
    );
    object.insert(
        "poolQuoteTokenReservesRaw".to_string(),
        serde_json::Value::String(anchor_event.pool_quote_token_reserves_raw.clone()),
    );
    object.insert(
        "quoteAmountInRaw".to_string(),
        serde_json::Value::String(anchor_event.quote_amount_in_raw.clone()),
    );
    object.insert(
        "lpFeeBasisPointsRaw".to_string(),
        serde_json::Value::String(anchor_event.lp_fee_basis_points_raw.clone()),
    );
    object.insert(
        "lpFeeRaw".to_string(),
        serde_json::Value::String(anchor_event.lp_fee_raw.clone()),
    );
    object.insert(
        "protocolFeeBasisPointsRaw".to_string(),
        serde_json::Value::String(anchor_event.protocol_fee_basis_points_raw.clone()),
    );
    object.insert(
        "protocolFeeRaw".to_string(),
        serde_json::Value::String(anchor_event.protocol_fee_raw.clone()),
    );
    object.insert(
        "quoteAmountInWithLpFeeRaw".to_string(),
        serde_json::Value::String(anchor_event.quote_amount_in_with_lp_fee_raw.clone()),
    );
    object.insert(
        "userQuoteAmountInRaw".to_string(),
        serde_json::Value::String(anchor_event.user_quote_amount_in_raw.clone()),
    );
    object.insert("pool".to_string(), serde_json::Value::String(anchor_event.pool.clone()));
    object.insert("user".to_string(), serde_json::Value::String(anchor_event.user.clone()));
    object.insert(
        "userBaseTokenAccount".to_string(),
        serde_json::Value::String(anchor_event.user_base_token_account.clone()),
    );
    object.insert(
        "userQuoteTokenAccount".to_string(),
        serde_json::Value::String(anchor_event.user_quote_token_account.clone()),
    );
    object.insert(
        "protocolFeeRecipient".to_string(),
        serde_json::Value::String(anchor_event.protocol_fee_recipient.clone()),
    );
    object.insert(
        "protocolFeeRecipientTokenAccount".to_string(),
        serde_json::Value::String(anchor_event.protocol_fee_recipient_token_account.clone()),
    );
    object.insert(
        "coinCreator".to_string(),
        serde_json::Value::String(anchor_event.coin_creator.clone()),
    );
    object.insert(
        "coinCreatorFeeBasisPointsRaw".to_string(),
        serde_json::Value::String(anchor_event.coin_creator_fee_basis_points_raw.clone()),
    );
    object.insert(
        "coinCreatorFeeRaw".to_string(),
        serde_json::Value::String(anchor_event.coin_creator_fee_raw.clone()),
    );
    object.insert("trackVolume".to_string(), serde_json::Value::Bool(anchor_event.track_volume));
    object.insert(
        "totalUnclaimedTokensRaw".to_string(),
        serde_json::Value::String(anchor_event.total_unclaimed_tokens_raw.clone()),
    );
    object.insert(
        "totalClaimedTokensRaw".to_string(),
        serde_json::Value::String(anchor_event.total_claimed_tokens_raw.clone()),
    );
    object.insert(
        "currentSolVolumeRaw".to_string(),
        serde_json::Value::String(anchor_event.current_sol_volume_raw.clone()),
    );
    object.insert(
        "lastUpdateTimestampRaw".to_string(),
        serde_json::Value::String(anchor_event.last_update_timestamp_raw.clone()),
    );
    object.insert(
        "minBaseAmountOutRaw".to_string(),
        serde_json::Value::String(anchor_event.min_base_amount_out_raw.clone()),
    );
    insert_optional_string(
        &mut object,
        "cashbackFeeBasisPointsRaw",
        anchor_event.cashback_fee_basis_points_raw.clone(),
    );
    insert_optional_string(&mut object, "cashbackRaw", anchor_event.cashback_raw.clone());
    insert_optional_string(
        &mut object,
        "buybackFeeBasisPointsRaw",
        anchor_event.buyback_fee_basis_points_raw.clone(),
    );
    insert_optional_string(&mut object, "buybackFeeRaw", anchor_event.buyback_fee_raw.clone());
    return serde_json::Value::Object(object);
}

fn decode_pump_swap_anchor_event_instruction_events(
    transaction: &crate::ChainTransactionDto,
    instruction_links: &[PumpSwapInstructionLink],
    log_messages: &[std::string::String],
) -> std::vec::Vec<crate::PumpSwapDecodedEvent> {
    let transaction_id = match transaction.id {
        Some(transaction_id) => transaction_id,
        None => return std::vec::Vec::new(),
    };
    let mut decoded_events = std::vec::Vec::new();
    let mut used_instruction_event_keys = std::collections::HashSet::<std::string::String>::new();
    for message in log_messages {
        let encoded = match extract_program_data_base64(message.as_str()) {
            Some(encoded) => encoded,
            None => continue,
        };
        let anchor_event = match decode_pump_swap_anchor_event_from_base64(encoded) {
            Some(anchor_event) => anchor_event,
            None => continue,
        };
        let instruction_id = match resolve_anchor_event_instruction_id(
            &anchor_event,
            instruction_links,
            &mut used_instruction_event_keys,
        ) {
            Some(instruction_id) => instruction_id,
            None => continue,
        };
        let payload_json = build_anchor_event_payload(transaction, instruction_id, &anchor_event);
        decoded_events.push(crate::PumpSwapDecodedEvent::Instruction(
            crate::PumpSwapInstructionDecoded {
                transaction_id,
                instruction_id,
                signature: transaction.signature.clone(),
                program_id: crate::PUMP_SWAP_PROGRAM_ID.to_string(),
                event_kind: anchor_event.event_kind.to_string(),
                pool_account: anchor_event.pool_account.clone(),
                token_a_mint: anchor_event.token_a_mint.clone(),
                token_b_mint: anchor_event.token_b_mint.clone(),
                lp_mint: anchor_event.lp_mint.clone(),
                payload_json,
            },
        ));
    }
    return decoded_events;
}

fn resolve_anchor_event_instruction_id(
    anchor_event: &PumpSwapAnchorEventDecoded,
    instruction_links: &[PumpSwapInstructionLink],
    used_instruction_event_keys: &mut std::collections::HashSet<std::string::String>,
) -> std::option::Option<i64> {
    let expected_kind = expected_instruction_kind_for_anchor_event(anchor_event);
    for link in instruction_links {
        if let Some(expected_kind) = expected_kind {
            if link.instruction_kind != expected_kind {
                continue;
            }
        }
        if !anchor_event_pool_matches_link(anchor_event, link) {
            continue;
        }
        let key = format!("{}:{}", link.instruction_id, anchor_event.event_kind);
        if used_instruction_event_keys.contains(key.as_str()) {
            continue;
        }
        used_instruction_event_keys.insert(key);
        return Some(link.instruction_id);
    }
    for link in instruction_links {
        let key = format!("{}:{}", link.instruction_id, anchor_event.event_kind);
        if used_instruction_event_keys.contains(key.as_str()) {
            continue;
        }
        used_instruction_event_keys.insert(key);
        return Some(link.instruction_id);
    }
    return None;
}

fn anchor_event_pool_matches_link(
    anchor_event: &PumpSwapAnchorEventDecoded,
    link: &PumpSwapInstructionLink,
) -> bool {
    let anchor_pool = match anchor_event.pool_account.as_ref() {
        Some(anchor_pool) => anchor_pool,
        None => return true,
    };
    let link_pool = match link.pool_account.as_ref() {
        Some(link_pool) => link_pool,
        None => return true,
    };
    return anchor_pool == link_pool;
}

fn expected_instruction_kind_for_anchor_event(
    anchor_event: &PumpSwapAnchorEventDecoded,
) -> std::option::Option<PumpSwapInstructionKind> {
    match anchor_event.event_name {
        "admin_set_coin_creator_event" => {
            return Some(PumpSwapInstructionKind::AdminSetCoinCreator);
        },
        "admin_update_token_incentives_event" => {
            return Some(PumpSwapInstructionKind::AdminUpdateTokenIncentives);
        },
        "buy_event" => {
            let ix_name = payload_string_value(&anchor_event.payload_json, "ixName");
            if let Some(ix_name) = ix_name {
                if ix_name == "buy_exact_quote_in" {
                    return Some(PumpSwapInstructionKind::BuyExactQuoteIn);
                }
            }
            return Some(PumpSwapInstructionKind::Buy);
        },
        "claim_cashback_event" => return Some(PumpSwapInstructionKind::ClaimCashback),
        "claim_token_incentives_event" => {
            return Some(PumpSwapInstructionKind::ClaimTokenIncentives);
        },
        "close_user_volume_accumulator_event" => {
            return Some(PumpSwapInstructionKind::CloseUserVolumeAccumulator);
        },
        "collect_coin_creator_fee_event" => {
            return Some(PumpSwapInstructionKind::CollectCoinCreatorFee);
        },
        "create_config_event" => return Some(PumpSwapInstructionKind::CreateConfig),
        "create_pool_event" => return Some(PumpSwapInstructionKind::CreatePool),
        "deposit_event" => return Some(PumpSwapInstructionKind::Deposit),
        "disable_event" => return Some(PumpSwapInstructionKind::Disable),
        "extend_account_event" => return Some(PumpSwapInstructionKind::ExtendAccount),
        "init_user_volume_accumulator_event" => {
            return Some(PumpSwapInstructionKind::InitUserVolumeAccumulator);
        },
        "migrate_pool_coin_creator_event" => {
            return Some(PumpSwapInstructionKind::MigratePoolCoinCreator);
        },
        "reserved_fee_recipients_event" => {
            return Some(PumpSwapInstructionKind::SetReservedFeeRecipients);
        },
        "sell_event" => return Some(PumpSwapInstructionKind::Sell),
        "set_bonding_curve_coin_creator_event" => {
            return Some(PumpSwapInstructionKind::SetCoinCreator);
        },
        "set_metaplex_coin_creator_event" => return Some(PumpSwapInstructionKind::SetCoinCreator),
        "sync_user_volume_accumulator_event" => {
            return Some(PumpSwapInstructionKind::SyncUserVolumeAccumulator);
        },
        "update_admin_event" => return Some(PumpSwapInstructionKind::UpdateAdmin),
        "update_fee_config_event" => return Some(PumpSwapInstructionKind::UpdateFeeConfig),
        "withdraw_event" => return Some(PumpSwapInstructionKind::Withdraw),
        _ => return None,
    }
}

fn build_anchor_event_payload(
    transaction: &crate::ChainTransactionDto,
    instruction_id: i64,
    anchor_event: &PumpSwapAnchorEventDecoded,
) -> serde_json::Value {
    let mut object = match anchor_event.payload_json.clone() {
        serde_json::Value::Object(object) => object,
        other => {
            let mut object = serde_json::Map::new();
            object.insert("rawAnchorEventPayload".to_string(), other);
            object
        },
    };
    object.insert("decoder".to_string(), serde_json::Value::String("pump_swap".to_string()));
    object.insert(
        "signature".to_string(),
        serde_json::Value::String(transaction.signature.clone()),
    );
    object.insert(
        "instructionId".to_string(),
        serde_json::Value::Number(serde_json::Number::from(instruction_id)),
    );
    object.insert(
        "eventKind".to_string(),
        serde_json::Value::String(anchor_event.event_kind.to_string()),
    );
    object.insert(
        "anchorEventName".to_string(),
        serde_json::Value::String(anchor_event.event_name.to_string()),
    );
    object.insert(
        "anchorEventDiscriminatorHex".to_string(),
        serde_json::Value::String(anchor_event.discriminator_hex.clone()),
    );
    object.insert(
        "anchorEventPayloadSize".to_string(),
        serde_json::json!(anchor_event.payload_size),
    );
    object.insert(
        "anchorEventSource".to_string(),
        serde_json::Value::String("anchor_program_data".to_string()),
    );
    object.insert("protocolName".to_string(), serde_json::Value::String("pump_swap".to_string()));
    object.insert(
        "programId".to_string(),
        serde_json::Value::String(crate::PUMP_SWAP_PROGRAM_ID.to_string()),
    );
    add_anchor_event_aliases(anchor_event.event_name, &mut object);
    if !is_materializable_pump_swap_anchor_event(anchor_event.event_name) {
        object.insert("anchorEventAuditOnly".to_string(), serde_json::Value::Bool(true));
        object.insert(
            "skipCatalogReason".to_string(),
            serde_json::Value::String("pump_swap_anchor_event_audit_only".to_string()),
        );
        if anchor_event.event_name == "buy_event" || anchor_event.event_name == "sell_event" {
            object.insert(
                "skipTradeReason".to_string(),
                serde_json::Value::String("pump_swap_anchor_event_audit_only".to_string()),
            );
            object.insert(
                "skipCandleReason".to_string(),
                serde_json::Value::String("pump_swap_anchor_event_audit_only".to_string()),
            );
        }
    } else {
        object.insert("anchorEventAuditOnly".to_string(), serde_json::Value::Bool(false));
    }
    return crate::enrich_dex_decoded_payload(
        "pump_swap",
        anchor_event.event_kind,
        serde_json::Value::Object(object),
    );
}

fn is_materializable_pump_swap_anchor_event(event_name: &str) -> bool {
    return matches!(
        event_name,
        "admin_set_coin_creator_event"
            | "admin_update_token_incentives_event"
            | "buy_event"
            | "claim_cashback_event"
            | "claim_token_incentives_event"
            | "close_user_volume_accumulator_event"
            | "collect_coin_creator_fee_event"
            | "create_config_event"
            | "create_pool_event"
            | "deposit_event"
            | "disable_event"
            | "extend_account_event"
            | "init_user_volume_accumulator_event"
            | "migrate_pool_coin_creator_event"
            | "reserved_fee_recipients_event"
            | "sell_event"
            | "set_bonding_curve_coin_creator_event"
            | "set_metaplex_coin_creator_event"
            | "sync_user_volume_accumulator_event"
            | "update_admin_event"
            | "update_fee_config_event"
            | "withdraw_event"
    );
}

fn add_anchor_event_aliases(
    event_name: &str,
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
) {
    clone_string_value(object, "pool", "poolAccount");
    clone_string_value(object, "base_mint", "baseMint");
    clone_string_value(object, "base_mint", "tokenAMint");
    clone_string_value(object, "quote_mint", "quoteMint");
    clone_string_value(object, "quote_mint", "tokenBMint");
    clone_string_value(object, "lp_mint", "lpMint");
    clone_string_value(object, "user", "actorWallet");
    clone_string_value(object, "admin", "actorWallet");
    clone_string_value(object, "payer", "actorWallet");
    clone_string_value(object, "creator", "actorWallet");
    clone_string_value(object, "coin_creator", "actorWallet");
    clone_string_value(object, "mint", "tokenMint");
    clone_string_value(object, "amount", "amountRaw");
    if event_name == "claim_token_incentives_event" {
        clone_string_value(object, "mint", "rewardTokenMint");
        clone_string_value(object, "amount", "rewardAmountRaw");
        object.insert(
            "anchorEventMaterializationRole".to_string(),
            serde_json::Value::String("reward".to_string()),
        );
    }
    if event_name == "claim_cashback_event" {
        clone_string_value(object, "amount", "rewardAmountRaw");
    }
    if event_name == "collect_coin_creator_fee_event" {
        clone_string_value(object, "coin_creator_fee", "feeAmountRaw");
        clone_string_value(object, "coin_creator", "actorWallet");
    }
    if event_name == "deposit_event" {
        clone_string_value(object, "base_amount_in", "baseAmountRaw");
        clone_string_value(object, "quote_amount_in", "quoteAmountRaw");
        clone_string_value(object, "lp_token_amount_out", "lpAmountRaw");
    }
    if event_name == "withdraw_event" {
        clone_string_value(object, "base_amount_out", "baseAmountRaw");
        clone_string_value(object, "quote_amount_out", "quoteAmountRaw");
        clone_string_value(object, "lp_token_amount_in", "lpAmountRaw");
    }
    if event_name == "buy_event" {
        clone_string_value(object, "base_amount_out", "baseAmountRaw");
        clone_string_value(object, "baseAmountOutRaw", "baseAmountRaw");
        clone_string_value(object, "user_quote_amount_in", "quoteAmountRaw");
        clone_string_value(object, "userQuoteAmountInRaw", "quoteAmountRaw");
    }
    if event_name == "sell_event" {
        clone_string_value(object, "base_amount_in", "baseAmountRaw");
        clone_string_value(object, "user_quote_amount_out", "quoteAmountRaw");
    }
}

fn clone_string_value(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    source_key: &str,
    target_key: &str,
) {
    if object.contains_key(target_key) {
        return;
    }
    let value = match object.get(source_key) {
        Some(value) => value.clone(),
        None => return,
    };
    match value {
        serde_json::Value::String(text) => {
            if !text.trim().is_empty() {
                object.insert(target_key.to_string(), serde_json::Value::String(text));
            }
        },
        serde_json::Value::Number(number) => {
            object.insert(target_key.to_string(), serde_json::Value::String(number.to_string()));
        },
        _ => {},
    }
}

fn payload_string_value(
    value: &serde_json::Value,
    key: &str,
) -> std::option::Option<std::string::String> {
    let object = match value.as_object() {
        Some(object) => object,
        None => return None,
    };
    let candidate = match object.get(key) {
        Some(candidate) => candidate,
        None => return None,
    };
    if let Some(text) = candidate.as_str() {
        if !text.trim().is_empty() {
            return Some(text.to_string());
        }
    }
    return None;
}

fn decode_pump_swap_anchor_event_from_base64(
    encoded: &str,
) -> std::option::Option<PumpSwapAnchorEventDecoded> {
    let decoded = match decode_base64_standard(encoded) {
        Some(decoded) => decoded,
        None => return None,
    };
    let first_discriminator = read_8_byte_array(decoded.as_slice(), 0);
    if decoded.len() >= 16
        && first_discriminator == Some(PUMP_SWAP_ANCHOR_SELF_CPI_LOG_DISCRIMINATOR)
    {
        return decode_pump_swap_anchor_event_data(&decoded[8..]);
    }
    return decode_pump_swap_anchor_event_data(decoded.as_slice());
}

fn decode_pump_swap_anchor_event_data(
    data: &[u8],
) -> std::option::Option<PumpSwapAnchorEventDecoded> {
    let discriminator = match read_8_byte_array(data, 0) {
        Some(discriminator) => discriminator,
        None => return None,
    };
    if discriminator == PUMP_SWAP_BUY_EVENT_DISCRIMINATOR {
        let buy_event = match decode_pump_swap_anchor_buy_event_data(data) {
            Some(buy_event) => buy_event,
            None => return None,
        };
        let mut payload = anchor_buy_event_to_json(&buy_event);
        payload = snake_alias_anchor_buy_event_payload(payload);
        return Some(PumpSwapAnchorEventDecoded {
            event_name: "buy_event",
            event_kind: "pump_swap.buy_event",
            discriminator_hex: bytes_to_lower_hex(&PUMP_SWAP_BUY_EVENT_DISCRIMINATOR),
            payload_size: buy_event.payload_size,
            payload_json: payload,
            pool_account: Some(buy_event.pool),
            token_a_mint: None,
            token_b_mint: None,
            lp_mint: None,
        });
    }
    let descriptor = match pump_swap_anchor_event_descriptor(discriminator) {
        Some(descriptor) => descriptor,
        None => return None,
    };
    let payload = match decode_pump_swap_anchor_event_fields(data, &descriptor) {
        Some(payload) => payload,
        None => return None,
    };
    let pool_account = payload_string_value(&payload, "pool");
    let token_a_mint = payload_string_value(&payload, "base_mint");
    let token_b_mint = payload_string_value(&payload, "quote_mint");
    let lp_mint = payload_string_value(&payload, "lp_mint");
    return Some(PumpSwapAnchorEventDecoded {
        event_name: descriptor.event_name,
        event_kind: descriptor.event_kind,
        discriminator_hex: bytes_to_lower_hex(&descriptor.discriminator),
        payload_size: data.len().saturating_sub(8),
        payload_json: payload,
        pool_account,
        token_a_mint,
        token_b_mint,
        lp_mint,
    });
}

fn snake_alias_anchor_buy_event_payload(payload: serde_json::Value) -> serde_json::Value {
    let mut object = match payload {
        serde_json::Value::Object(object) => object,
        other => {
            let mut object = serde_json::Map::new();
            object.insert("rawAnchorEventPayload".to_string(), other);
            object
        },
    };
    clone_string_value(&mut object, "ixName", "ix_name");
    return serde_json::Value::Object(object);
}

fn pump_swap_anchor_event_descriptor(
    discriminator: [u8; 8],
) -> std::option::Option<PumpSwapAnchorEventDescriptor> {
    let descriptors: &[PumpSwapAnchorEventDescriptor] = &[
        PumpSwapAnchorEventDescriptor {
            event_name: "admin_set_coin_creator_event",
            event_kind: "pump_swap.admin_set_coin_creator_event",
            discriminator: PUMP_SWAP_ADMIN_SET_COIN_CREATOR_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_ADMIN_SET_COIN_CREATOR_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "admin_update_token_incentives_event",
            event_kind: "pump_swap.admin_update_token_incentives_event",
            discriminator: PUMP_SWAP_ADMIN_UPDATE_TOKEN_INCENTIVES_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_ADMIN_UPDATE_TOKEN_INCENTIVES_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "claim_cashback_event",
            event_kind: "pump_swap.claim_cashback_event",
            discriminator: PUMP_SWAP_CLAIM_CASHBACK_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_CLAIM_CASHBACK_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "claim_token_incentives_event",
            event_kind: "pump_swap.claim_token_incentives_event",
            discriminator: PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "close_user_volume_accumulator_event",
            event_kind: "pump_swap.close_user_volume_accumulator_event",
            discriminator: PUMP_SWAP_CLOSE_USER_VOLUME_ACCUMULATOR_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_CLOSE_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "collect_coin_creator_fee_event",
            event_kind: "pump_swap.collect_coin_creator_fee_event",
            discriminator: PUMP_SWAP_COLLECT_COIN_CREATOR_FEE_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_COLLECT_COIN_CREATOR_FEE_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "create_config_event",
            event_kind: "pump_swap.create_config_event",
            discriminator: PUMP_SWAP_CREATE_CONFIG_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_CREATE_CONFIG_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "create_pool_event",
            event_kind: "pump_swap.create_pool_event",
            discriminator: PUMP_SWAP_CREATE_POOL_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_CREATE_POOL_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "deposit_event",
            event_kind: "pump_swap.deposit_event",
            discriminator: PUMP_SWAP_DEPOSIT_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_DEPOSIT_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "disable_event",
            event_kind: "pump_swap.disable_event",
            discriminator: PUMP_SWAP_DISABLE_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_DISABLE_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "extend_account_event",
            event_kind: "pump_swap.extend_account_event",
            discriminator: PUMP_SWAP_EXTEND_ACCOUNT_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_EXTEND_ACCOUNT_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "init_user_volume_accumulator_event",
            event_kind: "pump_swap.init_user_volume_accumulator_event",
            discriminator: PUMP_SWAP_INIT_USER_VOLUME_ACCUMULATOR_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_INIT_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "migrate_pool_coin_creator_event",
            event_kind: "pump_swap.migrate_pool_coin_creator_event",
            discriminator: PUMP_SWAP_MIGRATE_POOL_COIN_CREATOR_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_MIGRATE_POOL_COIN_CREATOR_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "reserved_fee_recipients_event",
            event_kind: "pump_swap.reserved_fee_recipients_event",
            discriminator: PUMP_SWAP_RESERVED_FEE_RECIPIENTS_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_RESERVED_FEE_RECIPIENTS_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "sell_event",
            event_kind: "pump_swap.sell_event",
            discriminator: PUMP_SWAP_SELL_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_SELL_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "set_bonding_curve_coin_creator_event",
            event_kind: "pump_swap.set_bonding_curve_coin_creator_event",
            discriminator: PUMP_SWAP_SET_BONDING_CURVE_COIN_CREATOR_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_SET_BONDING_CURVE_COIN_CREATOR_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "set_metaplex_coin_creator_event",
            event_kind: "pump_swap.set_metaplex_coin_creator_event",
            discriminator: PUMP_SWAP_SET_METAPLEX_COIN_CREATOR_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_SET_METAPLEX_COIN_CREATOR_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "sync_user_volume_accumulator_event",
            event_kind: "pump_swap.sync_user_volume_accumulator_event",
            discriminator: PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "update_admin_event",
            event_kind: "pump_swap.update_admin_event",
            discriminator: PUMP_SWAP_UPDATE_ADMIN_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_UPDATE_ADMIN_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "update_fee_config_event",
            event_kind: "pump_swap.update_fee_config_event",
            discriminator: PUMP_SWAP_UPDATE_FEE_CONFIG_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_UPDATE_FEE_CONFIG_EVENT_FIELDS,
        },
        PumpSwapAnchorEventDescriptor {
            event_name: "withdraw_event",
            event_kind: "pump_swap.withdraw_event",
            discriminator: PUMP_SWAP_WITHDRAW_EVENT_DISCRIMINATOR,
            fields: PUMP_SWAP_WITHDRAW_EVENT_FIELDS,
        },
    ];
    for descriptor in descriptors {
        if descriptor.discriminator == discriminator {
            return Some(*descriptor);
        }
    }
    return None;
}

fn decode_pump_swap_anchor_event_fields(
    data: &[u8],
    descriptor: &PumpSwapAnchorEventDescriptor,
) -> std::option::Option<serde_json::Value> {
    let mut offset = 8usize;
    let mut object = serde_json::Map::new();
    for field in descriptor.fields {
        let decoded = match decode_anchor_field(data, offset, field.kind) {
            Some(decoded) => decoded,
            None => return None,
        };
        offset = decoded.1;
        object.insert(field.name.to_string(), decoded.0);
    }
    return Some(serde_json::Value::Object(object));
}

fn decode_anchor_field(
    data: &[u8],
    offset: usize,
    kind: PumpSwapAnchorFieldKind,
) -> std::option::Option<(serde_json::Value, usize)> {
    match kind {
        PumpSwapAnchorFieldKind::U8 => {
            if data.len() < offset + 1 {
                return None;
            }
            return Some((serde_json::json!(data[offset]), offset + 1));
        },
        PumpSwapAnchorFieldKind::U16 => {
            if data.len() < offset + 2 {
                return None;
            }
            let value = u16::from_le_bytes([data[offset], data[offset + 1]]);
            return Some((serde_json::json!(value), offset + 2));
        },
        PumpSwapAnchorFieldKind::U64 => {
            let value = match decode_u64_le_as_string(data, offset) {
                Some(value) => value,
                None => return None,
            };
            return Some((serde_json::Value::String(value), offset + 8));
        },
        PumpSwapAnchorFieldKind::I64 => {
            let value = match decode_i64_le_as_string(data, offset) {
                Some(value) => value,
                None => return None,
            };
            return Some((serde_json::Value::String(value), offset + 8));
        },
        PumpSwapAnchorFieldKind::Bool => {
            let value = match read_bool(data, offset) {
                Some(value) => value,
                None => return None,
            };
            return Some((serde_json::Value::Bool(value), offset + 1));
        },
        PumpSwapAnchorFieldKind::Pubkey => {
            let value = match decode_pubkey32_as_string(data, offset) {
                Some(value) => value,
                None => return None,
            };
            return Some((serde_json::Value::String(value), offset + 32));
        },
        PumpSwapAnchorFieldKind::PubkeyArray(count) => {
            let mut values = std::vec::Vec::new();
            let mut current_offset = offset;
            for _ in 0..count {
                let value = match decode_pubkey32_as_string(data, current_offset) {
                    Some(value) => value,
                    None => return None,
                };
                values.push(serde_json::Value::String(value));
                current_offset = current_offset.saturating_add(32);
            }
            return Some((serde_json::Value::Array(values), current_offset));
        },
    }
}

fn find_pump_swap_anchor_buy_event_for_instruction(
    log_messages: &[std::string::String],
    pool_account: std::option::Option<&str>,
    ix_name: &str,
) -> std::option::Option<PumpSwapAnchorBuyEvent> {
    for message in log_messages {
        let data_text = extract_program_data_base64(message.as_str());
        let data_text = match data_text {
            Some(data_text) => data_text,
            None => continue,
        };
        let anchor_event = decode_pump_swap_anchor_buy_event_from_base64(data_text);
        let anchor_event = match anchor_event {
            Some(anchor_event) => anchor_event,
            None => continue,
        };
        if anchor_event.ix_name != ix_name {
            continue;
        }
        if let Some(pool_account) = pool_account {
            if anchor_event.pool != pool_account {
                continue;
            }
        }
        return Some(anchor_event);
    }
    return None;
}

fn extract_program_data_base64(message: &str) -> std::option::Option<&str> {
    let trimmed = message.trim();
    if let Some(rest) = trimmed.strip_prefix("Program data: ") {
        return Some(rest.trim());
    }
    return None;
}

fn decode_pump_swap_anchor_buy_event_from_base64(
    encoded: &str,
) -> std::option::Option<PumpSwapAnchorBuyEvent> {
    let decoded = decode_base64_standard(encoded);
    let decoded = match decoded {
        Some(decoded) => decoded,
        None => return None,
    };
    let first_discriminator = read_8_byte_array(decoded.as_slice(), 0);
    if decoded.len() >= 16
        && first_discriminator == Some(PUMP_SWAP_ANCHOR_SELF_CPI_LOG_DISCRIMINATOR)
    {
        return decode_pump_swap_anchor_buy_event_data(&decoded[8..]);
    }
    return decode_pump_swap_anchor_buy_event_data(decoded.as_slice());
}

fn decode_pump_swap_anchor_buy_event_data(
    data: &[u8],
) -> std::option::Option<PumpSwapAnchorBuyEvent> {
    if data.len() < 405 {
        return None;
    }
    let discriminator = match read_8_byte_array(data, 0) {
        Some(discriminator) => discriminator,
        None => return None,
    };
    if discriminator != PUMP_SWAP_BUY_EVENT_DISCRIMINATOR {
        return None;
    }
    let string_result = match read_borsh_string(data, 401) {
        Some(string_result) => string_result,
        None => return None,
    };
    let ix_name = string_result.0;
    let trailing_offset = string_result.1;
    let timestamp_raw = match decode_i64_le_as_string(data, 8) {
        Some(value) => value,
        None => return None,
    };
    let base_amount_out_raw = match decode_u64_le_as_string(data, 16) {
        Some(value) => value,
        None => return None,
    };
    let max_quote_amount_in_raw = match decode_u64_le_as_string(data, 24) {
        Some(value) => value,
        None => return None,
    };
    let user_base_token_reserves_raw = match decode_u64_le_as_string(data, 32) {
        Some(value) => value,
        None => return None,
    };
    let user_quote_token_reserves_raw = match decode_u64_le_as_string(data, 40) {
        Some(value) => value,
        None => return None,
    };
    let pool_base_token_reserves_raw = match decode_u64_le_as_string(data, 48) {
        Some(value) => value,
        None => return None,
    };
    let pool_quote_token_reserves_raw = match decode_u64_le_as_string(data, 56) {
        Some(value) => value,
        None => return None,
    };
    let quote_amount_in_raw = match decode_u64_le_as_string(data, 64) {
        Some(value) => value,
        None => return None,
    };
    let lp_fee_basis_points_raw = match decode_u64_le_as_string(data, 72) {
        Some(value) => value,
        None => return None,
    };
    let lp_fee_raw = match decode_u64_le_as_string(data, 80) {
        Some(value) => value,
        None => return None,
    };
    let protocol_fee_basis_points_raw = match decode_u64_le_as_string(data, 88) {
        Some(value) => value,
        None => return None,
    };
    let protocol_fee_raw = match decode_u64_le_as_string(data, 96) {
        Some(value) => value,
        None => return None,
    };
    let quote_amount_in_with_lp_fee_raw = match decode_u64_le_as_string(data, 104) {
        Some(value) => value,
        None => return None,
    };
    let user_quote_amount_in_raw = match decode_u64_le_as_string(data, 112) {
        Some(value) => value,
        None => return None,
    };
    let pool = match decode_pubkey32_as_string(data, 120) {
        Some(value) => value,
        None => return None,
    };
    let user = match decode_pubkey32_as_string(data, 152) {
        Some(value) => value,
        None => return None,
    };
    let user_base_token_account = match decode_pubkey32_as_string(data, 184) {
        Some(value) => value,
        None => return None,
    };
    let user_quote_token_account = match decode_pubkey32_as_string(data, 216) {
        Some(value) => value,
        None => return None,
    };
    let protocol_fee_recipient = match decode_pubkey32_as_string(data, 248) {
        Some(value) => value,
        None => return None,
    };
    let protocol_fee_recipient_token_account = match decode_pubkey32_as_string(data, 280) {
        Some(value) => value,
        None => return None,
    };
    let coin_creator = match decode_pubkey32_as_string(data, 312) {
        Some(value) => value,
        None => return None,
    };
    let coin_creator_fee_basis_points_raw = match decode_u64_le_as_string(data, 344) {
        Some(value) => value,
        None => return None,
    };
    let coin_creator_fee_raw = match decode_u64_le_as_string(data, 352) {
        Some(value) => value,
        None => return None,
    };
    let track_volume = match read_bool(data, 360) {
        Some(value) => value,
        None => return None,
    };
    let total_unclaimed_tokens_raw = match decode_u64_le_as_string(data, 361) {
        Some(value) => value,
        None => return None,
    };
    let total_claimed_tokens_raw = match decode_u64_le_as_string(data, 369) {
        Some(value) => value,
        None => return None,
    };
    let current_sol_volume_raw = match decode_u64_le_as_string(data, 377) {
        Some(value) => value,
        None => return None,
    };
    let last_update_timestamp_raw = match decode_i64_le_as_string(data, 385) {
        Some(value) => value,
        None => return None,
    };
    let min_base_amount_out_raw = match decode_u64_le_as_string(data, 393) {
        Some(value) => value,
        None => return None,
    };
    return Some(PumpSwapAnchorBuyEvent {
        payload_size: data.len().saturating_sub(8),
        timestamp_raw,
        base_amount_out_raw,
        max_quote_amount_in_raw,
        user_base_token_reserves_raw,
        user_quote_token_reserves_raw,
        pool_base_token_reserves_raw,
        pool_quote_token_reserves_raw,
        quote_amount_in_raw,
        lp_fee_basis_points_raw,
        lp_fee_raw,
        protocol_fee_basis_points_raw,
        protocol_fee_raw,
        quote_amount_in_with_lp_fee_raw,
        user_quote_amount_in_raw,
        pool,
        user,
        user_base_token_account,
        user_quote_token_account,
        protocol_fee_recipient,
        protocol_fee_recipient_token_account,
        coin_creator,
        coin_creator_fee_basis_points_raw,
        coin_creator_fee_raw,
        track_volume,
        total_unclaimed_tokens_raw,
        total_claimed_tokens_raw,
        current_sol_volume_raw,
        last_update_timestamp_raw,
        min_base_amount_out_raw,
        ix_name,
        cashback_fee_basis_points_raw: decode_u64_le_as_string(data, trailing_offset),
        cashback_raw: decode_u64_le_as_string(data, trailing_offset.saturating_add(8)),
        buyback_fee_basis_points_raw: decode_u64_le_as_string(
            data,
            trailing_offset.saturating_add(16),
        ),
        buyback_fee_raw: decode_u64_le_as_string(data, trailing_offset.saturating_add(24)),
    });
}

fn read_8_byte_array(data: &[u8], offset: usize) -> std::option::Option<[u8; 8]> {
    if data.len() < offset + 8 {
        return None;
    }
    return Some([
        data[offset],
        data[offset + 1],
        data[offset + 2],
        data[offset + 3],
        data[offset + 4],
        data[offset + 5],
        data[offset + 6],
        data[offset + 7],
    ]);
}

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

fn read_borsh_string(
    data: &[u8],
    offset: usize,
) -> std::option::Option<(std::string::String, usize)> {
    if data.len() < offset + 4 {
        return None;
    }
    let len =
        u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]);
    let len_usize = len as usize;
    let start = offset + 4;
    let end = start.saturating_add(len_usize);
    if data.len() < end {
        return None;
    }
    let text_result = std::str::from_utf8(&data[start..end]);
    let text = match text_result {
        Ok(text) => text,
        Err(_) => return None,
    };
    return Some((text.to_string(), end));
}

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

fn insert_optional_string(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: std::option::Option<std::string::String>,
) {
    if let Some(value) = value {
        object.insert(key.to_string(), serde_json::Value::String(value));
    }
}

fn insert_optional_i64(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: std::option::Option<i64>,
) {
    if let Some(value) = value {
        object.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value)));
    }
}

fn insert_optional_u32(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: std::option::Option<u32>,
) {
    if let Some(value) = value {
        object.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value)));
    }
}

fn insert_optional_value(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: std::option::Option<serde_json::Value>,
) {
    if let Some(value) = value {
        object.insert(key.to_string(), value);
    }
}

fn resolve_pool_account(
    parsed_json: std::option::Option<&serde_json::Value>,
    accounts: &[std::string::String],
    layout: PumpSwapAccountLayout,
) -> std::option::Option<std::string::String> {
    let parsed = extract_string_by_candidate_keys(
        parsed_json,
        &["pool", "poolAccount", "amm", "ammPool", "poolState"],
    );
    if parsed.is_some() {
        return parsed;
    }
    return account_at(accounts, layout.pool_index);
}

fn resolve_token_a_mint(
    parsed_json: std::option::Option<&serde_json::Value>,
    accounts: &[std::string::String],
    layout: PumpSwapAccountLayout,
) -> std::option::Option<std::string::String> {
    let parsed = extract_string_by_candidate_keys(
        parsed_json,
        &["tokenAMint", "baseMint", "mintA", "coinMint", "token0Mint", "mint0"],
    );
    if parsed.is_some() {
        return parsed;
    }
    return account_at(accounts, layout.token_a_mint_index);
}

fn resolve_token_b_mint(
    parsed_json: std::option::Option<&serde_json::Value>,
    accounts: &[std::string::String],
    layout: PumpSwapAccountLayout,
) -> std::option::Option<std::string::String> {
    let parsed = extract_string_by_candidate_keys(
        parsed_json,
        &["tokenBMint", "quoteMint", "mintB", "pcMint", "token1Mint", "mint1"],
    );
    if parsed.is_some() {
        return parsed;
    }
    return account_at(accounts, layout.token_b_mint_index);
}

fn resolve_lp_mint(
    parsed_json: std::option::Option<&serde_json::Value>,
    accounts: &[std::string::String],
    layout: PumpSwapAccountLayout,
) -> std::option::Option<std::string::String> {
    let parsed = extract_string_by_candidate_keys(
        parsed_json,
        &["poolV2", "pool_v2", "lpMint", "lp_mint", "poolTokenMint"],
    );
    if parsed.is_some() {
        return parsed;
    }
    return account_at(accounts, layout.lp_mint_index);
}

fn account_at(
    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,
    };
    if index >= accounts.len() {
        return None;
    }
    return Some(accounts[index].clone());
}

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 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 = extract_account_value_text(&value);
        if let Some(text) = text_option {
            accounts.push(text);
        }
    }
    return Ok(accounts);
}

fn extract_account_value_text(
    value: &serde_json::Value,
) -> std::option::Option<std::string::String> {
    if let Some(text) = value.as_str() {
        return Some(text.to_string());
    }
    if let Some(object) = value.as_object() {
        for key in ["pubkey", "account", "address", "mint"] {
            let field = object.get(key);
            if let Some(field) = field {
                if let Some(text) = field.as_str() {
                    return Some(text.to_string());
                }
            }
        }
    }
    return None;
}

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 parse_pump_swap_instruction_data(
    data_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<PumpSwapInstructionData>, crate::Error> {
    let data_json = match data_json {
        Some(data_json) => data_json,
        None => return Ok(None),
    };
    let data_text_result = parse_instruction_data_text(data_json.as_str());
    let data_text = match data_text_result {
        Ok(data_text) => data_text,
        Err(error) => return Err(error),
    };
    if data_text.trim().is_empty() {
        return Ok(None);
    }
    let decoded_result = bs58::decode(data_text.trim()).into_vec();
    let decoded = match decoded_result {
        Ok(decoded) => decoded,
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot base58-decode PumpSwap instruction data '{}': {}",
                data_text, error
            )));
        },
    };
    if decoded.len() < 8 {
        return Ok(None);
    }
    let discriminator = [
        decoded[0], decoded[1], decoded[2], decoded[3], decoded[4], decoded[5], decoded[6],
        decoded[7],
    ];
    let instruction_kind_option = pump_swap_instruction_kind_from_discriminator(discriminator);
    let instruction_kind = match instruction_kind_option {
        Some(instruction_kind) => instruction_kind,
        None => return Ok(None),
    };
    let discriminator_hex = bytes_to_lower_hex(&discriminator);
    let args_json = decode_instruction_args(instruction_kind, decoded.as_slice());
    return Ok(Some(PumpSwapInstructionData {
        instruction_kind,
        discriminator_hex,
        args_json,
    }));
}

fn pump_swap_instruction_kind_from_discriminator(
    discriminator: [u8; 8],
) -> std::option::Option<PumpSwapInstructionKind> {
    if discriminator == PUMP_SWAP_BUY_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::Buy);
    }
    if discriminator == PUMP_SWAP_SELL_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::Sell);
    }
    if discriminator == PUMP_SWAP_BUY_EXACT_QUOTE_IN_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::BuyExactQuoteIn);
    }
    if discriminator == PUMP_SWAP_ADMIN_SET_COIN_CREATOR_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::AdminSetCoinCreator);
    }
    if discriminator == PUMP_SWAP_ADMIN_UPDATE_TOKEN_INCENTIVES_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::AdminUpdateTokenIncentives);
    }
    if discriminator == PUMP_SWAP_CLAIM_CASHBACK_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::ClaimCashback);
    }
    if discriminator == PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::ClaimTokenIncentives);
    }
    if discriminator == PUMP_SWAP_CLOSE_USER_VOLUME_ACCUMULATOR_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::CloseUserVolumeAccumulator);
    }
    if discriminator == PUMP_SWAP_COLLECT_COIN_CREATOR_FEE_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::CollectCoinCreatorFee);
    }
    if discriminator == PUMP_SWAP_CREATE_CONFIG_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::CreateConfig);
    }
    if discriminator == PUMP_SWAP_CREATE_POOL_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::CreatePool);
    }
    if discriminator == PUMP_SWAP_DEPOSIT_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::Deposit);
    }
    if discriminator == PUMP_SWAP_DISABLE_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::Disable);
    }
    if discriminator == PUMP_SWAP_EXTEND_ACCOUNT_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::ExtendAccount);
    }
    if discriminator == PUMP_SWAP_INIT_USER_VOLUME_ACCUMULATOR_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::InitUserVolumeAccumulator);
    }
    if discriminator == PUMP_SWAP_MIGRATE_POOL_COIN_CREATOR_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::MigratePoolCoinCreator);
    }
    if discriminator == PUMP_SWAP_SET_COIN_CREATOR_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::SetCoinCreator);
    }
    if discriminator == PUMP_SWAP_SET_RESERVED_FEE_RECIPIENTS_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::SetReservedFeeRecipients);
    }
    if discriminator == PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::SyncUserVolumeAccumulator);
    }
    if discriminator == PUMP_SWAP_TOGGLE_CASHBACK_ENABLED_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::ToggleCashbackEnabled);
    }
    if discriminator == PUMP_SWAP_TOGGLE_MAYHEM_MODE_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::ToggleMayhemMode);
    }
    if discriminator == PUMP_SWAP_TRANSFER_CREATOR_FEES_TO_PUMP_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::TransferCreatorFeesToPump);
    }
    if discriminator == PUMP_SWAP_UPDATE_ADMIN_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::UpdateAdmin);
    }
    if discriminator == PUMP_SWAP_UPDATE_FEE_CONFIG_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::UpdateFeeConfig);
    }
    if discriminator == PUMP_SWAP_WITHDRAW_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::Withdraw);
    }
    if discriminator == PUMP_SWAP_TRANSFER_CREATOR_FEES_TO_PUMP_V2_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::TransferCreatorFeesToPumpV2);
    }
    if discriminator == PUMP_SWAP_UPDATE_BUYBACK_CONFIG_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::UpdateBuybackConfig);
    }
    if discriminator == PUMP_SWAP_SET_RESERVED_FEE_RECIPIENT_DISCRIMINATOR {
        return Some(PumpSwapInstructionKind::SetReservedFeeRecipient);
    }
    return None;
}

fn decode_instruction_args(kind: PumpSwapInstructionKind, data: &[u8]) -> serde_json::Value {
    let mut object = serde_json::Map::new();
    let raw_u64_args = decode_u64_arg_array(data);
    object.insert("rawU64Args".to_string(), raw_u64_args);
    match kind {
        PumpSwapInstructionKind::Buy => {
            insert_optional_string(
                &mut object,
                "baseAmountOutRaw",
                decode_u64_le_as_string(data, 8),
            );
            insert_optional_string(
                &mut object,
                "maxQuoteAmountInRaw",
                decode_u64_le_as_string(data, 16),
            );
            insert_optional_bool(&mut object, "trackVolume", decode_anchor_option_bool(data, 24));
        },
        PumpSwapInstructionKind::Sell => {
            insert_optional_string(
                &mut object,
                "baseAmountInRaw",
                decode_u64_le_as_string(data, 8),
            );
            insert_optional_string(
                &mut object,
                "minQuoteAmountOutRaw",
                decode_u64_le_as_string(data, 16),
            );
            insert_optional_bool(&mut object, "trackVolume", decode_anchor_option_bool(data, 24));
        },
        PumpSwapInstructionKind::BuyExactQuoteIn => {
            insert_optional_string(
                &mut object,
                "spendableQuoteAmountInRaw",
                decode_u64_le_as_string(data, 8),
            );
            insert_optional_string(
                &mut object,
                "minBaseAmountOutRaw",
                decode_u64_le_as_string(data, 16),
            );
            insert_optional_bool(&mut object, "trackVolume", decode_anchor_option_bool(data, 24));
        },
        PumpSwapInstructionKind::CreatePool => {
            insert_optional_string(&mut object, "poolIndexRaw", decode_u16_le_as_string(data, 8));
            insert_optional_string(
                &mut object,
                "baseAmountInRaw",
                decode_u64_le_as_string(data, 10),
            );
            insert_optional_string(
                &mut object,
                "quoteAmountInRaw",
                decode_u64_le_as_string(data, 18),
            );
        },
        PumpSwapInstructionKind::Deposit => {
            insert_optional_string(
                &mut object,
                "lpTokenAmountOutRaw",
                decode_u64_le_as_string(data, 8),
            );
            insert_optional_string(
                &mut object,
                "maxBaseAmountInRaw",
                decode_u64_le_as_string(data, 16),
            );
            insert_optional_string(
                &mut object,
                "maxQuoteAmountInRaw",
                decode_u64_le_as_string(data, 24),
            );
        },
        PumpSwapInstructionKind::Withdraw => {
            insert_optional_string(
                &mut object,
                "lpTokenAmountInRaw",
                decode_u64_le_as_string(data, 8),
            );
            insert_optional_string(
                &mut object,
                "minBaseAmountOutRaw",
                decode_u64_le_as_string(data, 16),
            );
            insert_optional_string(
                &mut object,
                "minQuoteAmountOutRaw",
                decode_u64_le_as_string(data, 24),
            );
        },
        PumpSwapInstructionKind::CreateConfig => {
            insert_optional_string(
                &mut object,
                "lpFeeBasisPointsRaw",
                decode_u64_le_as_string(data, 8),
            );
            insert_optional_string(
                &mut object,
                "protocolFeeBasisPointsRaw",
                decode_u64_le_as_string(data, 16),
            );
        },
        PumpSwapInstructionKind::UpdateFeeConfig => {
            insert_optional_string(
                &mut object,
                "lpFeeBasisPointsRaw",
                decode_u64_le_as_string(data, 8),
            );
            insert_optional_string(
                &mut object,
                "protocolFeeBasisPointsRaw",
                decode_u64_le_as_string(data, 16),
            );
        },
        PumpSwapInstructionKind::UpdateBuybackConfig => {
            insert_optional_string(
                &mut object,
                "buybackConfigRaw",
                decode_u64_le_as_string(data, 8),
            );
        },
        PumpSwapInstructionKind::SetReservedFeeRecipient => {
            insert_optional_string(
                &mut object,
                "rawPubkeyArg0",
                decode_pubkey32_as_string(data, 8),
            );
            insert_optional_string(
                &mut object,
                "rawPubkeyArg1",
                decode_pubkey32_as_string(data, 40),
            );
        },
        _ => {},
    }
    return serde_json::Value::Object(object);
}

fn insert_optional_bool(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: std::option::Option<bool>,
) {
    if let Some(value) = value {
        object.insert(key.to_string(), serde_json::Value::Bool(value));
    }
}

fn decode_u64_arg_array(data: &[u8]) -> serde_json::Value {
    let mut values = std::vec::Vec::new();
    let mut offset = 8_usize;
    while offset + 8 <= data.len() {
        let value = decode_u64_le_as_string(data, offset);
        if let Some(value) = value {
            values.push(serde_json::Value::String(value));
        }
        offset += 8;
    }
    return serde_json::Value::Array(values);
}

fn parse_instruction_data_text(data_json: &str) -> Result<std::string::String, crate::Error> {
    let trimmed = data_json.trim();
    if trimmed.starts_with('"') {
        let parsed_result = serde_json::from_str::<std::string::String>(trimmed);
        return match parsed_result {
            Ok(parsed) => Ok(parsed),
            Err(error) => Err(crate::Error::Json(format!(
                "cannot parse instruction data JSON string '{}': {}",
                data_json, error
            ))),
        };
    }
    return Ok(trimmed.to_string());
}

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

fn decode_u64_le_as_string(data: &[u8], offset: usize) -> std::option::Option<std::string::String> {
    if data.len() < offset + 8 {
        return None;
    }
    let value = u64::from_le_bytes([
        data[offset],
        data[offset + 1],
        data[offset + 2],
        data[offset + 3],
        data[offset + 4],
        data[offset + 5],
        data[offset + 6],
        data[offset + 7],
    ]);
    return Some(value.to_string());
}

fn decode_i64_le_as_string(data: &[u8], offset: usize) -> std::option::Option<std::string::String> {
    if data.len() < offset + 8 {
        return None;
    }
    let value = i64::from_le_bytes([
        data[offset],
        data[offset + 1],
        data[offset + 2],
        data[offset + 3],
        data[offset + 4],
        data[offset + 5],
        data[offset + 6],
        data[offset + 7],
    ]);
    return Some(value.to_string());
}

fn decode_pubkey32_as_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 decode_anchor_option_bool(data: &[u8], offset: usize) -> std::option::Option<bool> {
    if data.len() < offset + 2 {
        return None;
    }
    if data[offset] == 0 {
        return None;
    }
    return Some(data[offset + 1] != 0);
}

fn bytes_to_lower_hex(bytes: &[u8]) -> std::string::String {
    let mut text = std::string::String::new();
    for byte in bytes {
        push_hex_nibble(&mut text, byte >> 4);
        push_hex_nibble(&mut text, byte & 15);
    }
    return text;
}

fn push_hex_nibble(text: &mut std::string::String, value: u8) {
    let byte = if value < 10 { b'0' + value } else { b'a' + value - 10 };
    text.push(char::from(byte));
}

#[cfg(test)]
mod tests {
    fn make_pump_swap_buy_instruction_data_json() -> std::string::String {
        return serde_json::json!("AJTQ2h9DXrBfqJi53PDQG2Fvki5tkaTU3").to_string();
    }

    fn make_pump_swap_buy_exact_quote_in_instruction_data_json() -> std::string::String {
        let mut data = std::vec::Vec::new();
        data.extend_from_slice(&[198, 46, 21, 82, 180, 217, 232, 112]);
        data.extend_from_slice(&123_u64.to_le_bytes());
        data.extend_from_slice(&45_u64.to_le_bytes());
        return serde_json::json!(bs58::encode(data).into_string()).to_string();
    }

    fn make_pump_swap_deposit_instruction_data_json() -> std::string::String {
        let mut data = std::vec::Vec::new();
        data.extend_from_slice(&[242, 35, 198, 137, 82, 225, 242, 182]);
        data.extend_from_slice(&11_u64.to_le_bytes());
        data.extend_from_slice(&22_u64.to_le_bytes());
        data.extend_from_slice(&33_u64.to_le_bytes());
        return serde_json::json!(bs58::encode(data).into_string()).to_string();
    }

    fn make_transaction_with_buy_log() -> crate::ChainTransactionDto {
        let mut dto = crate::ChainTransactionDto::new(
            "sig-pump-swap-test-1".to_string(),
            Some(777002),
            Some(1779200002),
            Some("helius_primary_http".to_string()),
            Some("0".to_string()),
            None,
            None,
            serde_json::json!({
                "slot": 777002,
                "meta": {
                    "logMessages": [
                        "Program log: Instruction: Buy"
                    ]
                },
                "transaction": {
                    "message": {
                        "instructions": []
                    }
                }
            })
            .to_string(),
        );
        dto.id = Some(92);
        return dto;
    }

    fn make_instruction() -> crate::ChainInstructionDto {
        let mut dto = crate::ChainInstructionDto::new(
            92,
            None,
            0,
            None,
            Some(crate::PUMP_SWAP_PROGRAM_ID.to_string()),
            Some("pump-amm".to_string()),
            Some(1),
            serde_json::json!([
                "PumpPool111",
                "User111",
                "GlobalConfig111",
                "TokenA111",
                "TokenB111",
                "UserBaseAta111",
                "UserQuoteAta111",
                "PoolBaseVault111",
                "PoolQuoteVault111",
                "ProtocolFee111",
                "ProtocolFeeAta111",
                "BaseProgram111",
                "QuoteProgram111",
                "System111",
                "Assoc111",
                "EventAuth111",
                crate::PUMP_SWAP_PROGRAM_ID,
                "CoinCreatorVaultAta111",
                "CoinCreatorVaultAuth111",
                "GlobalVolumeAccumulator111",
                "UserVolumeAccumulator111"
            ])
            .to_string(),
            Some(make_pump_swap_buy_instruction_data_json()),
            None,
            Some(
                serde_json::json!({
                    "info": {
                        "pool": "PumpPool111",
                        "baseMint": "TokenA111",
                        "quoteMint": "TokenB111",
                        "poolV2": "PoolV2_111"
                    }
                })
                .to_string(),
            ),
        );
        dto.id = Some(18);
        return dto;
    }

    #[test]
    fn pump_swap_buy_is_detected() {
        let decoder = crate::PumpSwapDecoder::new();
        let transaction = make_transaction_with_buy_log();
        let instructions = vec![make_instruction()];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::PumpSwapDecodedEvent::BuyTrade(event) => {
                assert_eq!(event.transaction_id, 92);
                assert_eq!(event.instruction_id, 18);
                assert_eq!(event.pool_account, Some("PumpPool111".to_string()));
                assert_eq!(event.token_a_mint, Some("TokenA111".to_string()));
                assert_eq!(event.token_b_mint, Some("TokenB111".to_string()));
                assert_eq!(event.pool_v2, Some("PoolV2_111".to_string()));
                assert_eq!(
                    event.payload_json.get("poolBaseTokenAccount"),
                    Some(&serde_json::Value::String("PoolBaseVault111".to_string()))
                );
                assert_eq!(
                    event.payload_json.get("poolQuoteTokenAccount"),
                    Some(&serde_json::Value::String("PoolQuoteVault111".to_string()))
                );
                assert_eq!(
                    event.payload_json.get("globalVolumeAccumulator"),
                    Some(&serde_json::Value::String("GlobalVolumeAccumulator111".to_string()))
                );
                assert_eq!(event.trade_side, crate::SwapTradeSide::BuyBase);
            },
            _ => {
                panic!("unexpected event")
            },
        }
    }

    #[test]
    fn pump_swap_buy_exact_quote_in_is_decoded_only_until_exact_amounts_are_proven() {
        let decoder = crate::PumpSwapDecoder::new();
        let transaction = make_transaction_with_buy_log();
        let mut instruction = make_instruction();
        instruction.data_json = Some(make_pump_swap_buy_exact_quote_in_instruction_data_json());
        let instructions = vec![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::PumpSwapDecodedEvent::Instruction(event) => {
                assert_eq!(event.event_kind, "pump_swap.buy_exact_quote_in");
                assert_eq!(
                    event.payload_json.get("tradeCandidate"),
                    Some(&serde_json::Value::Bool(false))
                );
                assert_eq!(
                    event.payload_json.get("skipTradeReason"),
                    Some(&serde_json::Value::String(
                        "pump_swap_buy_exact_quote_in_exact_amounts_not_proven".to_string()
                    ))
                );
            },
            _ => panic!("unexpected event"),
        }
    }

    fn make_pump_swap_anchor_buy_event_base64(ix_name: &str) -> std::string::String {
        let mut data = std::vec::Vec::new();
        data.extend_from_slice(&super::PUMP_SWAP_BUY_EVENT_DISCRIMINATOR);
        data.extend_from_slice(&1700000000_i64.to_le_bytes());
        data.extend_from_slice(&45_u64.to_le_bytes());
        data.extend_from_slice(&123_u64.to_le_bytes());
        data.extend_from_slice(&1000_u64.to_le_bytes());
        data.extend_from_slice(&2000_u64.to_le_bytes());
        data.extend_from_slice(&3000_u64.to_le_bytes());
        data.extend_from_slice(&4000_u64.to_le_bytes());
        data.extend_from_slice(&120_u64.to_le_bytes());
        data.extend_from_slice(&30_u64.to_le_bytes());
        data.extend_from_slice(&3_u64.to_le_bytes());
        data.extend_from_slice(&20_u64.to_le_bytes());
        data.extend_from_slice(&2_u64.to_le_bytes());
        data.extend_from_slice(&123_u64.to_le_bytes());
        data.extend_from_slice(&125_u64.to_le_bytes());
        append_pubkey_bytes(&mut data, 1);
        append_pubkey_bytes(&mut data, 2);
        append_pubkey_bytes(&mut data, 3);
        append_pubkey_bytes(&mut data, 4);
        append_pubkey_bytes(&mut data, 5);
        append_pubkey_bytes(&mut data, 6);
        append_pubkey_bytes(&mut data, 7);
        data.extend_from_slice(&10_u64.to_le_bytes());
        data.extend_from_slice(&1_u64.to_le_bytes());
        data.push(1);
        data.extend_from_slice(&8_u64.to_le_bytes());
        data.extend_from_slice(&9_u64.to_le_bytes());
        data.extend_from_slice(&10_u64.to_le_bytes());
        data.extend_from_slice(&1700000001_i64.to_le_bytes());
        data.extend_from_slice(&45_u64.to_le_bytes());
        let ix_name_bytes = ix_name.as_bytes();
        data.extend_from_slice(&(ix_name_bytes.len() as u32).to_le_bytes());
        data.extend_from_slice(ix_name_bytes);
        data.extend_from_slice(&11_u64.to_le_bytes());
        data.extend_from_slice(&12_u64.to_le_bytes());
        data.extend_from_slice(&13_u64.to_le_bytes());
        data.extend_from_slice(&14_u64.to_le_bytes());
        use base64::Engine as _;
        return base64::engine::general_purpose::STANDARD.encode(data);
    }

    fn append_pubkey_bytes(data: &mut std::vec::Vec<u8>, seed: u8) {
        for _ in 0..32 {
            data.push(seed);
        }
    }

    fn make_pump_swap_instruction_data_json_for_discriminator(
        discriminator: [u8; 8],
    ) -> std::string::String {
        let mut data = std::vec::Vec::new();
        data.extend_from_slice(&discriminator);
        return serde_json::json!(bs58::encode(data).into_string()).to_string();
    }

    fn make_pump_swap_anchor_event_base64_from_fields(
        discriminator: [u8; 8],
        fields: &[super::PumpSwapAnchorFieldDescriptor],
    ) -> std::string::String {
        let mut data = std::vec::Vec::new();
        data.extend_from_slice(&discriminator);
        let mut seed = 11_u8;
        let mut scalar = 1000_u64;
        for field in fields {
            append_anchor_field_value(&mut data, field.kind, seed, scalar);
            seed = seed.wrapping_add(1);
            scalar = scalar.saturating_add(17);
        }
        use base64::Engine as _;
        return base64::engine::general_purpose::STANDARD.encode(data);
    }

    fn append_anchor_field_value(
        data: &mut std::vec::Vec<u8>,
        kind: super::PumpSwapAnchorFieldKind,
        seed: u8,
        scalar: u64,
    ) {
        match kind {
            super::PumpSwapAnchorFieldKind::U8 => {
                data.push(seed);
            },
            super::PumpSwapAnchorFieldKind::U16 => {
                data.extend_from_slice(&(u16::from(seed) + 257_u16).to_le_bytes());
            },
            super::PumpSwapAnchorFieldKind::U64 => {
                data.extend_from_slice(&scalar.to_le_bytes());
            },
            super::PumpSwapAnchorFieldKind::I64 => {
                let value = 1_700_000_000_i64 + i64::from(seed);
                data.extend_from_slice(&value.to_le_bytes());
            },
            super::PumpSwapAnchorFieldKind::Bool => {
                data.push(seed % 2);
            },
            super::PumpSwapAnchorFieldKind::Pubkey => {
                append_pubkey_bytes(data, seed);
            },
            super::PumpSwapAnchorFieldKind::PubkeyArray(count) => {
                let mut current_seed = seed;
                for _ in 0..count {
                    append_pubkey_bytes(data, current_seed);
                    current_seed = current_seed.wrapping_add(1);
                }
            },
        }
    }

    fn make_transaction_with_program_data_log(
        encoded: std::string::String,
    ) -> crate::ChainTransactionDto {
        let mut dto = make_transaction_with_buy_log();
        dto.transaction_json = serde_json::json!({
            "slot": 777002,
            "meta": {
                "logMessages": [
                    format!("Program data: {}", encoded)
                ]
            },
            "transaction": {
                "message": {
                    "instructions": []
                }
            }
        })
        .to_string();
        return dto;
    }

    fn decoded_instruction_event_kind(
        decoder: &crate::PumpSwapDecoder,
        discriminator: [u8; 8],
    ) -> std::string::String {
        let transaction = make_transaction_with_buy_log();
        let mut instruction = make_instruction();
        instruction.data_json =
            Some(make_pump_swap_instruction_data_json_for_discriminator(discriminator));
        let instructions = vec![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::PumpSwapDecodedEvent::Instruction(event) => return event.event_kind.clone(),
            _ => panic!("expected decoded PumpSwap instruction"),
        }
    }

    #[test]
    fn pump_swap_synthetic_unobserved_instruction_discriminators_decode_by_idl() {
        let decoder = crate::PumpSwapDecoder::new();
        let cases = [
            (
                super::PUMP_SWAP_ADMIN_UPDATE_TOKEN_INCENTIVES_DISCRIMINATOR,
                "pump_swap.admin_update_token_incentives",
            ),
            (super::PUMP_SWAP_DISABLE_DISCRIMINATOR, "pump_swap.disable"),
        ];
        for case in cases {
            let event_kind = decoded_instruction_event_kind(&decoder, case.0);
            assert_eq!(event_kind, case.1);
        }
    }

    #[test]
    fn pump_swap_synthetic_unobserved_anchor_events_decode_by_idl_layout() {
        let cases = [
            (
                super::PUMP_SWAP_ADMIN_UPDATE_TOKEN_INCENTIVES_EVENT_DISCRIMINATOR,
                super::PUMP_SWAP_ADMIN_UPDATE_TOKEN_INCENTIVES_EVENT_FIELDS,
                "admin_update_token_incentives_event",
                "pump_swap.admin_update_token_incentives_event",
                "token_supply_per_day",
            ),
            (
                super::PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_EVENT_DISCRIMINATOR,
                super::PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_EVENT_FIELDS,
                "claim_token_incentives_event",
                "pump_swap.claim_token_incentives_event",
                "amount",
            ),
            (
                super::PUMP_SWAP_CREATE_CONFIG_EVENT_DISCRIMINATOR,
                super::PUMP_SWAP_CREATE_CONFIG_EVENT_FIELDS,
                "create_config_event",
                "pump_swap.create_config_event",
                "protocol_fee_recipients",
            ),
            (
                super::PUMP_SWAP_DISABLE_EVENT_DISCRIMINATOR,
                super::PUMP_SWAP_DISABLE_EVENT_FIELDS,
                "disable_event",
                "pump_swap.disable_event",
                "disable_buy",
            ),
            (
                super::PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_DISCRIMINATOR,
                super::PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS,
                "sync_user_volume_accumulator_event",
                "pump_swap.sync_user_volume_accumulator_event",
                "total_claimed_tokens_before",
            ),
        ];
        for case in cases {
            let encoded = make_pump_swap_anchor_event_base64_from_fields(case.0, case.1);
            let decoded_option = super::decode_pump_swap_anchor_event_from_base64(encoded.as_str());
            let decoded = match decoded_option {
                Some(decoded) => decoded,
                None => panic!("synthetic anchor event must decode: {}", case.2),
            };
            assert_eq!(decoded.event_name, case.2);
            assert_eq!(decoded.event_kind, case.3);
            assert!(decoded.payload_json.get(case.4).is_some());
        }
    }

    #[test]
    fn pump_swap_claim_token_incentives_anchor_event_is_materializable_reward() {
        let decoder = crate::PumpSwapDecoder::new();
        let encoded = make_pump_swap_anchor_event_base64_from_fields(
            super::PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_EVENT_DISCRIMINATOR,
            super::PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_EVENT_FIELDS,
        );
        let transaction = make_transaction_with_program_data_log(encoded);
        let mut instruction = make_instruction();
        instruction.data_json = Some(make_pump_swap_instruction_data_json_for_discriminator(
            super::PUMP_SWAP_CLAIM_TOKEN_INCENTIVES_DISCRIMINATOR,
        ));
        let instructions = vec![instruction];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        let mut found_anchor_event = false;
        for event in decoded {
            if let crate::PumpSwapDecodedEvent::Instruction(event) = event {
                if event.event_kind == "pump_swap.claim_token_incentives_event" {
                    found_anchor_event = true;
                    assert_eq!(
                        event.payload_json.get("anchorEventAuditOnly"),
                        Some(&serde_json::Value::Bool(false))
                    );
                    assert!(event.payload_json.get("rewardTokenMint").is_some());
                    assert!(event.payload_json.get("rewardAmountRaw").is_some());
                }
            }
        }
        assert!(found_anchor_event);
    }

    #[test]
    fn pump_swap_sync_user_volume_accumulator_anchor_event_is_materializable_reward_when_present() {
        let decoder = crate::PumpSwapDecoder::new();
        let encoded = make_pump_swap_anchor_event_base64_from_fields(
            super::PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_DISCRIMINATOR,
            super::PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_EVENT_FIELDS,
        );
        let transaction = make_transaction_with_program_data_log(encoded);
        let mut instruction = make_instruction();
        instruction.data_json = Some(make_pump_swap_instruction_data_json_for_discriminator(
            super::PUMP_SWAP_SYNC_USER_VOLUME_ACCUMULATOR_DISCRIMINATOR,
        ));
        let instructions = vec![instruction];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        let mut found_anchor_event = false;
        for event in decoded {
            if let crate::PumpSwapDecodedEvent::Instruction(event) = event {
                if event.event_kind == "pump_swap.sync_user_volume_accumulator_event" {
                    found_anchor_event = true;
                    assert_eq!(
                        event.payload_json.get("anchorEventAuditOnly"),
                        Some(&serde_json::Value::Bool(false))
                    );
                    assert_eq!(event.payload_json.get("skipCatalogReason"), None);
                    assert_eq!(event.payload_json.get("skipRewardReason"), None);
                }
            }
        }
        assert!(found_anchor_event);
    }

    fn make_transaction_with_anchor_buy_event_log() -> crate::ChainTransactionDto {
        let mut dto = make_transaction_with_buy_log();
        let event_base64 = make_pump_swap_anchor_buy_event_base64("buy_exact_quote_in");
        dto.transaction_json = serde_json::json!({
            "slot": 777002,
            "meta": {
                "logMessages": [
                    "Program log: Instruction: BuyExactQuoteIn",
                    format!("Program data: {}", event_base64)
                ]
            },
            "transaction": {
                "message": {
                    "instructions": []
                }
            }
        })
        .to_string();
        return dto;
    }

    #[test]
    fn pump_swap_buy_exact_quote_in_uses_anchor_buy_event_when_available() {
        let decoder = crate::PumpSwapDecoder::new();
        let transaction = make_transaction_with_anchor_buy_event_log();
        let mut instruction = make_instruction();
        instruction.data_json = Some(make_pump_swap_buy_exact_quote_in_instruction_data_json());
        let anchor_pool = bs58::encode([1_u8; 32]).into_string();
        instruction.accounts_json = serde_json::json!([
            anchor_pool,
            "User111",
            "GlobalConfig111",
            "TokenA111",
            "TokenB111",
            "UserBaseAta111",
            "UserQuoteAta111",
            "PoolBaseVault111",
            "PoolQuoteVault111",
            "ProtocolFee111",
            "ProtocolFeeAta111",
            "BaseProgram111",
            "QuoteProgram111",
            "System111",
            "Assoc111",
            "EventAuth111",
            crate::PUMP_SWAP_PROGRAM_ID,
            "CoinCreatorVaultAta111",
            "CoinCreatorVaultAuth111",
            "GlobalVolumeAccumulator111",
            "UserVolumeAccumulator111"
        ])
        .to_string();
        instruction.parsed_json = None;
        let instructions = vec![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(), 2);
        let mut found_buy_exact_trade = false;
        let mut found_buy_anchor_event = false;
        for decoded_event in &decoded {
            match decoded_event {
                crate::PumpSwapDecodedEvent::BuyExactQuoteInTrade(event) => {
                    found_buy_exact_trade = true;
                    assert_eq!(event.trade_side, crate::SwapTradeSide::BuyBase);
                    assert_eq!(
                        event.payload_json.get("amountSource"),
                        Some(&serde_json::Value::String("pump_swap_anchor_buy_event".to_string()))
                    );
                    assert_eq!(
                        event.payload_json.get("baseAmountRaw"),
                        Some(&serde_json::Value::String("45".to_string()))
                    );
                    assert_eq!(
                        event.payload_json.get("quoteAmountRaw"),
                        Some(&serde_json::Value::String("125".to_string()))
                    );
                    assert_eq!(event.payload_json.get("skipTradeReason"), None);
                },
                crate::PumpSwapDecodedEvent::Instruction(event) => {
                    if event.event_kind == "pump_swap.buy_event" {
                        found_buy_anchor_event = true;
                        assert_eq!(
                            event.payload_json.get("anchorEventAuditOnly"),
                            Some(&serde_json::Value::Bool(false))
                        );
                    }
                },
                _ => {},
            }
        }
        assert!(found_buy_exact_trade);
        assert!(found_buy_anchor_event);
    }

    #[test]
    fn pump_swap_deposit_is_specialized_non_trade() {
        let decoder = crate::PumpSwapDecoder::new();
        let transaction = make_transaction_with_buy_log();
        let mut instruction = make_instruction();
        instruction.data_json = Some(make_pump_swap_deposit_instruction_data_json());
        let instructions = vec![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::PumpSwapDecodedEvent::Instruction(event) => {
                assert_eq!(event.event_kind, "pump_swap.deposit");
                assert_eq!(
                    event.payload_json.get("tradeCandidate"),
                    Some(&serde_json::Value::Bool(false))
                );
            },
            _ => panic!("unexpected event"),
        }
    }

    #[test]
    fn pump_swap_transfer_creator_fees_to_pump_v2_discriminator_is_decoded_fee() {
        let decoder = crate::PumpSwapDecoder::new();
        let transaction = make_transaction_with_buy_log();
        let mut instruction = make_instruction();
        instruction.data_json = Some(
            serde_json::json!(
                bs58::encode(super::PUMP_SWAP_TRANSFER_CREATOR_FEES_TO_PUMP_V2_DISCRIMINATOR)
                    .into_string()
            )
            .to_string(),
        );
        let instructions = vec![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::PumpSwapDecodedEvent::Instruction(event) => {
                assert_eq!(event.event_kind, "pump_swap.transfer_creator_fees_to_pump_v2");
                assert_eq!(
                    event.payload_json.get("skipTradeReason"),
                    Some(&serde_json::Value::String(
                        "pump_swap_non_trade_or_incomplete_trade_instruction".to_string()
                    ))
                );
            },
            _ => panic!("expected decoded PumpSwap fee instruction"),
        }
    }

    #[test]
    fn pump_swap_returns_none_without_known_discriminator() {
        let decoder = crate::PumpSwapDecoder::new();
        let transaction = make_transaction_with_buy_log();
        let mut instruction = make_instruction();
        instruction.data_json = None;
        let instructions = vec![instruction];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 0);
    }

    #[test]
    fn pump_swap_inner_buy_is_detected() {
        let decoder = crate::PumpSwapDecoder::new();
        let transaction = make_transaction_with_buy_log();
        let mut instruction = make_instruction();
        instruction.parent_instruction_id = Some(17);
        instruction.inner_instruction_index = Some(0);
        let instructions = vec![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::PumpSwapDecodedEvent::BuyTrade(event) => {
                assert_eq!(event.transaction_id, 92);
                assert_eq!(event.instruction_id, 18);
                assert_eq!(event.pool_account, Some("PumpPool111".to_string()));
                assert_eq!(event.token_a_mint, Some("TokenA111".to_string()));
                assert_eq!(event.token_b_mint, Some("TokenB111".to_string()));
                assert_eq!(
                    event.payload_json.get("parentInstructionId"),
                    Some(&serde_json::Value::Number(serde_json::Number::from(17)))
                );
                assert_eq!(
                    event.payload_json.get("innerInstructionIndex"),
                    Some(&serde_json::Value::Number(serde_json::Number::from(0)))
                );
            },
            _ => {
                panic!("unexpected event")
            },
        }
    }
}
