diff --git a/src/models/blockchain/mod.rs b/src/models/blockchain/mod.rs index 00c9c37f1..650bfe77b 100644 --- a/src/models/blockchain/mod.rs +++ b/src/models/blockchain/mod.rs @@ -5,12 +5,20 @@ //! platform-specific logic for blocks, transactions, and event monitoring. use serde::{Deserialize, Serialize}; +use std::fmt; pub mod evm; pub mod midnight; pub mod solana; pub mod stellar; +/// Rules for function and event signature validation +#[derive(Debug, Clone)] +pub struct SignatureRules { + /// Whether the blockchain requires parentheses in signatures + pub requires_parentheses: bool, +} + /// Supported blockchain platform types #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(deny_unknown_fields)] @@ -25,6 +33,37 @@ pub enum BlockChainType { Solana, } +impl BlockChainType { + /// Returns the signature validation rules for this blockchain type. + /// + /// Different blockchains have different signature formats: + /// - EVM-style chains require signatures like `transfer(address,uint256)` + /// - Solana allows raw instruction names like `transfer` + pub fn signature_rules(&self) -> SignatureRules { + match self { + BlockChainType::EVM | BlockChainType::Stellar | BlockChainType::Midnight => { + SignatureRules { + requires_parentheses: true, + } + } + BlockChainType::Solana => SignatureRules { + requires_parentheses: false, + }, + } + } +} + +impl fmt::Display for BlockChainType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BlockChainType::EVM => write!(f, "EVM"), + BlockChainType::Stellar => write!(f, "Stellar"), + BlockChainType::Midnight => write!(f, "Midnight"), + BlockChainType::Solana => write!(f, "Solana"), + } + } +} + /// Block data from different blockchain platforms #[derive(Debug, Clone, Serialize, Deserialize)] pub enum BlockType { @@ -229,4 +268,31 @@ mod tests { assert_ne!(BlockChainType::EVM, BlockChainType::Solana); assert_ne!(BlockChainType::Stellar, BlockChainType::Midnight); } + + #[test] + fn test_signature_rules() { + // Test EVM requires parentheses + let evm_rules = BlockChainType::EVM.signature_rules(); + assert!(evm_rules.requires_parentheses); + + // Test Stellar requires parentheses + let stellar_rules = BlockChainType::Stellar.signature_rules(); + assert!(stellar_rules.requires_parentheses); + + // Test Midnight requires parentheses + let midnight_rules = BlockChainType::Midnight.signature_rules(); + assert!(midnight_rules.requires_parentheses); + + // Test Solana does not require parentheses + let solana_rules = BlockChainType::Solana.signature_rules(); + assert!(!solana_rules.requires_parentheses); + } + + #[test] + fn test_blockchain_type_display() { + assert_eq!(format!("{}", BlockChainType::EVM), "EVM"); + assert_eq!(format!("{}", BlockChainType::Stellar), "Stellar"); + assert_eq!(format!("{}", BlockChainType::Midnight), "Midnight"); + assert_eq!(format!("{}", BlockChainType::Solana), "Solana"); + } } diff --git a/src/models/config/monitor_config.rs b/src/models/config/monitor_config.rs index 8fed026a5..8d6087c36 100644 --- a/src/models/config/monitor_config.rs +++ b/src/models/config/monitor_config.rs @@ -172,38 +172,6 @@ impl ConfigLoader for Monitor { )); } - // Check if this is a Solana monitor based on network slugs - // Note: Assumes Solana network slugs follow the "solana_*" naming convention - let is_solana_monitor = self.networks.iter().any(|slug| slug.starts_with("solana_")); - - // Validate function signatures - for func in &self.match_conditions.functions { - // Solana monitors don't require parentheses in function signatures - if !is_solana_monitor - && (!func.signature.contains('(') || !func.signature.contains(')')) - { - return Err(ConfigError::validation_error( - format!("Invalid function signature format: {}", func.signature), - None, - None, - )); - } - } - - // Validate event signatures - for event in &self.match_conditions.events { - // Solana monitors don't require parentheses in event signatures - if !is_solana_monitor - && (!event.signature.contains('(') || !event.signature.contains(')')) - { - return Err(ConfigError::validation_error( - format!("Invalid event signature format: {}", event.signature), - None, - None, - )); - } - } - // Validate trigger conditions (focus on script path, timeout, and language) for trigger_condition in &self.trigger_conditions { validate_script_config( diff --git a/src/repositories/monitor.rs b/src/repositories/monitor.rs index 0370e947d..d5c627d79 100644 --- a/src/repositories/monitor.rs +++ b/src/repositories/monitor.rs @@ -62,6 +62,52 @@ impl< } } + /// Validates function and event signatures for a monitor based on its target network types. + fn validate_monitor_signatures( + monitor_name: &str, + monitor: &Monitor, + networks: &HashMap, + validation_errors: &mut Vec, + ) { + for network_slug in &monitor.networks { + let Some(network) = networks.get(network_slug) else { + continue; // Network reference errors are handled separately + }; + + let rules = network.network_type.signature_rules(); + if !rules.requires_parentheses { + continue; + } + + let has_parens = |sig: &str| { + let sig = sig.trim(); + matches!((sig.find('('), sig.rfind(')')), (Some(open), Some(close)) if open < close && close == sig.len() - 1) + }; + + // Validate function signatures + for func in &monitor.match_conditions.functions { + if !has_parens(&func.signature) { + validation_errors.push(format!( + "Monitor '{}' has invalid function signature '{}' for {} network '{}' \ + (expected format: 'functionName(type1,type2)')", + monitor_name, func.signature, network.network_type, network_slug + )); + } + } + + // Validate event signatures + for event in &monitor.match_conditions.events { + if !has_parens(&event.signature) { + validation_errors.push(format!( + "Monitor '{}' has invalid event signature '{}' for {} network '{}' \ + (expected format: 'EventName(type1,type2)')", + monitor_name, event.signature, network.network_type, network_slug + )); + } + } + } + } + /// Returns an error if any monitor references a non-existent network or trigger. pub fn validate_monitor_references( monitors: &HashMap, @@ -100,6 +146,14 @@ impl< } } + // Validate signatures based on network type + Self::validate_monitor_signatures( + monitor_name, + monitor, + networks, + &mut validation_errors, + ); + // Validate custom trigger conditions for condition in &monitor.trigger_conditions { let script_path = Path::new(&condition.script_path); @@ -626,4 +680,164 @@ mod tests { _ => panic!("Expected RepositoryError::LoadError"), } } + + #[test] + fn test_signature_validation_with_network_types() { + use crate::models::{BlockChainType, EventCondition, FunctionCondition, MatchConditions}; + use crate::utils::tests::builders::network::NetworkBuilder; + + // Create networks of different types + let mut networks = HashMap::new(); + + // EVM network + networks.insert( + "ethereum_mainnet".to_string(), + NetworkBuilder::new() + .name("Ethereum Mainnet") + .slug("ethereum_mainnet") + .network_type(BlockChainType::EVM) + .chain_id(1) + .build(), + ); + + // Solana network (without "solana_" prefix to test proper type detection) + networks.insert( + "mainnet_beta".to_string(), + NetworkBuilder::new() + .name("Solana Mainnet Beta") + .slug("mainnet_beta") + .network_type(BlockChainType::Solana) + .build(), + ); + + // Another Solana network with traditional prefix + networks.insert( + "solana_devnet".to_string(), + NetworkBuilder::new() + .name("Solana Devnet") + .slug("solana_devnet") + .network_type(BlockChainType::Solana) + .build(), + ); + + let triggers = HashMap::new(); + let mut monitors = HashMap::new(); + + // Test 1: EVM monitor with invalid signatures (missing parentheses) + let evm_monitor_invalid = MonitorBuilder::new() + .name("evm_monitor_invalid") + .networks(vec!["ethereum_mainnet".to_string()]) + .match_conditions(MatchConditions { + functions: vec![FunctionCondition { + signature: "transfer".to_string(), // Invalid: missing parentheses + expression: None, + }], + events: vec![EventCondition { + signature: "Transfer".to_string(), // Invalid: missing parentheses + expression: None, + }], + transactions: vec![], + }) + .build(); + monitors.insert("evm_monitor_invalid".to_string(), evm_monitor_invalid); + + let result = + MonitorRepository::::validate_monitor_references( + &monitors, &triggers, &networks, + ); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err + .to_string() + .contains("invalid function signature 'transfer' for EVM network")); + assert!(err + .to_string() + .contains("invalid event signature 'Transfer' for EVM network")); + + // Test 2: Solana monitor with valid signatures (no parentheses required) + monitors.clear(); + let solana_monitor_valid = MonitorBuilder::new() + .name("solana_monitor_valid") + .networks(vec!["mainnet_beta".to_string()]) // Non-prefixed Solana network + .match_conditions(MatchConditions { + functions: vec![FunctionCondition { + signature: "transfer".to_string(), // Valid for Solana + expression: None, + }], + events: vec![EventCondition { + signature: "TransferEvent".to_string(), // Valid for Solana + expression: None, + }], + transactions: vec![], + }) + .build(); + monitors.insert("solana_monitor_valid".to_string(), solana_monitor_valid); + + let result = + MonitorRepository::::validate_monitor_references( + &monitors, &triggers, &networks, + ); + + // Should pass - Solana doesn't require parentheses + assert!(result.is_ok()); + + // Test 3: EVM monitor with valid signatures + monitors.clear(); + let evm_monitor_valid = MonitorBuilder::new() + .name("evm_monitor_valid") + .networks(vec!["ethereum_mainnet".to_string()]) + .match_conditions(MatchConditions { + functions: vec![FunctionCondition { + signature: "transfer(address,uint256)".to_string(), // Valid + expression: None, + }], + events: vec![EventCondition { + signature: "Transfer(address,address,uint256)".to_string(), // Valid + expression: None, + }], + transactions: vec![], + }) + .build(); + monitors.insert("evm_monitor_valid".to_string(), evm_monitor_valid); + + let result = + MonitorRepository::::validate_monitor_references( + &monitors, &triggers, &networks, + ); + + // Should pass + assert!(result.is_ok()); + + // Test 4: Mixed network monitor (EVM + Solana) + monitors.clear(); + let mixed_monitor = MonitorBuilder::new() + .name("mixed_monitor") + .networks(vec![ + "ethereum_mainnet".to_string(), + "mainnet_beta".to_string(), + ]) + .match_conditions(MatchConditions { + functions: vec![FunctionCondition { + signature: "transfer".to_string(), // Invalid for EVM, valid for Solana + expression: None, + }], + events: vec![], + transactions: vec![], + }) + .build(); + monitors.insert("mixed_monitor".to_string(), mixed_monitor); + + let result = + MonitorRepository::::validate_monitor_references( + &monitors, &triggers, &networks, + ); + + // Should fail because of EVM network requirement + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err + .to_string() + .contains("invalid function signature 'transfer' for EVM network 'ethereum_mainnet'")); + } } diff --git a/src/services/blockchain/clients/solana/client.rs b/src/services/blockchain/clients/solana/client.rs index d48c71d3f..36c145df4 100644 --- a/src/services/blockchain/clients/solana/client.rs +++ b/src/services/blockchain/clients/solana/client.rs @@ -705,7 +705,7 @@ impl SolanaClientTrait for SolanaC return Ok(Vec::new()); } - tracing::info!( + tracing::debug!( addresses = ?addresses, start_slot = start_slot, end_slot = end_slot, @@ -732,7 +732,7 @@ impl SolanaClientTrait for SolanaC } } - tracing::info!( + tracing::debug!( unique_signatures = all_signatures.len(), "Fetching transactions for unique signatures in slot range" ); @@ -758,7 +758,7 @@ impl SolanaClientTrait for SolanaC .collect() .await; - tracing::info!( + tracing::debug!( fetched_transactions = transactions.len(), "Successfully fetched transactions" ); @@ -856,7 +856,7 @@ impl SolanaClientTrait for SolanaC }) .collect(); - tracing::info!( + tracing::debug!( blocks_count = blocks.len(), "Created virtual blocks from address-filtered transactions" ); @@ -903,7 +903,7 @@ impl BlockChainClient for SolanaCl ) -> Result, anyhow::Error> { // If monitored addresses are configured, use the optimized approach if !self.monitored_addresses.is_empty() { - tracing::info!( + tracing::debug!( addresses = ?self.monitored_addresses, start_block = start_block, end_block = ?end_block, diff --git a/tests/properties/repositories/monitor.rs b/tests/properties/repositories/monitor.rs index e84b6d4cf..fe3780ab6 100644 --- a/tests/properties/repositories/monitor.rs +++ b/tests/properties/repositories/monitor.rs @@ -154,19 +154,6 @@ proptest! { invalid_monitor.name = "".to_string(); prop_assert!(invalid_monitor.validate().is_err()); - // Test invalid function signature - if let Some(func) = invalid_monitor.match_conditions.functions.first_mut() { - func.signature = "invalid_signature".to_string(); // Missing parentheses - prop_assert!(invalid_monitor.validate().is_err()); - } - - // Test invalid event signature - invalid_monitor = monitor.clone(); - if let Some(event) = invalid_monitor.match_conditions.events.first_mut() { - event.signature = "invalid_signature".to_string(); // Missing parentheses - prop_assert!(invalid_monitor.validate().is_err()); - } - // Test invalid script path invalid_monitor = monitor.clone(); if let Some(condition) = invalid_monitor.trigger_conditions.first_mut() {