// file: kb_lib/src/dex_decode.rs //! Persistence-oriented DEX decoding service. const METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX: &str = "e445a52e51cb9a1d"; /// DEX decode service. #[derive(Debug, Clone)] pub struct DexDecodeService { database: std::sync::Arc, persistence: crate::DetectionPersistenceService, raydium_amm_v4_decoder: crate::RaydiumAmmV4Decoder, raydium_clmm_decoder: crate::RaydiumClmmDecoder, raydium_stable_swap_decoder: crate::RaydiumStableSwapDecoder, 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, openbook_v2_decoder: crate::OpenBookV2Decoder, phoenix_v1_decoder: crate::PhoenixV1Decoder, } 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(), raydium_clmm_decoder: crate::RaydiumClmmDecoder::new(), raydium_stable_swap_decoder: crate::RaydiumStableSwapDecoder::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(), openbook_v2_decoder: crate::OpenBookV2Decoder::new(), phoenix_v1_decoder: crate::PhoenixV1Decoder::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_stable_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_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.preserve_unmatched_raydium_instruction_audits(&transaction, &instructions) .await, ); if let Err(error) = append_result { return Err(error); } let cleanup_result = self.cleanup_replaced_raydium_cpmm_instruction_audits(&transaction).await; if let Err(error) = cleanup_result { return Err(error); } let cleanup_result = self.cleanup_replaced_raydium_clmm_instruction_audits(&transaction).await; if let Err(error) = cleanup_result { return Err(error); } let cleanup_result = self .cleanup_replaced_raydium_launchpad_anchor_self_cpi_audits(&transaction) .await; if let Err(error) = cleanup_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.preserve_unmatched_meteora_instruction_audits(&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); } let append_result = append_persisted_events_result( &mut persisted, self.decode_and_persist_openbook_v2_audit_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_phoenix_v1_audit_events(&transaction, &instructions) .await, ); if let Err(error) = append_result { return Err(error); } let decoded_instruction_ids = decoded_instruction_ids_from_persisted_events(&persisted); let append_result = append_persisted_events_result( &mut persisted, self.decode_and_persist_upstream_registry_matches( &transaction, &instructions, &decoded_instruction_ids, ) .await, ); if let Err(error) = append_result { return Err(error); } let cleanup_result = self.cleanup_replaced_raydium_cpmm_instruction_audits(&transaction).await; if let Err(error) = cleanup_result { return Err(error); } let cleanup_result = self.cleanup_replaced_raydium_clmm_instruction_audits(&transaction).await; if let Err(error) = cleanup_result { return Err(error); } let cleanup_result = self .cleanup_replaced_raydium_launchpad_anchor_self_cpi_audits(&transaction) .await; if let Err(error) = cleanup_result { return Err(error); } let reconcile_result = self.reconcile_raydium_clmm_confirmed_non_trade_events(&transaction).await; if let Err(error) = reconcile_result { return Err(error); } return Ok(persisted); } async fn cleanup_replaced_raydium_cpmm_instruction_audits( &self, transaction: &crate::ChainTransactionDto, ) -> Result<(), crate::Error> { let transaction_id = match transaction.id { Some(transaction_id) => transaction_id, None => return Ok(()), }; let cleanup_result = crate::query_dex_decoded_events_delete_replaced_raydium_cpmm_instruction_audits( self.database.as_ref(), Some(transaction_id), ) .await; match cleanup_result { Ok(_) => return Ok(()), Err(error) => return Err(error), } } async fn cleanup_replaced_raydium_clmm_instruction_audits( &self, transaction: &crate::ChainTransactionDto, ) -> Result<(), crate::Error> { let transaction_id = match transaction.id { Some(transaction_id) => transaction_id, None => return Ok(()), }; let cleanup_result = crate::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits( self.database.as_ref(), Some(transaction_id), ) .await; match cleanup_result { Ok(deleted_count) => { if deleted_count > 0 { tracing::debug!( signature = %transaction.signature, deleted_count, "cleaned replaced Raydium CLMM instruction audits" ); } return Ok(()); }, Err(error) => { return Err(crate::Error::Db(format!( "cannot cleanup replaced Raydium CLMM instruction audits for signature '{}': {}", transaction.signature, error ))); }, } } async fn reconcile_raydium_clmm_confirmed_non_trade_events( &self, transaction: &crate::ChainTransactionDto, ) -> Result<(), crate::Error> { if dex_decode_transaction_has_effective_error(transaction) { return Ok(()); } 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 decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id( self.database.as_ref(), transaction_id, ) .await; let decoded_events = match decoded_events_result { Ok(decoded_events) => decoded_events, Err(error) => return Err(error), }; let mut delete_create_pool_audit = false; let mut delete_collect_protocol_fee_audit = false; for decoded_event in &decoded_events { if decoded_event.protocol_name != "raydium_clmm" { continue; } if decoded_event.event_kind == "raydium_clmm.create_pool" { let materialize_result = self .materialize_raydium_clmm_create_pool_lifecycle(transaction, decoded_event) .await; if let Err(error) = materialize_result { return Err(error); } delete_create_pool_audit = true; continue; } if decoded_event.event_kind == "raydium_clmm.collect_protocol_fee" { let materialize_result = self .materialize_raydium_clmm_collect_protocol_fee(transaction, decoded_event) .await; if let Err(error) = materialize_result { return Err(error); } delete_collect_protocol_fee_audit = true; } } if delete_create_pool_audit { let delete_result = self .delete_raydium_clmm_instruction_audit_by_discriminator( transaction_id, "e992d18ecf6840bc", ) .await; if let Err(error) = delete_result { return Err(error); } } if delete_collect_protocol_fee_audit { let delete_result = self .delete_raydium_clmm_instruction_audit_by_discriminator( transaction_id, "8888fcddc2427e59", ) .await; if let Err(error) = delete_result { return Err(error); } } return Ok(()); } async fn materialize_raydium_clmm_create_pool_lifecycle( &self, transaction: &crate::ChainTransactionDto, decoded_event: &crate::DexDecodedEventDto, ) -> Result<(), crate::Error> { let decoded_event_id = match decoded_event.id { Some(decoded_event_id) => decoded_event_id, None => return Ok(()), }; let context_result = self.resolve_decoded_event_db_context(decoded_event).await; let context = match context_result { Ok(context) => context, Err(error) => return Err(error), }; let dto = crate::PoolLifecycleEventDto::new( decoded_event.transaction_id, Some(decoded_event_id), context.0, context.1, context.2, transaction.signature.clone(), transaction.slot, decoded_event.protocol_name.clone(), decoded_event.program_id.clone(), decoded_event.event_kind.clone(), decoded_event.pool_account.clone(), decoded_event.token_a_mint.clone(), decoded_event.token_b_mint.clone(), decoded_event.payload_json.clone(), ); let upsert_result = crate::query_pool_lifecycle_events_upsert(self.database.as_ref(), &dto).await; match upsert_result { Ok(_) => return Ok(()), Err(error) => return Err(error), } } async fn materialize_raydium_clmm_collect_protocol_fee( &self, transaction: &crate::ChainTransactionDto, decoded_event: &crate::DexDecodedEventDto, ) -> Result<(), crate::Error> { let decoded_event_id = match decoded_event.id { Some(decoded_event_id) => decoded_event_id, None => return Ok(()), }; let payload = dex_decode_payload_value(decoded_event.payload_json.as_str()); let context_result = self.resolve_decoded_event_db_context(decoded_event).await; let context = match context_result { Ok(context) => context, Err(error) => return Err(error), }; let actor_wallet = dex_decode_extract_first_string( &payload, &["authority", "actorWallet", "actor_wallet", "owner", "payer", "user"], ); let fee_token_mint = dex_decode_extract_first_string( &payload, &[ "vault_0_mint", "vault0Mint", "feeTokenMint", "fee_token_mint", "tokenMint", "token_mint", "mint", ], ); let fee_amount_raw = dex_decode_extract_first_amount_string( &payload, &[ "amount0RequestedRaw", "amount_0_requested_raw", "tokenAAmount", "token_a_amount", "feeAmountRaw", "fee_amount_raw", "protocolFeeAmount", "protocol_fee_amount", "amount", ], ); let dto = crate::FeeEventDto::new( decoded_event.transaction_id, Some(decoded_event_id), context.0, context.1, context.2, transaction.signature.clone(), transaction.slot, decoded_event.protocol_name.clone(), decoded_event.program_id.clone(), decoded_event.event_kind.clone(), decoded_event.pool_account.clone(), actor_wallet, fee_token_mint, fee_amount_raw, decoded_event.payload_json.clone(), ); let upsert_result = crate::query_fee_events_upsert(self.database.as_ref(), &dto).await; match upsert_result { Ok(_) => return Ok(()), Err(error) => return Err(error), } } async fn resolve_decoded_event_db_context( &self, decoded_event: &crate::DexDecodedEventDto, ) -> Result< (std::option::Option, std::option::Option, std::option::Option), crate::Error, > { let dex_result = crate::query_dexs_get_by_code( self.database.as_ref(), decoded_event.protocol_name.as_str(), ) .await; let dex_id = match dex_result { Ok(Some(dex)) => dex.id, Ok(None) => None, Err(error) => return Err(error), }; let pool_account = match decoded_event.pool_account.as_ref() { Some(pool_account) => pool_account, None => return Ok((dex_id, None, None)), }; let pool_result = crate::query_pools_get_by_address(self.database.as_ref(), pool_account.as_str()).await; let pool = match pool_result { Ok(Some(pool)) => pool, Ok(None) => return Ok((dex_id, None, None)), Err(error) => return Err(error), }; let pool_id = match pool.id { Some(pool_id) => pool_id, None => return Ok((dex_id, None, None)), }; let pair_result = crate::query_pairs_get_by_pool_id(self.database.as_ref(), pool_id).await; let pair = match pair_result { Ok(pair) => pair, Err(error) => return Err(error), }; let pair_id = match pair { Some(pair) => pair.id, None => None, }; return Ok((dex_id, Some(pool_id), pair_id)); } async fn delete_raydium_clmm_instruction_audit_by_discriminator( &self, transaction_id: i64, discriminator_hex: &str, ) -> Result<(), crate::Error> { let delete_result = crate::query_dex_decoded_events_delete_raydium_clmm_instruction_audit_by_discriminator( self.database.as_ref(), transaction_id, discriminator_hex, ) .await; match delete_result { Ok(_) => return Ok(()), Err(error) => return Err(error), } } 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 payload_json_for_cleanup = payload_json.clone(); 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(), }; let materialized_result = crate::dex_decoded_event_materialization::materialize_dex_decoded_event(input).await; let materialized = match materialized_result { Ok(materialized) => materialized, Err(error) => return Err(error), }; let cleanup_result = self .delete_replaced_instruction_audit( transaction_id, instruction_id, protocol_name, event_kind, ) .await; if let Err(error) = cleanup_result { return Err(error); } let cleanup_result = self .delete_replaced_instruction_audit_by_discriminator( transaction_id, protocol_name, event_kind, &payload_json_for_cleanup, ) .await; if let Err(error) = cleanup_result { return Err(error); } let cleanup_result = self .delete_replaced_upstream_registry_match( transaction_id, instruction_id, protocol_name, event_kind, ) .await; if let Err(error) = cleanup_result { return Err(error); } let non_trade_result = self .materialize_direct_decoded_non_trade_if_needed(transaction, &materialized) .await; if let Err(error) = non_trade_result { return Err(error); } return Ok(materialized); } async fn materialize_direct_decoded_non_trade_if_needed( &self, transaction: &crate::ChainTransactionDto, decoded_event: &crate::DexDecodedEventDto, ) -> Result<(), crate::Error> { if dex_decode_transaction_has_effective_error(transaction) { return Ok(()); } if !should_immediately_materialize_decoded_non_trade_event( decoded_event.event_kind.as_str(), ) { return Ok(()); } if decoded_event.event_kind == "raydium_clmm.create_pool" { return self .materialize_raydium_clmm_create_pool_lifecycle(transaction, decoded_event) .await; } if decoded_event.event_kind == "raydium_clmm.collect_protocol_fee" { return self .materialize_raydium_clmm_collect_protocol_fee(transaction, decoded_event) .await; } return Ok(()); } async fn delete_replaced_instruction_audit_by_discriminator( &self, transaction_id: i64, protocol_name: &str, event_kind: &str, payload_json: &serde_json::Value, ) -> Result<(), crate::Error> { if event_kind.ends_with(".instruction_audit") { return Ok(()); } let discriminator_hex = match instruction_discriminator_hex_from_payload(payload_json) { Some(discriminator_hex) => discriminator_hex, None => return Ok(()), }; return self .delete_replaced_instruction_audit_by_discriminator_hex( transaction_id, protocol_name, discriminator_hex.as_str(), ) .await; } async fn delete_replaced_instruction_audit_by_discriminator_hex( &self, transaction_id: i64, protocol_name: &str, discriminator_hex: &str, ) -> Result<(), crate::Error> { let audit_event_kind = match instruction_audit_event_kind_by_protocol(protocol_name) { Some(audit_event_kind) => audit_event_kind, None => return Ok(()), }; let delete_result = crate::query_dex_decoded_events_delete_instruction_audit_by_discriminator( self.database.as_ref(), transaction_id, protocol_name, audit_event_kind, discriminator_hex, ) .await; match delete_result { Ok(_) => return Ok(()), Err(error) => return Err(error), } } async fn delete_replaced_raydium_launchpad_anchor_self_cpi_audit( &self, transaction_id: i64, _instruction_id: i64, anchor_event_discriminator_hex: &str, ) -> Result<(), crate::Error> { let delete_result = crate::query_dex_decoded_events_delete_raydium_launchpad_anchor_self_cpi_audit( self.database.as_ref(), transaction_id, METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX, anchor_event_discriminator_hex, ) .await; match delete_result { Ok(_) => return Ok(()), Err(error) => return Err(error), } } async fn cleanup_replaced_raydium_launchpad_anchor_self_cpi_audits( &self, transaction: &crate::ChainTransactionDto, ) -> Result<(), crate::Error> { let transaction_id = match transaction.id { Some(transaction_id) => transaction_id, None => return Ok(()), }; let cleanup_result = crate::query_dex_decoded_events_cleanup_raydium_launchpad_anchor_self_cpi_audits( self.database.as_ref(), transaction_id, METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX, ) .await; match cleanup_result { Ok(_) => return Ok(()), Err(error) => return Err(error), } } async fn delete_replaced_upstream_registry_match( &self, transaction_id: i64, instruction_id: i64, protocol_name: &str, event_kind: &str, ) -> Result<(), crate::Error> { if protocol_name == crate::UPSTREAM_REGISTRY_PROTOCOL_NAME { return Ok(()); } if event_kind == crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND { return Ok(()); } let delete_result = crate::query_dex_decoded_events_delete_by_key( self.database.as_ref(), transaction_id, Some(instruction_id), crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND, ) .await; match delete_result { Ok(_) => return Ok(()), Err(error) => return Err(error), } } async fn delete_replaced_instruction_audit( &self, transaction_id: i64, instruction_id: i64, protocol_name: &str, event_kind: &str, ) -> Result<(), crate::Error> { if event_kind.ends_with(".instruction_audit") { return Ok(()); } let audit_event_kind = match instruction_audit_event_kind_by_protocol(protocol_name) { Some(audit_event_kind) => audit_event_kind, None => return Ok(()), }; let delete_result = crate::query_dex_decoded_events_delete_related_instruction_audit( self.database.as_ref(), transaction_id, instruction_id, audit_event_kind, ) .await; match delete_result { Ok(_) => return Ok(()), Err(error) => return Err(error), } } async fn decode_and_persist_upstream_registry_matches( &self, transaction: &crate::ChainTransactionDto, instructions: &[crate::ChainInstructionDto], already_decoded_instruction_ids: &std::collections::HashSet, ) -> Result, 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 decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id( self.database.as_ref(), transaction_id, ) .await; let decoded_events = match decoded_events_result { Ok(decoded_events) => decoded_events, Err(error) => return Err(error), }; let mut decoded_instruction_ids = already_decoded_instruction_ids.clone(); for decoded_event in &decoded_events { let instruction_id = match decoded_event.instruction_id { Some(instruction_id) => instruction_id, None => continue, }; decoded_instruction_ids.insert(instruction_id); } let mut persisted = std::vec::Vec::new(); for instruction in instructions { let instruction_id = match instruction.id { Some(instruction_id) => instruction_id, None => continue, }; if decoded_instruction_ids.contains(&instruction_id) { continue; } let program_id = match instruction.program_id.as_ref() { Some(program_id) => program_id, None => continue, }; let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref()); let registry_match = crate::upstream_registry_match::upstream_registry_match_instruction_data( program_id.as_str(), data_base58.as_deref(), ); let registry_match = match registry_match { Some(registry_match) => registry_match, None => continue, }; if upstream_registry_instruction_match_is_locally_covered(®istry_match) { continue; } let payload = build_upstream_registry_instruction_match_payload( transaction, instruction, ®istry_match, data_base58.as_deref(), ); let persist_result = self .materialize_named_dex_event( transaction, transaction_id, instruction_id, crate::UPSTREAM_REGISTRY_PROTOCOL_NAME, program_id.clone(), crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND, None, None, None, None, None, payload, ) .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 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; }, crate::MeteoraDlmmDecodedEvent::Fee(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, None, None, None, event.payload_json.clone(), ) .await; }, crate::MeteoraDlmmDecodedEvent::Reward(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, None, None, 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.lp_mint.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; }, crate::MeteoraDammV1DecodedEvent::Liquidity(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "meteora_damm_v1", event.program_id.clone(), event.event_kind.as_str(), 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::MeteoraDammV1DecodedEvent::Fee(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "meteora_damm_v1", event.program_id.clone(), event.event_kind.as_str(), event.pool_account.clone(), None, None, None, event.lp_mint.clone(), event.payload_json.clone(), ) .await; }, crate::MeteoraDammV1DecodedEvent::PoolLifecycle(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "meteora_damm_v1", event.program_id.clone(), event.event_kind.as_str(), 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::MeteoraDammV1DecodedEvent::PoolAdmin(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "meteora_damm_v1", event.program_id.clone(), event.event_kind.as_str(), event.pool_account.clone(), None, None, None, None, event.payload_json.clone(), ) .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; }, crate::RaydiumAmmV4DecodedEvent::Swap(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "raydium_amm_v4", event.program_id.clone(), event.event_kind.as_str(), Some(event.pool_account.clone()), None, Some(event.token_a_mint.clone()), Some(event.token_b_mint.clone()), None, event.payload_json.clone(), ) .await; }, crate::RaydiumAmmV4DecodedEvent::Instruction(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "raydium_amm_v4", event.program_id.clone(), event.event_kind.as_str(), 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_id: i64, 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 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(), decoded_event.pool_account_option().map(|value| return value.to_string()), None, decoded_event.base_mint_option().map(|value| return value.to_string()), decoded_event.quote_mint_option().map(|value| return value.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(), decoded_event.pool_account().map(|value| return value.to_string()), None, decoded_event.base_mint().map(|value| return value.to_string()), decoded_event.quote_mint().map(|value| return value.to_string()), decoded_event.lp_mint().map(|value| return value.to_string()), payload_value, ) .await; } async fn persist_raydium_stable_swap_event( &self, transaction: &crate::ChainTransactionDto, decoded_event: &crate::RaydiumStableSwapDecodedEvent, ) -> 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 decoded_event.instruction_id() { Some(instruction_id) => instruction_id, None => { return Err(crate::Error::InvalidState(format!( "raydium stable swap decoded event for transaction '{}' has no instruction 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 stable swap payload".to_string(), )); }, }; let payload_value_result = enriched_raydium_payload_value( "raydium_stable_swap", 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_stable_swap", crate::RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID.to_string(), event_kind.as_str(), decoded_event.pool_account().map(|value| return value.to_string()), decoded_event.market_account().map(|value| return value.to_string()), decoded_event.base_mint().map(|value| return value.to_string()), decoded_event.quote_mint().map(|value| return value.to_string()), decoded_event.lp_mint().map(|value| return value.to_string()), 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(); let mut program_data_events = collect_raydium_cpmm_program_data_events(transaction); 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 instruction_kind = crate::classify_raydium_cpmm_instruction_data(data_json.as_str()); 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); } let program_data_persist_result = persist_matching_raydium_cpmm_program_data_event( self, transaction, instruction, instruction_kind, &mut program_data_events, &mut persisted, ) .await; if let Err(error) = program_data_persist_result { return Err(error); } } return Ok(persisted); } async fn decode_and_persist_raydium_stable_swap_events( &self, transaction: &crate::ChainTransactionDto, instructions: &[crate::ChainInstructionDto], ) -> Result, crate::Error> { let decoded_result = self .raydium_stable_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_raydium_stable_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_raydium_clmm_events( &self, transaction: &crate::ChainTransactionDto, instructions: &[crate::ChainInstructionDto], ) -> Result, crate::Error> { let decoded_result = self.raydium_clmm_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_clmm_event( transaction, decoded_event.instruction_id, &decoded_event.decoded_event, ) .await; let persisted_event = match persist_result { Ok(persisted_event) => persisted_event, Err(error) => return Err(error), }; persisted.push(persisted_event); } let mut program_data_events = collect_raydium_clmm_program_data_events(transaction); 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_base58 = parse_instruction_data_base58(instruction.data_json.as_deref()); let discriminator_hex = discriminator_hex_from_base58(data_base58.as_deref()); let persist_result = persist_matching_raydium_clmm_program_data_events( self, transaction, instruction, discriminator_hex.as_deref(), &mut program_data_events, &mut persisted, ) .await; if let Err(error) = persist_result { return Err(error); } } 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 preserve_unmatched_raydium_instruction_audits( &self, transaction: &crate::ChainTransactionDto, instructions: &[crate::ChainInstructionDto], ) -> Result, 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 decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id( self.database.as_ref(), transaction_id, ) .await; let decoded_events = match decoded_events_result { Ok(decoded_events) => decoded_events, Err(error) => return Err(error), }; let mut decoded_instruction_ids = std::collections::HashSet::::new(); let mut decoded_discriminator_keys = std::collections::HashSet::::new(); for decoded_event in &decoded_events { if !decoded_event.protocol_name.starts_with("raydium_") { continue; } if decoded_event.event_kind.ends_with(".instruction_audit") { continue; } if let Some(instruction_id) = decoded_event.instruction_id { decoded_instruction_ids.insert(instruction_id); } let discriminator = instruction_discriminator_hex_from_payload_str(decoded_event.payload_json.as_str()); if let Some(discriminator) = discriminator { decoded_discriminator_keys.insert(raydium_decoded_discriminator_key( decoded_event.protocol_name.as_str(), discriminator.as_str(), )); } } 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, }; let audit_spec = match raydium_instruction_audit_spec(program_id.as_str()) { Some(audit_spec) => audit_spec, None => continue, }; let instruction_id = match instruction.id { Some(instruction_id) => instruction_id, None => continue, }; let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str()); let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref()); let data_bytes = instruction_data_bytes_from_base58(data_base58.as_deref()); let discriminator_hex = raydium_instruction_discriminator_hex( audit_spec.protocol_name, data_bytes.as_deref(), 0, ); let anchor_event_spec = raydium_launchpad_anchor_self_cpi_event_spec( audit_spec.protocol_name, data_bytes.as_deref(), ); let dedupe_discriminator_hex = match anchor_event_spec { Some(anchor_event_spec) => Some(anchor_event_spec.discriminator_hex.to_string()), None => discriminator_hex.clone(), }; if decoded_instruction_ids.contains(&instruction_id) && anchor_event_spec.is_none() { if let Some(discriminator_hex) = dedupe_discriminator_hex.as_deref() { let cleanup_result = self .delete_replaced_instruction_audit_by_discriminator_hex( transaction_id, audit_spec.protocol_name, discriminator_hex, ) .await; if let Err(error) = cleanup_result { return Err(error); } } continue; } if anchor_event_spec.is_none() && raydium_instruction_already_decoded_by_discriminator( &decoded_discriminator_keys, audit_spec.protocol_name, dedupe_discriminator_hex.as_deref(), ) { if let Some(discriminator_hex) = dedupe_discriminator_hex.as_deref() { let cleanup_result = self .delete_replaced_instruction_audit_by_discriminator_hex( transaction_id, audit_spec.protocol_name, discriminator_hex, ) .await; if let Err(error) = cleanup_result { return Err(error); } } continue; } let mapped_spec = if anchor_event_spec.is_some() { None } else { raydium_mapped_non_trade_instruction_spec( audit_spec.protocol_name, discriminator_hex.as_deref(), accounts.len(), ) }; if let Some(mapped_spec) = mapped_spec { if raydium_mapped_event_kind_already_decoded( decoded_events.as_slice(), audit_spec.protocol_name, mapped_spec.event_kind, ) { if let Some(discriminator_hex) = dedupe_discriminator_hex.as_deref() { let cleanup_result = self .delete_replaced_instruction_audit_by_discriminator_hex( transaction_id, audit_spec.protocol_name, discriminator_hex, ) .await; if let Err(error) = cleanup_result { return Err(error); } } continue; } } let event_kind = match anchor_event_spec { Some(anchor_event_spec) => anchor_event_spec.event_kind, None => match mapped_spec { Some(mapped_spec) => mapped_spec.event_kind, None => audit_spec.event_kind, }, }; let mut payload = build_raydium_instruction_audit_payload( transaction, instruction, audit_spec.protocol_name, event_kind, program_id.as_str(), ); if let Some(anchor_event_spec) = anchor_event_spec { payload = enrich_raydium_launchpad_anchor_self_cpi_payload( payload, anchor_event_spec, data_bytes.as_deref(), ); } if let Some(mapped_spec) = mapped_spec { payload = enrich_raydium_mapped_non_trade_payload( payload, mapped_spec, data_base58.as_deref(), ); } let pool_account = match anchor_event_spec { Some(anchor_event_spec) => raydium_launchpad_anchor_self_cpi_pool_account( anchor_event_spec, data_bytes.as_deref(), ), None => candidate_raydium_mapped_pool_account( mapped_spec, accounts.as_slice(), audit_spec.protocol_name, instruction.accounts_json.as_str(), ), }; let token_a_mint = candidate_raydium_mapped_account( mapped_spec.and_then(|spec| return spec.token_a_mint_index), accounts.as_slice(), ); let token_b_mint = candidate_raydium_mapped_account( mapped_spec.and_then(|spec| return spec.token_b_mint_index), accounts.as_slice(), ); let lp_mint = candidate_raydium_mapped_account( mapped_spec.and_then(|spec| return spec.lp_mint_index), accounts.as_slice(), ); let persist_result = self .materialize_named_dex_event( transaction, transaction_id, instruction_id, audit_spec.protocol_name, program_id.clone(), event_kind, pool_account, None, token_a_mint, token_b_mint, lp_mint, payload, ) .await; let persisted_event = match persist_result { Ok(persisted_event) => persisted_event, Err(error) => return Err(error), }; if let Some(anchor_event_spec) = anchor_event_spec { let cleanup_result = self .delete_replaced_raydium_launchpad_anchor_self_cpi_audit( transaction_id, instruction_id, anchor_event_spec.discriminator_hex, ) .await; if let Err(error) = cleanup_result { return Err(error); } } if anchor_event_spec.is_none() { if let Some(discriminator_hex) = dedupe_discriminator_hex.as_deref() { let cleanup_result = self .delete_replaced_instruction_audit_by_discriminator_hex( transaction_id, audit_spec.protocol_name, discriminator_hex, ) .await; if let Err(error) = cleanup_result { return Err(error); } } } persisted.push(persisted_event); } return Ok(persisted); } async fn preserve_unmatched_meteora_instruction_audits( &self, transaction: &crate::ChainTransactionDto, instructions: &[crate::ChainInstructionDto], ) -> Result, 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 decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id( self.database.as_ref(), transaction_id, ) .await; let decoded_events = match decoded_events_result { Ok(decoded_events) => decoded_events, Err(error) => return Err(error), }; let mut decoded_instruction_ids = std::collections::HashSet::::new(); for decoded_event in &decoded_events { if !decoded_event.protocol_name.starts_with("meteora_") { continue; } if decoded_event.event_kind.ends_with(".instruction_audit") { continue; } let instruction_id = match decoded_event.instruction_id { Some(instruction_id) => instruction_id, None => continue, }; decoded_instruction_ids.insert(instruction_id); } 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, }; let audit_spec = match meteora_instruction_audit_spec(program_id.as_str()) { Some(audit_spec) => audit_spec, None => continue, }; let instruction_id = match instruction.id { Some(instruction_id) => instruction_id, None => continue, }; if decoded_instruction_ids.contains(&instruction_id) { continue; } if is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap( audit_spec.protocol_name, instruction, decoded_events.as_slice(), ) { continue; } let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str()); let payload = build_meteora_instruction_audit_payload( transaction, instruction, audit_spec.protocol_name, audit_spec.event_kind, program_id.as_str(), ); let pool_account = candidate_meteora_audit_pool_account(audit_spec, accounts.as_slice()); let persist_result = self .materialize_named_dex_event( transaction, transaction_id, instruction_id, audit_spec.protocol_name, program_id.clone(), audit_spec.event_kind, pool_account, None, None, None, None, payload, ) .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 persist_openbook_v2_event( &self, transaction: &crate::ChainTransactionDto, decoded_event: &crate::OpenBookV2DecodedEvent, ) -> Result { match decoded_event { crate::OpenBookV2DecodedEvent::Audit(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "openbook_v2", event.program_id.clone(), event.event_kind.as_str(), None, event.market_account.clone(), event.token_a_mint.clone(), event.token_b_mint.clone(), None, event.payload_json.clone(), ) .await; }, } } async fn decode_and_persist_openbook_v2_audit_events( &self, transaction: &crate::ChainTransactionDto, instructions: &[crate::ChainInstructionDto], ) -> Result, crate::Error> { let decoded_result = self.openbook_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_openbook_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 persist_phoenix_v1_event( &self, transaction: &crate::ChainTransactionDto, decoded_event: &crate::PhoenixV1DecodedEvent, ) -> Result { match decoded_event { crate::PhoenixV1DecodedEvent::Audit(event) => { return self .materialize_named_dex_event( transaction, event.transaction_id, event.instruction_id, "phoenix_v1", event.program_id.clone(), event.event_kind.as_str(), None, event.market_account.clone(), event.token_a_mint.clone(), event.token_b_mint.clone(), None, event.payload_json.clone(), ) .await; }, } } async fn decode_and_persist_phoenix_v1_audit_events( &self, transaction: &crate::ChainTransactionDto, instructions: &[crate::ChainInstructionDto], ) -> Result, crate::Error> { let decoded_result = self.phoenix_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_phoenix_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_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); } } struct RaydiumInstructionAuditSpec { protocol_name: &'static str, event_kind: &'static str, candidate_pool_account_index: usize, } #[derive(Clone, Copy)] struct MeteoraInstructionAuditSpec { protocol_name: &'static str, event_kind: &'static str, candidate_pool_account_index: std::option::Option, } #[derive(Clone, Copy)] struct RaydiumMappedNonTradeInstructionSpec { instruction_name: &'static str, event_kind: &'static str, pool_account_index: std::option::Option, token_a_mint_index: std::option::Option, token_b_mint_index: std::option::Option, lp_mint_index: std::option::Option, amount_layout: RaydiumMappedNonTradeAmountLayout, } #[derive(Clone, Copy)] enum RaydiumMappedNonTradeAmountLayout { None, ClmmCreatePool, ClmmFeePair, ClmmLiquidityV2, ClmmOpenLimitOrder, ClmmIncreaseLimitOrder, ClmmDecreaseLimitOrder, AnchorIdl, CpmmAmmConfig, CpmmDeposit, CpmmFeePair, CpmmInitialize, CpmmPoolStatus, CpmmWithdraw, LaunchpadInitialize, AmmV4Initialize, AmmV4Initialize2, AmmV4MonitorStep, AmmV4Deposit, AmmV4Withdraw, AmmV4SetParams, AmmV4WithdrawSrm, AmmV4PreInitialize, AmmV4SimulateInfo, AmmV4AdminCancelOrders, AmmV4UpdateConfigAccount, } fn raydium_instruction_audit_spec( program_id: &str, ) -> std::option::Option { if program_id == crate::RAYDIUM_AMM_V4_PROGRAM_ID { return Some(RaydiumInstructionAuditSpec { protocol_name: "raydium_amm_v4", event_kind: "raydium_amm_v4.instruction_audit", candidate_pool_account_index: 1, }); } if program_id == crate::RAYDIUM_CLMM_PROGRAM_ID { return Some(RaydiumInstructionAuditSpec { protocol_name: "raydium_clmm", event_kind: "raydium_clmm.instruction_audit", candidate_pool_account_index: 2, }); } if program_id == crate::RAYDIUM_CPMM_PROGRAM_ID { return Some(RaydiumInstructionAuditSpec { protocol_name: "raydium_cpmm", event_kind: "raydium_cpmm.instruction_audit", candidate_pool_account_index: 3, }); } if program_id == crate::RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID { return Some(RaydiumInstructionAuditSpec { protocol_name: "raydium_stable_swap", event_kind: "raydium_stable_swap.instruction_audit", candidate_pool_account_index: 1, }); } if program_id == crate::RAYDIUM_LAUNCHPAD_PROGRAM_ID { return Some(RaydiumInstructionAuditSpec { protocol_name: "raydium_launchpad", event_kind: "raydium_launchpad.instruction_audit", candidate_pool_account_index: 4, }); } return None; } fn raydium_mapped_non_trade_instruction_spec( protocol_name: &str, discriminator_hex: std::option::Option<&str>, account_count: usize, ) -> std::option::Option { let discriminator_hex = match discriminator_hex { Some(discriminator_hex) => discriminator_hex, None => return None, }; if protocol_name == "raydium_launchpad" { return raydium_launchpad_mapped_non_trade_instruction_spec( discriminator_hex, account_count, ); } if protocol_name == "raydium_amm_v4" { return raydium_amm_v4_mapped_non_trade_instruction_spec(discriminator_hex, account_count); } if protocol_name == "raydium_clmm" { if discriminator_hex == "e445a52e51cb9a1d" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "cpi_event", event_kind: "raydium_clmm.cpi_event", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "4c7c800fd55725fa" && account_count >= 3 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "close_limit_order", event_kind: "raydium_clmm.close_limit_order", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "9d20dab7471d1293" && account_count >= 11 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "open_limit_order", event_kind: "raydium_clmm.open_limit_order", pool_account_index: Some(1), token_a_mint_index: Some(7), token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmOpenLimitOrder, }); } if discriminator_hex == "b19059ecfaba7d63" && account_count >= 8 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "increase_limit_order", event_kind: "raydium_clmm.increase_limit_order", pool_account_index: Some(1), token_a_mint_index: Some(6), token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmIncreaseLimitOrder, }); } if discriminator_hex == "759d3c674231a300" && account_count >= 12 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "decrease_limit_order", event_kind: "raydium_clmm.decrease_limit_order", pool_account_index: Some(1), token_a_mint_index: Some(8), token_b_mint_index: Some(9), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmDecreaseLimitOrder, }); } if discriminator_hex == "c975989055556cb2" && account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "close_protocol_position", event_kind: "raydium_clmm.close_protocol_position", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "a78a4e95dfc2067e" && account_count >= 7 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "collect_fund_fee", event_kind: "raydium_clmm.collect_fund_fee", pool_account_index: Some(1), token_a_mint_index: Some(5), token_b_mint_index: Some(6), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmFeePair, }); } if discriminator_hex == "8888fcddc2427e59" && account_count >= 7 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "collect_protocol_fee", event_kind: "raydium_clmm.collect_protocol_fee", pool_account_index: Some(1), token_a_mint_index: Some(5), token_b_mint_index: Some(6), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmFeePair, }); } if discriminator_hex == "e992d18ecf6840bc" && account_count >= 13 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "create_pool", event_kind: "raydium_clmm.create_pool", pool_account_index: Some(2), token_a_mint_index: Some(3), token_b_mint_index: Some(4), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmCreatePool, }); } if discriminator_hex == "12eda6c52210d590" && account_count >= 5 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "collect_remaining_rewards", event_kind: "raydium_clmm.collect_remaining_rewards", pool_account_index: Some(2), token_a_mint_index: Some(4), token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "8934edd4d7756c68" && account_count >= 3 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "create_amm_config", event_kind: "raydium_clmm.create_amm_config", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "2b44d4a7592fa401" && account_count >= 13 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "create_customizable_pool", event_kind: "raydium_clmm.create_customizable_pool", pool_account_index: Some(2), token_a_mint_index: Some(3), token_b_mint_index: Some(4), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "bd0eb5785576e33e" && account_count >= 3 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "create_dynamic_fee_config", event_kind: "raydium_clmm.create_dynamic_fee_config", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "3f5794216d230868" && account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "create_operation_account", event_kind: "raydium_clmm.create_operation_account", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "11fb415c88f20ea9" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "create_support_mint_associated", event_kind: "raydium_clmm.create_support_mint_associated", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "a026d06f685b2c01" && account_count >= 4 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "decrease_liquidity", event_kind: "raydium_clmm.decrease_liquidity", pool_account_index: Some(3), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: Some(1), amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "2e9cf3760dcdfbb2" && account_count >= 3 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "increase_liquidity", event_kind: "raydium_clmm.increase_liquidity", pool_account_index: Some(2), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: Some(1), amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "5f87c0c4f281e644" && account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "initialize_reward", event_kind: "raydium_clmm.initialize_reward", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "87802f4d0f98f031" && account_count >= 6 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "open_position", event_kind: "raydium_clmm.open_position", pool_account_index: Some(5), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: Some(2), amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "4db84ad67056f1c7" && account_count >= 6 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "open_position_v2", event_kind: "raydium_clmm.open_position_v2", pool_account_index: Some(5), token_a_mint_index: if account_count >= 22 { Some(20) } else { None }, token_b_mint_index: if account_count >= 22 { Some(21) } else { None }, lp_mint_index: Some(2), amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "7034a74b20c9d389" && account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "set_reward_params", event_kind: "raydium_clmm.set_reward_params", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "cd4e74215c691a60" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "settle_limit_order", event_kind: "raydium_clmm.settle_limit_order", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "457d73daf5baf2c4" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "swap_router_base_in", event_kind: "raydium_clmm.swap_router_base_in", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "07160c53f22b3079" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "transfer_reward_owner", event_kind: "raydium_clmm.transfer_reward_owner", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "313cae889a1c74c8" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "update_amm_config", event_kind: "raydium_clmm.update_amm_config", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "0707500802c784f0" && account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "update_dynamic_fee_config", event_kind: "raydium_clmm.update_dynamic_fee_config", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "7f467728bce33d07" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "update_operation_account", event_kind: "raydium_clmm.update_operation_account", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "82576c062ee0757b" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "update_pool_status", event_kind: "raydium_clmm.update_pool_status", pool_account_index: Some(0), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "a3ace0340b9a6adf" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "update_reward_infos", event_kind: "raydium_clmm.update_reward_infos", pool_account_index: Some(0), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "3a7fbc3e4f52c460" && account_count >= 16 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "decrease_liquidity_v2", event_kind: "raydium_clmm.decrease_liquidity_v2", pool_account_index: Some(3), token_a_mint_index: Some(14), token_b_mint_index: Some(15), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmLiquidityV2, }); } if discriminator_hex == "851d59df45eeb00a" && account_count >= 15 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "increase_liquidity_v2", event_kind: "raydium_clmm.increase_liquidity_v2", pool_account_index: Some(2), token_a_mint_index: Some(13), token_b_mint_index: Some(14), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmLiquidityV2, }); } if discriminator_hex == "4dffae527d1dc92e" && account_count >= 20 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "open_position_with_token22_nft", event_kind: "raydium_clmm.open_position_with_token22_nft", pool_account_index: Some(4), token_a_mint_index: Some(18), token_b_mint_index: Some(19), lp_mint_index: Some(2), amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "7b86510031446262" && account_count >= 6 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "close_position", event_kind: "raydium_clmm.close_position", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: Some(1), amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } } if protocol_name == "raydium_cpmm" { if discriminator_hex == "40f4bc78a7e9690a" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "anchor_idl_instruction", event_kind: "raydium_cpmm.anchor_idl_instruction", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::AnchorIdl, }); } if discriminator_hex == "e445a52e51cb9a1d" { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "cpi_event", event_kind: "raydium_cpmm.cpi_event", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "9c5420764587467b" && account_count >= 4 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "close_permission_pda", event_kind: "raydium_cpmm.close_permission_pda", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "1416567bc61cdb84" && account_count >= 13 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "collect_creator_fee", event_kind: "raydium_cpmm.collect_creator_fee", pool_account_index: Some(2), token_a_mint_index: Some(6), token_b_mint_index: Some(7), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "a78a4e95dfc2067e" && account_count >= 12 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "collect_fund_fee", event_kind: "raydium_cpmm.collect_fund_fee", pool_account_index: Some(2), token_a_mint_index: Some(6), token_b_mint_index: Some(7), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair, }); } if discriminator_hex == "8888fcddc2427e59" && account_count >= 12 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "collect_protocol_fee", event_kind: "raydium_cpmm.collect_protocol_fee", pool_account_index: Some(2), token_a_mint_index: Some(6), token_b_mint_index: Some(7), lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair, }); } if discriminator_hex == "8934edd4d7756c68" && account_count >= 3 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "create_amm_config", event_kind: "raydium_cpmm.create_amm_config", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig, }); } if discriminator_hex == "878802d889a9b5ca" && account_count >= 4 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "create_permission_pda", event_kind: "raydium_cpmm.create_permission_pda", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } if discriminator_hex == "f223c68952e1f2b6" && account_count >= 13 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "deposit", event_kind: "raydium_cpmm.deposit", pool_account_index: Some(2), token_a_mint_index: Some(10), token_b_mint_index: Some(11), lp_mint_index: Some(12), amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmDeposit, }); } if discriminator_hex == "afaf6d1f0d989bed" && account_count >= 20 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "initialize", event_kind: "raydium_cpmm.initialize", pool_account_index: Some(3), token_a_mint_index: Some(4), token_b_mint_index: Some(5), lp_mint_index: Some(6), amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize, }); } if discriminator_hex == "3f37fe4131b25979" && account_count >= 21 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "initialize_with_permission", event_kind: "raydium_cpmm.initialize_with_permission", pool_account_index: Some(4), token_a_mint_index: Some(5), token_b_mint_index: Some(6), lp_mint_index: Some(7), amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize, }); } if discriminator_hex == "313cae889a1c74c8" && account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "update_amm_config", event_kind: "raydium_cpmm.update_amm_config", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig, }); } if discriminator_hex == "82576c062ee0757b" && account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "update_pool_status", event_kind: "raydium_cpmm.update_pool_status", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus, }); } if discriminator_hex == "b712469c946da122" && account_count >= 14 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "withdraw", event_kind: "raydium_cpmm.withdraw", pool_account_index: Some(2), token_a_mint_index: Some(10), token_b_mint_index: Some(11), lp_mint_index: Some(12), amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw, }); } } return None; } fn raydium_amm_v4_mapped_non_trade_instruction_spec( discriminator_hex: &str, account_count: usize, ) -> std::option::Option { match discriminator_hex { "00" => { if account_count >= 4 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "initialize", event_kind: "raydium_amm_v4.initialize", pool_account_index: Some(3), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4Initialize, }); } }, "01" => { if account_count >= 10 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "initialize2", event_kind: "raydium_amm_v4.initialize2_pool", pool_account_index: Some(4), token_a_mint_index: Some(8), token_b_mint_index: Some(9), lp_mint_index: Some(7), amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4Initialize2, }); } }, "02" => { if account_count >= 4 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "monitor_step", event_kind: "raydium_amm_v4.monitor_step", pool_account_index: Some(3), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4MonitorStep, }); } }, "03" => { if account_count >= 8 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "deposit", event_kind: "raydium_amm_v4.deposit", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: Some(5), amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4Deposit, }); } }, "04" => { if account_count >= 8 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "withdraw", event_kind: "raydium_amm_v4.withdraw", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: Some(5), amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4Withdraw, }); } }, "05" => { if account_count >= 4 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "migrate_to_open_book", event_kind: "raydium_amm_v4.migrate_to_open_book", pool_account_index: Some(3), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } }, "06" => { if account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "set_params", event_kind: "raydium_amm_v4.set_params", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4SetParams, }); } }, "07" => { if account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "withdraw_pnl", event_kind: "raydium_amm_v4.withdraw_pnl", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } }, "08" => { if account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "withdraw_srm", event_kind: "raydium_amm_v4.withdraw_srm", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4WithdrawSrm, }); } }, "0a" => { if account_count >= 5 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "pre_initialize", event_kind: "raydium_amm_v4.pre_initialize", pool_account_index: Some(4), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4PreInitialize, }); } }, "0c" => { if account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "simulate_info", event_kind: "raydium_amm_v4.simulate_info", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4SimulateInfo, }); } }, "0d" => { if account_count >= 2 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "admin_cancel_orders", event_kind: "raydium_amm_v4.admin_cancel_orders", pool_account_index: Some(1), token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4AdminCancelOrders, }); } }, "0e" => { if account_count >= 1 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "create_config_account", event_kind: "raydium_amm_v4.create_config_account", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::None, }); } }, "0f" => { if account_count >= 1 { return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: "update_config_account", event_kind: "raydium_amm_v4.update_config_account", pool_account_index: None, token_a_mint_index: None, token_b_mint_index: None, lp_mint_index: None, amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4UpdateConfigAccount, }); } }, _ => {}, } return None; } fn raydium_launchpad_mapped_non_trade_instruction_spec( discriminator_hex: &str, account_count: usize, ) -> std::option::Option { let layout = match crate::dex::raydium_launchpad::account_layout(discriminator_hex, account_count) { Some(layout) => layout, None => return None, }; let amount_layout = if layout.creates_pool { RaydiumMappedNonTradeAmountLayout::LaunchpadInitialize } else { RaydiumMappedNonTradeAmountLayout::None }; return Some(RaydiumMappedNonTradeInstructionSpec { instruction_name: layout.instruction_name, event_kind: layout.event_kind, pool_account_index: layout.pool_account_index, token_a_mint_index: layout.base_mint_index, token_b_mint_index: layout.quote_mint_index, lp_mint_index: None, amount_layout, }); } fn candidate_raydium_mapped_pool_account( mapped_spec: std::option::Option, accounts: &[std::string::String], protocol_name: &str, accounts_json: &str, ) -> std::option::Option { if let Some(mapped_spec) = mapped_spec { if let Some(pool_account_index) = mapped_spec.pool_account_index { return candidate_raydium_mapped_account(Some(pool_account_index), accounts); } return None; } return candidate_raydium_audit_pool_account(protocol_name, accounts_json); } fn candidate_raydium_mapped_account( index: std::option::Option, accounts: &[std::string::String], ) -> std::option::Option { let index = match index { Some(index) => index, None => return None, }; return accounts.get(index).cloned(); } fn enrich_raydium_mapped_non_trade_payload( payload: serde_json::Value, mapped_spec: RaydiumMappedNonTradeInstructionSpec, data_base58: std::option::Option<&str>, ) -> serde_json::Value { let mut object = match payload { serde_json::Value::Object(object) => object, other => { let mut object = serde_json::Map::new(); object.insert("rawPayload".to_string(), other); object }, }; object.remove("tradeCandidate"); object.remove("candleCandidate"); object.remove("nonTradeUseful"); object.remove("skipTradeReason"); object.remove("skipCandleReason"); object.insert( "instructionName".to_string(), serde_json::Value::String(mapped_spec.instruction_name.to_string()), ); object.insert( "upstreamInstructionName".to_string(), serde_json::Value::String(mapped_spec.instruction_name.to_string()), ); object.insert("localSpecializedDecoder".to_string(), serde_json::Value::Bool(true)); object.insert( "adminAction".to_string(), serde_json::Value::String(mapped_spec.instruction_name.to_string()), ); object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(true)); object.insert( "auditReason".to_string(), serde_json::Value::String("raydium_non_swap_instruction_mapped_from_corpus".to_string()), ); object.insert( "proofSource".to_string(), serde_json::Value::String( "local_corpus_discriminator_and_raydium_idl_instruction_name".to_string(), ), ); let data_bytes = instruction_data_bytes_from_base58(data_base58); if let Some(data_bytes) = data_bytes { insert_raydium_mapped_amounts( &mut object, mapped_spec.amount_layout, data_bytes.as_slice(), ); } return serde_json::Value::Object(object); } fn insert_raydium_mapped_amounts( object: &mut serde_json::Map, amount_layout: RaydiumMappedNonTradeAmountLayout, data: &[u8], ) { match amount_layout { RaydiumMappedNonTradeAmountLayout::None => return, RaydiumMappedNonTradeAmountLayout::AnchorIdl => { let payload_len = data.len(); object.insert("idlManagementInstruction".to_string(), serde_json::Value::Bool(true)); object.insert( "instructionDataLength".to_string(), serde_json::Value::Number(serde_json::Number::from(payload_len as u64)), ); if payload_len >= 8 { object.insert( "idlManagementDiscriminatorHex".to_string(), serde_json::Value::String("40f4bc78a7e9690a".to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::ClmmCreatePool => { if let Some(sqrt_price_x64) = read_u128_le_from_bytes(data, 8) { object.insert( "sqrtPriceX64".to_string(), serde_json::Value::String(sqrt_price_x64.to_string()), ); } if let Some(open_time) = read_u64_le_from_bytes(data, 24) { object.insert( "openTime".to_string(), serde_json::Value::String(open_time.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::ClmmFeePair => { if let Some(amount_0) = read_u64_le_from_bytes(data, 8) { object.insert( "tokenAAmount".to_string(), serde_json::Value::String(amount_0.to_string()), ); object.insert( "amount0RequestedRaw".to_string(), serde_json::Value::String(amount_0.to_string()), ); } if let Some(amount_1) = read_u64_le_from_bytes(data, 16) { object.insert( "tokenBAmount".to_string(), serde_json::Value::String(amount_1.to_string()), ); object.insert( "amount1RequestedRaw".to_string(), serde_json::Value::String(amount_1.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::ClmmLiquidityV2 => { if let Some(liquidity) = read_u128_le_from_bytes(data, 8) { object.insert( "liquidity".to_string(), serde_json::Value::String(liquidity.to_string()), ); object.insert( "lpAmountRaw".to_string(), serde_json::Value::String(liquidity.to_string()), ); } if let Some(amount_0) = read_u64_le_from_bytes(data, 24) { object.insert( "tokenAAmount".to_string(), serde_json::Value::String(amount_0.to_string()), ); } if let Some(amount_1) = read_u64_le_from_bytes(data, 32) { object.insert( "tokenBAmount".to_string(), serde_json::Value::String(amount_1.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::ClmmOpenLimitOrder => { if let Some(nonce_index) = read_u8_from_bytes(data, 8) { object.insert( "nonceIndex".to_string(), serde_json::Value::Number(serde_json::Number::from(nonce_index as u64)), ); } if let Some(zero_for_one) = read_u8_from_bytes(data, 9) { object.insert("zeroForOne".to_string(), serde_json::Value::Bool(zero_for_one != 0)); } if let Some(tick_index) = read_i32_le_from_bytes(data, 10) { object.insert( "tickIndex".to_string(), serde_json::Value::Number(serde_json::Number::from(tick_index as i64)), ); } if let Some(amount) = read_u64_le_from_bytes(data, 14) { object .insert("amountRaw".to_string(), serde_json::Value::String(amount.to_string())); object.insert( "orderAmountRaw".to_string(), serde_json::Value::String(amount.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::ClmmIncreaseLimitOrder => { if let Some(amount) = read_u64_le_from_bytes(data, 8) { object .insert("amountRaw".to_string(), serde_json::Value::String(amount.to_string())); object.insert( "increasedAmountRaw".to_string(), serde_json::Value::String(amount.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::ClmmDecreaseLimitOrder => { if let Some(amount) = read_u64_le_from_bytes(data, 8) { object .insert("amountRaw".to_string(), serde_json::Value::String(amount.to_string())); object.insert( "decreasedAmountRaw".to_string(), serde_json::Value::String(amount.to_string()), ); } if let Some(amount_min) = read_u64_le_from_bytes(data, 16) { object.insert( "amountMinRaw".to_string(), serde_json::Value::String(amount_min.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig => { if let Some(param) = read_u8_from_bytes(data, 8) { object.insert( "configParam".to_string(), serde_json::Value::Number(serde_json::Number::from(param as u64)), ); } if let Some(value) = read_u64_le_from_bytes(data, 9) { object.insert( "configValue".to_string(), serde_json::Value::String(value.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::CpmmDeposit => { if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) { object.insert( "lpAmountRaw".to_string(), serde_json::Value::String(lp_amount.to_string()), ); object.insert( "liquidity".to_string(), serde_json::Value::String(lp_amount.to_string()), ); } if let Some(amount_0) = read_u64_le_from_bytes(data, 16) { object.insert( "tokenAAmount".to_string(), serde_json::Value::String(amount_0.to_string()), ); } if let Some(amount_1) = read_u64_le_from_bytes(data, 24) { object.insert( "tokenBAmount".to_string(), serde_json::Value::String(amount_1.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::CpmmFeePair => { if let Some(amount_0) = read_u64_le_from_bytes(data, 8) { object.insert( "tokenAAmount".to_string(), serde_json::Value::String(amount_0.to_string()), ); object.insert( "amount0RequestedRaw".to_string(), serde_json::Value::String(amount_0.to_string()), ); } if let Some(amount_1) = read_u64_le_from_bytes(data, 16) { object.insert( "tokenBAmount".to_string(), serde_json::Value::String(amount_1.to_string()), ); object.insert( "amount1RequestedRaw".to_string(), serde_json::Value::String(amount_1.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::CpmmInitialize => { if let Some(amount_0) = read_u64_le_from_bytes(data, 8) { object.insert( "tokenAAmount".to_string(), serde_json::Value::String(amount_0.to_string()), ); } if let Some(amount_1) = read_u64_le_from_bytes(data, 16) { object.insert( "tokenBAmount".to_string(), serde_json::Value::String(amount_1.to_string()), ); } if let Some(open_time) = read_u64_le_from_bytes(data, 24) { object.insert( "openTime".to_string(), serde_json::Value::String(open_time.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus => { if let Some(status) = read_u8_from_bytes(data, 8) { object.insert( "poolStatus".to_string(), serde_json::Value::Number(serde_json::Number::from(status as u64)), ); } }, RaydiumMappedNonTradeAmountLayout::AmmV4Initialize => { if let Some(nonce) = read_u8_from_bytes(data, 1) { object.insert( "nonce".to_string(), serde_json::Value::Number(serde_json::Number::from(nonce as u64)), ); } if let Some(open_time) = read_u64_le_from_bytes(data, 2) { object.insert("openTime".to_string(), serde_json::Value::String(open_time.to_string())); } }, RaydiumMappedNonTradeAmountLayout::AmmV4Initialize2 => { if let Some(nonce) = read_u8_from_bytes(data, 1) { object.insert( "nonce".to_string(), serde_json::Value::Number(serde_json::Number::from(nonce as u64)), ); } if let Some(open_time) = read_u64_le_from_bytes(data, 2) { object.insert("openTime".to_string(), serde_json::Value::String(open_time.to_string())); } if let Some(init_pc_amount) = read_u64_le_from_bytes(data, 10) { object.insert( "initPcAmount".to_string(), serde_json::Value::String(init_pc_amount.to_string()), ); object.insert( "tokenBAmount".to_string(), serde_json::Value::String(init_pc_amount.to_string()), ); } if let Some(init_coin_amount) = read_u64_le_from_bytes(data, 18) { object.insert( "initCoinAmount".to_string(), serde_json::Value::String(init_coin_amount.to_string()), ); object.insert( "tokenAAmount".to_string(), serde_json::Value::String(init_coin_amount.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::AmmV4MonitorStep => { if let Some(plan_order_limit) = read_u16_le_from_bytes(data, 1) { object.insert( "planOrderLimit".to_string(), serde_json::Value::Number(serde_json::Number::from(plan_order_limit as u64)), ); } if let Some(place_order_limit) = read_u16_le_from_bytes(data, 3) { object.insert( "placeOrderLimit".to_string(), serde_json::Value::Number(serde_json::Number::from(place_order_limit as u64)), ); } if let Some(cancel_order_limit) = read_u16_le_from_bytes(data, 5) { object.insert( "cancelOrderLimit".to_string(), serde_json::Value::Number(serde_json::Number::from(cancel_order_limit as u64)), ); } }, RaydiumMappedNonTradeAmountLayout::AmmV4Deposit => { if let Some(max_coin_amount) = read_u64_le_from_bytes(data, 1) { object.insert( "maxCoinAmount".to_string(), serde_json::Value::String(max_coin_amount.to_string()), ); object.insert( "tokenAAmount".to_string(), serde_json::Value::String(max_coin_amount.to_string()), ); } if let Some(max_pc_amount) = read_u64_le_from_bytes(data, 9) { object.insert( "maxPcAmount".to_string(), serde_json::Value::String(max_pc_amount.to_string()), ); object.insert( "tokenBAmount".to_string(), serde_json::Value::String(max_pc_amount.to_string()), ); } if let Some(base_side) = read_u64_le_from_bytes(data, 17) { object.insert("baseSide".to_string(), serde_json::Value::String(base_side.to_string())); } if let Some(other_amount_min) = read_u64_le_from_bytes(data, 25) { object.insert( "otherAmountMin".to_string(), serde_json::Value::String(other_amount_min.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::AmmV4Withdraw => { if let Some(lp_amount) = read_u64_le_from_bytes(data, 1) { object.insert("lpAmountRaw".to_string(), serde_json::Value::String(lp_amount.to_string())); object.insert("liquidity".to_string(), serde_json::Value::String(lp_amount.to_string())); } if let Some(min_coin_amount) = read_u64_le_from_bytes(data, 9) { object.insert( "minCoinAmount".to_string(), serde_json::Value::String(min_coin_amount.to_string()), ); } if let Some(min_pc_amount) = read_u64_le_from_bytes(data, 17) { object.insert( "minPcAmount".to_string(), serde_json::Value::String(min_pc_amount.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::AmmV4SetParams => { if let Some(param) = read_u8_from_bytes(data, 1) { object.insert( "configParam".to_string(), serde_json::Value::Number(serde_json::Number::from(param as u64)), ); } if let Some(value) = read_u64_le_from_bytes(data, 2) { object.insert("configValue".to_string(), serde_json::Value::String(value.to_string())); } if let Some(last_order_denominator) = read_u64_le_from_bytes(data, 10) { object.insert( "lastOrderDenominator".to_string(), serde_json::Value::String(last_order_denominator.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::AmmV4WithdrawSrm => { if let Some(amount) = read_u64_le_from_bytes(data, 1) { object.insert("amountRaw".to_string(), serde_json::Value::String(amount.to_string())); } }, RaydiumMappedNonTradeAmountLayout::AmmV4PreInitialize => { object.insert("deprecatedInstruction".to_string(), serde_json::Value::Bool(true)); object.insert("partialLifecycle".to_string(), serde_json::Value::Bool(true)); object.insert( "skipCatalogReason".to_string(), serde_json::Value::String("missing_token_mints".to_string()), ); if let Some(nonce) = read_u8_from_bytes(data, 1) { object.insert( "nonce".to_string(), serde_json::Value::Number(serde_json::Number::from(nonce as u64)), ); } }, RaydiumMappedNonTradeAmountLayout::AmmV4SimulateInfo => { if let Some(param) = read_u8_from_bytes(data, 1) { object.insert( "simulateParam".to_string(), serde_json::Value::Number(serde_json::Number::from(param as u64)), ); } if let Some(amount_in) = read_u64_le_from_bytes(data, 2) { object.insert("amountIn".to_string(), serde_json::Value::String(amount_in.to_string())); } if let Some(amount_out) = read_u64_le_from_bytes(data, 10) { object.insert("amountOutOrMinimumAmountOut".to_string(), serde_json::Value::String(amount_out.to_string())); } }, RaydiumMappedNonTradeAmountLayout::AmmV4AdminCancelOrders => { if let Some(limit) = read_u16_le_from_bytes(data, 1) { object.insert( "orderCancelLimit".to_string(), serde_json::Value::Number(serde_json::Number::from(limit as u64)), ); } }, RaydiumMappedNonTradeAmountLayout::AmmV4UpdateConfigAccount => { if let Some(param) = read_u8_from_bytes(data, 1) { object.insert( "configParam".to_string(), serde_json::Value::Number(serde_json::Number::from(param as u64)), ); } if let Some(create_pool_fee) = read_u64_le_from_bytes(data, 2) { object.insert( "createPoolFee".to_string(), serde_json::Value::String(create_pool_fee.to_string()), ); } }, RaydiumMappedNonTradeAmountLayout::LaunchpadInitialize => { object.insert( "poolKindHint".to_string(), serde_json::Value::String("bonding_curve".to_string()), ); object.insert( "poolStatusHint".to_string(), serde_json::Value::String("pending".to_string()), ); }, RaydiumMappedNonTradeAmountLayout::CpmmWithdraw => { if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) { object.insert( "lpAmountRaw".to_string(), serde_json::Value::String(lp_amount.to_string()), ); object.insert( "liquidity".to_string(), serde_json::Value::String(lp_amount.to_string()), ); } if let Some(amount_0) = read_u64_le_from_bytes(data, 16) { object.insert( "tokenAAmount".to_string(), serde_json::Value::String(amount_0.to_string()), ); } if let Some(amount_1) = read_u64_le_from_bytes(data, 24) { object.insert( "tokenBAmount".to_string(), serde_json::Value::String(amount_1.to_string()), ); } }, } } fn instruction_data_bytes_from_base58( data_base58: std::option::Option<&str>, ) -> std::option::Option> { let data_base58 = match data_base58 { Some(data_base58) => data_base58, None => return None, }; let bytes_result = bs58::decode(data_base58).into_vec(); match bytes_result { Ok(bytes) => return Some(bytes), Err(_) => return None, } } fn read_u8_from_bytes(data: &[u8], offset: usize) -> std::option::Option { if data.len() < offset + 1 { return None; } return Some(data[offset]); } fn read_u16_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option { if data.len() < offset + 2 { return None; } let mut bytes = [0_u8; 2]; let mut index = 0_usize; while index < 2 { bytes[index] = data[offset + index]; index += 1; } return Some(u16::from_le_bytes(bytes)); } fn read_i32_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option { if data.len() < offset + 4 { return None; } let mut bytes = [0_u8; 4]; let mut index = 0_usize; while index < 4 { bytes[index] = data[offset + index]; index += 1; } return Some(i32::from_le_bytes(bytes)); } fn read_pubkey_base58_from_bytes( data: std::option::Option<&[u8]>, offset: usize, ) -> std::option::Option { let data = match data { Some(data) => data, None => return None, }; let end = offset.checked_add(32_usize); let end = match end { Some(end) => end, None => return None, }; if data.len() < end { return None; } return Some(bs58::encode(&data[offset..end]).into_string()); } fn read_u64_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option { if data.len() < offset + 8 { return None; } let mut bytes = [0_u8; 8]; let mut index = 0_usize; while index < 8 { bytes[index] = data[offset + index]; index += 1; } return Some(u64::from_le_bytes(bytes)); } fn read_u128_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option { if data.len() < offset + 16 { return None; } let mut bytes = [0_u8; 16]; let mut index = 0_usize; while index < 16 { bytes[index] = data[offset + index]; index += 1; } return Some(u128::from_le_bytes(bytes)); } fn meteora_instruction_audit_spec( program_id: &str, ) -> std::option::Option { if program_id == crate::METEORA_DBC_PROGRAM_ID { return Some(MeteoraInstructionAuditSpec { protocol_name: "meteora_dbc", event_kind: "meteora_dbc.instruction_audit", candidate_pool_account_index: Some(1), }); } if program_id == crate::METEORA_DLMM_PROGRAM_ID { return Some(MeteoraInstructionAuditSpec { protocol_name: "meteora_dlmm", event_kind: "meteora_dlmm.instruction_audit", candidate_pool_account_index: Some(0), }); } if program_id == crate::METEORA_DAMM_V1_PROGRAM_ID { return Some(MeteoraInstructionAuditSpec { protocol_name: "meteora_damm_v1", event_kind: "meteora_damm_v1.instruction_audit", candidate_pool_account_index: Some(0), }); } if program_id == crate::METEORA_DAMM_V2_PROGRAM_ID { return Some(MeteoraInstructionAuditSpec { protocol_name: "meteora_damm_v2", event_kind: "meteora_damm_v2.instruction_audit", candidate_pool_account_index: Some(1), }); } return None; } fn candidate_meteora_audit_pool_account( audit_spec: MeteoraInstructionAuditSpec, accounts: &[std::string::String], ) -> std::option::Option { let index = match audit_spec.candidate_pool_account_index { Some(index) => index, None => return None, }; return accounts.get(index).cloned(); } fn is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap( protocol_name: &str, instruction: &crate::ChainInstructionDto, decoded_events: &[crate::DexDecodedEventDto], ) -> bool { if protocol_name != "meteora_dlmm" { return false; } let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref()); let data_bytes = instruction_data_bytes_from_base58(data_base58.as_deref()); let selector_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 0); if selector_hex.as_deref() != Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX) { return false; } let event_discriminator_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 8); match event_discriminator_hex.as_deref() { Some("516ce3becdd00ac4") | Some("2e7452d7941b544d") => {}, _ => return false, } for decoded_event in decoded_events { if decoded_event.protocol_name == "meteora_dlmm" && decoded_event.event_kind == "meteora_dlmm.swap" { return true; } } return false; } fn build_meteora_instruction_audit_payload( transaction: &crate::ChainTransactionDto, instruction: &crate::ChainInstructionDto, protocol_name: &str, event_kind: &str, program_id: &str, ) -> serde_json::Value { let accounts = parse_instruction_accounts_value(instruction.accounts_json.as_str()); let account_count = match accounts.as_array() { Some(items) => items.len(), None => 0, }; let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref()); let data_bytes = instruction_data_bytes_from_base58(data_base58.as_deref()); let discriminator_hex = raydium_instruction_discriminator_hex(protocol_name, data_bytes.as_deref(), 0); let anchor_self_cpi_log = discriminator_hex.as_deref() == Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX); let anchor_event_discriminator_hex = if anchor_self_cpi_log { discriminator_hex_from_bytes(data_bytes.as_deref(), 8) } else { None }; let anchor_event_payload_size = if anchor_self_cpi_log { match data_bytes.as_ref() { Some(data_bytes) => data_bytes.len().checked_sub(8), None => None, } } else { None }; let data_prefix = data_base58 .as_ref() .map(|value| return value.chars().take(16).collect::()); let audit_reason = if anchor_self_cpi_log { "meteora_anchor_self_cpi_log_not_decoded_by_specific_event_decoder" } else { "meteora_instruction_not_decoded_by_specific_decoder" }; let proof_status = if anchor_self_cpi_log { "observed_local_corpus_anchor_self_cpi_log" } else { "unclassified_local_corpus_instruction" }; return serde_json::json!({ "decoder": protocol_name, "eventKind": event_kind, "signature": transaction.signature.clone(), "instructionId": instruction.id, "instructionIndex": instruction.instruction_index, "innerInstructionIndex": instruction.inner_instruction_index, "innerInstruction": instruction.inner_instruction_index.is_some(), "parentInstructionId": instruction.parent_instruction_id, "programId": program_id, "programFamily": "meteora", "accounts": accounts, "accountCount": account_count, "data": data_base58, "dataPrefix": data_prefix, "discriminatorHex": discriminator_hex, "anchorSelfCpiLog": anchor_self_cpi_log, "anchorSelfCpiLogSelectorHex": if anchor_self_cpi_log { Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX) } else { None }, "anchorEventDiscriminatorHex": anchor_event_discriminator_hex, "anchorEventPayloadSize": anchor_event_payload_size, "auditReason": audit_reason, "proofStatus": proof_status, "tradeCandidate": false, "candleCandidate": false, "nonTradeUseful": false, "skipTradeReason": "instruction_audit_only", "skipCandleReason": "instruction_audit_only" }); } fn instruction_discriminator_hex_from_payload( payload_json: &serde_json::Value, ) -> std::option::Option { let candidates = [ "instructionDiscriminatorHex", "instruction_discriminator_hex", "discriminatorHex", "discriminator_hex", "anchorEventDiscriminatorHex", "anchor_event_discriminator_hex", ]; for candidate in candidates { let value = payload_json.get(candidate).and_then(serde_json::Value::as_str); let value = match value { Some(value) => value.trim(), None => continue, }; if !value.is_empty() { return Some(value.to_string()); } } return None; } fn instruction_discriminator_hex_from_payload_str( payload_json: &str, ) -> std::option::Option { let parsed = serde_json::from_str::(payload_json); let parsed = match parsed { Ok(parsed) => parsed, Err(_) => return None, }; return instruction_discriminator_hex_from_payload(&parsed); } fn raydium_decoded_discriminator_key( protocol_name: &str, discriminator_hex: &str, ) -> std::string::String { return format!("{}:{}", protocol_name, discriminator_hex); } fn raydium_instruction_already_decoded_by_discriminator( decoded_discriminator_keys: &std::collections::HashSet, protocol_name: &str, discriminator_hex: std::option::Option<&str>, ) -> bool { let discriminator_hex = match discriminator_hex { Some(discriminator_hex) => discriminator_hex, None => return false, }; let key = raydium_decoded_discriminator_key(protocol_name, discriminator_hex); return decoded_discriminator_keys.contains(&key); } fn raydium_mapped_event_kind_already_decoded( decoded_events: &[crate::DexDecodedEventDto], protocol_name: &str, event_kind: &str, ) -> bool { for decoded_event in decoded_events { if decoded_event.protocol_name != protocol_name { continue; } if decoded_event.event_kind == event_kind { return true; } } return false; } fn should_immediately_materialize_decoded_non_trade_event(event_kind: &str) -> bool { if event_kind == "raydium_clmm.create_pool" { return true; } if event_kind == "raydium_clmm.collect_protocol_fee" { return true; } return false; } fn dex_decode_transaction_has_effective_error(transaction: &crate::ChainTransactionDto) -> bool { let err_json = match transaction.err_json.as_ref() { Some(err_json) => err_json.trim(), None => return false, }; if err_json.is_empty() { return false; } if err_json == "null" { return false; } return true; } fn dex_decode_payload_value(payload_json: &str) -> serde_json::Value { let parsed = serde_json::from_str::(payload_json); match parsed { Ok(parsed) => return parsed, Err(_) => return serde_json::Value::Object(serde_json::Map::new()), } } fn dex_decode_extract_first_amount_string( value: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { let text = dex_decode_extract_first_string(value, candidate_keys); if text.is_some() { return text; } return dex_decode_extract_first_number_as_string(value, candidate_keys); } fn dex_decode_extract_first_string( value: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { if let Some(object) = value.as_object() { for candidate_key in candidate_keys { let candidate_value = object.get(*candidate_key); let candidate_value = match candidate_value { Some(candidate_value) => candidate_value, None => continue, }; if let Some(text) = candidate_value.as_str() { let trimmed = text.trim(); if !trimmed.is_empty() { return Some(trimmed.to_string()); } } } for nested_value in object.values() { let nested = dex_decode_extract_first_string(nested_value, candidate_keys); if nested.is_some() { return nested; } } return None; } if let Some(array) = value.as_array() { for nested_value in array { let nested = dex_decode_extract_first_string(nested_value, candidate_keys); if nested.is_some() { return nested; } } } return None; } fn dex_decode_extract_first_number_as_string( value: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { if let Some(object) = value.as_object() { for candidate_key in candidate_keys { let candidate_value = object.get(*candidate_key); let candidate_value = match candidate_value { Some(candidate_value) => candidate_value, None => continue, }; if let Some(number) = candidate_value.as_i64() { return Some(number.to_string()); } if let Some(number) = candidate_value.as_u64() { return Some(number.to_string()); } if let Some(number) = candidate_value.as_f64() { return Some(number.to_string()); } } for nested_value in object.values() { let nested = dex_decode_extract_first_number_as_string(nested_value, candidate_keys); if nested.is_some() { return nested; } } return None; } if let Some(array) = value.as_array() { for nested_value in array { let nested = dex_decode_extract_first_number_as_string(nested_value, candidate_keys); if nested.is_some() { return nested; } } } return None; } fn instruction_audit_event_kind_by_protocol( protocol_name: &str, ) -> std::option::Option<&'static str> { match protocol_name { "raydium_amm_v4" => return Some("raydium_amm_v4.instruction_audit"), "raydium_clmm" => return Some("raydium_clmm.instruction_audit"), "raydium_cpmm" => return Some("raydium_cpmm.instruction_audit"), "raydium_stable_swap" => return Some("raydium_stable_swap.instruction_audit"), "raydium_launchpad" => return Some("raydium_launchpad.instruction_audit"), "meteora_dlmm" => return Some("meteora_dlmm.instruction_audit"), "meteora_damm_v1" => return Some("meteora_damm_v1.instruction_audit"), "meteora_damm_v2" => return Some("meteora_damm_v2.instruction_audit"), "meteora_dbc" => return Some("meteora_dbc.instruction_audit"), _ => return None, } } #[derive(Clone, Copy)] struct RaydiumAnchorSelfCpiEventSpec { entry_name: &'static str, event_kind: &'static str, event_family: &'static str, discriminator_hex: &'static str, } fn raydium_launchpad_anchor_self_cpi_event_spec( protocol_name: &str, data_bytes: std::option::Option<&[u8]>, ) -> std::option::Option { if protocol_name != "raydium_launchpad" { return None; } let selector_hex = discriminator_hex_from_bytes(data_bytes, 0); if selector_hex.as_deref() != Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX) { return None; } let event_discriminator_hex = discriminator_hex_from_bytes(data_bytes, 8); let event_discriminator_hex = match event_discriminator_hex.as_deref() { Some(event_discriminator_hex) => event_discriminator_hex, None => return None, }; match event_discriminator_hex { "bddb7fd34ee661ee" => { return Some(RaydiumAnchorSelfCpiEventSpec { entry_name: "trade_event", event_kind: "raydium_launchpad.trade_event", event_family: "swap", discriminator_hex: "bddb7fd34ee661ee", }); }, "97d7e20976a173ae" => { return Some(RaydiumAnchorSelfCpiEventSpec { entry_name: "pool_create_event", event_kind: "raydium_launchpad.pool_create_event", event_family: "pool_create", discriminator_hex: "97d7e20976a173ae", }); }, "15c2725778d3e220" => { return Some(RaydiumAnchorSelfCpiEventSpec { entry_name: "claim_vested_event", event_kind: "raydium_launchpad.claim_vested_event", event_family: "vesting", discriminator_hex: "15c2725778d3e220", }); }, "96980bb334d2bf7d" => { return Some(RaydiumAnchorSelfCpiEventSpec { entry_name: "create_vesting_event", event_kind: "raydium_launchpad.create_vesting_event", event_family: "vesting", discriminator_hex: "96980bb334d2bf7d", }); }, _ => return None, } } fn raydium_launchpad_anchor_self_cpi_pool_account( spec: RaydiumAnchorSelfCpiEventSpec, data_bytes: std::option::Option<&[u8]>, ) -> std::option::Option { match spec.entry_name { "trade_event" => return read_pubkey_base58_from_bytes(data_bytes, 16), "pool_create_event" => return read_pubkey_base58_from_bytes(data_bytes, 16), _ => return None, } } fn enrich_raydium_launchpad_anchor_self_cpi_payload( payload: serde_json::Value, spec: RaydiumAnchorSelfCpiEventSpec, data_bytes: std::option::Option<&[u8]>, ) -> serde_json::Value { let mut object = match payload { serde_json::Value::Object(object) => object, other => { let mut object = serde_json::Map::new(); object.insert("rawPayload".to_string(), other); object }, }; object.insert( "entryKind".to_string(), serde_json::Value::String(crate::ENTRY_KIND_EVENT.to_string()), ); object.insert("entryName".to_string(), serde_json::Value::String(spec.entry_name.to_string())); object.insert( "eventFamily".to_string(), serde_json::Value::String(spec.event_family.to_string()), ); object.insert("eventName".to_string(), serde_json::Value::String(spec.entry_name.to_string())); object.insert( "upstreamEventName".to_string(), serde_json::Value::String(spec.entry_name.to_string()), ); object.insert( "upstreamEntryName".to_string(), serde_json::Value::String(spec.entry_name.to_string()), ); object.insert( "upstreamEntryKind".to_string(), serde_json::Value::String(crate::ENTRY_KIND_EVENT.to_string()), ); object.insert( "upstreamDiscriminatorHex".to_string(), serde_json::Value::String(spec.discriminator_hex.to_string()), ); object.insert("localSpecializedDecoder".to_string(), serde_json::Value::Bool(true)); object.insert("decodedFromAnchorSelfCpiLog".to_string(), serde_json::Value::Bool(true)); object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(false)); object.insert( "auditReason".to_string(), serde_json::Value::String("raydium_launchpad_anchor_self_cpi_event_decoded".to_string()), ); object.insert( "proofSource".to_string(), serde_json::Value::String( "local_corpus_anchor_self_cpi_and_raydium_launchpad_event_discriminator".to_string(), ), ); if let Some(data_bytes) = data_bytes { object.insert( "anchorSelfCpiDataLength".to_string(), serde_json::Value::Number(serde_json::Number::from(data_bytes.len() as u64)), ); if data_bytes.len() >= 16 { object.insert( "anchorEventPayloadSize".to_string(), serde_json::Value::Number(serde_json::Number::from((data_bytes.len() - 16) as u64)), ); } } if let Some(pool_state) = raydium_launchpad_anchor_self_cpi_pool_account(spec, data_bytes) { object.insert("poolState".to_string(), serde_json::Value::String(pool_state.clone())); object.insert("poolAccount".to_string(), serde_json::Value::String(pool_state)); } if spec.entry_name == "trade_event" { insert_raydium_launchpad_trade_event_amounts(&mut object, data_bytes); object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(true)); object.insert("candleCandidate".to_string(), serde_json::Value::Bool(true)); object.insert("nonTradeUseful".to_string(), serde_json::Value::Bool(false)); object.remove("skipTradeReason"); object.remove("skipCandleReason"); } else if spec.entry_name == "pool_create_event" { object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false)); object.insert("candleCandidate".to_string(), serde_json::Value::Bool(false)); object.insert("nonTradeUseful".to_string(), serde_json::Value::Bool(true)); object.insert( "skipTradeReason".to_string(), serde_json::Value::String("pool_lifecycle_event".to_string()), ); object.insert( "skipCandleReason".to_string(), serde_json::Value::String("pool_lifecycle_event".to_string()), ); } else { object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false)); object.insert("candleCandidate".to_string(), serde_json::Value::Bool(false)); object.insert("nonTradeUseful".to_string(), serde_json::Value::Bool(false)); object.insert( "skipTradeReason".to_string(), serde_json::Value::String("launchpad_event_audit_only".to_string()), ); object.insert( "skipCandleReason".to_string(), serde_json::Value::String("launchpad_event_audit_only".to_string()), ); } return serde_json::Value::Object(object); } fn insert_raydium_launchpad_trade_event_amounts( object: &mut serde_json::Map, data_bytes: std::option::Option<&[u8]>, ) { let data = match data_bytes { Some(data) => data, None => return, }; let fields = [ ("totalBaseSellRaw", 48_usize), ("virtualBaseRaw", 56_usize), ("virtualQuoteRaw", 64_usize), ("realBaseBeforeRaw", 72_usize), ("realQuoteBeforeRaw", 80_usize), ("realBaseAfterRaw", 88_usize), ("realQuoteAfterRaw", 96_usize), ("amountInRaw", 104_usize), ("amountOutRaw", 112_usize), ("protocolFeeRaw", 120_usize), ("platformFeeRaw", 128_usize), ("creatorFeeRaw", 136_usize), ("shareFeeRaw", 144_usize), ]; for field in fields { if let Some(value) = read_u64_le_from_bytes(data, field.1) { object.insert(field.0.to_string(), serde_json::Value::String(value.to_string())); } } if let Some(direction) = read_u8_from_bytes(data, 152) { object.insert( "tradeDirectionRaw".to_string(), serde_json::Value::Number(serde_json::Number::from(direction as u64)), ); } if let Some(pool_status) = read_u8_from_bytes(data, 153) { object.insert( "poolStatusRaw".to_string(), serde_json::Value::Number(serde_json::Number::from(pool_status as u64)), ); } if let Some(exact_in) = read_u8_from_bytes(data, 154) { object.insert("exactIn".to_string(), serde_json::Value::Bool(exact_in != 0)); } insert_raydium_launchpad_normalized_trade_amounts(object); } fn insert_raydium_launchpad_normalized_trade_amounts( object: &mut serde_json::Map, ) { let direction = object.get("tradeDirectionRaw").and_then(serde_json::Value::as_u64); let amount_in = object .get("amountInRaw") .and_then(serde_json::Value::as_str) .map(str::to_string); let amount_out = object .get("amountOutRaw") .and_then(serde_json::Value::as_str) .map(str::to_string); match direction { Some(0) => { object .insert("tradeSide".to_string(), serde_json::Value::String("BuyBase".to_string())); if let Some(amount_out) = amount_out { object.insert("baseAmountRaw".to_string(), serde_json::Value::String(amount_out)); } if let Some(amount_in) = amount_in { object.insert("quoteAmountRaw".to_string(), serde_json::Value::String(amount_in)); } }, Some(1) => { object .insert("tradeSide".to_string(), serde_json::Value::String("SellBase".to_string())); if let Some(amount_in) = amount_in { object.insert("baseAmountRaw".to_string(), serde_json::Value::String(amount_in)); } if let Some(amount_out) = amount_out { object.insert("quoteAmountRaw".to_string(), serde_json::Value::String(amount_out)); } }, _ => {}, } } fn build_raydium_instruction_audit_payload( transaction: &crate::ChainTransactionDto, instruction: &crate::ChainInstructionDto, protocol_name: &str, event_kind: &str, program_id: &str, ) -> serde_json::Value { let accounts = parse_instruction_accounts_value(instruction.accounts_json.as_str()); let account_count = match accounts.as_array() { Some(items) => items.len(), None => 0, }; let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref()); let data_bytes = instruction_data_bytes_from_base58(data_base58.as_deref()); let discriminator_hex = raydium_instruction_discriminator_hex(protocol_name, data_bytes.as_deref(), 0); let anchor_self_cpi_log = discriminator_hex.as_deref() == Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX); let anchor_event_discriminator_hex = if anchor_self_cpi_log { discriminator_hex_from_bytes(data_bytes.as_deref(), 8) } else { None }; let anchor_event_payload_size = if anchor_self_cpi_log { match data_bytes.as_ref() { Some(data_bytes) => data_bytes.len().checked_sub(8), None => None, } } else { None }; let audit_reason = if anchor_self_cpi_log { "raydium_anchor_self_cpi_log_not_decoded_by_specific_event_decoder" } else { "raydium_instruction_not_decoded_by_specific_decoder" }; let proof_status = if anchor_self_cpi_log { "observed_local_corpus_anchor_self_cpi_log" } else { "unclassified_local_corpus_instruction" }; return serde_json::json!({ "decoder": protocol_name, "eventKind": event_kind, "signature": transaction.signature.clone(), "instructionId": instruction.id, "instructionIndex": instruction.instruction_index, "innerInstructionIndex": instruction.inner_instruction_index, "innerInstruction": instruction.inner_instruction_index.is_some(), "parentInstructionId": instruction.parent_instruction_id, "programId": program_id, "accounts": accounts, "accountCount": account_count, "data": data_base58, "discriminatorHex": discriminator_hex, "anchorSelfCpiLog": anchor_self_cpi_log, "anchorSelfCpiLogSelectorHex": if anchor_self_cpi_log { Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX) } else { None }, "anchorEventDiscriminatorHex": anchor_event_discriminator_hex, "anchorEventPayloadSize": anchor_event_payload_size, "auditReason": audit_reason, "proofStatus": proof_status, "tradeCandidate": false, "candleCandidate": false, "nonTradeUseful": false, "skipTradeReason": "instruction_audit_only", "skipCandleReason": "instruction_audit_only" }); } fn candidate_raydium_audit_pool_account( protocol_name: &str, accounts_json: &str, ) -> std::option::Option { let spec = match protocol_name { "raydium_amm_v4" => RaydiumInstructionAuditSpec { protocol_name: "raydium_amm_v4", event_kind: "raydium_amm_v4.instruction_audit", candidate_pool_account_index: 1, }, "raydium_clmm" => RaydiumInstructionAuditSpec { protocol_name: "raydium_clmm", event_kind: "raydium_clmm.instruction_audit", candidate_pool_account_index: 2, }, "raydium_cpmm" => RaydiumInstructionAuditSpec { protocol_name: "raydium_cpmm", event_kind: "raydium_cpmm.instruction_audit", candidate_pool_account_index: 3, }, "raydium_launchpad" => RaydiumInstructionAuditSpec { protocol_name: "raydium_launchpad", event_kind: "raydium_launchpad.instruction_audit", candidate_pool_account_index: 4, }, _ => return None, }; let accounts_result = serde_json::from_str::>(accounts_json); let accounts = match accounts_result { Ok(accounts) => accounts, Err(_) => return None, }; return accounts.get(spec.candidate_pool_account_index).cloned(); } fn upstream_registry_instruction_match_is_locally_covered( registry_match: &crate::UpstreamRegistryEntryDto, ) -> bool { if registry_match.entry_kind != crate::ENTRY_KIND_INSTRUCTION { return false; } let local_event_kind = crate::dex_event_coverage::known_local_event_kind( registry_match.decoder_code.as_str(), registry_match.entry_name.as_str(), ); match local_event_kind { Some(_) => return true, None => return false, } } fn build_upstream_registry_instruction_match_payload( transaction: &crate::ChainTransactionDto, instruction: &crate::ChainInstructionDto, registry_match: &crate::UpstreamRegistryEntryDto, data_base58: std::option::Option<&str>, ) -> serde_json::Value { let data_bytes = instruction_data_bytes_from_base58(data_base58); let data_byte_len = match data_bytes.as_ref() { Some(data_bytes) => { let len_result = u64::try_from(data_bytes.len()); match len_result { Ok(len) => serde_json::Value::Number(serde_json::Number::from(len)), Err(_) => serde_json::Value::Null, } }, None => serde_json::Value::Null, }; return serde_json::json!({ "decoder": crate::UPSTREAM_REGISTRY_PROTOCOL_NAME, "matchKind": "instruction_discriminator", "signature": transaction.signature.clone(), "slot": transaction.slot, "instructionId": instruction.id, "instructionIndex": instruction.instruction_index, "innerInstructionIndex": instruction.inner_instruction_index, "parentInstructionId": instruction.parent_instruction_id, "stackHeight": instruction.stack_height, "programId": instruction.program_id.clone(), "programName": instruction.program_name.clone(), "accounts": parse_instruction_accounts_value(instruction.accounts_json.as_str()), "accountCount": parse_instruction_accounts_vec(instruction.accounts_json.as_str()).len(), "dataBase58": data_base58, "dataByteLen": data_byte_len, "upstreamSourceRepo": registry_match.source_repo.clone(), "upstreamSourcePath": registry_match.source_path.clone(), "upstreamDecoderCode": registry_match.decoder_code.clone(), "upstreamProgramFamily": registry_match.program_family.clone(), "upstreamSurfaceKind": registry_match.surface_kind.clone(), "upstreamEntryKind": registry_match.entry_kind.clone(), "upstreamEntryName": registry_match.entry_name.clone(), "upstreamDiscriminatorHex": registry_match.discriminator_hex.clone(), "upstreamDiscriminatorLen": registry_match.discriminator_len, "upstreamProofStatus": registry_match.proof_status.clone(), "upstreamNotes": registry_match.notes.clone(), "tradeCandidate": false, "candleCandidate": false, "nonTradeUseful": false, "skipTradeReason": "upstream_git_unverified_registry_match", "skipCandleReason": "upstream_git_unverified_registry_match" }); } fn parse_instruction_accounts_vec(accounts_json: &str) -> std::vec::Vec { let accounts_result = serde_json::from_str::>(accounts_json); match accounts_result { Ok(accounts) => return accounts, Err(_) => return std::vec::Vec::new(), } } fn parse_instruction_accounts_value(accounts_json: &str) -> serde_json::Value { let accounts_result = serde_json::from_str::(accounts_json); match accounts_result { Ok(accounts) => return accounts, Err(_) => return serde_json::Value::Array(std::vec::Vec::new()), } } fn parse_instruction_data_base58( data_json: std::option::Option<&str>, ) -> std::option::Option { let data_json = match data_json { Some(data_json) => data_json, None => return None, }; let data_result = serde_json::from_str::(data_json); match data_result { Ok(data) => return Some(data), Err(_) => { if data_json.trim().is_empty() { return None; } return Some(data_json.to_string()); }, } } fn discriminator_hex_from_base58( data_base58: std::option::Option<&str>, ) -> std::option::Option { let bytes = instruction_data_bytes_from_base58(data_base58); return discriminator_hex_from_bytes(bytes.as_deref(), 0); } fn raydium_instruction_discriminator_hex( protocol_name: &str, bytes: std::option::Option<&[u8]>, offset: usize, ) -> std::option::Option { if protocol_name == "raydium_amm_v4" || protocol_name == "raydium_stable_swap" { return discriminator_hex_from_bytes_with_len(bytes, offset, 1); } return discriminator_hex_from_bytes(bytes, offset); } fn discriminator_hex_from_bytes_with_len( bytes: std::option::Option<&[u8]>, offset: usize, length: usize, ) -> std::option::Option { let bytes = match bytes { Some(bytes) => bytes, None => return None, }; if bytes.len() < offset + length { return None; } let mut text = std::string::String::new(); let mut index = offset; let end = offset + length; while index < end { let byte = bytes[index]; text.push_str(format!("{byte:02x}").as_str()); index += 1; } return Some(text); } fn discriminator_hex_from_bytes( bytes: std::option::Option<&[u8]>, offset: usize, ) -> std::option::Option { let bytes = match bytes { Some(bytes) => bytes, None => return None, }; if bytes.len() < offset + 8 { return None; } let mut text = std::string::String::new(); let mut index = offset; let end = offset + 8; while index < end { let byte = bytes[index]; text.push_str(format!("{byte:02x}").as_str()); index += 1; } return Some(text); } fn append_persisted_events( target: &mut std::vec::Vec, source: std::vec::Vec, ) { for persisted_event in source { target.push(persisted_event); } } #[derive(Clone, Debug)] struct RaydiumClmmProgramDataEventCandidate { decoded_event: crate::RaydiumClmmDecodedEvent, consumed: bool, } fn collect_raydium_clmm_program_data_events( transaction: &crate::ChainTransactionDto, ) -> std::vec::Vec { let logs = extract_transaction_log_messages(transaction.transaction_json.as_str()); let mut events = std::vec::Vec::new(); let mut clmm_stack_depth = 0_u32; for log_message in logs { if is_program_invoke_log(log_message.as_str(), crate::RAYDIUM_CLMM_PROGRAM_ID) { clmm_stack_depth += 1; continue; } if is_program_success_or_failed_log(log_message.as_str(), crate::RAYDIUM_CLMM_PROGRAM_ID) { clmm_stack_depth = clmm_stack_depth.saturating_sub(1); continue; } if clmm_stack_depth == 0 { continue; } let data_base64 = match log_message.strip_prefix("Program data: ") { Some(data_base64) => data_base64.trim(), None => continue, }; if data_base64.is_empty() { continue; } let decoded_event = crate::decode_raydium_clmm_program_data_event(data_base64); if let Some(decoded_event) = decoded_event { events.push(RaydiumClmmProgramDataEventCandidate { decoded_event, consumed: false }); } } return events; } async fn persist_matching_raydium_clmm_program_data_events( service: &DexDecodeService, transaction: &crate::ChainTransactionDto, instruction: &crate::ChainInstructionDto, instruction_discriminator_hex: std::option::Option<&str>, program_data_events: &mut [RaydiumClmmProgramDataEventCandidate], persisted: &mut std::vec::Vec, ) -> Result<(), crate::Error> { let instruction_id = match instruction.id { Some(instruction_id) => instruction_id, None => return Ok(()), }; let expected_event_kinds = raydium_clmm_program_data_event_kinds_for_instruction(instruction_discriminator_hex); if expected_event_kinds.is_empty() { return Ok(()); } let mut index = 0_usize; while index < program_data_events.len() { if program_data_events[index].consumed { index += 1; continue; } let event_kind = program_data_events[index].decoded_event.event_kind(); if !string_slice_contains(expected_event_kinds.as_slice(), event_kind) { index += 1; continue; } program_data_events[index].consumed = true; let persist_result = service .persist_raydium_clmm_event( transaction, instruction_id, &program_data_events[index].decoded_event, ) .await; let persisted_event = match persist_result { Ok(persisted_event) => persisted_event, Err(error) => return Err(error), }; persisted.push(persisted_event); index += 1; } return Ok(()); } fn raydium_clmm_program_data_event_kinds_for_instruction( instruction_discriminator_hex: std::option::Option<&str>, ) -> std::vec::Vec<&'static str> { let discriminator = match instruction_discriminator_hex { Some(discriminator) => discriminator, None => return std::vec::Vec::new(), }; match discriminator { "e992d18ecf6840bc" | "2b44d4a7592fa401" => { return vec!["raydium_clmm.pool_created_event"]; }, "8888fcddc2427e59" => { return vec!["raydium_clmm.collect_protocol_fee_event"]; }, "f8c69e91e17587c8" | "2b04ed0b1ac91e62" | "457d73daf5baf2c4" => { return vec!["raydium_clmm.swap_event"]; }, "87802f4d0f98f031" | "4db84ad67056f1c7" | "4dffae527d1dc92e" => { return vec![ "raydium_clmm.liquidity_calculate_event", "raydium_clmm.create_personal_position_event", "raydium_clmm.increase_liquidity_event", "raydium_clmm.liquidity_change_event", ]; }, "2e9cf3760dcdfbb2" | "851d59df45eeb00a" => { return vec![ "raydium_clmm.liquidity_calculate_event", "raydium_clmm.increase_liquidity_event", "raydium_clmm.liquidity_change_event", ]; }, "a026d06f685b2c01" | "3a7fbc3e4f52c460" => { return vec![ "raydium_clmm.liquidity_calculate_event", "raydium_clmm.decrease_liquidity_event", "raydium_clmm.liquidity_change_event", ]; }, "7b86510031446262" | "c975989055556cb2" => { return vec![ "raydium_clmm.decrease_liquidity_event", "raydium_clmm.collect_personal_fee_event", "raydium_clmm.liquidity_change_event", ]; }, "8934edd4d7756c68" | "313cae889a1c74c8" | "bd0eb5785576e33e" => { return vec!["raydium_clmm.config_change_event"]; }, "5f87c0c4f281e644" | "7034a74b20c9d389" | "a3ace0340b9a6adf" => { return vec!["raydium_clmm.update_reward_infos_event"]; }, _ => return std::vec::Vec::new(), } } fn string_slice_contains(values: &[&'static str], expected: &str) -> bool { for value in values { if *value == expected { return true; } } return false; } #[derive(Clone, Debug)] struct RaydiumCpmmProgramDataEventCandidate { decoded_event: crate::RaydiumCpmmDecodedEvent, consumed: bool, } fn collect_raydium_cpmm_program_data_events( transaction: &crate::ChainTransactionDto, ) -> std::vec::Vec { let logs = extract_transaction_log_messages(transaction.transaction_json.as_str()); let mut events = std::vec::Vec::new(); let mut cpmm_stack_depth = 0_u32; for log_message in logs { if is_program_invoke_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) { cpmm_stack_depth += 1; continue; } if is_program_success_or_failed_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) { cpmm_stack_depth = cpmm_stack_depth.saturating_sub(1); continue; } if cpmm_stack_depth == 0 { continue; } let data_base64 = match log_message.strip_prefix("Program data: ") { Some(data_base64) => data_base64.trim(), None => continue, }; if data_base64.is_empty() { continue; } let decoded_event = crate::decode_raydium_cpmm_program_data_event(data_base64); if let Some(decoded_event) = decoded_event { events.push(RaydiumCpmmProgramDataEventCandidate { decoded_event, consumed: false }); } } return events; } async fn persist_matching_raydium_cpmm_program_data_event( service: &DexDecodeService, transaction: &crate::ChainTransactionDto, instruction: &crate::ChainInstructionDto, instruction_kind: std::option::Option<&str>, program_data_events: &mut [RaydiumCpmmProgramDataEventCandidate], persisted: &mut std::vec::Vec, ) -> Result<(), crate::Error> { let expected_event_kind = match instruction_kind { Some("swap_base_input") => Some("swap_event"), Some("swap_base_output") => Some("swap_event"), Some("deposit") => Some("lp_change_event"), Some("withdraw") => Some("lp_change_event"), _ => None, }; let expected_event_kind = match expected_event_kind { Some(expected_event_kind) => expected_event_kind, None => return Ok(()), }; let mut index = 0_usize; while index < program_data_events.len() { if program_data_events[index].consumed { index += 1; continue; } let event_matches = match (&program_data_events[index].decoded_event, expected_event_kind) { (crate::RaydiumCpmmDecodedEvent::SwapEvent(_), "swap_event") => true, (crate::RaydiumCpmmDecodedEvent::LpChangeEvent(_), "lp_change_event") => true, _ => false, }; if !event_matches { index += 1; continue; } program_data_events[index].consumed = true; let persist_result = service .persist_raydium_cpmm_event( transaction, instruction, &program_data_events[index].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(()); } return Ok(()); } fn extract_transaction_log_messages(transaction_json: &str) -> std::vec::Vec { let value_result = serde_json::from_str::(transaction_json); let value = match value_result { Ok(value) => value, Err(_) => return std::vec::Vec::new(), }; let meta = match value.get("meta") { Some(meta) => meta, None => return std::vec::Vec::new(), }; let logs = match meta.get("logMessages") { Some(logs) => logs, None => return std::vec::Vec::new(), }; let logs = match logs.as_array() { Some(logs) => logs, None => return std::vec::Vec::new(), }; let mut output = std::vec::Vec::new(); for log in logs { if let Some(log) = log.as_str() { output.push(log.to_string()); } } return output; } fn is_program_invoke_log(log_message: &str, program_id: &str) -> bool { if !log_message.starts_with("Program ") { return false; } if !log_message.contains(" invoke [") { return false; } return log_message.contains(program_id); } fn is_program_success_or_failed_log(log_message: &str, program_id: &str) -> bool { if !log_message.starts_with("Program ") { return false; } if !log_message.contains(program_id) { return false; } if log_message.ends_with(" success") { return true; } if log_message.contains(" failed: ") { return true; } return false; } fn decoded_instruction_ids_from_persisted_events( persisted: &[crate::DexDecodedEventDto], ) -> std::collections::HashSet { let mut decoded_instruction_ids = std::collections::HashSet::::new(); for decoded_event in persisted { let instruction_id = match decoded_event.instruction_id { Some(instruction_id) => instruction_id, None => continue, }; decoded_instruction_ids.insert(instruction_id); } return decoded_instruction_ids; } 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" ); assert_eq!( crate::classify_dex_event_category_code("raydium_clmm.instruction_audit"), "informational" ); assert_eq!( crate::classify_dex_event_lifecycle_kind_code("raydium_clmm.instruction_audit"), "instruction_audit" ); assert_eq!( crate::classify_dex_event_actionability_code( "raydium_clmm.instruction_audit", false, false ), "informational" ); } #[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())) ); } #[test] fn maps_observed_raydium_clmm_non_swap_discriminators() { let create_pool = super::raydium_mapped_non_trade_instruction_spec( "raydium_clmm", Some("e992d18ecf6840bc"), 13, ); let create_pool = match create_pool { Some(create_pool) => create_pool, None => panic!("create_pool discriminator must be mapped"), }; assert_eq!(create_pool.event_kind, "raydium_clmm.create_pool"); assert_eq!(create_pool.pool_account_index, Some(2)); assert_eq!(create_pool.token_a_mint_index, Some(3)); assert_eq!(create_pool.token_b_mint_index, Some(4)); let collect_protocol_fee = super::raydium_mapped_non_trade_instruction_spec( "raydium_clmm", Some("8888fcddc2427e59"), 11, ); let collect_protocol_fee = match collect_protocol_fee { Some(collect_protocol_fee) => collect_protocol_fee, None => panic!("collect_protocol_fee discriminator must be mapped"), }; assert_eq!(collect_protocol_fee.event_kind, "raydium_clmm.collect_protocol_fee"); assert_eq!(collect_protocol_fee.pool_account_index, Some(1)); assert_eq!(collect_protocol_fee.token_a_mint_index, Some(5)); assert_eq!(collect_protocol_fee.token_b_mint_index, Some(6)); let decrease = super::raydium_mapped_non_trade_instruction_spec( "raydium_clmm", Some("3a7fbc3e4f52c460"), 19, ); let decrease = match decrease { Some(decrease) => decrease, None => panic!("decrease_liquidity_v2 discriminator must be mapped"), }; assert_eq!(decrease.event_kind, "raydium_clmm.decrease_liquidity_v2"); assert_eq!(decrease.pool_account_index, Some(3)); assert_eq!(decrease.token_a_mint_index, Some(14)); assert_eq!(decrease.token_b_mint_index, Some(15)); let increase = super::raydium_mapped_non_trade_instruction_spec( "raydium_clmm", Some("851d59df45eeb00a"), 15, ); let increase = match increase { Some(increase) => increase, None => panic!("increase_liquidity_v2 discriminator must be mapped"), }; assert_eq!(increase.event_kind, "raydium_clmm.increase_liquidity_v2"); assert_eq!(increase.pool_account_index, Some(2)); let open_limit_order = super::raydium_mapped_non_trade_instruction_spec( "raydium_clmm", Some("9d20dab7471d1293"), 11, ); let open_limit_order = match open_limit_order { Some(open_limit_order) => open_limit_order, None => panic!("open_limit_order discriminator must be mapped"), }; assert_eq!(open_limit_order.event_kind, "raydium_clmm.open_limit_order"); assert_eq!(open_limit_order.pool_account_index, Some(1)); assert_eq!(open_limit_order.token_a_mint_index, Some(7)); let increase_limit_order = super::raydium_mapped_non_trade_instruction_spec( "raydium_clmm", Some("b19059ecfaba7d63"), 8, ); let increase_limit_order = match increase_limit_order { Some(increase_limit_order) => increase_limit_order, None => panic!("increase_limit_order discriminator must be mapped"), }; assert_eq!(increase_limit_order.event_kind, "raydium_clmm.increase_limit_order"); assert_eq!(increase_limit_order.pool_account_index, Some(1)); assert_eq!(increase_limit_order.token_a_mint_index, Some(6)); let decrease_limit_order = super::raydium_mapped_non_trade_instruction_spec( "raydium_clmm", Some("759d3c674231a300"), 13, ); let decrease_limit_order = match decrease_limit_order { Some(decrease_limit_order) => decrease_limit_order, None => panic!("decrease_limit_order discriminator must be mapped"), }; assert_eq!(decrease_limit_order.event_kind, "raydium_clmm.decrease_limit_order"); assert_eq!(decrease_limit_order.pool_account_index, Some(1)); assert_eq!(decrease_limit_order.token_a_mint_index, Some(8)); assert_eq!(decrease_limit_order.token_b_mint_index, Some(9)); let update_dynamic_fee_config = super::raydium_mapped_non_trade_instruction_spec( "raydium_clmm", Some("0707500802c784f0"), 2, ); let update_dynamic_fee_config = match update_dynamic_fee_config { Some(update_dynamic_fee_config) => update_dynamic_fee_config, None => panic!("update_dynamic_fee_config discriminator must be mapped"), }; assert_eq!(update_dynamic_fee_config.event_kind, "raydium_clmm.update_dynamic_fee_config"); let cpi_event = super::raydium_mapped_non_trade_instruction_spec( "raydium_clmm", Some("e445a52e51cb9a1d"), 1, ); let cpi_event = match cpi_event { Some(cpi_event) => cpi_event, None => panic!("clmm cpi_event discriminator must be mapped"), }; assert_eq!(cpi_event.event_kind, "raydium_clmm.cpi_event"); } #[test] fn maps_observed_raydium_cpmm_non_swap_discriminators() { let expected = [ ("9c5420764587467b", 4_usize, "raydium_cpmm.close_permission_pda"), ("1416567bc61cdb84", 13_usize, "raydium_cpmm.collect_creator_fee"), ("a78a4e95dfc2067e", 12_usize, "raydium_cpmm.collect_fund_fee"), ("8888fcddc2427e59", 12_usize, "raydium_cpmm.collect_protocol_fee"), ("8934edd4d7756c68", 3_usize, "raydium_cpmm.create_amm_config"), ("878802d889a9b5ca", 4_usize, "raydium_cpmm.create_permission_pda"), ("f223c68952e1f2b6", 13_usize, "raydium_cpmm.deposit"), ("afaf6d1f0d989bed", 20_usize, "raydium_cpmm.initialize"), ("3f37fe4131b25979", 21_usize, "raydium_cpmm.initialize_with_permission"), ("313cae889a1c74c8", 2_usize, "raydium_cpmm.update_amm_config"), ("82576c062ee0757b", 2_usize, "raydium_cpmm.update_pool_status"), ("b712469c946da122", 14_usize, "raydium_cpmm.withdraw"), ("e445a52e51cb9a1d", 1_usize, "raydium_cpmm.cpi_event"), ("40f4bc78a7e9690a", 3_usize, "raydium_cpmm.anchor_idl_instruction"), ("40f4bc78a7e9690a", 6_usize, "raydium_cpmm.anchor_idl_instruction"), ]; for (discriminator, account_count, event_kind) in expected { let mapped = super::raydium_mapped_non_trade_instruction_spec( "raydium_cpmm", Some(discriminator), account_count, ); let mapped = match mapped { Some(mapped) => mapped, None => panic!("raydium cpmm discriminator must be mapped: {}", discriminator), }; assert_eq!(mapped.event_kind, event_kind); } } #[test] fn extracts_instruction_discriminator_from_camel_and_snake_payload_keys() { let camel_payload = serde_json::json!({ "instructionDiscriminatorHex": "e992d18ecf6840bc" }); assert_eq!( super::instruction_discriminator_hex_from_payload(&camel_payload), Some("e992d18ecf6840bc".to_string()) ); let snake_payload = serde_json::json!({ "instruction_discriminator_hex": "8888fcddc2427e59" }); assert_eq!( super::instruction_discriminator_hex_from_payload(&snake_payload), Some("8888fcddc2427e59".to_string()) ); } #[test] fn skips_raydium_audit_when_discriminator_was_already_decoded() { let mut keys = std::collections::HashSet::::new(); keys.insert(super::raydium_decoded_discriminator_key("raydium_clmm", "e992d18ecf6840bc")); assert!(super::raydium_instruction_already_decoded_by_discriminator( &keys, "raydium_clmm", Some("e992d18ecf6840bc"), )); assert!(!super::raydium_instruction_already_decoded_by_discriminator( &keys, "raydium_clmm", Some("8888fcddc2427e59"), )); } #[test] fn immediately_materializes_only_targeted_clmm_non_trade_events() { assert!(super::should_immediately_materialize_decoded_non_trade_event( "raydium_clmm.create_pool", )); assert!(super::should_immediately_materialize_decoded_non_trade_event( "raydium_clmm.collect_protocol_fee", )); assert!(!super::should_immediately_materialize_decoded_non_trade_event( "raydium_clmm.swap", )); assert!(!super::should_immediately_materialize_decoded_non_trade_event( "raydium_cpmm.collect_protocol_fee", )); } #[test] fn maps_raydium_launchpad_non_trade_discriminators() { let buy = super::raydium_mapped_non_trade_instruction_spec( "raydium_launchpad", Some("faea0d7bd59c13ec"), 11, ); let buy = match buy { Some(buy) => buy, None => panic!("buy_exact_in discriminator must map"), }; assert_eq!(buy.instruction_name, "buy_exact_in"); assert_eq!(buy.event_kind, "raydium_launchpad.buy_exact_in"); assert_eq!(buy.pool_account_index, Some(4)); assert_eq!(buy.token_a_mint_index, Some(9)); assert_eq!(buy.token_b_mint_index, Some(10)); assert_eq!(buy.lp_mint_index, None); let migration = super::raydium_mapped_non_trade_instruction_spec( "raydium_launchpad", Some("cf52c091fecf91df"), 1, ); let migration = match migration { Some(migration) => migration, None => panic!("migrate_to_amm discriminator must map"), }; assert_eq!(migration.event_kind, "raydium_launchpad.migrate_to_amm"); assert_eq!(migration.pool_account_index, None); } #[test] fn maps_instruction_audit_event_kind_for_raydium_and_meteora_dlmm_protocols() { assert_eq!( super::instruction_audit_event_kind_by_protocol("raydium_clmm"), Some("raydium_clmm.instruction_audit") ); assert_eq!( super::instruction_audit_event_kind_by_protocol("raydium_launchpad"), Some("raydium_launchpad.instruction_audit") ); assert_eq!( super::instruction_audit_event_kind_by_protocol("meteora_dlmm"), Some("meteora_dlmm.instruction_audit") ); assert_eq!( super::instruction_audit_event_kind_by_protocol("meteora_damm_v1"), Some("meteora_damm_v1.instruction_audit") ); assert_eq!( super::instruction_audit_event_kind_by_protocol("meteora_damm_v2"), Some("meteora_damm_v2.instruction_audit") ); assert_eq!( super::instruction_audit_event_kind_by_protocol("meteora_dbc"), Some("meteora_dbc.instruction_audit") ); assert_eq!(super::instruction_audit_event_kind_by_protocol("unknown"), None); } #[test] fn upstream_registry_match_payload_is_never_trade_or_candle_candidate() { let transaction = crate::ChainTransactionDto::new( "upstream-registry-test-signature".to_string(), Some(123), Some(123456), Some("test".to_string()), None, None, None, "{}".to_string(), ); let instruction = crate::ChainInstructionDto::new( 1, None, 0, None, Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()), None, None, "[]".to_string(), Some("data".to_string()), None, None, ); let registry_match = crate::UpstreamRegistryEntryDto { source_repo: Some("sevenlabs-hq/carbon".to_string()), source_path: Some("decoders/example.rs".to_string()), decoder_code: "meteora_damm_v2".to_string(), program_id: Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()), program_family: "meteora".to_string(), surface_kind: "amm".to_string(), entry_kind: crate::ENTRY_KIND_INSTRUCTION.to_string(), entry_name: "swap".to_string(), discriminator_hex: Some("f8c69e91e17587c8".to_string()), discriminator_len: Some(8), proof_status: crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED.to_string(), notes: "test".to_string(), }; let payload = super::build_upstream_registry_instruction_match_payload( &transaction, &instruction, ®istry_match, Some("data"), ); assert_eq!(payload.get("tradeCandidate").and_then(serde_json::Value::as_bool), Some(false)); assert_eq!( payload.get("candleCandidate").and_then(serde_json::Value::as_bool), Some(false) ); assert_eq!( payload.get("upstreamProofStatus").and_then(serde_json::Value::as_str), Some(crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED) ); } }