Skip to content
Merged
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
66 changes: 45 additions & 21 deletions contracts/Cargo.lock

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

1 change: 1 addition & 0 deletions contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"workspace_booking",
"payment_escrow",
"resource_credits",
"cntr",
]

[workspace.dependencies]
Expand Down
4 changes: 4 additions & 0 deletions contracts/cntr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ edition = "2021"
publish = false

[lib]
path = "src/lib.rs"

[dependencies]
regex = "1"
doctest = false
23 changes: 23 additions & 0 deletions contracts/cntr/src/credit_deduction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// Error types for credit deduction operations.
#[derive(Debug, Clone, PartialEq)]
pub enum DeductionError {
InsufficientBalance,
InvalidAmount,
}

/// Deducts `amount` from the given `balance`.
///
/// Returns the new balance on success, or a `DeductionError` on failure.
///
/// # Errors
/// - `InvalidAmount` if amount is zero or negative (represented as 0 for u128).
/// - `InsufficientBalance` if balance < amount.
pub fn deduct_credits(balance: u128, amount: u128) -> Result<u128, DeductionError> {
if amount == 0 {
return Err(DeductionError::InvalidAmount);
}
if balance < amount {
return Err(DeductionError::InsufficientBalance);
}
Ok(balance - amount)
}
27 changes: 27 additions & 0 deletions contracts/cntr/src/credit_topup.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
/// Maximum credit balance a user can hold.
pub const MAX_BALANCE: u128 = 1_000_000_000;

/// Error types for credit top-up operations.
#[derive(Debug, Clone, PartialEq)]
pub enum TopupError {
InvalidAmount,
ExceedsMaxBalance,
}

/// Tops up `amount` to the given `balance`.
///
/// Returns the new balance on success, or a `TopupError` on failure.
///
/// # Errors
/// - `InvalidAmount` if amount is zero.
/// - `ExceedsMaxBalance` if balance + amount > MAX_BALANCE.
pub fn topup_credits(balance: u128, amount: u128) -> Result<u128, TopupError> {
if amount == 0 {
return Err(TopupError::InvalidAmount);
}
let new_balance = balance.checked_add(amount).unwrap_or(u128::MAX);
if new_balance > MAX_BALANCE {
return Err(TopupError::ExceedsMaxBalance);
}
Ok(new_balance)
}
/// Validates a credit top-up operation.
///
/// Returns `Ok(new_balance)` when `topup_amount > 0` and
Expand Down
3 changes: 3 additions & 0 deletions contracts/cntr/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
pub mod credit_deduction;
pub mod credit_topup;
pub mod token_validator;
pub mod grace_period;
pub mod payment_validator;
pub mod credit_topup;
Expand Down
105 changes: 105 additions & 0 deletions contracts/cntr/src/token_validator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use regex::Regex;

/// Validates whether a given string is a valid UUID v4 format.
///
/// UUID v4 format: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`
/// where y is one of [8, 9, a, b].
pub fn is_valid_token_id(token_id: &str) -> bool {
let pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$";
let re = Regex::new(pattern).unwrap();
re.is_match(token_id)
}

/// Checks if the claimed owner is the actual owner of a token in the registry.
///
/// The registry is a list of (token_id, owner_address) pairs.
/// Returns `true` only if the token_id exists in the registry AND the owner matches.
pub fn is_token_owner(token_id: &str, claimed_owner: &str, registry: &[(String, String)]) -> bool {
registry
.iter()
.any(|(tid, owner)| tid == token_id && owner == claimed_owner)
}

#[cfg(test)]
mod tests {
use super::*;

// ============ is_valid_token_id tests ============

#[test]
fn valid_uuid_v4() {
assert!(is_valid_token_id("550e8400-e29b-41d4-a716-446655440000"));
}

#[test]
fn valid_uuid_v4_variant_9() {
assert!(is_valid_token_id("6ba7b810-9dad-41d2-9e18-7c3c9a0f1a2b"));
}

#[test]
fn rejects_uuid_v1() {
// UUID v1 has version digit '1' in the third group
assert!(!is_valid_token_id("550e8400-e29b-11d4-a716-446655440000"));
}

#[test]
fn rejects_uuid_v3() {
// UUID v3 has version digit '3' in the third group
assert!(!is_valid_token_id("550e8400-e29b-31d4-a716-446655440000"));
}

#[test]
fn rejects_uuid_v5() {
// UUID v5 has version digit '5' in the third group
assert!(!is_valid_token_id("550e8400-e29b-51d4-a716-446655440000"));
}

#[test]
fn rejects_malformed_string() {
assert!(!is_valid_token_id("not-a-uuid"));
}

#[test]
fn rejects_uppercase_uuid() {
assert!(!is_valid_token_id("550E8400-E29B-41D4-A716-446655440000"));
}

#[test]
fn rejects_invalid_variant_digit() {
// Variant digit must be [89ab], using '7' here
assert!(!is_valid_token_id("550e8400-e29b-41d4-7716-446655440000"));
}

// ============ is_token_owner tests ============

#[test]
fn owner_matches_in_registry() {
let registry = vec![
("token-1".to_string(), "owner-a".to_string()),
("token-2".to_string(), "owner-b".to_string()),
];
assert!(is_token_owner("token-1", "owner-a", &registry));
}

#[test]
fn owner_does_not_match() {
let registry = vec![
("token-1".to_string(), "owner-a".to_string()),
];
assert!(!is_token_owner("token-1", "wrong-owner", &registry));
}

#[test]
fn unregistered_token_id_returns_false() {
let registry = vec![
("token-1".to_string(), "owner-a".to_string()),
];
assert!(!is_token_owner("unknown-token", "owner-a", &registry));
}

#[test]
fn empty_registry_returns_false() {
let registry: Vec<(String, String)> = vec![];
assert!(!is_token_owner("token-1", "owner-a", &registry));
}
}
89 changes: 89 additions & 0 deletions contracts/cntr/tests/resource_credits_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use cntr::credit_deduction::{deduct_credits, DeductionError};
use cntr::credit_topup::{topup_credits, TopupError, MAX_BALANCE};

// ============ Deduction Tests ============

#[test]
fn deduct_from_full_balance_success() {
let balance: u128 = 1000;
let result = deduct_credits(balance, 200);
assert_eq!(result, Ok(800));
}

#[test]
fn deduct_exact_balance_leaving_zero() {
let balance: u128 = 500;
let result = deduct_credits(balance, 500);
assert_eq!(result, Ok(0));
}

#[test]
fn deduct_more_than_balance_fails() {
let balance: u128 = 100;
let result = deduct_credits(balance, 200);
assert_eq!(result, Err(DeductionError::InsufficientBalance));
}

#[test]
fn deduct_zero_fails() {
let balance: u128 = 100;
let result = deduct_credits(balance, 0);
assert_eq!(result, Err(DeductionError::InvalidAmount));
}

#[test]
fn deduct_from_zero_balance_fails() {
let balance: u128 = 0;
let result = deduct_credits(balance, 1);
assert_eq!(result, Err(DeductionError::InsufficientBalance));
}

#[test]
fn deduct_one_from_one_leaves_zero() {
let result = deduct_credits(1, 1);
assert_eq!(result, Ok(0));
}

// ============ Top-up Tests ============

#[test]
fn topup_from_zero_success() {
let result = topup_credits(0, 500);
assert_eq!(result, Ok(500));
}

#[test]
fn topup_to_exact_max_balance() {
let current = MAX_BALANCE - 100;
let result = topup_credits(current, 100);
assert_eq!(result, Ok(MAX_BALANCE));
}

#[test]
fn topup_one_above_max_fails() {
let current = MAX_BALANCE - 99;
let result = topup_credits(current, 100);
assert_eq!(result, Err(TopupError::ExceedsMaxBalance));
}

#[test]
fn topup_zero_fails() {
let result = topup_credits(100, 0);
assert_eq!(result, Err(TopupError::InvalidAmount));
}

#[test]
fn large_balance_accumulation() {
let mut balance: u128 = 0;
for _ in 0..10 {
balance = topup_credits(balance, 100_000_000).unwrap();
}
assert_eq!(balance, 1_000_000_000);
assert_eq!(balance, MAX_BALANCE);
}

#[test]
fn topup_at_max_balance_fails() {
let result = topup_credits(MAX_BALANCE, 1);
assert_eq!(result, Err(TopupError::ExceedsMaxBalance));
}
Loading
Loading