From a946237b1f3cb44be59390a5560c07e95819b199 Mon Sep 17 00:00:00 2001 From: shahnami Date: Mon, 26 Jan 2026 20:34:46 +0400 Subject: [PATCH 1/4] chore: Improve signature validation logic for Solana --- Cargo.lock | 6 +- src/models/blockchain/mod.rs | 66 +++++++ src/models/config/monitor_config.rs | 32 ---- src/repositories/monitor.rs | 209 +++++++++++++++++++++++ tests/properties/repositories/monitor.rs | 13 -- 5 files changed, 278 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16d569e24..1ab175b43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2955,8 +2955,8 @@ checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling 0.20.11", "proc-macro2", - "quote 1.0.41", - "syn 2.0.107", + "quote 1.0.43", + "syn 2.0.114", ] [[package]] @@ -2966,7 +2966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.107", + "syn 2.0.114", ] [[package]] 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..a0f99d23e 100644 --- a/src/repositories/monitor.rs +++ b/src/repositories/monitor.rs @@ -62,6 +62,47 @@ 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; + } + + // Validate function signatures + for func in &monitor.match_conditions.functions { + if !func.signature.contains('(') || !func.signature.contains(')') { + 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 !event.signature.contains('(') || !event.signature.contains(')') { + 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 +141,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 +675,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/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() { From 4c23ff9a84f9f737e5b87d2ad690ac28a7bd31b7 Mon Sep 17 00:00:00 2001 From: Nami Date: Mon, 26 Jan 2026 20:49:48 +0400 Subject: [PATCH 2/4] Update src/repositories/monitor.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/repositories/monitor.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/repositories/monitor.rs b/src/repositories/monitor.rs index a0f99d23e..f2f05644c 100644 --- a/src/repositories/monitor.rs +++ b/src/repositories/monitor.rs @@ -79,9 +79,17 @@ impl< continue; } + let has_parens = |sig: &str| { + let sig = sig.trim(); + match (sig.find('('), sig.rfind(')')) { + (Some(open), Some(close)) if open < close && close == sig.len() - 1 => true, + _ => false, + } + }; + // Validate function signatures for func in &monitor.match_conditions.functions { - if !func.signature.contains('(') || !func.signature.contains(')') { + if !has_parens(&func.signature) { validation_errors.push(format!( "Monitor '{}' has invalid function signature '{}' for {} network '{}' \ (expected format: 'functionName(type1,type2)')", @@ -92,7 +100,7 @@ impl< // Validate event signatures for event in &monitor.match_conditions.events { - if !event.signature.contains('(') || !event.signature.contains(')') { + if !has_parens(&event.signature) { validation_errors.push(format!( "Monitor '{}' has invalid event signature '{}' for {} network '{}' \ (expected format: 'EventName(type1,type2)')", From 3c783e710b4c38b3fb4e585abaa39653fe0c787f Mon Sep 17 00:00:00 2001 From: shahnami Date: Mon, 26 Jan 2026 21:08:45 +0400 Subject: [PATCH 3/4] fix: Clippy warning --- src/repositories/monitor.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/repositories/monitor.rs b/src/repositories/monitor.rs index f2f05644c..d5c627d79 100644 --- a/src/repositories/monitor.rs +++ b/src/repositories/monitor.rs @@ -81,10 +81,7 @@ impl< let has_parens = |sig: &str| { let sig = sig.trim(); - match (sig.find('('), sig.rfind(')')) { - (Some(open), Some(close)) if open < close && close == sig.len() - 1 => true, - _ => false, - } + matches!((sig.find('('), sig.rfind(')')), (Some(open), Some(close)) if open < close && close == sig.len() - 1) }; // Validate function signatures From eaf792bd14afe423811d01faceec8007c9cbd449 Mon Sep 17 00:00:00 2001 From: shahnami Date: Wed, 28 Jan 2026 13:54:29 +0400 Subject: [PATCH 4/4] chore: Reduce verbosity on logs --- src/services/blockchain/clients/solana/client.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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,