Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
209 changes: 209 additions & 0 deletions src/repositories/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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;
}

// 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<String, Monitor>,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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::<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'"));
}
}
13 changes: 0 additions & 13 deletions tests/properties/repositories/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading