// 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, 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) -> 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, 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, market_account: std::option::Option, token_a_mint: std::option::Option, token_b_mint: std::option::Option, lp_mint: std::option::Option, payload_json: serde_json::Value, ) -> Result { 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 { 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 { 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 { 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 { 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 { 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; }, crate::MeteoraDlmmDecodedEvent::Liquidity(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "meteora_dlmm", event.program_id.clone(), event.event_kind.as_str(), event.pool_account.clone(), None, event.token_a_mint.clone(), event.token_b_mint.clone(), None, event.payload_json.clone(), ) .await; }, crate::MeteoraDlmmDecodedEvent::PoolLifecycle(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "meteora_dlmm", event.program_id.clone(), event.event_kind.as_str(), 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, source: std::vec::Vec, ) { for persisted_event in source { target.push(persisted_event); } } fn append_persisted_events_result( target: &mut std::vec::Vec, source_result: Result, 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 { let payload_value_result = serde_json::from_str::(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 { 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, 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, 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, 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, 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, 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, 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, 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, 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, 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())) ); } }