// file: kb_lib/src/dex_decode.rs

//! Persistence-oriented DEX decoding service.

/// DEX decode service.
#[derive(Debug, Clone)]
pub struct DexDecodeService {
    database: std::sync::Arc<crate::Database>,
    persistence: crate::DetectionPersistenceService,
    raydium_amm_v4_decoder: crate::RaydiumAmmV4Decoder,
    pump_fun_decoder: crate::PumpFunDecoder,
    pump_swap_decoder: crate::PumpSwapDecoder,
    orca_whirlpools_decoder: crate::OrcaWhirlpoolsDecoder,
    meteora_dbc_decoder: crate::MeteoraDbcDecoder,
    meteora_dlmm_decoder: crate::MeteoraDlmmDecoder,
    meteora_damm_v1_decoder: crate::MeteoraDammV1Decoder,
    meteora_damm_v2_decoder: crate::MeteoraDammV2Decoder,
    fluxbeam_decoder: crate::FluxbeamDecoder,
    dexlab_decoder: crate::DexlabDecoder,
}

impl DexDecodeService {
    /// Creates a new DEX decode service.
    pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
        let persistence = crate::DetectionPersistenceService::new(database.clone());
        return Self {
            database,
            persistence,
            raydium_amm_v4_decoder: crate::RaydiumAmmV4Decoder::new(),
            pump_fun_decoder: crate::PumpFunDecoder::new(),
            pump_swap_decoder: crate::PumpSwapDecoder::new(),
            orca_whirlpools_decoder: crate::OrcaWhirlpoolsDecoder::new(),
            meteora_dbc_decoder: crate::MeteoraDbcDecoder::new(),
            meteora_dlmm_decoder: crate::MeteoraDlmmDecoder::new(),
            meteora_damm_v1_decoder: crate::MeteoraDammV1Decoder::new(),
            meteora_damm_v2_decoder: crate::MeteoraDammV2Decoder::new(),
            fluxbeam_decoder: crate::FluxbeamDecoder::new(),
            dexlab_decoder: crate::DexlabDecoder::new(),
        };
    }

    /// Decodes one projected transaction and persists the decoded events.
    pub async fn decode_transaction_by_signature(
        &self,
        signature: &str,
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let context_result = crate::dex_decode_context::load_dex_decode_transaction_context(
            self.database.as_ref(),
            signature,
        )
        .await;
        let context = match context_result {
            Ok(context) => context,
            Err(error) => return Err(error),
        };
        let transaction = context.transaction;
        let instructions = context.instructions;
        let mut persisted = std::vec::Vec::new();
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_raydium_amm_v4_events(&transaction, &instructions).await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_raydium_cpmm_events(&transaction, &instructions).await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_raydium_clmm_events(&transaction, &instructions).await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_pump_fun_events(&transaction, &instructions).await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_pump_swap_events(&transaction, &instructions).await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_meteora_dbc_events(&transaction, &instructions).await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_meteora_dlmm_events(&transaction, &instructions).await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_meteora_damm_v1_events(&transaction, &instructions)
                .await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_meteora_damm_v2_events(&transaction, &instructions)
                .await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_orca_whirlpools_events(&transaction, &instructions)
                .await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_fluxbeam_events(&transaction, &instructions).await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        let append_result = append_persisted_events_result(
            &mut persisted,
            self.decode_and_persist_dexlab_events(&transaction, &instructions).await,
        );
        if let Err(error) = append_result {
            return Err(error);
        }
        return Ok(persisted);
    }

    async fn materialize_named_dex_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        transaction_id: i64,
        instruction_id: i64,
        protocol_name: &str,
        program_id: std::string::String,
        event_kind: &str,
        pool_account: std::option::Option<std::string::String>,
        market_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>,
        payload_json: serde_json::Value,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        let input = crate::dex_decoded_event_materialization::DexDecodedEventMaterializationInput {
            database: self.database.as_ref(),
            persistence: &self.persistence,
            transaction,
            transaction_id,
            instruction_id: Some(instruction_id),
            protocol_name: protocol_name.to_string(),
            program_id,
            event_kind: event_kind.to_string(),
            pool_account,
            market_account,
            token_a_mint,
            token_b_mint,
            lp_mint,
            enrichment_payload_json: payload_json.clone(),
            observation_payload_json: payload_json,
            observation_kind: format!("dex.{event_kind}"),
            signal_kind: format!("signal.dex.{event_kind}"),
            missing_after_upsert_message: "decoded event disappeared after upsert".to_string(),
        };
        return crate::dex_decoded_event_materialization::materialize_dex_decoded_event(input)
            .await;
    }

    async fn persist_dexlab_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::DexlabDecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::DexlabDecodedEvent::CreatePool(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "dexlab",
                        event.program_id.clone(),
                        "dexlab.create_pool",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        None,
                        event.payload_json.clone(),
                    )
                    .await;
            },
            crate::DexlabDecodedEvent::Swap(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "dexlab",
                        event.program_id.clone(),
                        "dexlab.swap",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        None,
                        event.payload_json.clone(),
                    )
                    .await;
            },
        }
    }

    async fn persist_fluxbeam_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::FluxbeamDecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::FluxbeamDecodedEvent::CreatePool(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "fluxbeam",
                        event.program_id.clone(),
                        "fluxbeam.create_pool",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        event.lp_mint.clone(),
                        event.payload_json.clone(),
                    )
                    .await;
            },
            crate::FluxbeamDecodedEvent::Swap(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "fluxbeam",
                        event.program_id.clone(),
                        "fluxbeam.swap",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        None,
                        event.payload_json.clone(),
                    )
                    .await;
            },
        }
    }

    async fn persist_orca_whirlpools_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::OrcaWhirlpoolsDecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::OrcaWhirlpoolsDecodedEvent::CreatePool(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "orca_whirlpools",
                        event.program_id.clone(),
                        "orca_whirlpools.create_pool",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        event.config_account.clone(),
                        event.payload_json.clone(),
                    )
                    .await;
            },
            crate::OrcaWhirlpoolsDecodedEvent::Swap(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "orca_whirlpools",
                        event.program_id.clone(),
                        "orca_whirlpools.swap",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        None,
                        event.payload_json.clone(),
                    )
                    .await;
            },
        }
    }

    async fn persist_meteora_dbc_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::MeteoraDbcDecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::MeteoraDbcDecodedEvent::CreatePool(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "meteora_dbc",
                        event.program_id.clone(),
                        "meteora_dbc.create_pool",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        event.config_account.clone(),
                        event.payload_json.clone(),
                    )
                    .await;
            },
            crate::MeteoraDbcDecodedEvent::Swap(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "meteora_dbc",
                        event.program_id.clone(),
                        "meteora_dbc.swap",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        None,
                        event.payload_json.clone(),
                    )
                    .await;
            },
        }
    }

    async fn persist_meteora_dlmm_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::MeteoraDlmmDecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::MeteoraDlmmDecodedEvent::CreatePool(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "meteora_dlmm",
                        event.program_id.clone(),
                        "meteora_dlmm.create_pool",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        event.config_account.clone(),
                        event.payload_json.clone(),
                    )
                    .await;
            },
            crate::MeteoraDlmmDecodedEvent::Swap(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "meteora_dlmm",
                        event.program_id.clone(),
                        "meteora_dlmm.swap",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        None,
                        event.payload_json.clone(),
                    )
                    .await;
            },
        }
    }

    async fn persist_meteora_damm_v1_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::MeteoraDammV1DecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::MeteoraDammV1DecodedEvent::CreatePool(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "meteora_damm_v1",
                        event.program_id.clone(),
                        "meteora_damm_v1.create_pool",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        event.config_account.clone(),
                        event.payload_json.clone(),
                    )
                    .await;
            },
            crate::MeteoraDammV1DecodedEvent::Swap(event) => {
                let enrichment_payload_json =
                    prepare_meteora_damm_v1_swap_payload_for_classification(event);
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "meteora_damm_v1",
                        event.program_id.clone(),
                        "meteora_damm_v1.swap",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        None,
                        enrichment_payload_json,
                    )
                    .await;
            },
        }
    }

    async fn persist_meteora_damm_v2_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::MeteoraDammV2DecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::MeteoraDammV2DecodedEvent::CreatePool(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "meteora_damm_v2",
                        event.program_id.clone(),
                        "meteora_damm_v2.create_pool",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        event.config_account.clone(),
                        event.payload_json.clone(),
                    )
                    .await;
            },
            crate::MeteoraDammV2DecodedEvent::Swap(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "meteora_damm_v2",
                        event.program_id.clone(),
                        "meteora_damm_v2.swap",
                        event.pool_account.clone(),
                        None,
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        None,
                        event.payload_json.clone(),
                    )
                    .await;
            },
        }
    }

    async fn persist_raydium_amm_v4_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::RaydiumAmmV4DecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "raydium_amm_v4",
                        event.program_id.clone(),
                        "raydium_amm_v4.initialize2_pool",
                        event.pool_account.clone(),
                        event.market_account.clone(),
                        event.token_a_mint.clone(),
                        event.token_b_mint.clone(),
                        event.lp_mint.clone(),
                        event.payload_json.clone(),
                    )
                    .await;
            },
        }
    }

    async fn persist_raydium_clmm_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        instruction: &crate::ChainInstructionDto,
        decoded_event: &crate::RaydiumClmmDecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        let transaction_id = match transaction.id {
            Some(transaction_id) => transaction_id,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "transaction '{}' has no internal id",
                    transaction.signature
                )));
            },
        };
        let instruction_id = match instruction.id {
            Some(instruction_id) => instruction_id,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "raydium clmm instruction for transaction '{}' has no internal id",
                    transaction.signature
                )));
            },
        };
        let event_kind = decoded_event.event_kind().to_string();
        let raw_payload_json = match decoded_event.to_payload_json() {
            Some(payload_json) => payload_json,
            None => {
                return Err(crate::Error::Json(
                    "cannot serialize decoded raydium clmm payload".to_string(),
                ));
            },
        };
        let payload_value_result = enriched_raydium_payload_value(
            "raydium_clmm",
            event_kind.as_str(),
            raw_payload_json.as_str(),
        );
        let payload_value = match payload_value_result {
            Ok(payload_value) => payload_value,
            Err(error) => return Err(error),
        };
        return self
            .materialize_named_dex_event(
                transaction,
                transaction_id,
                instruction_id,
                "raydium_clmm",
                crate::RAYDIUM_CLMM_PROGRAM_ID.to_string(),
                event_kind.as_str(),
                Some(decoded_event.pool_account().to_string()),
                None,
                Some(decoded_event.base_mint().to_string()),
                Some(decoded_event.quote_mint().to_string()),
                None,
                payload_value,
            )
            .await;
    }

    async fn persist_raydium_cpmm_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        instruction: &crate::ChainInstructionDto,
        decoded_event: &crate::RaydiumCpmmDecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        let transaction_id = match transaction.id {
            Some(transaction_id) => transaction_id,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "transaction '{}' has no internal id",
                    transaction.signature
                )));
            },
        };
        let instruction_id = match instruction.id {
            Some(instruction_id) => instruction_id,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "raydium cpmm instruction for transaction '{}' has no internal id",
                    transaction.signature
                )));
            },
        };
        let event_kind = decoded_event.event_kind().to_string();
        let raw_payload_json = match decoded_event.to_payload_json() {
            Some(payload_json) => payload_json,
            None => {
                return Err(crate::Error::Json(
                    "cannot serialize decoded raydium cpmm payload".to_string(),
                ));
            },
        };
        let payload_value_result = enriched_raydium_payload_value(
            "raydium_cpmm",
            event_kind.as_str(),
            raw_payload_json.as_str(),
        );
        let payload_value = match payload_value_result {
            Ok(payload_value) => payload_value,
            Err(error) => return Err(error),
        };
        return self
            .materialize_named_dex_event(
                transaction,
                transaction_id,
                instruction_id,
                "raydium_cpmm",
                crate::RAYDIUM_CPMM_PROGRAM_ID.to_string(),
                event_kind.as_str(),
                Some(decoded_event.pool_account().to_string()),
                None,
                Some(decoded_event.base_mint().to_string()),
                Some(decoded_event.quote_mint().to_string()),
                None,
                payload_value,
            )
            .await;
    }

    async fn persist_pump_fun_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::PumpFunDecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::PumpFunDecodedEvent::CreateV2Token(event) => {
                return self
                    .materialize_named_dex_event(
                        transaction,
                        event.transaction_id,
                        event.instruction_id,
                        "pump_fun",
                        event.program_id.clone(),
                        "pump_fun.create_v2_token",
                        event.bonding_curve.clone(),
                        None,
                        event.mint.clone(),
                        Some(crate::WSOL_MINT_ID.to_string()),
                        event.associated_bonding_curve.clone(),
                        event.payload_json.clone(),
                    )
                    .await;
            },
            crate::PumpFunDecodedEvent::BuyTrade(event) => {
                return self
                    .persist_pump_fun_trade_event(
                        transaction,
                        event,
                        "pump_fun.buy",
                        "signal.dex.pump_fun.buy",
                        "dex.pump_fun.buy",
                    )
                    .await;
            },
            crate::PumpFunDecodedEvent::SellTrade(event) => {
                return self
                    .persist_pump_fun_trade_event(
                        transaction,
                        event,
                        "pump_fun.sell",
                        "signal.dex.pump_fun.sell",
                        "dex.pump_fun.sell",
                    )
                    .await;
            },
        }
    }

    async fn persist_pump_fun_trade_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        event: &crate::PumpFunTradeDecoded,
        event_kind: &str,
        signal_kind: &str,
        observation_kind: &str,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        let input = crate::dex_decoded_event_materialization::DexDecodedEventMaterializationInput {
            database: self.database.as_ref(),
            persistence: &self.persistence,
            transaction,
            transaction_id: event.transaction_id,
            instruction_id: Some(event.instruction_id),
            protocol_name: "pump_fun".to_string(),
            program_id: event.program_id.clone(),
            event_kind: event_kind.to_string(),
            pool_account: event.bonding_curve.clone(),
            market_account: None,
            token_a_mint: event.mint.clone(),
            token_b_mint: Some(crate::WSOL_MINT_ID.to_string()),
            lp_mint: event.associated_bonding_curve.clone(),
            enrichment_payload_json: event.payload_json.clone(),
            observation_payload_json: event.payload_json.clone(),
            observation_kind: observation_kind.to_string(),
            signal_kind: signal_kind.to_string(),
            missing_after_upsert_message: "decoded pump.fun trade event disappeared after upsert"
                .to_string(),
        };
        return crate::dex_decoded_event_materialization::materialize_dex_decoded_event(input)
            .await;
    }

    async fn persist_pump_swap_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        decoded_event: &crate::PumpSwapDecodedEvent,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        match decoded_event {
            crate::PumpSwapDecodedEvent::BuyTrade(event) => {
                return self
                    .persist_pump_swap_trade_event(
                        transaction,
                        event,
                        "pump_swap.buy",
                        "signal.dex.pump_swap.buy",
                        "dex.pump_swap.buy",
                    )
                    .await;
            },
            crate::PumpSwapDecodedEvent::SellTrade(event) => {
                return self
                    .persist_pump_swap_trade_event(
                        transaction,
                        event,
                        "pump_swap.sell",
                        "signal.dex.pump_swap.sell",
                        "dex.pump_swap.sell",
                    )
                    .await;
            },
        }
    }

    async fn persist_pump_swap_trade_event(
        &self,
        transaction: &crate::ChainTransactionDto,
        event: &crate::PumpSwapTradeDecoded,
        event_kind: &str,
        signal_kind: &str,
        observation_kind: &str,
    ) -> Result<crate::DexDecodedEventDto, crate::Error> {
        let enrichment_payload_json = prepare_pump_swap_trade_payload_for_classification(event);
        let input = crate::dex_decoded_event_materialization::DexDecodedEventMaterializationInput {
            database: self.database.as_ref(),
            persistence: &self.persistence,
            transaction,
            transaction_id: event.transaction_id,
            instruction_id: Some(event.instruction_id),
            protocol_name: "pump_swap".to_string(),
            program_id: event.program_id.clone(),
            event_kind: event_kind.to_string(),
            pool_account: event.pool_account.clone(),
            market_account: None,
            token_a_mint: event.token_a_mint.clone(),
            token_b_mint: event.token_b_mint.clone(),
            lp_mint: event.pool_v2.clone(),
            enrichment_payload_json,
            observation_payload_json: event.payload_json.clone(),
            observation_kind: observation_kind.to_string(),
            signal_kind: signal_kind.to_string(),
            missing_after_upsert_message: "decoded event disappeared after upsert".to_string(),
        };
        return crate::dex_decoded_event_materialization::materialize_dex_decoded_event(input)
            .await;
    }

    async fn decode_and_persist_raydium_cpmm_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let mut persisted = std::vec::Vec::new();
        for instruction in instructions {
            let program_id = match instruction.program_id.as_ref() {
                Some(program_id) => program_id,
                None => continue,
            };
            if program_id.as_str() != crate::RAYDIUM_CPMM_PROGRAM_ID {
                continue;
            }
            let data_json = match instruction.data_json.as_ref() {
                Some(data_json) => data_json,
                None => continue,
            };
            let decoded_events = crate::decode_raydium_cpmm_instruction(
                instruction.accounts_json.as_str(),
                data_json.as_str(),
            );
            for decoded_event in &decoded_events {
                let persist_result =
                    self.persist_raydium_cpmm_event(transaction, instruction, decoded_event).await;
                let persisted_event = match persist_result {
                    Ok(persisted_event) => persisted_event,
                    Err(error) => return Err(error),
                };
                persisted.push(persisted_event);
            }
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_raydium_clmm_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let mut persisted = std::vec::Vec::new();
        for instruction in instructions {
            let program_id = match instruction.program_id.as_ref() {
                Some(program_id) => program_id,
                None => continue,
            };
            if program_id.as_str() != crate::RAYDIUM_CLMM_PROGRAM_ID {
                continue;
            }
            let data_json = match instruction.data_json.as_ref() {
                Some(data_json) => data_json,
                None => continue,
            };
            let decoded_events = crate::decode_raydium_clmm_instruction(
                instruction.accounts_json.as_str(),
                data_json.as_str(),
            );
            for decoded_event in &decoded_events {
                let persist_result =
                    self.persist_raydium_clmm_event(transaction, instruction, decoded_event).await;
                let persisted_event = match persist_result {
                    Ok(persisted_event) => persisted_event,
                    Err(error) => return Err(error),
                };
                persisted.push(persisted_event);
            }
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_raydium_amm_v4_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result =
            self.raydium_amm_v4_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result =
                self.persist_raydium_amm_v4_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };
            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_pump_fun_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result = self.pump_fun_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result = self.persist_pump_fun_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };
            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_pump_swap_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result = self.pump_swap_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result = self.persist_pump_swap_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };

            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_meteora_dbc_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result = self.meteora_dbc_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result = self.persist_meteora_dbc_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };
            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_meteora_dlmm_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result =
            self.meteora_dlmm_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result = self.persist_meteora_dlmm_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };
            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_meteora_damm_v1_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result =
            self.meteora_damm_v1_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result =
                self.persist_meteora_damm_v1_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };
            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_meteora_damm_v2_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result =
            self.meteora_damm_v2_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result =
                self.persist_meteora_damm_v2_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };
            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_orca_whirlpools_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result =
            self.orca_whirlpools_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result =
                self.persist_orca_whirlpools_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };
            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_fluxbeam_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result = self.fluxbeam_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result = self.persist_fluxbeam_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };
            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }

    async fn decode_and_persist_dexlab_events(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
        let decoded_result = self.dexlab_decoder.decode_transaction(transaction, instructions);
        let decoded_events = match decoded_result {
            Ok(decoded_events) => decoded_events,
            Err(error) => return Err(error),
        };
        let mut persisted = std::vec::Vec::new();
        for decoded_event in &decoded_events {
            let persist_result = self.persist_dexlab_event(transaction, decoded_event).await;
            let persisted_event = match persist_result {
                Ok(persisted_event) => persisted_event,
                Err(error) => return Err(error),
            };
            persisted.push(persisted_event);
        }
        return Ok(persisted);
    }
}

fn append_persisted_events(
    target: &mut std::vec::Vec<crate::DexDecodedEventDto>,
    source: std::vec::Vec<crate::DexDecodedEventDto>,
) {
    for persisted_event in source {
        target.push(persisted_event);
    }
}

fn append_persisted_events_result(
    target: &mut std::vec::Vec<crate::DexDecodedEventDto>,
    source_result: Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error>,
) -> Result<(), crate::Error> {
    let source = match source_result {
        Ok(source) => source,
        Err(error) => return Err(error),
    };
    append_persisted_events(target, source);
    return Ok(());
}

fn enriched_raydium_payload_value(
    protocol_name: &str,
    event_kind: &str,
    raw_payload_json: &str,
) -> Result<serde_json::Value, crate::Error> {
    let payload_value_result = serde_json::from_str::<serde_json::Value>(raw_payload_json);
    let payload_value = match payload_value_result {
        Ok(payload_value) => payload_value,
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse decoded {} payload for '{}': {}",
                protocol_name, event_kind, error
            )));
        },
    };
    return Ok(crate::enrich_dex_decoded_payload(protocol_name, event_kind, payload_value));
}

// Marks Meteora DAMM v1 swaps without direct amounts as non-materializable candidates
// before generic classification metadata is inserted.
fn prepare_meteora_damm_v1_swap_payload_for_classification(
    event: &crate::MeteoraDammV1SwapDecoded,
) -> serde_json::Value {
    let mut object = match event.payload_json.clone() {
        serde_json::Value::Object(object) => object,
        other => {
            let mut object = serde_json::Map::new();
            object.insert("rawPayload".to_string(), other);
            object
        },
    };
    let payload_json = serde_json::Value::Object(object.clone());
    if crate::dex_event_classification::decoded_payload_has_trade_amount_or_price_payload(
        &payload_json,
    ) {
        return serde_json::Value::Object(object);
    }
    object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
    object.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
    object.insert(
        "skipTradeReason".to_string(),
        serde_json::Value::String("meteora_damm_v1_swap_without_amount_payload".to_string()),
    );
    object.insert(
        "skipCandleReason".to_string(),
        serde_json::Value::String("meteora_damm_v1_swap_without_amount_payload".to_string()),
    );
    return serde_json::Value::Object(object);
}

// Marks incomplete PumpSwap decoded trades as non-materializable candidates before generic
// classification metadata is inserted.
fn prepare_pump_swap_trade_payload_for_classification(
    event: &crate::PumpSwapTradeDecoded,
) -> serde_json::Value {
    let mut object = match event.payload_json.clone() {
        serde_json::Value::Object(object) => object,
        other => {
            let mut object = serde_json::Map::new();
            object.insert("rawPayload".to_string(), other);
            object
        },
    };
    if event.pool_account.is_some() && event.token_a_mint.is_some() && event.token_b_mint.is_some()
    {
        return serde_json::Value::Object(object);
    }
    object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
    object.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
    object.insert(
        "skipTradeReason".to_string(),
        serde_json::Value::String("incomplete_pump_swap_trade_accounts".to_string()),
    );
    object.insert(
        "skipCandleReason".to_string(),
        serde_json::Value::String("incomplete_pump_swap_trade_accounts".to_string()),
    );
    return serde_json::Value::Object(object);
}

#[cfg(test)]
mod tests {

    async fn make_database() -> std::sync::Arc<crate::Database> {
        let tempdir_result = tempfile::tempdir();
        let tempdir = match tempdir_result {
            Ok(tempdir) => tempdir,
            Err(error) => panic!("tempdir must succeed: {}", error),
        };
        let database_path = tempdir.path().join("dex_decode.sqlite3");
        let config = crate::DatabaseConfig {
            enabled: true,
            backend: crate::DatabaseBackend::Sqlite,
            sqlite: crate::SqliteDatabaseConfig {
                path: database_path.to_string_lossy().to_string(),
                create_if_missing: true,
                busy_timeout_ms: 5000,
                max_connections: 1,
                auto_initialize_schema: true,
                use_wal: true,
            },
        };
        let database_result = crate::Database::connect_and_initialize(&config).await;
        let database = match database_result {
            Ok(database) => database,
            Err(error) => panic!("database init must succeed: {}", error),
        };
        return std::sync::Arc::new(database);
    }

    async fn seed_projected_raydium_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
    ) {
        let service = crate::TransactionModelService::new(database);
        let resolved_transaction = serde_json::json!({
            "slot": 999001,
            "blockTime": 1779000001,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
                            "program": "raydium-amm-v4",
                            "stackHeight": 1,
                            "accounts": [
                                "Account0",
                                "Account1",
                                "Account2",
                                "Account3",
                                "PoolXYZ",
                                "Account5",
                                "Account6",
                                "LpMintXYZ",
                                "TokenAXYZ",
                                "TokenBXYZ",
                                "Account10",
                                "Account11",
                                "Account12",
                                "Account13",
                                "Account14",
                                "Account15",
                                "MarketXYZ"
                            ],
                            "data": "opaque"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: initialize2"
                ]
            }
        });
        let persist_result = service
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = persist_result {
            panic!("projection must succeed: {}", error);
        }
    }

    async fn seed_projected_pump_fun_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
    ) {
        let service = crate::TransactionModelService::new(database);
        let resolved_transaction = serde_json::json!({
            "slot": 999002,
            "blockTime": 1779000002,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::PUMP_FUN_PROGRAM_ID,
                            "program": "pump",
                            "stackHeight": 1,
                            "accounts": [
                                "MintPF111",
                                "MintAuthorityPF111",
                                "BondingCurvePF111",
                                "AssociatedBondingCurvePF111",
                                "GlobalPF111",
                                "CreatorPF111",
                                "System111",
                                "Token2022Program111",
                                "AtaProgram111"
                            ],
                            "data": "opaque"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: Instruction: CreateV2"
                ]
            }
        });
        let persist_result = service
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = persist_result {
            panic!("projection must succeed: {}", error);
        }
    }

    async fn seed_projected_pump_swap_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
    ) {
        let service = crate::TransactionModelService::new(database);
        let resolved_transaction = serde_json::json!({
            "slot": 999003,
            "blockTime": 1779000003,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::PUMP_SWAP_PROGRAM_ID,
                            "program": "pump-amm",
                            "stackHeight": 1,
                            "accounts": [
                                "PumpSwapPool111",
                                "PumpSwapTokenA111",
                                "PumpSwapTokenB111",
                                "PumpSwapPoolV2_111"
                            ],
                            "parsed": {
                                "info": {
                                    "pool": "PumpSwapPool111",
                                    "baseMint": "PumpSwapTokenA111",
                                    "quoteMint": "PumpSwapTokenB111",
                                    "poolV2": "PumpSwapPoolV2_111"
                                }
                            },
                            "data": "AJTQ2h9DXrBfqJi53PDQG2Fvki5tkaTU3"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: Instruction: Buy"
                ]
            }
        });
        let persist_result = service
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = persist_result {
            panic!("projection must succeed: {}", error);
        }
    }

    #[tokio::test]
    async fn decode_transaction_by_signature_persists_decoded_raydium_event() {
        let database = make_database().await;
        seed_projected_raydium_transaction(database.clone(), "sig-dex-decode-1").await;
        let service = crate::DexDecodeService::new(database.clone());
        let decoded_result = service.decode_transaction_by_signature("sig-dex-decode-1").await;
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].protocol_name, "raydium_amm_v4");
        assert_eq!(decoded[0].event_kind, "raydium_amm_v4.initialize2_pool");
        assert_eq!(decoded[0].pool_account, Some("PoolXYZ".to_string()));
    }

    #[tokio::test]
    async fn decode_transaction_by_signature_persists_decoded_pump_fun_event() {
        let database = make_database().await;
        seed_projected_pump_fun_transaction(database.clone(), "sig-dex-decode-pump-1").await;
        let service = crate::DexDecodeService::new(database.clone());
        let decoded_result = service.decode_transaction_by_signature("sig-dex-decode-pump-1").await;
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].protocol_name, "pump_fun");
        assert_eq!(decoded[0].event_kind, "pump_fun.create_v2_token");
        assert_eq!(decoded[0].pool_account, Some("BondingCurvePF111".to_string()));
        assert_eq!(decoded[0].token_a_mint, Some("MintPF111".to_string()));
    }

    #[test]
    fn prepare_meteora_damm_v1_swap_without_amounts_marks_event_non_actionable() {
        let event = crate::MeteoraDammV1SwapDecoded {
            transaction_id: 1,
            instruction_id: 2,
            signature: "sig-damm-v1-no-amounts".to_string(),
            program_id: crate::METEORA_DAMM_V1_PROGRAM_ID.to_string(),
            trade_side: crate::SwapTradeSide::Unknown,
            pool_account: Some("PoolDammV1".to_string()),
            token_a_mint: Some("TokenA".to_string()),
            token_b_mint: Some("TokenB".to_string()),
            payload_json: serde_json::json!({
                "decoder": "meteora_damm_v1",
                "eventKind": "swap"
            }),
        };
        let prepared_payload =
            super::prepare_meteora_damm_v1_swap_payload_for_classification(&event);
        let object_option = prepared_payload.as_object();
        let object = match object_option {
            Some(object) => object,
            None => {
                panic!("expected prepared payload object");
            },
        };
        assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(false)));
        assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(false)));
        assert_eq!(
            object.get("skipTradeReason"),
            Some(&serde_json::Value::String(
                "meteora_damm_v1_swap_without_amount_payload".to_string()
            ))
        );
    }

    #[test]
    fn prepare_meteora_damm_v1_swap_with_amounts_keeps_event_actionable() {
        let event = crate::MeteoraDammV1SwapDecoded {
            transaction_id: 1,
            instruction_id: 2,
            signature: "sig-damm-v1-with-amounts".to_string(),
            program_id: crate::METEORA_DAMM_V1_PROGRAM_ID.to_string(),
            trade_side: crate::SwapTradeSide::Unknown,
            pool_account: Some("PoolDammV1".to_string()),
            token_a_mint: Some("TokenA".to_string()),
            token_b_mint: Some("TokenB".to_string()),
            payload_json: serde_json::json!({
                "decoder": "meteora_damm_v1",
                "eventKind": "swap",
                "baseAmountRaw": "100",
                "quoteAmountRaw": "250"
            }),
        };
        let prepared_payload =
            super::prepare_meteora_damm_v1_swap_payload_for_classification(&event);
        let object_option = prepared_payload.as_object();
        let object = match object_option {
            Some(object) => object,
            None => {
                panic!("expected prepared payload object");
            },
        };
        assert_eq!(object.get("tradeCandidate"), None);
        assert_eq!(object.get("candleCandidate"), None);
        assert_eq!(object.get("skipTradeReason"), None);
    }

    #[tokio::test]
    async fn decode_transaction_by_signature_persists_decoded_pump_swap_event() {
        let database = make_database().await;
        seed_projected_pump_swap_transaction(database.clone(), "sig-dex-decode-pumpswap-1").await;
        let service = crate::DexDecodeService::new(database.clone());
        let decoded_result =
            service.decode_transaction_by_signature("sig-dex-decode-pumpswap-1").await;
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].protocol_name, "pump_swap");
        assert_eq!(decoded[0].event_kind, "pump_swap.buy");
        assert_eq!(decoded[0].pool_account, Some("PumpSwapPool111".to_string()));
        assert_eq!(decoded[0].token_a_mint, Some("PumpSwapTokenA111".to_string()));
        assert_eq!(decoded[0].token_b_mint, Some("PumpSwapTokenB111".to_string()));
        assert_eq!(decoded[0].market_account, Some("PumpSwapPoolV2_111".to_string()));
    }

    async fn seed_projected_meteora_dbc_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
    ) {
        let service = crate::TransactionModelService::new(database);
        let resolved_transaction = serde_json::json!({
            "slot": 999004,
            "blockTime": 1779000004,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::METEORA_DBC_PROGRAM_ID,
                            "program": "meteora-dbc",
                            "stackHeight": 1,
                            "accounts": [
                                "DbcPoolDecode111",
                                "DbcTokenDecode111",
                                crate::WSOL_MINT_ID,
                                "DbcConfigDecode111",
                                "DbcCreatorDecode111"
                            ],
                            "parsed": {
                                "info": {
                                    "pool": "DbcPoolDecode111",
                                    "baseMint": "DbcTokenDecode111",
                                    "quoteMint": crate::WSOL_MINT_ID,
                                    "poolConfig": "DbcConfigDecode111",
                                    "creator": "DbcCreatorDecode111"
                                }
                            },
                            "data": "opaque"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: Instruction: CreatePool"
                ]
            }
        });
        let persist_result = service
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = persist_result {
            panic!("projection must succeed: {}", error);
        }
    }

    #[tokio::test]
    async fn decode_transaction_by_signature_persists_decoded_meteora_dbc_event() {
        let database = make_database().await;
        seed_projected_meteora_dbc_transaction(database.clone(), "sig-dex-decode-dbc-1").await;
        let service = crate::DexDecodeService::new(database.clone());
        let decoded_result = service.decode_transaction_by_signature("sig-dex-decode-dbc-1").await;
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].protocol_name, "meteora_dbc");
        assert_eq!(decoded[0].event_kind, "meteora_dbc.create_pool");
        assert_eq!(decoded[0].pool_account, Some("DbcPoolDecode111".to_string()));
        assert_eq!(decoded[0].token_a_mint, Some("DbcTokenDecode111".to_string()));
        assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
        assert_eq!(decoded[0].market_account, Some("DbcConfigDecode111".to_string()));
    }

    async fn seed_projected_meteora_damm_v2_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
    ) {
        let service = crate::TransactionModelService::new(database);
        let resolved_transaction = serde_json::json!({
            "slot": 999005,
            "blockTime": 1779000005,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
                            "program": "meteora-damm-v2",
                            "stackHeight": 1,
                            "accounts": [
                                "DammV2DecodePool111",
                                "DammV2DecodeTokenA111",
                                crate::WSOL_MINT_ID,
                                "DammV2DecodeConfig111",
                                "DammV2DecodeCreator111"
                            ],
                            "parsed": {
                                "info": {
                                    "instruction": "initialize_customizable_pool",
                                    "pool": "DammV2DecodePool111",
                                    "tokenAMint": "DammV2DecodeTokenA111",
                                    "tokenBMint": crate::WSOL_MINT_ID,
                                    "creator": "DammV2DecodeCreator111",
                                    "isCustomizablePool": true
                                }
                            },
                            "data": "opaque"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: Instruction: InitializeCustomizablePool"
                ]
            }
        });
        let persist_result = service
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = persist_result {
            panic!("projection must succeed: {}", error);
        }
    }

    #[tokio::test]
    async fn decode_transaction_by_signature_persists_decoded_meteora_damm_v2_event() {
        let database = make_database().await;
        seed_projected_meteora_damm_v2_transaction(database.clone(), "sig-dex-decode-dammv2-1")
            .await;
        let service = crate::DexDecodeService::new(database.clone());
        let decoded_result =
            service.decode_transaction_by_signature("sig-dex-decode-dammv2-1").await;
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].protocol_name, "meteora_damm_v2");
        assert_eq!(decoded[0].event_kind, "meteora_damm_v2.create_pool");
        assert_eq!(decoded[0].pool_account, Some("DammV2DecodePool111".to_string()));
        assert_eq!(decoded[0].token_a_mint, Some("DammV2DecodeTokenA111".to_string()));
        assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
    }

    async fn seed_projected_meteora_damm_v1_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
    ) {
        let service = crate::TransactionModelService::new(database);
        let resolved_transaction = serde_json::json!({
            "slot": 999006,
            "blockTime": 1779000006,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
                            "program": "meteora-damm-v1",
                            "stackHeight": 1,
                            "accounts": [
                                "DammV1DecodePool111",
                                "DammV1DecodeTokenA111",
                                crate::WSOL_MINT_ID,
                                "DammV1DecodeConfig111",
                                "DammV1DecodeCreator111"
                            ],
                            "parsed": {
                                "info": {
                                    "instruction": "initialize_pool_with_config",
                                    "pool": "DammV1DecodePool111",
                                    "tokenAMint": "DammV1DecodeTokenA111",
                                    "tokenBMint": crate::WSOL_MINT_ID,
                                    "config": "DammV1DecodeConfig111",
                                    "creator": "DammV1DecodeCreator111"
                                }
                            },
                            "data": "opaque"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: Instruction: InitializePoolWithConfig"
                ]
            }
        });
        let persist_result = service
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = persist_result {
            panic!("projection must succeed: {}", error);
        }
    }

    #[tokio::test]
    async fn decode_transaction_by_signature_persists_decoded_meteora_damm_v1_event() {
        let database = make_database().await;
        seed_projected_meteora_damm_v1_transaction(database.clone(), "sig-dex-decode-dammv1-1")
            .await;
        let service = crate::DexDecodeService::new(database.clone());
        let decoded_result =
            service.decode_transaction_by_signature("sig-dex-decode-dammv1-1").await;
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].protocol_name, "meteora_damm_v1");
        assert_eq!(decoded[0].event_kind, "meteora_damm_v1.create_pool");
        assert_eq!(decoded[0].pool_account, Some("DammV1DecodePool111".to_string()));
        assert_eq!(decoded[0].token_a_mint, Some("DammV1DecodeTokenA111".to_string()));
        assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
    }

    async fn seed_projected_orca_whirlpools_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
    ) {
        let service = crate::TransactionModelService::new(database);
        let resolved_transaction = serde_json::json!({
            "slot": 999007,
            "blockTime": 1779000007,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
                            "program": "orca-whirlpools",
                            "stackHeight": 1,
                            "accounts": [
                                "OrcaDecodePool111",
                                "OrcaDecodeTokenA111",
                                crate::WSOL_MINT_ID,
                                "OrcaDecodeConfig111",
                                "OrcaDecodeCreator111"
                            ],
                            "parsed": {
                                "info": {
                                    "instruction": "initialize_pool_v2",
                                    "whirlpool": "OrcaDecodePool111",
                                    "tokenMintA": "OrcaDecodeTokenA111",
                                    "tokenMintB": crate::WSOL_MINT_ID,
                                    "whirlpoolsConfig": "OrcaDecodeConfig111",
                                    "funder": "OrcaDecodeCreator111",
                                    "tokenProgramA": crate::SPL_TOKEN_PROGRAM_ID,
                                    "tokenProgramB": crate::SPL_TOKEN_PROGRAM_ID
                                }
                            },
                            "data": "opaque"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: Instruction: InitializePoolV2"
                ]
            }
        });
        let persist_result = service
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = persist_result {
            panic!("projection must succeed: {}", error);
        }
    }

    #[tokio::test]
    async fn decode_transaction_by_signature_persists_decoded_orca_whirlpools_event() {
        let database = make_database().await;
        seed_projected_orca_whirlpools_transaction(database.clone(), "sig-dex-decode-orca-1").await;
        let service = crate::DexDecodeService::new(database.clone());
        let decoded_result = service.decode_transaction_by_signature("sig-dex-decode-orca-1").await;
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].protocol_name, "orca_whirlpools");
        assert_eq!(decoded[0].event_kind, "orca_whirlpools.create_pool");
        assert_eq!(decoded[0].pool_account, Some("OrcaDecodePool111".to_string()));
        assert_eq!(decoded[0].token_a_mint, Some("OrcaDecodeTokenA111".to_string()));
        assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
        assert_eq!(decoded[0].market_account, Some("OrcaDecodeConfig111".to_string()));
    }

    async fn seed_projected_fluxbeam_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
    ) {
        let service = crate::TransactionModelService::new(database);
        let resolved_transaction = serde_json::json!({
            "slot": 999008,
            "blockTime": 1779000008,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::FLUXBEAM_PROGRAM_ID,
                            "program": "fluxbeam",
                            "stackHeight": 1,
                            "accounts": [
                                "FluxDecodePool111",
                                "FluxDecodeLpMint111",
                                "FluxDecodeTokenA111",
                                crate::WSOL_MINT_ID,
                                "FluxDecodeCreator111"
                            ],
                            "parsed": {
                                "info": {
                                    "instruction": "create_pool",
                                    "pool": "FluxDecodePool111",
                                    "lpMint": "FluxDecodeLpMint111",
                                    "tokenA": "FluxDecodeTokenA111",
                                    "tokenB": crate::WSOL_MINT_ID,
                                    "payer": "FluxDecodeCreator111"
                                }
                            },
                            "data": "opaque"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: Instruction: CreatePool"
                ]
            }
        });
        let persist_result = service
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = persist_result {
            panic!("projection must succeed: {}", error);
        }
    }

    #[tokio::test]
    async fn decode_transaction_by_signature_persists_decoded_fluxbeam_event() {
        let database = make_database().await;
        seed_projected_fluxbeam_transaction(database.clone(), "sig-dex-decode-fluxbeam-1").await;
        let service = crate::DexDecodeService::new(database.clone());
        let decoded_result =
            service.decode_transaction_by_signature("sig-dex-decode-fluxbeam-1").await;
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].protocol_name, "fluxbeam");
        assert_eq!(decoded[0].event_kind, "fluxbeam.create_pool");
        assert_eq!(decoded[0].pool_account, Some("FluxDecodePool111".to_string()));
        assert_eq!(decoded[0].market_account, Some("FluxDecodeLpMint111".to_string()));
        assert_eq!(decoded[0].token_a_mint, Some("FluxDecodeTokenA111".to_string()));
        assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
    }

    async fn seed_projected_dexlab_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
    ) {
        let service = crate::TransactionModelService::new(database);
        let resolved_transaction = serde_json::json!({
            "slot": 999009,
            "blockTime": 1779000009,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::DEXLAB_PROGRAM_ID,
                            "program": "dexlab",
                            "stackHeight": 1,
                            "accounts": [
                                "DexlabDecodePool111",
                                "DexlabDecodeTokenA111",
                                crate::WSOL_MINT_ID,
                                "DexlabDecodeCreator111"
                            ],
                            "parsed": {
                                "info": {
                                    "instruction": "create_pool",
                                    "pool": "DexlabDecodePool111",
                                    "tokenA": "DexlabDecodeTokenA111",
                                    "tokenB": crate::WSOL_MINT_ID,
                                    "payer": "DexlabDecodeCreator111",
                                    "feeTier": "0.3%"
                                }
                            },
                            "data": "opaque"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: Instruction: CreatePool"
                ]
            }
        });
        let persist_result = service
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = persist_result {
            panic!("projection must succeed: {}", error);
        }
    }

    #[tokio::test]
    async fn decode_transaction_by_signature_persists_decoded_dexlab_event() {
        let database = make_database().await;
        seed_projected_dexlab_transaction(database.clone(), "sig-dex-decode-dexlab-1").await;
        let service = crate::DexDecodeService::new(database.clone());
        let decoded_result =
            service.decode_transaction_by_signature("sig-dex-decode-dexlab-1").await;
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].protocol_name, "dexlab");
        assert_eq!(decoded[0].event_kind, "dexlab.create_pool");
        assert_eq!(decoded[0].pool_account, Some("DexlabDecodePool111".to_string()));
        assert_eq!(decoded[0].token_a_mint, Some("DexlabDecodeTokenA111".to_string()));
        assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
    }

    #[test]
    fn classifies_swap_events_as_trade_candidates() {
        assert_eq!(
            crate::classify_dex_event_category_code("raydium_cpmm.swap_base_input"),
            "trade"
        );
        assert_eq!(
            crate::classify_dex_event_category_code("raydium_cpmm.swap_base_output"),
            "trade"
        );
        assert_eq!(crate::classify_dex_event_category_code("raydium_clmm.swap"), "trade");
        assert_eq!(crate::classify_dex_event_category_code("raydium_clmm.swap_v2"), "trade");
        assert_eq!(crate::classify_dex_event_category_code("pump_fun.buy"), "trade");
        assert!(crate::is_dex_trade_event_kind("raydium_cpmm.swap_base_input"));
        assert!(crate::is_dex_candle_candidate_event_kind("raydium_cpmm.swap_base_input"));
    }

    #[test]
    fn classifies_router_swap_as_trade_but_not_direct_candle_candidate() {
        assert_eq!(
            crate::classify_dex_event_category_code("raydium_clmm.swap_router_base_in"),
            "trade"
        );
        assert!(crate::is_dex_trade_event_kind("raydium_clmm.swap_router_base_in"));
        assert!(!crate::is_dex_candle_candidate_event_kind("raydium_clmm.swap_router_base_in"));
    }

    #[test]
    fn classifies_fee_reward_liquidity_and_lifecycle_events() {
        assert_eq!(
            crate::classify_dex_event_category_code("raydium_cpmm.collect_creator_fee"),
            "fee"
        );
        assert_eq!(
            crate::classify_dex_event_category_code("raydium_clmm.collect_protocol_fee"),
            "fee"
        );
        assert_eq!(
            crate::classify_dex_event_category_code("raydium_clmm.set_reward_params"),
            "reward"
        );
        assert_eq!(
            crate::classify_dex_event_category_code("raydium_clmm.increase_liquidity_v2"),
            "liquidity"
        );
        assert_eq!(
            crate::classify_dex_event_category_code("raydium_cpmm.initialize"),
            "pool_lifecycle"
        );
    }

    #[test]
    fn enriches_payload_without_overriding_existing_fields() {
        let payload_json = serde_json::json!({
            "eventCategory": "custom",
            "amountIn": "10"
        });
        let enriched_payload = crate::enrich_dex_decoded_payload(
            "raydium_cpmm",
            "raydium_cpmm.swap_base_input",
            payload_json,
        );
        let object_option = enriched_payload.as_object();
        let object = match object_option {
            Some(object) => object,
            None => {
                panic!("expected enriched payload object");
            },
        };
        assert_eq!(
            object.get("eventCategory"),
            Some(&serde_json::Value::String("custom".to_owned()))
        );
        assert_eq!(
            object.get("protocolName"),
            Some(&serde_json::Value::String("raydium_cpmm".to_owned()))
        );
        assert_eq!(
            object.get("eventKind"),
            Some(&serde_json::Value::String("raydium_cpmm.swap_base_input".to_owned()))
        );
        assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(true)));
        assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(true)));
    }

    #[test]
    fn enriches_non_object_payload_as_raw_payload() {
        let payload_json = serde_json::Value::String("raw".to_owned());
        let enriched_payload = crate::enrich_dex_decoded_payload(
            "raydium_clmm",
            "raydium_clmm.collect_protocol_fee",
            payload_json,
        );
        let object_option = enriched_payload.as_object();
        let object = match object_option {
            Some(object) => object,
            None => {
                panic!("expected enriched payload object");
            },
        };
        assert_eq!(object.get("rawPayload"), Some(&serde_json::Value::String("raw".to_owned())));
        assert_eq!(object.get("eventCategory"), Some(&serde_json::Value::String("fee".to_owned())));
        assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(false)));
        assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(false)));
        assert_eq!(
            object.get("skipTradeReason"),
            Some(&serde_json::Value::String("non_trade_event".to_owned()))
        );
    }
}
