Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions src/models/blockchain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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 {
Expand Down Expand Up @@ -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");
}
}
32 changes: 0 additions & 32 deletions src/models/config/monitor_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
214 changes: 214 additions & 0 deletions src/repositories/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Network>,
validation_errors: &mut Vec<String>,
) {
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<String, Monitor>,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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::<NetworkRepository, TriggerRepository>::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::<NetworkRepository, TriggerRepository>::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::<NetworkRepository, TriggerRepository>::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::<NetworkRepository, TriggerRepository>::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'"));
}
}
Loading
Loading