diff --git a/contracts/cntr/Cargo.toml b/contracts/cntr/Cargo.toml index 6bc8946..342774c 100644 --- a/contracts/cntr/Cargo.toml +++ b/contracts/cntr/Cargo.toml @@ -1,10 +1,12 @@ [package] name = "cntr" +version = "0.0.0" version = "0.1.0" edition = "2021" publish = false [lib] +crate-type = ["lib"] path = "src/lib.rs" [dependencies] diff --git a/contracts/cntr/src/credit_deduction.rs b/contracts/cntr/src/credit_deduction.rs index dadbaa9..63bfd9e 100644 --- a/contracts/cntr/src/credit_deduction.rs +++ b/contracts/cntr/src/credit_deduction.rs @@ -1,3 +1,46 @@ +pub fn validate_deduction(current_balance: i128, deduction_amount: i128) -> Result { + if deduction_amount <= 0 { + return Err("Deduction amount must be positive"); + } + if current_balance < deduction_amount { + return Err("Insufficient credits"); + } + Ok(current_balance - deduction_amount) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn success() { + assert_eq!(validate_deduction(100, 40), Ok(60)); + } + + #[test] + fn exact_balance() { + assert_eq!(validate_deduction(50, 50), Ok(0)); + } + + #[test] + fn insufficient() { + assert_eq!(validate_deduction(10, 20), Err("Insufficient credits")); + } + + #[test] + fn zero_deduction() { + assert_eq!(validate_deduction(100, 0), Err("Deduction amount must be positive")); + } + + #[test] + fn negative_deduction() { + assert_eq!(validate_deduction(100, -5), Err("Deduction amount must be positive")); + } + + #[test] + fn zero_balance_positive_deduction() { + assert_eq!(validate_deduction(0, 1), Err("Insufficient credits")); + } /// Error types for credit deduction operations. #[derive(Debug, Clone, PartialEq)] pub enum DeductionError { diff --git a/contracts/cntr/src/lib.rs b/contracts/cntr/src/lib.rs index 273e226..ca4756e 100644 --- a/contracts/cntr/src/lib.rs +++ b/contracts/cntr/src/lib.rs @@ -1,4 +1,7 @@ pub mod credit_deduction; +pub mod referral_reward; +pub mod role_checker; +pub mod withdrawal_validator; pub mod credit_topup; pub mod token_validator; pub mod grace_period; diff --git a/contracts/cntr/src/referral_reward.rs b/contracts/cntr/src/referral_reward.rs new file mode 100644 index 0000000..7d35b26 --- /dev/null +++ b/contracts/cntr/src/referral_reward.rs @@ -0,0 +1,52 @@ +pub fn calculate_referral_split( + total_reward_stroops: i128, + referrer_percent: u32, +) -> Result<(i128, i128), &'static str> { + if referrer_percent > 100 { + return Err("Referrer percent cannot exceed 100"); + } + let referrer_amount = total_reward_stroops * referrer_percent as i128 / 100; + let referee_amount = total_reward_stroops - referrer_amount; + Ok((referrer_amount, referee_amount)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn split_50_50() { + assert_eq!(calculate_referral_split(100, 50), Ok((50, 50))); + } + + #[test] + fn split_100_0() { + assert_eq!(calculate_referral_split(100, 100), Ok((100, 0))); + } + + #[test] + fn split_0_100() { + assert_eq!(calculate_referral_split(100, 0), Ok((0, 100))); + } + + #[test] + fn split_70_30() { + assert_eq!(calculate_referral_split(100, 70), Ok((70, 30))); + } + + #[test] + fn odd_total_remainder_goes_to_referrer() { + // 7 * 70 / 100 = 4 (integer), referee = 7 - 4 = 3, sum = 7 + let (r, e) = calculate_referral_split(7, 70).unwrap(); + assert_eq!(r + e, 7); + assert_eq!(r, 4); + } + + #[test] + fn percent_over_100_errors() { + assert_eq!( + calculate_referral_split(100, 101), + Err("Referrer percent cannot exceed 100") + ); + } +} diff --git a/contracts/cntr/src/role_checker.rs b/contracts/cntr/src/role_checker.rs index d368bd2..44fdb3f 100644 --- a/contracts/cntr/src/role_checker.rs +++ b/contracts/cntr/src/role_checker.rs @@ -1,3 +1,54 @@ +pub fn has_role(roles: &[(String, String)], address: &str, required_role: &str) -> bool { + roles.iter().any(|(addr, role)| addr == address && role == required_role) +} + +pub fn get_roles_for_address(roles: &[(String, String)], address: &str) -> Vec { + roles.iter().filter(|(addr, _)| addr == address).map(|(_, role)| role.clone()).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn pair(a: &str, r: &str) -> (String, String) { + (a.to_string(), r.to_string()) + } + + #[test] + fn has_role_empty() { + assert!(!has_role(&[], "alice", "admin")); + } + + #[test] + fn has_role_match() { + let roles = vec![pair("alice", "admin")]; + assert!(has_role(&roles, "alice", "admin")); + } + + #[test] + fn has_role_case_sensitive() { + let roles = vec![pair("alice", "Admin")]; + assert!(!has_role(&roles, "alice", "admin")); + } + + #[test] + fn has_role_wrong_address() { + let roles = vec![pair("alice", "admin")]; + assert!(!has_role(&roles, "Alice", "admin")); + } + + #[test] + fn get_roles_multiple() { + let roles = vec![pair("alice", "admin"), pair("alice", "member"), pair("bob", "member")]; + let mut result = get_roles_for_address(&roles, "alice"); + result.sort(); + assert_eq!(result, vec!["admin", "member"]); + } + + #[test] + fn get_roles_unknown_address() { + let roles = vec![pair("alice", "admin")]; + assert!(get_roles_for_address(&roles, "unknown").is_empty()); use std::collections::HashMap; /// Simple in-memory role registry for access control. diff --git a/contracts/cntr/src/withdrawal_validator.rs b/contracts/cntr/src/withdrawal_validator.rs new file mode 100644 index 0000000..025986d --- /dev/null +++ b/contracts/cntr/src/withdrawal_validator.rs @@ -0,0 +1,64 @@ +pub fn validate_withdrawal( + owner_address: &str, + caller_address: &str, + available_balance: i128, + requested_amount: i128, +) -> Result<(), &'static str> { + if caller_address != owner_address { + return Err("Unauthorized: caller is not the owner"); + } + if requested_amount <= 0 { + return Err("Withdrawal amount must be positive"); + } + if requested_amount > available_balance { + return Err("Insufficient balance"); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn success() { + assert_eq!(validate_withdrawal("alice", "alice", 100, 50), Ok(())); + } + + #[test] + fn exact_balance() { + assert_eq!(validate_withdrawal("alice", "alice", 100, 100), Ok(())); + } + + #[test] + fn unauthorized() { + assert_eq!( + validate_withdrawal("alice", "bob", 100, 50), + Err("Unauthorized: caller is not the owner") + ); + } + + #[test] + fn zero_amount() { + assert_eq!( + validate_withdrawal("alice", "alice", 100, 0), + Err("Withdrawal amount must be positive") + ); + } + + #[test] + fn negative_amount() { + assert_eq!( + validate_withdrawal("alice", "alice", 100, -1), + Err("Withdrawal amount must be positive") + ); + } + + #[test] + fn insufficient_balance() { + assert_eq!( + validate_withdrawal("alice", "alice", 10, 20), + Err("Insufficient balance") + ); + } +}