// file: kb_lib/src/dex_event_classification.rs //! Shared DEX event classification and decoded-payload enrichment. //! //! This module contains deterministic helpers used by DEX decoding, //! trade aggregation and future non-trade event materialization. //! //! It intentionally does not decode protocol instructions and does not //! perform database access. /// Stable business category assigned to one decoded DEX event kind. #[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum DexEventCategory { /// Swap-like event that can potentially become a normalized trade. Trade, /// Liquidity deposit, withdraw, position open or position close event. Liquidity, /// Fee collection event. Fee, /// Reward or emission event. Reward, /// Pool creation, initialization or migration event. PoolLifecycle, /// Protocol administration, configuration or permission update event. Admin, /// Event kind that is not classified yet. Unknown, } impl DexEventCategory { /// Returns the stable string code persisted inside decoded payload metadata. pub fn as_str(self) -> &'static str { match self { Self::Trade => return "trade", Self::Liquidity => return "liquidity", Self::Fee => return "fee", Self::Reward => return "reward", Self::PoolLifecycle => return "pool_lifecycle", Self::Admin => return "admin", Self::Unknown => return "unknown", } } } /// Fine-grained lifecycle kind assigned to one decoded DEX event kind. #[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum DexEventLifecycleKind { /// Swap-like trade event. TradeSwap, /// Pool creation or initialization event. PoolCreation, /// Pair creation event when it can be distinguished from pool creation. PairCreation, /// Liquidity deposit or add-liquidity event. LiquidityAdd, /// Liquidity withdraw or remove-liquidity event. LiquidityRemove, /// Concentrated-liquidity position open event. PositionOpen, /// Concentrated-liquidity position close event. PositionClose, /// Migration event, for example launch surface to AMM/CLMM/DLMM. Migration, /// Launch or bonding-curve initialization event. Launch, /// Token mint event detected through a DEX or launch surface decoder. Mint, /// Token burn event detected through a DEX or launch surface decoder. Burn, /// Fee collection event. FeeCollection, /// Reward or emission event. Reward, /// Administration, configuration or permission update event. AdminConfig, /// Event kind that is not classified yet. Unknown, } impl DexEventLifecycleKind { /// Returns the stable string code persisted inside decoded payload metadata. pub fn as_str(self) -> &'static str { match self { Self::TradeSwap => return "trade_swap", Self::PoolCreation => return "pool_creation", Self::PairCreation => return "pair_creation", Self::LiquidityAdd => return "liquidity_add", Self::LiquidityRemove => return "liquidity_remove", Self::PositionOpen => return "position_open", Self::PositionClose => return "position_close", Self::Migration => return "migration", Self::Launch => return "launch", Self::Mint => return "mint", Self::Burn => return "burn", Self::FeeCollection => return "fee_collection", Self::Reward => return "reward", Self::AdminConfig => return "admin_config", Self::Unknown => return "unknown", } } } /// Stable actionability class assigned to one decoded DEX event. #[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum DexEventActionability { /// Direct swap-like event that can feed trade/candle materialization. TradeCandidate, /// Swap-like event detected but not materializable as trade/candle yet. NonActionableTrade, /// Useful non-trade event that should remain visible for future materialization. NonTradeUseful, /// Failed transaction event retained for diagnostics but never actionable. FailedTransaction, /// Classified event that is informational only for the current pipeline. Informational, /// Event that is not classified yet. Unknown, } impl DexEventActionability { /// Returns the stable string code persisted inside decoded payload metadata. pub fn as_str(self) -> &'static str { match self { Self::TradeCandidate => return "trade_candidate", Self::NonActionableTrade => return "non_actionable_trade", Self::NonTradeUseful => return "non_trade_useful", Self::FailedTransaction => return "failed_transaction", Self::Informational => return "informational", Self::Unknown => return "unknown", } } } /// Classifies a DEX event kind into a stable business category. pub fn classify_dex_event_category(event_kind: &str) -> DexEventCategory { if is_dex_reward_event_kind(event_kind) { return DexEventCategory::Reward; } if is_dex_fee_event_kind(event_kind) { return DexEventCategory::Fee; } if is_dex_liquidity_event_kind(event_kind) { return DexEventCategory::Liquidity; } if is_dex_pool_lifecycle_event_kind(event_kind) { return DexEventCategory::PoolLifecycle; } if is_dex_admin_event_kind(event_kind) { return DexEventCategory::Admin; } if is_dex_trade_event_kind(event_kind) { return DexEventCategory::Trade; } return DexEventCategory::Unknown; } /// Classifies a DEX event kind and returns the persisted category code. pub fn classify_dex_event_category_code(event_kind: &str) -> &'static str { return classify_dex_event_category(event_kind).as_str(); } /// Classifies a DEX event kind into a fine-grained lifecycle kind. pub fn classify_dex_event_lifecycle_kind(event_kind: &str) -> DexEventLifecycleKind { if is_dex_token_burn_event_kind(event_kind) { return DexEventLifecycleKind::Burn; } if is_dex_token_mint_event_kind(event_kind) { return DexEventLifecycleKind::Mint; } if is_dex_migration_event_kind(event_kind) { return DexEventLifecycleKind::Migration; } if is_dex_launch_event_kind(event_kind) { return DexEventLifecycleKind::Launch; } if is_dex_pair_creation_event_kind(event_kind) { return DexEventLifecycleKind::PairCreation; } if is_dex_pool_creation_event_kind(event_kind) { return DexEventLifecycleKind::PoolCreation; } if is_dex_liquidity_add_event_kind(event_kind) { return DexEventLifecycleKind::LiquidityAdd; } if is_dex_liquidity_remove_event_kind(event_kind) { return DexEventLifecycleKind::LiquidityRemove; } if is_dex_position_open_event_kind(event_kind) { return DexEventLifecycleKind::PositionOpen; } if is_dex_position_close_event_kind(event_kind) { return DexEventLifecycleKind::PositionClose; } if is_dex_fee_event_kind(event_kind) { return DexEventLifecycleKind::FeeCollection; } if is_dex_reward_event_kind(event_kind) { return DexEventLifecycleKind::Reward; } if is_dex_admin_event_kind(event_kind) { return DexEventLifecycleKind::AdminConfig; } if is_dex_trade_event_kind(event_kind) { return DexEventLifecycleKind::TradeSwap; } return DexEventLifecycleKind::Unknown; } /// Classifies a DEX event kind and returns the persisted lifecycle kind code. pub fn classify_dex_event_lifecycle_kind_code(event_kind: &str) -> &'static str { return classify_dex_event_lifecycle_kind(event_kind).as_str(); } /// Classifies one decoded DEX event actionability from its kind and candidate flags. pub fn classify_dex_event_actionability( event_kind: &str, trade_candidate: bool, transaction_failed: bool, ) -> DexEventActionability { if transaction_failed { return DexEventActionability::FailedTransaction; } if trade_candidate { return DexEventActionability::TradeCandidate; } if is_dex_trade_event_kind(event_kind) { return DexEventActionability::NonActionableTrade; } let category = classify_dex_event_category(event_kind); match category { DexEventCategory::Liquidity => return DexEventActionability::NonTradeUseful, DexEventCategory::Fee => return DexEventActionability::NonTradeUseful, DexEventCategory::Reward => return DexEventActionability::NonTradeUseful, DexEventCategory::PoolLifecycle => return DexEventActionability::NonTradeUseful, DexEventCategory::Admin => return DexEventActionability::NonTradeUseful, DexEventCategory::Trade => return DexEventActionability::NonActionableTrade, DexEventCategory::Unknown => return DexEventActionability::Unknown, } } /// Classifies one decoded DEX event actionability and returns its persisted code. pub fn classify_dex_event_actionability_code( event_kind: &str, trade_candidate: bool, transaction_failed: bool, ) -> &'static str { return classify_dex_event_actionability(event_kind, trade_candidate, transaction_failed) .as_str(); } /// Returns true when the event kind represents a swap-like event. pub fn is_dex_trade_event_kind(event_kind: &str) -> bool { if event_kind.ends_with(".buy") { return true; } if event_kind.ends_with(".sell") { return true; } if event_kind.ends_with(".swap") { return true; } if event_kind.contains(".swap_") { return true; } if event_kind.ends_with(".exact_input") { return true; } if event_kind.ends_with(".exact_output") { return true; } return false; } /// Returns true when the event kind can directly produce a candle candidate. pub fn is_dex_candle_candidate_event_kind(event_kind: &str) -> bool { if event_kind.contains("router") { return false; } if event_kind.contains("route") { return false; } return is_dex_trade_event_kind(event_kind); } /// Returns true for liquidity lifecycle changes that must not become candles. pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool { if event_kind.contains(".deposit") { return true; } if event_kind.contains(".withdraw") { return true; } if event_kind.contains(".add_liquidity") { return true; } if event_kind.contains(".remove_liquidity") { return true; } if event_kind.contains(".increase_liquidity") { return true; } if event_kind.contains(".decrease_liquidity") { return true; } if event_kind.contains(".initialize_position") { return true; } if event_kind.contains(".open_position") { return true; } if event_kind.contains(".close_position") { return true; } return false; } /// Returns true for liquidity add-like DEX events. pub fn is_dex_liquidity_add_event_kind(event_kind: &str) -> bool { if event_kind.contains(".deposit") { return true; } if event_kind.contains(".add_liquidity") { return true; } if event_kind.contains(".increase_liquidity") { return true; } return false; } /// Returns true for liquidity remove-like DEX events. pub fn is_dex_liquidity_remove_event_kind(event_kind: &str) -> bool { if event_kind.contains(".withdraw") { return true; } if event_kind.contains(".remove_liquidity") { return true; } if event_kind.contains(".decrease_liquidity") { return true; } return false; } /// Returns true for concentrated-liquidity position open events. pub fn is_dex_position_open_event_kind(event_kind: &str) -> bool { if event_kind.contains(".initialize_position") { return true; } if event_kind.contains(".open_position") { return true; } return false; } /// Returns true for concentrated-liquidity position close events. pub fn is_dex_position_close_event_kind(event_kind: &str) -> bool { if event_kind.contains(".close_position") { return true; } return false; } /// Returns true for fee collection events. pub fn is_dex_fee_event_kind(event_kind: &str) -> bool { if event_kind.contains("collect_creator_fee") { return true; } if event_kind.contains("collect_protocol_fee") { return true; } if event_kind.contains("collect_fund_fee") { return true; } if event_kind.contains("collect_fee") { return true; } return false; } /// Returns true for reward or incentive events. pub fn is_dex_reward_event_kind(event_kind: &str) -> bool { if event_kind.contains("reward") { return true; } if event_kind.contains("emission") { return true; } return false; } /// Returns true for pool, pair, launch, mint, burn or migration lifecycle events. pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool { if event_kind.contains(".initialize_bin_array") { return true; } if is_dex_pool_creation_event_kind(event_kind) { return true; } if is_dex_pair_creation_event_kind(event_kind) { return true; } if is_dex_launch_event_kind(event_kind) { return true; } if is_dex_token_mint_event_kind(event_kind) { return true; } if is_dex_token_burn_event_kind(event_kind) { return true; } if is_dex_migration_event_kind(event_kind) { return true; } return false; } /// Returns true for launch or bonding-curve creation events. pub fn is_dex_launch_event_kind(event_kind: &str) -> bool { if event_kind.contains("pump_fun.create") { return true; } if event_kind.contains(".launch") { return true; } if event_kind.contains(".create_v2_token") { return true; } if event_kind.contains(".create_bonding_curve") { return true; } return false; } /// Returns true for token mint events detected by DEX or launch-surface decoders. pub fn is_dex_token_mint_event_kind(event_kind: &str) -> bool { if event_kind.contains(".mint") { return true; } if event_kind.contains(".token_mint") { return true; } return false; } /// Returns true for token burn events detected by DEX or launch-surface decoders. pub fn is_dex_token_burn_event_kind(event_kind: &str) -> bool { if event_kind.contains(".burn") { return true; } if event_kind.contains(".token_burn") { return true; } return false; } /// Returns true for launch-surface or pool migration events. pub fn is_dex_migration_event_kind(event_kind: &str) -> bool { if event_kind.contains(".migrate") { return true; } if event_kind.contains(".migration") { return true; } return false; } /// Returns true for pool creation or initialization events. pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool { if event_kind.contains(".initialize_position") { return false; } if event_kind.contains(".initialize_bin_array") { return true; } if event_kind.contains(".initialize") { return true; } if event_kind.contains(".initialize_with_permission") { return true; } if event_kind.contains(".create_pool") { return true; } if event_kind.contains(".create_amm") { return true; } return false; } /// Returns true for pair creation events when they are distinguishable from pool creation. pub fn is_dex_pair_creation_event_kind(event_kind: &str) -> bool { if event_kind.contains(".create_pair") { return true; } if event_kind.contains(".pair_create") { return true; } return false; } /// Returns true for admin, configuration or permission changes. pub fn is_dex_admin_event_kind(event_kind: &str) -> bool { if event_kind.contains("admin") { return true; } if event_kind.contains("config") { return true; } if event_kind.contains("permission") { return true; } if event_kind.contains("set_") { return true; } if event_kind.contains("update_") { return true; } return false; } /// Returns true when a decoded payload contains at least one direct amount or price field. /// /// This is a conservative payload-level check. It does not inspect transaction /// token balance deltas and is intended for protocol decoders that cannot yet /// produce a deterministic materializable swap payload. pub(crate) fn decoded_payload_has_trade_amount_or_price_payload( payload: &serde_json::Value, ) -> bool { return value_contains_any_non_null_key( payload, &[ "baseAmountRaw", "base_amount_raw", "baseAmount", "amountBase", "amountInBase", "quoteAmountRaw", "quote_amount_raw", "quoteAmount", "amountQuote", "amountOutQuote", "amountIn", "amountOut", "priceQuotePerBase", "price_quote_per_base", "quotePerBase", "lastPriceQuotePerBase", ], ); } /// Returns true when a decoded payload is marked as a trade candidate. /// /// Explicit payload metadata wins over event-kind inference. This allows /// incomplete decoded events, such as incomplete PumpSwap trades, to opt out. pub fn is_decoded_event_trade_candidate(event_kind: &str, payload: &serde_json::Value) -> bool { let trade_candidate_option = extract_top_level_bool_by_candidate_keys(payload, &["tradeCandidate", "trade_candidate"]); if let Some(trade_candidate) = trade_candidate_option { return trade_candidate; } let event_category_option = extract_string_by_candidate_keys(payload, &["eventCategory", "event_category"]); if let Some(event_category) = event_category_option { return event_category.as_str() == DexEventCategory::Trade.as_str(); } return is_dex_trade_event_kind(event_kind); } /// Returns true when a decoded payload can be materialized as a candle candidate. pub fn is_decoded_event_candle_candidate(event_kind: &str, payload: &serde_json::Value) -> bool { let candle_candidate_option = extract_top_level_bool_by_candidate_keys(payload, &["candleCandidate", "candle_candidate"]); if let Some(candle_candidate) = candle_candidate_option { return candle_candidate; } if !is_decoded_event_trade_candidate(event_kind, payload) { return false; } return is_dex_candle_candidate_event_kind(event_kind); } /// Enriches a decoded payload with non-destructive classification metadata. pub fn enrich_dex_decoded_payload( protocol_name: &str, event_kind: &str, payload_json: serde_json::Value, ) -> serde_json::Value { let event_category = classify_dex_event_category_code(event_kind); let event_lifecycle_kind = classify_dex_event_lifecycle_kind_code(event_kind); let mut object = match payload_json { serde_json::Value::Object(object) => object, other => { let mut object = serde_json::Map::new(); object.insert("rawPayload".to_owned(), other); object }, }; let payload_snapshot = serde_json::Value::Object(object.clone()); let explicit_trade_candidate = extract_top_level_bool_by_candidate_keys( &payload_snapshot, &["tradeCandidate", "trade_candidate"], ); let trade_candidate = match explicit_trade_candidate { Some(trade_candidate) => trade_candidate, None => is_dex_trade_event_kind(event_kind), }; let explicit_candle_candidate = extract_top_level_bool_by_candidate_keys( &payload_snapshot, &["candleCandidate", "candle_candidate"], ); let candle_candidate = match explicit_candle_candidate { Some(candle_candidate) => candle_candidate, None => { if !trade_candidate { false } else { is_dex_candle_candidate_event_kind(event_kind) } }, }; let transaction_failed = match extract_top_level_bool_by_candidate_keys( &payload_snapshot, &["transactionFailed", "transaction_failed"], ) { Some(transaction_failed) => transaction_failed, None => false, }; let event_actionability = classify_dex_event_actionability_code(event_kind, trade_candidate, transaction_failed); let non_trade_useful = event_actionability == DexEventActionability::NonTradeUseful.as_str(); json_insert_string_if_missing(&mut object, "protocolName", protocol_name); json_insert_string_if_missing(&mut object, "eventKind", event_kind); json_insert_string_if_missing(&mut object, "eventCategory", event_category); json_insert_string_if_missing(&mut object, "eventLifecycleKind", event_lifecycle_kind); json_insert_string_if_missing(&mut object, "eventActionability", event_actionability); json_insert_bool_if_missing(&mut object, "nonTradeUseful", non_trade_useful); json_insert_bool_if_missing(&mut object, "tradeCandidate", trade_candidate); json_insert_bool_if_missing(&mut object, "candleCandidate", candle_candidate); json_insert_i64_if_missing(&mut object, "eventClassificationVersion", 2); if !trade_candidate { if is_dex_trade_event_kind(event_kind) { json_insert_string_if_missing(&mut object, "skipTradeReason", "non_actionable_trade"); } else { json_insert_string_if_missing(&mut object, "skipTradeReason", "non_trade_event"); } } else if !candle_candidate { json_insert_string_if_missing( &mut object, "skipCandleReason", "route_or_multihop_event_requires_leg_resolution", ); } return serde_json::Value::Object(object); } /// Enriches a decoded payload and serializes it as JSON. pub fn enrich_and_serialize_dex_decoded_payload( protocol_name: &str, event_kind: &str, payload_json: serde_json::Value, ) -> Result { let enriched_payload = enrich_dex_decoded_payload(protocol_name, event_kind, payload_json); let payload_json_result = serde_json::to_string(&enriched_payload); match payload_json_result { Ok(payload_json) => return Ok(payload_json), Err(error) => { return Err(crate::Error::Json(format!( "cannot serialize enriched decoded payload for '{}': {}", event_kind, error ))); }, } } /// Parses, enriches and serializes a decoded payload. pub fn enrich_serialized_dex_decoded_payload( protocol_name: &str, event_kind: &str, payload_json: &str, ) -> Result { let payload_value_result = serde_json::from_str::(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 '{}': {}", event_kind, error ))); }, }; return enrich_and_serialize_dex_decoded_payload(protocol_name, event_kind, payload_value); } fn json_insert_string_if_missing( object: &mut serde_json::Map, key: &str, value: &str, ) { if object.contains_key(key) { return; } object.insert(key.to_owned(), serde_json::Value::String(value.to_owned())); } fn json_insert_bool_if_missing( object: &mut serde_json::Map, key: &str, value: bool, ) { if object.contains_key(key) { return; } object.insert(key.to_owned(), serde_json::Value::Bool(value)); } fn json_insert_i64_if_missing( object: &mut serde_json::Map, key: &str, value: i64, ) { if object.contains_key(key) { return; } object.insert(key.to_owned(), serde_json::Value::Number(serde_json::Number::from(value))); } fn value_contains_any_non_null_key(value: &serde_json::Value, candidate_keys: &[&str]) -> bool { if let Some(object) = value.as_object() { for candidate_key in candidate_keys { let candidate_value_option = object.get(*candidate_key); if let Some(candidate_value) = candidate_value_option { if !candidate_value.is_null() { if let Some(text) = candidate_value.as_str() { if text.trim().is_empty() { continue; } } return true; } } } for nested_value in object.values() { if value_contains_any_non_null_key(nested_value, candidate_keys) { return true; } } return false; } if let Some(array) = value.as_array() { for nested_value in array { if value_contains_any_non_null_key(nested_value, candidate_keys) { return true; } } } return false; } fn extract_top_level_bool_by_candidate_keys( payload: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { let object = match payload.as_object() { Some(object) => object, None => return None, }; for candidate_key in candidate_keys { let value_option = object.get(*candidate_key); let value = match value_option { Some(value) => value, None => continue, }; if let Some(value_bool) = value.as_bool() { return Some(value_bool); } if let Some(value_i64) = value.as_i64() { return Some(value_i64 != 0); } if let Some(value_u64) = value.as_u64() { return Some(value_u64 != 0); } if let Some(value_text) = value.as_str() { let normalized = value_text.trim().to_ascii_lowercase(); if normalized.as_str() == "true" { return Some(true); } if normalized.as_str() == "false" { return Some(false); } if normalized.as_str() == "1" { return Some(true); } if normalized.as_str() == "0" { return Some(false); } } } return None; } fn extract_string_by_candidate_keys( value: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { if let Some(object) = value.as_object() { for candidate_key in candidate_keys { let direct_option = object.get(*candidate_key); if let Some(direct) = direct_option { let direct_text_option = direct.as_str(); if let Some(direct_text) = direct_text_option { return Some(direct_text.to_string()); } } } for nested_value in object.values() { let nested_result = extract_string_by_candidate_keys(nested_value, candidate_keys); if nested_result.is_some() { return nested_result; } } return None; } if let Some(array) = value.as_array() { for nested_value in array { let nested_result = extract_string_by_candidate_keys(nested_value, candidate_keys); if nested_result.is_some() { return nested_result; } } } return None; } #[cfg(test)] mod tests { #[test] fn classifies_swap_events_as_trade_candidates() { assert_eq!( super::classify_dex_event_category_code("raydium_cpmm.swap_base_input"), "trade" ); assert_eq!( super::classify_dex_event_category_code("raydium_cpmm.swap_base_output"), "trade" ); assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap"), "trade"); assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap_v2"), "trade"); assert_eq!(super::classify_dex_event_category_code("raydium_clmm.exact_output"), "trade"); assert_eq!(super::classify_dex_event_category_code("pump_fun.buy"), "trade"); assert!(super::is_dex_trade_event_kind("raydium_cpmm.swap_base_input")); assert!(super::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!( super::classify_dex_event_category_code("raydium_clmm.swap_router_base_in"), "trade" ); assert!(super::is_dex_trade_event_kind("raydium_clmm.swap_router_base_in")); assert!(!super::is_dex_candle_candidate_event_kind("raydium_clmm.swap_router_base_in")); } #[test] fn classifies_fee_reward_liquidity_and_lifecycle_events() { assert_eq!( super::classify_dex_event_category_code("raydium_cpmm.collect_creator_fee"), "fee" ); assert_eq!( super::classify_dex_event_category_code("raydium_clmm.collect_protocol_fee"), "fee" ); assert_eq!( super::classify_dex_event_category_code("raydium_clmm.set_reward_params"), "reward" ); assert_eq!( super::classify_dex_event_category_code("raydium_clmm.increase_liquidity_v2"), "liquidity" ); assert_eq!( super::classify_dex_event_category_code("raydium_cpmm.initialize"), "pool_lifecycle" ); } #[test] fn classifies_fine_grained_non_trade_lifecycle_kinds() { assert_eq!( super::classify_dex_event_lifecycle_kind_code("raydium_cpmm.initialize"), "pool_creation" ); assert_eq!(super::classify_dex_event_lifecycle_kind_code("pump_fun.create"), "launch"); assert_eq!( super::classify_dex_event_lifecycle_kind_code("meteora_dbc.migrate"), "migration" ); assert_eq!( super::classify_dex_event_lifecycle_kind_code("raydium_clmm.increase_liquidity_v2"), "liquidity_add" ); assert_eq!( super::classify_dex_event_lifecycle_kind_code("raydium_clmm.decrease_liquidity_v2"), "liquidity_remove" ); assert_eq!( super::classify_dex_event_actionability_code( "raydium_clmm.increase_liquidity_v2", false, false, ), "non_trade_useful" ); } #[test] fn enriched_payload_keeps_existing_fields() { let payload_json = serde_json::json!({ "eventCategory": "custom", "amountIn": "10" }); let enriched_payload = super::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))); assert_eq!( object.get("eventLifecycleKind"), Some(&serde_json::Value::String("trade_swap".to_owned())) ); assert_eq!( object.get("eventActionability"), Some(&serde_json::Value::String("trade_candidate".to_owned())) ); } #[test] fn enriched_non_trade_payload_is_visible_but_not_trade_candidate() { let enriched_payload = super::enrich_dex_decoded_payload( "raydium_clmm", "raydium_clmm.increase_liquidity_v2", serde_json::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("liquidity".to_owned())) ); assert_eq!( object.get("eventLifecycleKind"), Some(&serde_json::Value::String("liquidity_add".to_owned())) ); assert_eq!( object.get("eventActionability"), Some(&serde_json::Value::String("non_trade_useful".to_owned())) ); assert_eq!(object.get("nonTradeUseful"), Some(&serde_json::Value::Bool(true))); assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(false))); assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(false))); } #[test] fn decoded_event_payload_candidate_flags_win_over_event_kind() { let payload_json = serde_json::json!({ "tradeCandidate": false, "candleCandidate": false }); assert!(!super::is_decoded_event_trade_candidate("pump_swap.buy", &payload_json)); assert!(!super::is_decoded_event_candle_candidate("pump_swap.buy", &payload_json)); } #[test] fn detects_direct_amount_or_price_payload_recursively() { let payload_json = serde_json::json!({ "nested": { "quoteAmountRaw": "2500" } }); assert!(super::decoded_payload_has_trade_amount_or_price_payload(&payload_json)); let empty_payload_json = serde_json::json!({ "nested": { "quoteAmountRaw": "" } }); assert!(!super::decoded_payload_has_trade_amount_or_price_payload(&empty_payload_json)); } #[test] fn classifies_dlmm_add_remove_liquidity_and_positions_as_non_trade_useful() { assert_eq!( super::classify_dex_event_category_code("meteora_dlmm.add_liquidity"), "liquidity" ); assert_eq!( super::classify_dex_event_category_code("meteora_dlmm.remove_liquidity"), "liquidity" ); assert_eq!( super::classify_dex_event_lifecycle_kind_code("meteora_dlmm.initialize_position"), "position_open" ); assert_eq!( super::classify_dex_event_actionability_code( "meteora_dlmm.add_liquidity", false, false, ), "non_trade_useful" ); } #[test] fn classifies_dlmm_bin_array_initialization_as_pool_lifecycle() { assert_eq!( super::classify_dex_event_category_code("meteora_dlmm.initialize_bin_array"), "pool_lifecycle" ); assert_eq!( super::classify_dex_event_actionability_code( "meteora_dlmm.initialize_bin_array", false, false, ), "non_trade_useful" ); } }