Skip to content

Commit e065149

Browse files
committed
fix: Move signature validation to repository layer with BlockChainType rules
1 parent 5139ae0 commit e065149

File tree

3 files changed

+195
-32
lines changed

3 files changed

+195
-32
lines changed

src/models/blockchain/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,32 @@ pub enum BlockChainType {
2525
Solana,
2626
}
2727

28+
/// Rules for validating function and event signatures per blockchain type
29+
#[derive(Debug, Clone, PartialEq)]
30+
pub struct SignatureRules {
31+
/// Whether signatures must contain parentheses (e.g., `transfer(address,uint256)`)
32+
pub requires_parentheses: bool,
33+
}
34+
35+
impl BlockChainType {
36+
/// Returns the signature validation rules for this blockchain type
37+
///
38+
/// This provides a single source of truth for per-chain signature validation behavior.
39+
/// The exhaustive match ensures new blockchain types must define their rules.
40+
pub fn signature_rules(&self) -> SignatureRules {
41+
match self {
42+
BlockChainType::EVM | BlockChainType::Stellar | BlockChainType::Midnight => {
43+
SignatureRules {
44+
requires_parentheses: true,
45+
}
46+
}
47+
BlockChainType::Solana => SignatureRules {
48+
requires_parentheses: false,
49+
},
50+
}
51+
}
52+
}
53+
2854
/// Block data from different blockchain platforms
2955
#[derive(Debug, Clone, Serialize, Deserialize)]
3056
pub enum BlockType {

src/models/config/monitor_config.rs

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ impl ConfigLoader for Monitor {
153153
}
154154

155155
/// Validate the monitor configuration
156+
///
157+
/// This validates monitor-intrinsic properties only. Signature validation
158+
/// is handled at the repository layer where network context is available.
156159
fn validate(&self) -> Result<(), ConfigError> {
157160
// Validate monitor name
158161
if self.name.is_empty() {
@@ -172,38 +175,6 @@ impl ConfigLoader for Monitor {
172175
));
173176
}
174177

175-
// Check if this is a Solana monitor based on network slugs
176-
// Note: Assumes Solana network slugs follow the "solana_*" naming convention
177-
let is_solana_monitor = self.networks.iter().any(|slug| slug.starts_with("solana_"));
178-
179-
// Validate function signatures
180-
for func in &self.match_conditions.functions {
181-
// Solana monitors don't require parentheses in function signatures
182-
if !is_solana_monitor
183-
&& (!func.signature.contains('(') || !func.signature.contains(')'))
184-
{
185-
return Err(ConfigError::validation_error(
186-
format!("Invalid function signature format: {}", func.signature),
187-
None,
188-
None,
189-
));
190-
}
191-
}
192-
193-
// Validate event signatures
194-
for event in &self.match_conditions.events {
195-
// Solana monitors don't require parentheses in event signatures
196-
if !is_solana_monitor
197-
&& (!event.signature.contains('(') || !event.signature.contains(')'))
198-
{
199-
return Err(ConfigError::validation_error(
200-
format!("Invalid event signature format: {}", event.signature),
201-
None,
202-
None,
203-
));
204-
}
205-
}
206-
207178
// Validate trigger conditions (focus on script path, timeout, and language)
208179
for trigger_condition in &self.trigger_conditions {
209180
validate_script_config(

src/repositories/monitor.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,57 @@ use crate::{
1919
},
2020
};
2121

22+
/// Validates function and event signatures for a monitor based on network-specific rules.
23+
///
24+
/// This function checks that signatures conform to the requirements of their target
25+
/// blockchain type. For example, EVM networks require parentheses in signatures
26+
/// like `transfer(address,uint256)`, while Solana allows simple names like `transfer`.
27+
fn validate_monitor_signatures(
28+
monitor: &Monitor,
29+
monitor_name: &str,
30+
networks: &HashMap<String, Network>,
31+
validation_errors: &mut Vec<String>,
32+
metadata: &mut HashMap<String, String>,
33+
) {
34+
for network_slug in &monitor.networks {
35+
if let Some(network) = networks.get(network_slug) {
36+
let rules = network.network_type.signature_rules();
37+
38+
if rules.requires_parentheses {
39+
// Validate function signatures
40+
for func in &monitor.match_conditions.functions {
41+
if !func.signature.contains('(') || !func.signature.contains(')') {
42+
validation_errors.push(format!(
43+
"Monitor '{}' has invalid function signature '{}' for {:?} network '{}': \
44+
signatures must contain parentheses (e.g., 'transfer(address,uint256)')",
45+
monitor_name, func.signature, network.network_type, network_slug
46+
));
47+
metadata.insert(
48+
format!("monitor_{}_invalid_function_signature", monitor_name),
49+
func.signature.clone(),
50+
);
51+
}
52+
}
53+
54+
// Validate event signatures
55+
for event in &monitor.match_conditions.events {
56+
if !event.signature.contains('(') || !event.signature.contains(')') {
57+
validation_errors.push(format!(
58+
"Monitor '{}' has invalid event signature '{}' for {:?} network '{}': \
59+
signatures must contain parentheses (e.g., 'Transfer(address,address,uint256)')",
60+
monitor_name, event.signature, network.network_type, network_slug
61+
));
62+
metadata.insert(
63+
format!("monitor_{}_invalid_event_signature", monitor_name),
64+
event.signature.clone(),
65+
);
66+
}
67+
}
68+
}
69+
}
70+
}
71+
}
72+
2273
/// Repository for storing and retrieving monitor configurations
2374
#[derive(Clone)]
2475
pub struct MonitorRepository<
@@ -100,6 +151,15 @@ impl<
100151
}
101152
}
102153

154+
// Validate function and event signatures based on network-specific rules
155+
validate_monitor_signatures(
156+
monitor,
157+
monitor_name,
158+
networks,
159+
&mut validation_errors,
160+
&mut metadata,
161+
);
162+
103163
// Validate custom trigger conditions
104164
for condition in &monitor.trigger_conditions {
105165
let script_path = Path::new(&condition.script_path);
@@ -626,4 +686,110 @@ mod tests {
626686
_ => panic!("Expected RepositoryError::LoadError"),
627687
}
628688
}
689+
690+
#[test]
691+
fn test_validate_solana_network_type_detection() {
692+
use crate::models::BlockChainType;
693+
use crate::utils::tests::builders::network::NetworkBuilder;
694+
695+
// Test Case 1: Custom-named Solana network (not following solana_* convention)
696+
// This tests that we properly detect Solana networks by BlockChainType, not by name
697+
let mut networks = HashMap::new();
698+
let solana_network = NetworkBuilder::new()
699+
.slug("my-custom-solana")
700+
.network_type(BlockChainType::Solana)
701+
.build();
702+
networks.insert("my-custom-solana".to_string(), solana_network);
703+
704+
let mut monitors = HashMap::new();
705+
let monitor = MonitorBuilder::new()
706+
.name("test_solana_monitor")
707+
.networks(vec!["my-custom-solana".to_string()])
708+
// Solana monitors allow function signatures without parentheses
709+
.function("transfer", None)
710+
.build();
711+
monitors.insert("test_solana_monitor".to_string(), monitor);
712+
713+
let triggers = HashMap::new();
714+
715+
// Should pass because we properly detect Solana network type via BlockChainType
716+
let result =
717+
MonitorRepository::<NetworkRepository, TriggerRepository>::validate_monitor_references(
718+
&monitors, &triggers, &networks,
719+
);
720+
assert!(result.is_ok());
721+
722+
// Test Case 2: Misleading network name (contains "solana_" but is EVM)
723+
// This tests that we use BlockChainType, not name prefix matching
724+
let mut networks2 = HashMap::new();
725+
let evm_network = NetworkBuilder::new()
726+
.slug("solana_like_evm")
727+
.network_type(BlockChainType::EVM)
728+
.build();
729+
networks2.insert("solana_like_evm".to_string(), evm_network);
730+
731+
let mut monitors2 = HashMap::new();
732+
let monitor2 = MonitorBuilder::new()
733+
.name("test_evm_monitor")
734+
.networks(vec!["solana_like_evm".to_string()])
735+
// EVM monitors require parentheses in signatures
736+
.function("transfer", None)
737+
.build();
738+
monitors2.insert("test_evm_monitor".to_string(), monitor2);
739+
740+
// Should fail because it's actually an EVM network requiring proper signature format
741+
let result2 =
742+
MonitorRepository::<NetworkRepository, TriggerRepository>::validate_monitor_references(
743+
&monitors2, &triggers, &networks2,
744+
);
745+
assert!(result2.is_err());
746+
assert!(result2
747+
.unwrap_err()
748+
.to_string()
749+
.contains("invalid function signature"));
750+
751+
// Test Case 3: Actual Solana network with conventional name
752+
let mut networks3 = HashMap::new();
753+
let solana_network3 = NetworkBuilder::new()
754+
.slug("solana_mainnet")
755+
.network_type(BlockChainType::Solana)
756+
.build();
757+
networks3.insert("solana_mainnet".to_string(), solana_network3);
758+
759+
let mut monitors3 = HashMap::new();
760+
let monitor3 = MonitorBuilder::new()
761+
.name("test_conventional_solana")
762+
.networks(vec!["solana_mainnet".to_string()])
763+
.function("transfer", None)
764+
.build();
765+
monitors3.insert("test_conventional_solana".to_string(), monitor3);
766+
767+
// Should pass with conventional name too
768+
let result3 =
769+
MonitorRepository::<NetworkRepository, TriggerRepository>::validate_monitor_references(
770+
&monitors3, &triggers, &networks3,
771+
);
772+
assert!(result3.is_ok());
773+
}
774+
775+
#[test]
776+
fn test_signature_rules() {
777+
use crate::models::BlockChainType;
778+
779+
// EVM requires parentheses
780+
let evm_rules = BlockChainType::EVM.signature_rules();
781+
assert!(evm_rules.requires_parentheses);
782+
783+
// Stellar requires parentheses
784+
let stellar_rules = BlockChainType::Stellar.signature_rules();
785+
assert!(stellar_rules.requires_parentheses);
786+
787+
// Midnight requires parentheses
788+
let midnight_rules = BlockChainType::Midnight.signature_rules();
789+
assert!(midnight_rules.requires_parentheses);
790+
791+
// Solana does NOT require parentheses
792+
let solana_rules = BlockChainType::Solana.signature_rules();
793+
assert!(!solana_rules.requires_parentheses);
794+
}
629795
}

0 commit comments

Comments
 (0)