From 9c30162967dbe057b29904643db05a4848e7be4c Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Thu, 2 Oct 2025 23:27:22 +0100 Subject: [PATCH 01/10] lib & new modules --- .../distributed-energy-resource-manager/src/lib.rs | 2 +- .../distributed-energy-resource-manager/src/tests/mod.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 soroban/contracts/distributed-energy-resource-manager/src/tests/mod.rs diff --git a/soroban/contracts/distributed-energy-resource-manager/src/lib.rs b/soroban/contracts/distributed-energy-resource-manager/src/lib.rs index 519de8b..37d5b22 100644 --- a/soroban/contracts/distributed-energy-resource-manager/src/lib.rs +++ b/soroban/contracts/distributed-energy-resource-manager/src/lib.rs @@ -9,7 +9,7 @@ mod optimization; mod utils; #[cfg(test)] -mod test; +mod tests; use resource::*; use optimization::*; diff --git a/soroban/contracts/distributed-energy-resource-manager/src/tests/mod.rs b/soroban/contracts/distributed-energy-resource-manager/src/tests/mod.rs new file mode 100644 index 0000000..826abbb --- /dev/null +++ b/soroban/contracts/distributed-energy-resource-manager/src/tests/mod.rs @@ -0,0 +1,6 @@ +#![cfg(test)] + +pub mod optimization; +pub mod registration; +pub mod status; +pub mod utils; From 53b69b6e37eeca84f71d55320c2ee004d5c53a45 Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Thu, 2 Oct 2025 23:27:54 +0100 Subject: [PATCH 02/10] utils.rs --- .../src/tests/utils.rs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 soroban/contracts/distributed-energy-resource-manager/src/tests/utils.rs diff --git a/soroban/contracts/distributed-energy-resource-manager/src/tests/utils.rs b/soroban/contracts/distributed-energy-resource-manager/src/tests/utils.rs new file mode 100644 index 0000000..d524db7 --- /dev/null +++ b/soroban/contracts/distributed-energy-resource-manager/src/tests/utils.rs @@ -0,0 +1,108 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _}, + Address, Env, String, +}; + +use crate::{ + DistributedEnergyResourceManager, DistributedEnergyResourceManagerClient, ResourceType, + DERStatus +}; + +// ============ TEST CONTEXT ============ + +pub struct TestContext { + pub env: Env, + pub client: DistributedEnergyResourceManagerClient<'static>, + pub admin: Address, +} + +// ============ SETUP FUNCTIONS ============ + +/// Creates a basic test environment with initialized contract +#[allow(dead_code)] +pub fn setup_test_env() -> TestContext { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + let admin = Address::generate(&env); + + client.initialize(&admin); + + TestContext { + env, + client, + admin, + } +} + +/// Creates test environment with admin and grid operator +#[allow(dead_code)] +pub fn setup_with_operator() -> (TestContext, Address) { + let ctx = setup_test_env(); + let operator = Address::generate(&ctx.env); + let operator_name = String::from_str(&ctx.env, "Grid Operator"); + + ctx.client.add_grid_operator(&ctx.admin, &operator, &operator_name, &5); + + (ctx, operator) +} + +// ============ HELPER FUNCTIONS ============ + +/// Registers a basic DER for testing +#[allow(dead_code)] +pub fn register_test_der( + ctx: &TestContext, + owner: &Address, + der_id: &str, + resource_type: ResourceType, + capacity: u32, +) -> String { + let der_id_str = String::from_str(&ctx.env, der_id); + let location = String::from_str(&ctx.env, "Test Location"); + + ctx.client.register_der(owner, &der_id_str, &resource_type, &capacity, &location); + der_id_str +} + +/// Registers multiple DERs for testing (up to 10) +#[allow(dead_code)] +pub fn register_multiple_ders( + ctx: &TestContext, + owner: &Address, + count: u32, +) { + let location = String::from_str(&ctx.env, "Test Location"); + let der_ids = ["DER_00", "DER_01", "DER_02", "DER_03", "DER_04", + "DER_05", "DER_06", "DER_07", "DER_08", "DER_09"]; + + for i in 0..(count.min(10) as usize) { + let der_id = String::from_str(&ctx.env, der_ids[i]); + ctx.client.register_der(owner, &der_id, &ResourceType::Solar, &100, &location); + } +} + +// ============ ASSERTION HELPERS ============ + +/// Asserts DER has expected status +#[allow(dead_code)] +pub fn assert_der_status(ctx: &TestContext, der_id: &String, expected: DERStatus) { + let der_info = ctx.client.get_der_info(der_id); + assert_eq!(der_info.status, expected, "DER status should match expected"); +} + +/// Asserts stats match expected values +#[allow(dead_code)] +pub fn assert_stats( + ctx: &TestContext, + total: u32, + online: u32, + capacity: u32, +) { + let stats = ctx.client.get_stats(); + assert_eq!(stats.total_ders, total, "Total DERs mismatch"); + assert_eq!(stats.online_ders, online, "Online DERs mismatch"); + assert_eq!(stats.total_capacity, capacity, "Total capacity mismatch"); +} From b7478f378ea6c063190a11417c13bd868c56b519 Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Thu, 2 Oct 2025 23:33:35 +0100 Subject: [PATCH 03/10] optimization tests --- .../src/tests/optimization.rs | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 soroban/contracts/distributed-energy-resource-manager/src/tests/optimization.rs diff --git a/soroban/contracts/distributed-energy-resource-manager/src/tests/optimization.rs b/soroban/contracts/distributed-energy-resource-manager/src/tests/optimization.rs new file mode 100644 index 0000000..0ab05d6 --- /dev/null +++ b/soroban/contracts/distributed-energy-resource-manager/src/tests/optimization.rs @@ -0,0 +1,283 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _}, + Address, Env, String, +}; + +use crate::{ + DistributedEnergyResourceManager, DistributedEnergyResourceManagerClient, ResourceType, + DERStatus +}; + +// ============ RESOURCE OPTIMIZATION TESTS ============ + +#[test] +fn test_optimize_resources() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + let operator = Address::generate(&env); + + client.initialize(&admin); + + // Add grid operator + let operator_name = String::from_str(&env, "Grid Operator 1"); + client.add_grid_operator(&admin, &operator, &operator_name, &5); + + // Register some DERs + let solar_der = String::from_str(&env, "SOLAR_001"); + let battery_der = String::from_str(&env, "BATTERY_001"); + let location = String::from_str(&env, "Test Location"); + + client.register_der(&der_owner, &solar_der, &ResourceType::Solar, &1000, &location); + client.register_der(&der_owner, &battery_der, &ResourceType::Battery, &500, &location); + + // Optimize resources + let schedules = client.optimize_resources(&operator); + + // Verify optimization schedules were created + assert_eq!(schedules.len(), 2); + + // Check that we can retrieve the schedules + let retrieved_schedules = client.get_optimization_schedules(); + assert_eq!(retrieved_schedules.len(), 2); +} + +#[test] +fn test_emergency_allocation() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + let operator = Address::generate(&env); + + client.initialize(&admin); + + // Add grid operator + let operator_name = String::from_str(&env, "Grid Operator 1"); + client.add_grid_operator(&admin, &operator, &operator_name, &5); + + // Register a DER + let der_id = String::from_str(&env, "BATTERY_001"); + let location = String::from_str(&env, "Test Location"); + + client.register_der(&der_owner, &der_id, &ResourceType::Battery, &500, &location); + + // Emergency allocation + let result = client.emergency_allocation( + &operator, + &der_id, + &300, // 300 kW required + &3600 // 1 hour duration + ); + + assert!(result); + + // Verify DER status changed to emergency + let der_info = client.get_der_info(&der_id); + assert_eq!(der_info.status, DERStatus::Emergency); +} + +// ============ EDGE CASE TESTS ============ + +#[test] +#[should_panic(expected = "DER not found")] +fn test_emergency_allocation_non_existent_der() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + + client.initialize(&admin); + + // Add grid operator + let operator_name = String::from_str(&env, "Grid Operator 1"); + client.add_grid_operator(&admin, &operator, &operator_name, &5); + + let der_id = String::from_str(&env, "NON_EXISTENT_DER"); + + // Attempt emergency allocation on non-existent DER - should panic + client.emergency_allocation(&operator, &der_id, &300, &3600); +} + +#[test] +#[should_panic(expected = "DER is not available for emergency allocation")] +fn test_emergency_allocation_offline_der() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + let operator = Address::generate(&env); + + client.initialize(&admin); + + // Add grid operator + let operator_name = String::from_str(&env, "Grid Operator 1"); + client.add_grid_operator(&admin, &operator, &operator_name, &5); + + // Register a DER + let der_id = String::from_str(&env, "BATTERY_001"); + let location = String::from_str(&env, "Test Location"); + + client.register_der(&der_owner, &der_id, &ResourceType::Battery, &500, &location); + + // Set DER to offline + client.update_status(&der_owner, &der_id, &DERStatus::Offline); + + // Attempt emergency allocation on offline DER - should panic + client.emergency_allocation(&operator, &der_id, &300, &3600); +} + +#[test] +#[should_panic(expected = "Required power exceeds DER capacity")] +fn test_emergency_allocation_exceeds_capacity() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + let operator = Address::generate(&env); + + client.initialize(&admin); + + // Add grid operator + let operator_name = String::from_str(&env, "Grid Operator 1"); + client.add_grid_operator(&admin, &operator, &operator_name, &5); + + // Register a DER with 500 capacity + let der_id = String::from_str(&env, "BATTERY_001"); + let location = String::from_str(&env, "Test Location"); + + client.register_der(&der_owner, &der_id, &ResourceType::Battery, &500, &location); + + // Attempt to allocate more than capacity - should panic + client.emergency_allocation(&operator, &der_id, &600, &3600); +} + +#[test] +fn test_optimize_with_mixed_status_ders() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + let operator = Address::generate(&env); + + client.initialize(&admin); + + // Add grid operator + let operator_name = String::from_str(&env, "Grid Operator 1"); + client.add_grid_operator(&admin, &operator, &operator_name, &5); + + let location = String::from_str(&env, "Test Location"); + + // Register DERs with different statuses + let solar_der = String::from_str(&env, "SOLAR_001"); + let wind_der = String::from_str(&env, "WIND_001"); + let battery_der = String::from_str(&env, "BATTERY_001"); + + client.register_der(&der_owner, &solar_der, &ResourceType::Solar, &1000, &location); + client.register_der(&der_owner, &wind_der, &ResourceType::Wind, &500, &location); + client.register_der(&der_owner, &battery_der, &ResourceType::Battery, &200, &location); + + // Set one DER to offline + client.update_status(&der_owner, &battery_der, &DERStatus::Offline); + + // Optimize resources - should only include online/optimized DERs + let schedules = client.optimize_resources(&operator); + + // Only solar and wind should be in optimization (battery is offline) + assert_eq!(schedules.len(), 2); +} + +#[test] +fn test_optimize_empty_der_list() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + + client.initialize(&admin); + + // Add grid operator + let operator_name = String::from_str(&env, "Grid Operator 1"); + client.add_grid_operator(&admin, &operator, &operator_name, &5); + + // Optimize with no DERs registered + let schedules = client.optimize_resources(&operator); + + // Should return empty schedule + assert_eq!(schedules.len(), 0); +} + +#[test] +fn test_optimization_all_resource_types() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + let operator = Address::generate(&env); + + client.initialize(&admin); + + // Add grid operator + let operator_name = String::from_str(&env, "Grid Operator 1"); + client.add_grid_operator(&admin, &operator, &operator_name, &5); + + let location = String::from_str(&env, "Test Location"); + + // Register all resource types + let resource_types = [ + (ResourceType::Solar, "SOLAR_001", 1000), + (ResourceType::Wind, "WIND_001", 500), + (ResourceType::Battery, "BATTERY_001", 200), + (ResourceType::Hydro, "HYDRO_001", 800), + (ResourceType::Geothermal, "GEO_001", 600), + (ResourceType::FuelCell, "FUEL_001", 100), + ]; + + for (resource_type, der_id, capacity) in resource_types { + let der_id_str = String::from_str(&env, der_id); + client.register_der(&der_owner, &der_id_str, &resource_type, &capacity, &location); + } + + // Optimize all resources + let schedules = client.optimize_resources(&operator); + + // Should create schedules for all 6 resource types + assert_eq!(schedules.len(), 6); + + // Verify schedule priorities are set (different for each type) + // Battery should have highest priority (9), Geothermal lowest (4) + let mut has_battery_priority = false; + let mut has_geothermal_priority = false; + + for schedule in schedules.iter() { + if schedule.priority == 9 { + has_battery_priority = true; + } + if schedule.priority == 4 { + has_geothermal_priority = true; + } + } + + assert!(has_battery_priority, "Battery priority (9) should be present"); + assert!(has_geothermal_priority, "Geothermal priority (4) should be present"); +} + From 2b24587373e3ccf2d5ffba9dfe8b1ce3ce7f1e5d Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Thu, 2 Oct 2025 23:33:52 +0100 Subject: [PATCH 04/10] registration and status tests --- .../src/tests/registration.rs | 250 ++++++++++++++++++ .../src/tests/status.rs | 188 +++++++++++++ 2 files changed, 438 insertions(+) create mode 100644 soroban/contracts/distributed-energy-resource-manager/src/tests/registration.rs create mode 100644 soroban/contracts/distributed-energy-resource-manager/src/tests/status.rs diff --git a/soroban/contracts/distributed-energy-resource-manager/src/tests/registration.rs b/soroban/contracts/distributed-energy-resource-manager/src/tests/registration.rs new file mode 100644 index 0000000..0649d6d --- /dev/null +++ b/soroban/contracts/distributed-energy-resource-manager/src/tests/registration.rs @@ -0,0 +1,250 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _}, + Address, Env, String, +}; + +use crate::{ + DistributedEnergyResourceManager, DistributedEnergyResourceManagerClient, ResourceType, + DERStatus +}; + +// ============ INITIALIZATION TESTS ============ + +#[test] +fn test_initialize() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + + client.initialize(&admin); + + // Verify contract is initialized + let stats = client.get_stats(); + assert_eq!(stats.total_ders, 0); + assert_eq!(stats.online_ders, 0); + assert_eq!(stats.total_capacity, 0); +} + +// ============ DER REGISTRATION TESTS ============ + +#[test] +fn test_register_der() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + // Initialize contract + client.initialize(&admin); + + // Register a solar DER + let der_id = String::from_str(&env, "SOLAR_001"); + let location = String::from_str(&env, "San Francisco, CA"); + + let result = client.register_der( + &der_owner, + &der_id, + &ResourceType::Solar, + &1000, // 1 MW + &location + ); + + assert!(result); + + // Verify DER was registered + let der_info = client.get_der_info(&der_id); + assert_eq!(der_info.owner, der_owner); + assert_eq!(der_info.resource_type, ResourceType::Solar); + assert_eq!(der_info.capacity, 1000); + assert_eq!(der_info.status, DERStatus::Online); + assert_eq!(der_info.location, location); +} + +#[test] +fn test_register_multiple_ders() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + client.initialize(&admin); + + // Register multiple DERs + let solar_der = String::from_str(&env, "SOLAR_001"); + let wind_der = String::from_str(&env, "WIND_001"); + let battery_der = String::from_str(&env, "BATTERY_001"); + + let location = String::from_str(&env, "Test Location"); + + client.register_der(&der_owner, &solar_der, &ResourceType::Solar, &1000, &location); + client.register_der(&der_owner, &wind_der, &ResourceType::Wind, &500, &location); + client.register_der(&der_owner, &battery_der, &ResourceType::Battery, &200, &location); + + // Verify all DERs are registered + let stats = client.get_stats(); + assert_eq!(stats.total_ders, 3); + assert_eq!(stats.online_ders, 3); + assert_eq!(stats.total_capacity, 1700); + + // Verify owner can see all their DERs + let owner_ders = client.get_owner_ders(&der_owner); + assert_eq!(owner_ders.len(), 3); +} + +#[test] +fn test_different_resource_types() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + client.initialize(&admin); + + let location = String::from_str(&env, "Test Location"); + + // Test all resource types + let resource_types = [ + (ResourceType::Solar, "SOLAR_001", 1000), + (ResourceType::Wind, "WIND_001", 500), + (ResourceType::Battery, "BATTERY_001", 200), + (ResourceType::Hydro, "HYDRO_001", 800), + (ResourceType::Geothermal, "GEO_001", 600), + (ResourceType::FuelCell, "FUEL_001", 100), + ]; + + for (resource_type, der_id, capacity) in resource_types { + let der_id_str = String::from_str(&env, der_id); + + let result = client.register_der( + &der_owner, + &der_id_str, + &resource_type, + &capacity, + &location + ); + assert!(result); + + // Verify DER was registered correctly + let der_info = client.get_der_info(&der_id_str); + assert_eq!(der_info.resource_type, resource_type); + assert_eq!(der_info.capacity, capacity); + } + + let stats = client.get_stats(); + assert_eq!(stats.total_ders, 6); + assert_eq!(stats.total_capacity, 3200); +} + +// ============ GRID OPERATOR TESTS ============ + +#[test] +fn test_add_grid_operator() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + + client.initialize(&admin); + + let operator_name = String::from_str(&env, "Grid Operator 1"); + + let result = client.add_grid_operator( + &admin, + &operator, + &operator_name, + &5 // Authority level 5 + ); + + assert!(result); +} + +// ============ EDGE CASE TESTS ============ + +#[test] +#[should_panic(expected = "DER already exists")] +fn test_duplicate_der_registration() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + client.initialize(&admin); + + let der_id = String::from_str(&env, "SOLAR_001"); + let location = String::from_str(&env, "Test Location"); + + // Register DER first time + client.register_der(&der_owner, &der_id, &ResourceType::Solar, &1000, &location); + + // Attempt to register same DER again - should panic + client.register_der(&der_owner, &der_id, &ResourceType::Solar, &1000, &location); +} + +#[test] +#[should_panic(expected = "Contract already initialized")] +fn test_duplicate_initialization() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + + // Initialize first time + client.initialize(&admin); + + // Attempt to initialize again - should panic + client.initialize(&admin); +} + +#[test] +fn test_high_volume_der_registrations() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + client.initialize(&admin); + + let location = String::from_str(&env, "Test Location"); + + // Register 50 DERs to test scalability + let der_ids = [ + "DER_000", "DER_001", "DER_002", "DER_003", "DER_004", "DER_005", "DER_006", "DER_007", "DER_008", "DER_009", + "DER_010", "DER_011", "DER_012", "DER_013", "DER_014", "DER_015", "DER_016", "DER_017", "DER_018", "DER_019", + "DER_020", "DER_021", "DER_022", "DER_023", "DER_024", "DER_025", "DER_026", "DER_027", "DER_028", "DER_029", + "DER_030", "DER_031", "DER_032", "DER_033", "DER_034", "DER_035", "DER_036", "DER_037", "DER_038", "DER_039", + "DER_040", "DER_041", "DER_042", "DER_043", "DER_044", "DER_045", "DER_046", "DER_047", "DER_048", "DER_049", + ]; + + for der_id_str in der_ids { + let der_id = String::from_str(&env, der_id_str); + client.register_der(&der_owner, &der_id, &ResourceType::Solar, &100, &location); + } + + // Verify all DERs registered + let stats = client.get_stats(); + assert_eq!(stats.total_ders, 50); + assert_eq!(stats.online_ders, 50); + assert_eq!(stats.total_capacity, 5000); + + // Verify owner can see all their DERs + let owner_ders = client.get_owner_ders(&der_owner); + assert_eq!(owner_ders.len(), 50); +} + diff --git a/soroban/contracts/distributed-energy-resource-manager/src/tests/status.rs b/soroban/contracts/distributed-energy-resource-manager/src/tests/status.rs new file mode 100644 index 0000000..34726dd --- /dev/null +++ b/soroban/contracts/distributed-energy-resource-manager/src/tests/status.rs @@ -0,0 +1,188 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _}, + Address, Env, String, +}; + +use crate::{ + DistributedEnergyResourceManager, DistributedEnergyResourceManagerClient, ResourceType, + DERStatus +}; + +// ============ STATUS UPDATE TESTS ============ + +#[test] +fn test_update_status() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + client.initialize(&admin); + + let der_id = String::from_str(&env, "SOLAR_001"); + let location = String::from_str(&env, "Test Location"); + + client.register_der(&der_owner, &der_id, &ResourceType::Solar, &1000, &location); + + // Update status to maintenance + let result = client.update_status(&der_owner, &der_id, &DERStatus::Maintenance); + assert!(result); + + // Verify status was updated + let der_info = client.get_der_info(&der_id); + assert_eq!(der_info.status, DERStatus::Maintenance); +} + +#[test] +fn test_get_stats() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + client.initialize(&admin); + + // Register multiple DERs with different statuses + let solar_der = String::from_str(&env, "SOLAR_001"); + let wind_der = String::from_str(&env, "WIND_001"); + let battery_der = String::from_str(&env, "BATTERY_001"); + let location = String::from_str(&env, "Test Location"); + + // Register solar DER + client.register_der(&der_owner, &solar_der, &ResourceType::Solar, &1000, &location); + + // Register wind DER + client.register_der(&der_owner, &wind_der, &ResourceType::Wind, &500, &location); + + // Register battery DER + client.register_der(&der_owner, &battery_der, &ResourceType::Battery, &200, &location); + + // Put one DER in maintenance + client.update_status(&der_owner, &battery_der, &DERStatus::Maintenance); + + let stats = client.get_stats(); + assert_eq!(stats.total_ders, 3); + assert_eq!(stats.online_ders, 2); // Only solar and wind are online + assert_eq!(stats.total_capacity, 1700); + assert_eq!(stats.utilization_rate, 66); // 2 out of 3 DERs online +} + +// ============ EDGE CASE TESTS ============ + +#[test] +#[should_panic(expected = "DER not found")] +fn test_update_status_non_existent_der() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + client.initialize(&admin); + + let der_id = String::from_str(&env, "NON_EXISTENT_DER"); + + // Attempt to update status of non-existent DER - should panic + client.update_status(&der_owner, &der_id, &DERStatus::Maintenance); +} + +#[test] +#[should_panic(expected = "DER not found")] +fn test_get_info_non_existent_der() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + + client.initialize(&admin); + + let der_id = String::from_str(&env, "NON_EXISTENT_DER"); + + // Attempt to get info of non-existent DER - should panic + client.get_der_info(&der_id); +} + +#[test] +fn test_status_transitions_all_states() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + client.initialize(&admin); + + let der_id = String::from_str(&env, "SOLAR_001"); + let location = String::from_str(&env, "Test Location"); + + client.register_der(&der_owner, &der_id, &ResourceType::Solar, &1000, &location); + + // Test all status transitions + let statuses = [ + DERStatus::Online, + DERStatus::Maintenance, + DERStatus::Offline, + DERStatus::Online, + DERStatus::Optimized, + ]; + + for status in statuses { + client.update_status(&der_owner, &der_id, &status); + let der_info = client.get_der_info(&der_id); + assert_eq!(der_info.status, status); + } +} + +#[test] +fn test_multiple_ders_different_statuses() { + let env = Env::default(); + let contract_id = env.register(DistributedEnergyResourceManager, ()); + let client = DistributedEnergyResourceManagerClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let der_owner = Address::generate(&env); + + client.initialize(&admin); + + let location = String::from_str(&env, "Test Location"); + + // Register 10 DERs and set different statuses + let der_configs = [ + ("DER_0", DERStatus::Online), + ("DER_1", DERStatus::Maintenance), + ("DER_2", DERStatus::Offline), + ("DER_3", DERStatus::Optimized), + ("DER_4", DERStatus::Online), + ("DER_5", DERStatus::Maintenance), + ("DER_6", DERStatus::Offline), + ("DER_7", DERStatus::Optimized), + ("DER_8", DERStatus::Online), + ("DER_9", DERStatus::Maintenance), + ]; + + for (der_id_str, status) in der_configs { + let der_id = String::from_str(&env, der_id_str); + client.register_der(&der_owner, &der_id, &ResourceType::Solar, &100, &location); + + if status != DERStatus::Online { + client.update_status(&der_owner, &der_id, &status); + } + } + + // Verify stats reflect mixed statuses + let stats = client.get_stats(); + assert_eq!(stats.total_ders, 10); + // Online (3) + Optimized (2) = 5 + assert_eq!(stats.online_ders, 5); + assert_eq!(stats.total_capacity, 1000); +} + From bf9b0e391114fab77f1cc3bcefd12e141cf6bb55 Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Fri, 3 Oct 2025 21:44:30 +0100 Subject: [PATCH 05/10] deleted old test file --- soroban/contracts/carbon-credit-registry/src/test.rs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 soroban/contracts/carbon-credit-registry/src/test.rs diff --git a/soroban/contracts/carbon-credit-registry/src/test.rs b/soroban/contracts/carbon-credit-registry/src/test.rs deleted file mode 100644 index 2d79966..0000000 --- a/soroban/contracts/carbon-credit-registry/src/test.rs +++ /dev/null @@ -1 +0,0 @@ -// Insert the tests code here \ No newline at end of file From 587875e1d79e7ebedfc30adf7314d85a69c2ce53 Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Fri, 3 Oct 2025 21:44:50 +0100 Subject: [PATCH 06/10] lib and module setup --- soroban/contracts/carbon-credit-registry/src/lib.rs | 2 +- soroban/contracts/carbon-credit-registry/src/tests/mod.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 soroban/contracts/carbon-credit-registry/src/tests/mod.rs diff --git a/soroban/contracts/carbon-credit-registry/src/lib.rs b/soroban/contracts/carbon-credit-registry/src/lib.rs index b287343..4c1ba41 100644 --- a/soroban/contracts/carbon-credit-registry/src/lib.rs +++ b/soroban/contracts/carbon-credit-registry/src/lib.rs @@ -7,7 +7,7 @@ pub mod trading; pub mod utils; #[cfg(test)] -mod test; +mod tests; // Core data structures for carbon credits #[contracttype] diff --git a/soroban/contracts/carbon-credit-registry/src/tests/mod.rs b/soroban/contracts/carbon-credit-registry/src/tests/mod.rs new file mode 100644 index 0000000..9955364 --- /dev/null +++ b/soroban/contracts/carbon-credit-registry/src/tests/mod.rs @@ -0,0 +1,6 @@ +#![cfg(test)] + +pub mod issuance; +pub mod retirement; +pub mod trading; +pub mod utils; From ababd65a81ec38019cf776d8e2e91ad65f1e7737 Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Fri, 3 Oct 2025 21:45:30 +0100 Subject: [PATCH 07/10] utils and issuance tests --- .../src/tests/issuance.rs | 234 ++++++++++++++++++ .../carbon-credit-registry/src/tests/utils.rs | 234 ++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 soroban/contracts/carbon-credit-registry/src/tests/issuance.rs create mode 100644 soroban/contracts/carbon-credit-registry/src/tests/utils.rs diff --git a/soroban/contracts/carbon-credit-registry/src/tests/issuance.rs b/soroban/contracts/carbon-credit-registry/src/tests/issuance.rs new file mode 100644 index 0000000..18e77c1 --- /dev/null +++ b/soroban/contracts/carbon-credit-registry/src/tests/issuance.rs @@ -0,0 +1,234 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _}, + Address, BytesN, Map, String, Vec, +}; + +use crate::{CreditStatus}; + +use super::utils::*; + +// ============ INITIALIZATION TESTS ============ + +#[test] +fn test_contract_initialization() { + let ctx = setup_test_env(); + + // Verify contract stats after initialization + assert_contract_stats(&ctx, 0, 0, 0, 0); +} + +// ============ ISSUER REGISTRATION TESTS ============ + +#[test] +fn test_register_issuer() { + let ctx = setup_test_env(); + let issuer = Address::generate(&ctx.env); + + let issuer_name = String::from_str(&ctx.env, "Verra Verified Issuer"); + let mut standards = Vec::new(&ctx.env); + standards.push_back(String::from_str(&ctx.env, "Verra")); + standards.push_back(String::from_str(&ctx.env, "Gold Standard")); + + ctx.client.register_issuer(&issuer, &issuer_name, &standards); + + // Verify issuer profile + let profile = ctx.client.get_issuer_profile(&issuer); + assert!(profile.is_some(), "Issuer profile should exist"); + + let profile_data = profile.unwrap(); + assert_eq!(profile_data.address, issuer); + assert_eq!(profile_data.name, issuer_name); + assert_eq!(profile_data.is_active, true); + assert_eq!(profile_data.total_issued, 0); + assert_eq!(profile_data.total_retired, 0); + + // Verify issuer count increased + let (issuer_count, _, _, _) = ctx.client.get_contract_stats(); + assert_eq!(issuer_count, 1); +} + +#[test] +fn test_register_multiple_issuers() { + let ctx = setup_test_env(); + + let issuer1 = Address::generate(&ctx.env); + let issuer2 = Address::generate(&ctx.env); + let issuer3 = Address::generate(&ctx.env); + + let mut standards = Vec::new(&ctx.env); + standards.push_back(String::from_str(&ctx.env, "Verra")); + + ctx.client.register_issuer(&issuer1, &String::from_str(&ctx.env, "Issuer 1"), &standards); + ctx.client.register_issuer(&issuer2, &String::from_str(&ctx.env, "Issuer 2"), &standards); + ctx.client.register_issuer(&issuer3, &String::from_str(&ctx.env, "Issuer 3"), &standards); + + // Verify all issuers registered + let (issuer_count, _, _, _) = ctx.client.get_contract_stats(); + assert_eq!(issuer_count, 3); +} + +// ============ CREDIT ISSUANCE TESTS ============ + +#[test] +fn test_issue_credit() { + let (ctx, issuer) = setup_with_issuer(); + + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Verify credit was issued + let credit_opt = ctx.client.get_credit_status(&credit_id); + assert!(credit_opt.is_some(), "Credit should exist"); + + let credit = credit_opt.unwrap(); + assert_eq!(credit.issuer, issuer); + assert_eq!(credit.quantity, 1000); + assert_eq!(credit.status, CreditStatus::Issued); + assert_eq!(credit.current_owner, issuer); + assert_eq!(credit.vintage_year, 2024); + + // Verify contract stats updated + let (_, credit_count, _, _) = ctx.client.get_contract_stats(); + assert_eq!(credit_count, 1); +} + +#[test] +fn test_issue_multiple_credits() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue multiple credits + let credit1 = issue_test_credit(&ctx, &issuer, 500); + let credit2 = issue_test_credit(&ctx, &issuer, 1000); + let credit3 = issue_test_credit(&ctx, &issuer, 1500); + + // Verify all credits exist + assert!(ctx.client.get_credit_status(&credit1).is_some()); + assert!(ctx.client.get_credit_status(&credit2).is_some()); + assert!(ctx.client.get_credit_status(&credit3).is_some()); + + // Verify credit count + let (_, credit_count, _, _) = ctx.client.get_contract_stats(); + assert_eq!(credit_count, 3); +} + +#[test] +fn test_issue_credit_different_project_types() { + let (ctx, issuer) = setup_with_issuer(); + + let project_types = [ + ("Reforestation", "Brazil", "Verra"), + ("Solar Energy", "India", "Gold Standard"), + ("Wind Energy", "Denmark", "Verra"), + ("Methane Capture", "USA", "Gold Standard"), + ]; + + for (project_type, location, standard) in project_types { + let credit_id = issue_credit_with_params(&ctx, &issuer, project_type, location, standard, 2024, 1000); + + let credit = ctx.client.get_credit_status(&credit_id).unwrap(); + assert_eq!(credit.project_type, String::from_str(&ctx.env, project_type)); + assert_eq!(credit.project_location, String::from_str(&ctx.env, location)); + assert_eq!(credit.verification_standard, String::from_str(&ctx.env, standard)); + } + + let (_, credit_count, _, _) = ctx.client.get_contract_stats(); + assert_eq!(credit_count, 4); +} + +#[test] +fn test_issue_credit_different_vintage_years() { + let (ctx, issuer) = setup_with_issuer(); + + let vintages = [2020, 2021, 2022, 2023, 2024]; + + for vintage in vintages { + let credit_id = issue_credit_with_params( + &ctx, + &issuer, + "Reforestation", + "Brazil", + "Verra", + vintage, + 1000, + ); + + let credit = ctx.client.get_credit_status(&credit_id).unwrap(); + assert_eq!(credit.vintage_year, vintage); + } + + let (_, credit_count, _, _) = ctx.client.get_contract_stats(); + assert_eq!(credit_count, 5); +} + +// ============ CREDIT HISTORY TESTS ============ + +#[test] +fn test_credit_history_after_issuance() { + let (ctx, issuer) = setup_with_issuer(); + + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Get credit history + let history = ctx.client.get_credit_history(&credit_id); + + // Should have 1 event (issuance) + assert_eq!(history.len(), 1); + + let event = history.get(0).unwrap(); + assert_eq!(event.quantity, 1000); + assert_eq!(event.to.unwrap(), issuer); +} + +// ============ EDGE CASE TESTS ============ + +#[test] +fn test_high_volume_credit_issuance() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue 20 credits to test scalability + let credit_ids = [ + "CREDIT_00", "CREDIT_01", "CREDIT_02", "CREDIT_03", "CREDIT_04", + "CREDIT_05", "CREDIT_06", "CREDIT_07", "CREDIT_08", "CREDIT_09", + "CREDIT_10", "CREDIT_11", "CREDIT_12", "CREDIT_13", "CREDIT_14", + "CREDIT_15", "CREDIT_16", "CREDIT_17", "CREDIT_18", "CREDIT_19", + ]; + + for _ in credit_ids { + issue_test_credit(&ctx, &issuer, 100); + } + + // Verify all credits were issued + let (_, credit_count, _, _) = ctx.client.get_contract_stats(); + assert_eq!(credit_count, 20); +} + +#[test] +fn test_issue_credit_with_metadata() { + let (ctx, issuer) = setup_with_issuer(); + + let project_type = String::from_str(&ctx.env, "Reforestation"); + let project_location = String::from_str(&ctx.env, "Brazil"); + let verification_standard = String::from_str(&ctx.env, "Verra"); + let verification_hash = BytesN::from_array(&ctx.env, &[1u8; 32]); + + let mut metadata = Map::new(&ctx.env); + metadata.set(String::from_str(&ctx.env, "project_name"), String::from_str(&ctx.env, "Amazon Reforestation")); + metadata.set(String::from_str(&ctx.env, "sdg_goals"), String::from_str(&ctx.env, "13,15")); + metadata.set(String::from_str(&ctx.env, "co-benefits"), String::from_str(&ctx.env, "biodiversity")); + + let credit_id = ctx.client.issue_credit( + &issuer, + &project_type, + &project_location, + &verification_standard, + &2024, + &1000, + &verification_hash, + &metadata, + ); + + // Verify credit was created + let credit = ctx.client.get_credit_status(&credit_id).unwrap(); + assert_eq!(credit.quantity, 1000); +} diff --git a/soroban/contracts/carbon-credit-registry/src/tests/utils.rs b/soroban/contracts/carbon-credit-registry/src/tests/utils.rs new file mode 100644 index 0000000..41b1fe4 --- /dev/null +++ b/soroban/contracts/carbon-credit-registry/src/tests/utils.rs @@ -0,0 +1,234 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _, Ledger as _}, + Address, BytesN, Env, Map, String, Vec, +}; + +use crate::{ + CarbonCreditRegistry, CarbonCreditRegistryClient, CreditStatus, + RetirementParams, TradingParams, +}; + +// ============ TEST CONTEXT ============ + +pub struct TestContext { + pub env: Env, + pub client: CarbonCreditRegistryClient<'static>, + pub admin: Address, +} + +// ============ SETUP FUNCTIONS ============ + +/// Creates a basic test environment with initialized contract +#[allow(dead_code)] +pub fn setup_test_env() -> TestContext { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, CarbonCreditRegistry); + let client = CarbonCreditRegistryClient::new(&env, &contract_id); + let admin = Address::generate(&env); + + client.initialize(&admin, &25, &10); // 0.25% trading fee, 0.10% retirement fee + + TestContext { env, client, admin } +} + +/// Creates test environment with registered issuer +#[allow(dead_code)] +pub fn setup_with_issuer() -> (TestContext, Address) { + let ctx = setup_test_env(); + let issuer = Address::generate(&ctx.env); + + let issuer_name = String::from_str(&ctx.env, "Test Issuer"); + let mut standards = Vec::new(&ctx.env); + standards.push_back(String::from_str(&ctx.env, "Verra")); + standards.push_back(String::from_str(&ctx.env, "Gold Standard")); + + ctx.client.register_issuer(&issuer, &issuer_name, &standards); + + (ctx, issuer) +} + +// ============ HELPER FUNCTIONS ============ + +/// Issues a basic carbon credit for testing +/// Note: Uses incrementing timestamp to ensure unique credit IDs +#[allow(dead_code)] +pub fn issue_test_credit( + ctx: &TestContext, + issuer: &Address, + quantity: i128, +) -> BytesN<32> { + let project_type = String::from_str(&ctx.env, "Reforestation"); + let project_location = String::from_str(&ctx.env, "Brazil"); + let verification_standard = String::from_str(&ctx.env, "Verra"); + + // Create unique verification hash using current timestamp + let timestamp = ctx.env.ledger().timestamp(); + let mut hash_bytes = [1u8; 32]; + let ts_bytes = timestamp.to_be_bytes(); + hash_bytes[0..8].copy_from_slice(&ts_bytes); + let verification_hash = BytesN::from_array(&ctx.env, &hash_bytes); + + let metadata = Map::new(&ctx.env); + + let credit_id = ctx.client.issue_credit( + issuer, + &project_type, + &project_location, + &verification_standard, + &2024, + &quantity, + &verification_hash, + &metadata, + ); + + // Increment timestamp to ensure next credit gets different ID + ctx.env.ledger().with_mut(|li| li.timestamp += 1); + + credit_id +} + +/// Issues a credit with custom parameters +/// Note: Uses incrementing timestamp to ensure unique credit IDs +#[allow(dead_code)] +pub fn issue_credit_with_params( + ctx: &TestContext, + issuer: &Address, + project_type: &str, + location: &str, + standard: &str, + vintage: u32, + quantity: i128, +) -> BytesN<32> { + let project_type_str = String::from_str(&ctx.env, project_type); + let location_str = String::from_str(&ctx.env, location); + let standard_str = String::from_str(&ctx.env, standard); + + // Create unique verification hash using current timestamp + let timestamp = ctx.env.ledger().timestamp(); + let mut hash_bytes = [2u8; 32]; + let ts_bytes = timestamp.to_be_bytes(); + hash_bytes[0..8].copy_from_slice(&ts_bytes); + let verification_hash = BytesN::from_array(&ctx.env, &hash_bytes); + + let metadata = Map::new(&ctx.env); + + let credit_id = ctx.client.issue_credit( + issuer, + &project_type_str, + &location_str, + &standard_str, + &vintage, + &quantity, + &verification_hash, + &metadata, + ); + + // Increment timestamp to ensure next credit gets different ID + ctx.env.ledger().with_mut(|li| li.timestamp += 1); + + credit_id +} + +/// Trades a credit from one owner to another +#[allow(dead_code)] +pub fn trade_test_credit( + ctx: &TestContext, + credit_id: BytesN<32>, + from: &Address, + to: &Address, + quantity: i128, +) { + let payment_token = Address::generate(&ctx.env); + + let params = TradingParams { + credit_id, + from: from.clone(), + to: to.clone(), + quantity, + price: 50, + payment_token, + }; + + ctx.client.trade_credit(¶ms); +} + +/// Retires a credit +#[allow(dead_code)] +pub fn retire_test_credit( + ctx: &TestContext, + credit_id: BytesN<32>, + owner: &Address, + quantity: i128, +) { + let retirement_reason = String::from_str(&ctx.env, "Carbon offset claim"); + let retirement_certificate = BytesN::from_array(&ctx.env, &[3u8; 32]); + + let params = RetirementParams { + credit_id, + owner: owner.clone(), + quantity, + retirement_reason, + retirement_certificate, + }; + + ctx.client.retire_credit(¶ms); +} + +// ============ ASSERTION HELPERS ============ + +/// Asserts credit has expected status +#[allow(dead_code)] +pub fn assert_credit_status(ctx: &TestContext, credit_id: &BytesN<32>, expected: CreditStatus) { + let credit = ctx.client.get_credit_status(credit_id); + assert!(credit.is_some(), "Credit should exist"); + assert_eq!( + credit.unwrap().status, + expected, + "Credit status should match expected" + ); +} + +/// Asserts credit has expected owner +#[allow(dead_code)] +pub fn assert_credit_owner(ctx: &TestContext, credit_id: &BytesN<32>, expected_owner: &Address) { + let credit = ctx.client.get_credit_status(credit_id); + assert!(credit.is_some(), "Credit should exist"); + assert_eq!( + credit.unwrap().current_owner, + expected_owner.clone(), + "Credit owner should match expected" + ); +} + +/// Asserts credit has expected quantity +#[allow(dead_code)] +pub fn assert_credit_quantity(ctx: &TestContext, credit_id: &BytesN<32>, expected: i128) { + let credit = ctx.client.get_credit_status(credit_id); + assert!(credit.is_some(), "Credit should exist"); + assert_eq!( + credit.unwrap().quantity, + expected, + "Credit quantity should match expected" + ); +} + +/// Asserts contract stats match expected values +#[allow(dead_code)] +pub fn assert_contract_stats( + ctx: &TestContext, + expected_issuer_count: u32, + expected_credit_count: u32, + expected_total_issued: i128, + expected_total_retired: i128, +) { + let (issuer_count, credit_count, total_issued, total_retired) = ctx.client.get_contract_stats(); + + assert_eq!(issuer_count, expected_issuer_count, "Issuer count mismatch"); + assert_eq!(credit_count, expected_credit_count, "Credit count mismatch"); + assert_eq!(total_issued, expected_total_issued, "Total issued mismatch"); + assert_eq!(total_retired, expected_total_retired, "Total retired mismatch"); +} From 4018b55aa9e3992915199df28e80514864102511 Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Fri, 3 Oct 2025 21:45:56 +0100 Subject: [PATCH 08/10] credit trading tests --- .../src/tests/trading.rs | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 soroban/contracts/carbon-credit-registry/src/tests/trading.rs diff --git a/soroban/contracts/carbon-credit-registry/src/tests/trading.rs b/soroban/contracts/carbon-credit-registry/src/tests/trading.rs new file mode 100644 index 0000000..b030c9c --- /dev/null +++ b/soroban/contracts/carbon-credit-registry/src/tests/trading.rs @@ -0,0 +1,192 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _}, + Address, +}; + +use crate::{CreditStatus}; + +use super::utils::*; + +// ============ CREDIT TRADING TESTS ============ + +#[test] +fn test_trade_credit() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Trade the credit + let buyer = Address::generate(&ctx.env); + trade_test_credit(&ctx, credit_id.clone(), &issuer, &buyer, 1000); + + // Verify ownership changed + assert_credit_owner(&ctx, &credit_id, &buyer); + + // Verify status changed to Traded + assert_credit_status(&ctx, &credit_id, CreditStatus::Traded); +} + +#[test] +fn test_multiple_trades() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // First trade: issuer -> buyer1 + let buyer1 = Address::generate(&ctx.env); + trade_test_credit(&ctx, credit_id.clone(), &issuer, &buyer1, 1000); + assert_credit_owner(&ctx, &credit_id, &buyer1); + + // Second trade: buyer1 -> buyer2 + let buyer2 = Address::generate(&ctx.env); + trade_test_credit(&ctx, credit_id.clone(), &buyer1, &buyer2, 1000); + assert_credit_owner(&ctx, &credit_id, &buyer2); + + // Third trade: buyer2 -> buyer3 + let buyer3 = Address::generate(&ctx.env); + trade_test_credit(&ctx, credit_id.clone(), &buyer2, &buyer3, 1000); + assert_credit_owner(&ctx, &credit_id, &buyer3); +} + +#[test] +fn test_trade_partial_credit() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit with 1000 quantity + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Trade partial quantity + let buyer = Address::generate(&ctx.env); + trade_test_credit(&ctx, credit_id.clone(), &issuer, &buyer, 500); + + // Verify ownership changed + assert_credit_owner(&ctx, &credit_id, &buyer); +} + +#[test] +fn test_trading_history() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Make multiple trades + let buyer1 = Address::generate(&ctx.env); + let buyer2 = Address::generate(&ctx.env); + + trade_test_credit(&ctx, credit_id.clone(), &issuer, &buyer1, 1000); + trade_test_credit(&ctx, credit_id.clone(), &buyer1, &buyer2, 1000); + + // Get credit history (should include issuance + 2 trades = 3 events) + let history = ctx.client.get_credit_history(&credit_id); + assert!(history.len() >= 3, "Should have at least 3 events (issuance + 2 trades)"); +} + +// ============ EDGE CASE TESTS ============ + +#[test] +#[should_panic] +fn test_trade_non_existent_credit() { + let ctx = setup_test_env(); + + let seller = Address::generate(&ctx.env); + let buyer = Address::generate(&ctx.env); + let non_existent_credit_id = soroban_sdk::BytesN::from_array(&ctx.env, &[99u8; 32]); + + // Attempt to trade non-existent credit - should panic + trade_test_credit(&ctx, non_existent_credit_id, &seller, &buyer, 1000); +} + +#[test] +fn test_trade_multiple_credits() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue multiple credits + let credit1 = issue_test_credit(&ctx, &issuer, 500); + let credit2 = issue_test_credit(&ctx, &issuer, 1000); + let credit3 = issue_test_credit(&ctx, &issuer, 1500); + + // Trade each credit to different buyers + let buyer1 = Address::generate(&ctx.env); + let buyer2 = Address::generate(&ctx.env); + let buyer3 = Address::generate(&ctx.env); + + trade_test_credit(&ctx, credit1.clone(), &issuer, &buyer1, 500); + trade_test_credit(&ctx, credit2.clone(), &issuer, &buyer2, 1000); + trade_test_credit(&ctx, credit3.clone(), &issuer, &buyer3, 1500); + + // Verify each credit has correct owner + assert_credit_owner(&ctx, &credit1, &buyer1); + assert_credit_owner(&ctx, &credit2, &buyer2); + assert_credit_owner(&ctx, &credit3, &buyer3); +} + +#[test] +fn test_trade_credit_same_buyer_multiple_times() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue credits + let credit1 = issue_test_credit(&ctx, &issuer, 1000); + let credit2 = issue_test_credit(&ctx, &issuer, 2000); + + // Same buyer purchases multiple credits + let buyer = Address::generate(&ctx.env); + + trade_test_credit(&ctx, credit1.clone(), &issuer, &buyer, 1000); + trade_test_credit(&ctx, credit2.clone(), &issuer, &buyer, 2000); + + // Verify both credits owned by same buyer + assert_credit_owner(&ctx, &credit1, &buyer); + assert_credit_owner(&ctx, &credit2, &buyer); +} + +#[test] +fn test_trade_chain() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Create a chain of trades + let buyer1 = Address::generate(&ctx.env); + let buyer2 = Address::generate(&ctx.env); + let buyer3 = Address::generate(&ctx.env); + let buyer4 = Address::generate(&ctx.env); + let buyer5 = Address::generate(&ctx.env); + + trade_test_credit(&ctx, credit_id.clone(), &issuer, &buyer1, 1000); + trade_test_credit(&ctx, credit_id.clone(), &buyer1, &buyer2, 1000); + trade_test_credit(&ctx, credit_id.clone(), &buyer2, &buyer3, 1000); + trade_test_credit(&ctx, credit_id.clone(), &buyer3, &buyer4, 1000); + trade_test_credit(&ctx, credit_id.clone(), &buyer4, &buyer5, 1000); + + // Verify final owner + assert_credit_owner(&ctx, &credit_id, &buyer5); + + // Verify history has all trades + let history = ctx.client.get_credit_history(&credit_id); + assert!(history.len() >= 6, "Should have issuance + 5 trades"); +} + +#[test] +fn test_trade_different_verification_standards() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue credits with different standards + let verra_credit = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2024, 1000); + let gold_credit = issue_credit_with_params(&ctx, &issuer, "Solar", "India", "Gold Standard", 2024, 1000); + + let buyer = Address::generate(&ctx.env); + + // Trade both credits + trade_test_credit(&ctx, verra_credit.clone(), &issuer, &buyer, 1000); + trade_test_credit(&ctx, gold_credit.clone(), &issuer, &buyer, 1000); + + // Verify both traded successfully + assert_credit_owner(&ctx, &verra_credit, &buyer); + assert_credit_owner(&ctx, &gold_credit, &buyer); +} From 0f4d737936024d3bf21172e84b7b3e57fa138e66 Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Fri, 3 Oct 2025 21:46:11 +0100 Subject: [PATCH 09/10] credit retirement tests --- .../src/tests/retirement.rs | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 soroban/contracts/carbon-credit-registry/src/tests/retirement.rs diff --git a/soroban/contracts/carbon-credit-registry/src/tests/retirement.rs b/soroban/contracts/carbon-credit-registry/src/tests/retirement.rs new file mode 100644 index 0000000..1a2a02f --- /dev/null +++ b/soroban/contracts/carbon-credit-registry/src/tests/retirement.rs @@ -0,0 +1,257 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _}, + Address, +}; + +use crate::{CreditStatus}; + +use super::utils::*; + +// ============ CREDIT RETIREMENT TESTS ============ + +#[test] +fn test_retire_credit() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Retire the credit + retire_test_credit(&ctx, credit_id.clone(), &issuer, 1000); + + // Verify status changed to Retired + assert_credit_status(&ctx, &credit_id, CreditStatus::Retired); + + // Verify quantity is now 0 + assert_credit_quantity(&ctx, &credit_id, 0); +} + +#[test] +fn test_retire_partial_credit() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit with 1000 quantity + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Retire partial quantity (500 out of 1000) + retire_test_credit(&ctx, credit_id.clone(), &issuer, 500); + + // Verify quantity reduced + assert_credit_quantity(&ctx, &credit_id, 500); + + // Status should still be Issued (not fully retired) + assert_credit_status(&ctx, &credit_id, CreditStatus::Issued); +} + +#[test] +fn test_retire_credit_in_stages() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit with 1000 quantity + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Retire in stages: 300, 200, 500 (total 1000) + retire_test_credit(&ctx, credit_id.clone(), &issuer, 300); + assert_credit_quantity(&ctx, &credit_id, 700); + + retire_test_credit(&ctx, credit_id.clone(), &issuer, 200); + assert_credit_quantity(&ctx, &credit_id, 500); + + retire_test_credit(&ctx, credit_id.clone(), &issuer, 500); + assert_credit_quantity(&ctx, &credit_id, 0); + + // Now should be fully retired + assert_credit_status(&ctx, &credit_id, CreditStatus::Retired); +} + +#[test] +fn test_retire_traded_credit() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Trade to buyer + let buyer = Address::generate(&ctx.env); + trade_test_credit(&ctx, credit_id.clone(), &issuer, &buyer, 1000); + + // Buyer retires the credit + retire_test_credit(&ctx, credit_id.clone(), &buyer, 1000); + + // Verify retired + assert_credit_status(&ctx, &credit_id, CreditStatus::Retired); + assert_credit_quantity(&ctx, &credit_id, 0); +} + +#[test] +fn test_retirement_updates_stats() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue credits + let credit1 = issue_test_credit(&ctx, &issuer, 500); + let credit2 = issue_test_credit(&ctx, &issuer, 1000); + + // Retire both credits + retire_test_credit(&ctx, credit1, &issuer, 500); + retire_test_credit(&ctx, credit2, &issuer, 1000); + + // Check contract stats + let (_, _, _, total_retired) = ctx.client.get_contract_stats(); + assert_eq!(total_retired, 1500, "Total retired should be 1500"); +} + +#[test] +fn test_retirement_history() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Retire the credit + retire_test_credit(&ctx, credit_id.clone(), &issuer, 1000); + + // Get history (should have issuance + retirement) + let history = ctx.client.get_credit_history(&credit_id); + assert!(history.len() >= 2, "Should have at least 2 events (issuance + retirement)"); +} + +// ============ EDGE CASE TESTS ============ + +#[test] +#[should_panic] +fn test_retire_non_existent_credit() { + let ctx = setup_test_env(); + + let owner = Address::generate(&ctx.env); + let non_existent_credit_id = soroban_sdk::BytesN::from_array(&ctx.env, &[99u8; 32]); + + // Attempt to retire non-existent credit - should panic + retire_test_credit(&ctx, non_existent_credit_id, &owner, 1000); +} + +#[test] +fn test_retire_multiple_credits() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue multiple credits + let credit1 = issue_test_credit(&ctx, &issuer, 500); + let credit2 = issue_test_credit(&ctx, &issuer, 1000); + let credit3 = issue_test_credit(&ctx, &issuer, 1500); + + // Retire all credits + retire_test_credit(&ctx, credit1.clone(), &issuer, 500); + retire_test_credit(&ctx, credit2.clone(), &issuer, 1000); + retire_test_credit(&ctx, credit3.clone(), &issuer, 1500); + + // Verify all retired + assert_credit_status(&ctx, &credit1, CreditStatus::Retired); + assert_credit_status(&ctx, &credit2, CreditStatus::Retired); + assert_credit_status(&ctx, &credit3, CreditStatus::Retired); + + // Check total retired + let (_, _, _, total_retired) = ctx.client.get_contract_stats(); + assert_eq!(total_retired, 3000); +} + +#[test] +fn test_trade_then_retire() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue a credit + let credit_id = issue_test_credit(&ctx, &issuer, 1000); + + // Trade chain: issuer -> buyer1 -> buyer2 + let buyer1 = Address::generate(&ctx.env); + let buyer2 = Address::generate(&ctx.env); + + trade_test_credit(&ctx, credit_id.clone(), &issuer, &buyer1, 1000); + trade_test_credit(&ctx, credit_id.clone(), &buyer1, &buyer2, 1000); + + // Final buyer retires the credit + retire_test_credit(&ctx, credit_id.clone(), &buyer2, 1000); + + // Verify retired + assert_credit_status(&ctx, &credit_id, CreditStatus::Retired); + + // Get full history (issuance + 2 trades + retirement = 4 events) + let history = ctx.client.get_credit_history(&credit_id); + assert!(history.len() >= 4, "Should have at least 4 events"); +} + +#[test] +fn test_retire_different_project_types() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue credits for different project types + let reforestation = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2024, 1000); + let solar = issue_credit_with_params(&ctx, &issuer, "Solar Energy", "India", "Gold Standard", 2024, 2000); + let wind = issue_credit_with_params(&ctx, &issuer, "Wind Energy", "Denmark", "Verra", 2024, 1500); + + // Retire all credits + retire_test_credit(&ctx, reforestation.clone(), &issuer, 1000); + retire_test_credit(&ctx, solar.clone(), &issuer, 2000); + retire_test_credit(&ctx, wind.clone(), &issuer, 1500); + + // Verify all retired + assert_credit_status(&ctx, &reforestation, CreditStatus::Retired); + assert_credit_status(&ctx, &solar, CreditStatus::Retired); + assert_credit_status(&ctx, &wind, CreditStatus::Retired); + + // Check total + let (_, _, _, total_retired) = ctx.client.get_contract_stats(); + assert_eq!(total_retired, 4500); +} + +#[test] +fn test_retire_different_vintages() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue credits with different vintage years + let vintage_2020 = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2020, 1000); + let vintage_2022 = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2022, 1000); + let vintage_2024 = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2024, 1000); + + // Retire all vintages + retire_test_credit(&ctx, vintage_2020, &issuer, 1000); + retire_test_credit(&ctx, vintage_2022, &issuer, 1000); + retire_test_credit(&ctx, vintage_2024, &issuer, 1000); + + // Check total retired + let (_, _, _, total_retired) = ctx.client.get_contract_stats(); + assert_eq!(total_retired, 3000); +} + +#[test] +fn test_mixed_operations_comprehensive() { + let (ctx, issuer) = setup_with_issuer(); + + // Issue multiple credits + let credit1 = issue_test_credit(&ctx, &issuer, 2000); + let credit2 = issue_test_credit(&ctx, &issuer, 3000); + + // Trade credit1 + let buyer1 = Address::generate(&ctx.env); + trade_test_credit(&ctx, credit1.clone(), &issuer, &buyer1, 2000); + + // Partial retirement of credit1 by buyer1 + retire_test_credit(&ctx, credit1.clone(), &buyer1, 500); + assert_credit_quantity(&ctx, &credit1, 1500); + + // Trade remaining credit1 + let buyer2 = Address::generate(&ctx.env); + trade_test_credit(&ctx, credit1.clone(), &buyer1, &buyer2, 1500); + + // Retire credit2 fully by issuer + retire_test_credit(&ctx, credit2.clone(), &issuer, 3000); + assert_credit_status(&ctx, &credit2, CreditStatus::Retired); + + // Retire remaining credit1 by buyer2 + retire_test_credit(&ctx, credit1.clone(), &buyer2, 1500); + assert_credit_status(&ctx, &credit1, CreditStatus::Retired); + + // Check total retired + let (_, _, _, total_retired) = ctx.client.get_contract_stats(); + assert_eq!(total_retired, 5000); +} From fd18ec8e0c139b0403a8ad2947245d9fc34034c4 Mon Sep 17 00:00:00 2001 From: frankie-powers Date: Fri, 3 Oct 2025 21:51:02 +0100 Subject: [PATCH 10/10] cargo fmt --- .../carbon-credit-registry/src/credits.rs | 43 +++--- .../carbon-credit-registry/src/lib.rs | 135 +++++++++++------- .../src/tests/issuance.rs | 85 ++++++++--- .../src/tests/retirement.rs | 65 +++++++-- .../src/tests/trading.rs | 25 ++-- .../carbon-credit-registry/src/tests/utils.rs | 17 ++- .../carbon-credit-registry/src/trading.rs | 120 ++++++++++------ .../carbon-credit-registry/src/utils.rs | 8 +- 8 files changed, 328 insertions(+), 170 deletions(-) diff --git a/soroban/contracts/carbon-credit-registry/src/credits.rs b/soroban/contracts/carbon-credit-registry/src/credits.rs index f671187..34d7e42 100644 --- a/soroban/contracts/carbon-credit-registry/src/credits.rs +++ b/soroban/contracts/carbon-credit-registry/src/credits.rs @@ -1,8 +1,6 @@ -use soroban_sdk::{Env, Address, BytesN, String, Map, Vec, Bytes}; +use soroban_sdk::{Address, Bytes, BytesN, Env, Map, String, Vec}; -use crate::{ - CarbonCredit, CreditStatus, CreditEvent, EventType, DataKey, -}; +use crate::{CarbonCredit, CreditEvent, CreditStatus, DataKey, EventType}; /// Record a credit event in the history pub fn record_credit_event( @@ -13,7 +11,10 @@ pub fn record_credit_event( to: Option
, quantity: i128, ) { - let event_count: u32 = env.storage().instance().get(&DataKey::EventCount(credit_id.clone())) + let event_count: u32 = env + .storage() + .instance() + .get(&DataKey::EventCount(credit_id.clone())) .unwrap_or(0); let event = CreditEvent { @@ -27,32 +28,38 @@ pub fn record_credit_event( metadata: Map::new(env), }; - env.storage().instance().set(&DataKey::CreditEvent(credit_id.clone(), event_count), &event); - env.storage().instance().set(&DataKey::EventCount(credit_id.clone()), &(event_count + 1)); + env.storage().instance().set( + &DataKey::CreditEvent(credit_id.clone(), event_count), + &event, + ); + env.storage() + .instance() + .set(&DataKey::EventCount(credit_id.clone()), &(event_count + 1)); } /// Get credit status and details -pub fn get_credit_status( - env: &Env, - credit_id: BytesN<32>, -) -> Option { +pub fn get_credit_status(env: &Env, credit_id: BytesN<32>) -> Option { env.storage().instance().get(&DataKey::Credit(credit_id)) } /// Get credit transaction history -pub fn get_credit_history( - env: &Env, - credit_id: BytesN<32>, -) -> Vec { - let event_count: u32 = env.storage().instance().get(&DataKey::EventCount(credit_id.clone())) +pub fn get_credit_history(env: &Env, credit_id: BytesN<32>) -> Vec { + let event_count: u32 = env + .storage() + .instance() + .get(&DataKey::EventCount(credit_id.clone())) .unwrap_or(0); let mut events = Vec::new(env); for i in 0..event_count { - if let Some(event) = env.storage().instance().get::(&DataKey::CreditEvent(credit_id.clone(), i)) { + if let Some(event) = env + .storage() + .instance() + .get::(&DataKey::CreditEvent(credit_id.clone(), i)) + { events.push_back(event); } } events -} \ No newline at end of file +} diff --git a/soroban/contracts/carbon-credit-registry/src/lib.rs b/soroban/contracts/carbon-credit-registry/src/lib.rs index 4c1ba41..bd73c25 100644 --- a/soroban/contracts/carbon-credit-registry/src/lib.rs +++ b/soroban/contracts/carbon-credit-registry/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Vec, BytesN, String, Map}; +use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Map, String, Vec}; pub mod credits; pub mod trading; @@ -77,7 +77,7 @@ pub struct TradingParams { pub from: Address, pub to: Address, pub quantity: i128, - pub price: i128, // Price per ton of CO2 + pub price: i128, // Price per ton of CO2 pub payment_token: Address, // Payment token address } @@ -101,7 +101,7 @@ pub enum DataKey { CreditCount, EventCount(BytesN<32>), // credit_id -> event_count Admin, - TradingFeeRate, // Fee rate for trading (in basis points) + TradingFeeRate, // Fee rate for trading (in basis points) RetirementFeeRate, // Fee rate for retirement (in basis points) TotalCreditsIssued, TotalCreditsRetired, @@ -120,14 +120,22 @@ impl CarbonCreditRegistry { retirement_fee_rate: u32, ) { admin.require_auth(); - + env.storage().instance().set(&DataKey::Admin, &admin); - env.storage().instance().set(&DataKey::TradingFeeRate, &trading_fee_rate); - env.storage().instance().set(&DataKey::RetirementFeeRate, &retirement_fee_rate); + env.storage() + .instance() + .set(&DataKey::TradingFeeRate, &trading_fee_rate); + env.storage() + .instance() + .set(&DataKey::RetirementFeeRate, &retirement_fee_rate); env.storage().instance().set(&DataKey::IssuerCount, &0u32); env.storage().instance().set(&DataKey::CreditCount, &0u32); - env.storage().instance().set(&DataKey::TotalCreditsIssued, &0i128); - env.storage().instance().set(&DataKey::TotalCreditsRetired, &0i128); + env.storage() + .instance() + .set(&DataKey::TotalCreditsIssued, &0i128); + env.storage() + .instance() + .set(&DataKey::TotalCreditsRetired, &0i128); } /// Register a new issuer @@ -137,8 +145,7 @@ impl CarbonCreditRegistry { name: String, verification_standards: Vec, ) { - let admin: Address = env.storage().instance().get(&DataKey::Admin) - .unwrap(); + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth(); let issuer_profile = IssuerProfile { @@ -150,12 +157,19 @@ impl CarbonCreditRegistry { total_retired: 0, }; - env.storage().instance().set(&DataKey::Issuer(issuer_address), &issuer_profile); - - let mut issuer_count: u32 = env.storage().instance().get(&DataKey::IssuerCount) + env.storage() + .instance() + .set(&DataKey::Issuer(issuer_address), &issuer_profile); + + let mut issuer_count: u32 = env + .storage() + .instance() + .get(&DataKey::IssuerCount) .unwrap_or(0); issuer_count += 1; - env.storage().instance().set(&DataKey::IssuerCount, &issuer_count); + env.storage() + .instance() + .set(&DataKey::IssuerCount, &issuer_count); } /// Issue a new carbon credit @@ -192,7 +206,9 @@ impl CarbonCreditRegistry { }; // Store the credit - env.storage().instance().set(&DataKey::Credit(credit_id.clone()), &credit); + env.storage() + .instance() + .set(&DataKey::Credit(credit_id.clone()), &credit); // Record issuance event credits::record_credit_event( @@ -205,66 +221,66 @@ impl CarbonCreditRegistry { ); // Increment credit count - let mut credit_count: u32 = env.storage().instance().get(&DataKey::CreditCount) + let mut credit_count: u32 = env + .storage() + .instance() + .get(&DataKey::CreditCount) .unwrap_or(0); credit_count += 1; - env.storage().instance().set(&DataKey::CreditCount, &credit_count); + env.storage() + .instance() + .set(&DataKey::CreditCount, &credit_count); credit_id } /// Trade a carbon credit - pub fn trade_credit( - env: Env, - params: TradingParams, - ) { + pub fn trade_credit(env: Env, params: TradingParams) { trading::trade_credit(env, params) } /// Retire a carbon credit - pub fn retire_credit( - env: Env, - params: RetirementParams, - ) { + pub fn retire_credit(env: Env, params: RetirementParams) { trading::retire_credit(env, params) } /// Suspend a credit (admin only) - pub fn suspend_credit( - env: Env, - credit_id: BytesN<32>, - reason: String, - ) { - let admin: Address = env.storage().instance().get(&DataKey::Admin) - .unwrap(); + pub fn suspend_credit(env: Env, credit_id: BytesN<32>, reason: String) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth(); - let mut credit: CarbonCredit = env.storage().instance().get(&DataKey::Credit(credit_id.clone())) + let mut credit: CarbonCredit = env + .storage() + .instance() + .get(&DataKey::Credit(credit_id.clone())) .unwrap(); credit.status = CreditStatus::Suspended; - env.storage().instance().set(&DataKey::Credit(credit_id.clone()), &credit); + env.storage() + .instance() + .set(&DataKey::Credit(credit_id.clone()), &credit); } /// Get credit status and details - pub fn get_credit_status( - env: Env, - credit_id: BytesN<32>, - ) -> Option { + pub fn get_credit_status(env: Env, credit_id: BytesN<32>) -> Option { env.storage().instance().get(&DataKey::Credit(credit_id)) } /// Get credit transaction history - pub fn get_credit_history( - env: Env, - credit_id: BytesN<32>, - ) -> Vec { - let event_count: u32 = env.storage().instance().get(&DataKey::EventCount(credit_id.clone())) + pub fn get_credit_history(env: Env, credit_id: BytesN<32>) -> Vec { + let event_count: u32 = env + .storage() + .instance() + .get(&DataKey::EventCount(credit_id.clone())) .unwrap_or(0); let mut events = Vec::new(&env); for i in 0..event_count { - if let Some(event) = env.storage().instance().get(&DataKey::CreditEvent(credit_id.clone(), i)) { + if let Some(event) = env + .storage() + .instance() + .get(&DataKey::CreditEvent(credit_id.clone(), i)) + { events.push_back(event); } } @@ -273,24 +289,35 @@ impl CarbonCreditRegistry { } /// Get issuer profile - pub fn get_issuer_profile( - env: Env, - issuer_address: Address, - ) -> Option { - env.storage().instance().get(&DataKey::Issuer(issuer_address)) + pub fn get_issuer_profile(env: Env, issuer_address: Address) -> Option { + env.storage() + .instance() + .get(&DataKey::Issuer(issuer_address)) } /// Get contract statistics pub fn get_contract_stats(env: Env) -> (u32, u32, i128, i128) { - let issuer_count: u32 = env.storage().instance().get(&DataKey::IssuerCount) + let issuer_count: u32 = env + .storage() + .instance() + .get(&DataKey::IssuerCount) .unwrap_or(0); - let credit_count: u32 = env.storage().instance().get(&DataKey::CreditCount) + let credit_count: u32 = env + .storage() + .instance() + .get(&DataKey::CreditCount) .unwrap_or(0); - let total_issued: i128 = env.storage().instance().get(&DataKey::TotalCreditsIssued) + let total_issued: i128 = env + .storage() + .instance() + .get(&DataKey::TotalCreditsIssued) .unwrap_or(0); - let total_retired: i128 = env.storage().instance().get(&DataKey::TotalCreditsRetired) + let total_retired: i128 = env + .storage() + .instance() + .get(&DataKey::TotalCreditsRetired) .unwrap_or(0); (issuer_count, credit_count, total_issued, total_retired) } -} \ No newline at end of file +} diff --git a/soroban/contracts/carbon-credit-registry/src/tests/issuance.rs b/soroban/contracts/carbon-credit-registry/src/tests/issuance.rs index 18e77c1..19c94bc 100644 --- a/soroban/contracts/carbon-credit-registry/src/tests/issuance.rs +++ b/soroban/contracts/carbon-credit-registry/src/tests/issuance.rs @@ -1,11 +1,8 @@ #![cfg(test)] -use soroban_sdk::{ - testutils::{Address as _}, - Address, BytesN, Map, String, Vec, -}; +use soroban_sdk::{testutils::Address as _, Address, BytesN, Map, String, Vec}; -use crate::{CreditStatus}; +use crate::CreditStatus; use super::utils::*; @@ -31,7 +28,8 @@ fn test_register_issuer() { standards.push_back(String::from_str(&ctx.env, "Verra")); standards.push_back(String::from_str(&ctx.env, "Gold Standard")); - ctx.client.register_issuer(&issuer, &issuer_name, &standards); + ctx.client + .register_issuer(&issuer, &issuer_name, &standards); // Verify issuer profile let profile = ctx.client.get_issuer_profile(&issuer); @@ -60,9 +58,21 @@ fn test_register_multiple_issuers() { let mut standards = Vec::new(&ctx.env); standards.push_back(String::from_str(&ctx.env, "Verra")); - ctx.client.register_issuer(&issuer1, &String::from_str(&ctx.env, "Issuer 1"), &standards); - ctx.client.register_issuer(&issuer2, &String::from_str(&ctx.env, "Issuer 2"), &standards); - ctx.client.register_issuer(&issuer3, &String::from_str(&ctx.env, "Issuer 3"), &standards); + ctx.client.register_issuer( + &issuer1, + &String::from_str(&ctx.env, "Issuer 1"), + &standards, + ); + ctx.client.register_issuer( + &issuer2, + &String::from_str(&ctx.env, "Issuer 2"), + &standards, + ); + ctx.client.register_issuer( + &issuer3, + &String::from_str(&ctx.env, "Issuer 3"), + &standards, + ); // Verify all issuers registered let (issuer_count, _, _, _) = ctx.client.get_contract_stats(); @@ -124,12 +134,22 @@ fn test_issue_credit_different_project_types() { ]; for (project_type, location, standard) in project_types { - let credit_id = issue_credit_with_params(&ctx, &issuer, project_type, location, standard, 2024, 1000); + let credit_id = + issue_credit_with_params(&ctx, &issuer, project_type, location, standard, 2024, 1000); let credit = ctx.client.get_credit_status(&credit_id).unwrap(); - assert_eq!(credit.project_type, String::from_str(&ctx.env, project_type)); - assert_eq!(credit.project_location, String::from_str(&ctx.env, location)); - assert_eq!(credit.verification_standard, String::from_str(&ctx.env, standard)); + assert_eq!( + credit.project_type, + String::from_str(&ctx.env, project_type) + ); + assert_eq!( + credit.project_location, + String::from_str(&ctx.env, location) + ); + assert_eq!( + credit.verification_standard, + String::from_str(&ctx.env, standard) + ); } let (_, credit_count, _, _) = ctx.client.get_contract_stats(); @@ -188,10 +208,26 @@ fn test_high_volume_credit_issuance() { // Issue 20 credits to test scalability let credit_ids = [ - "CREDIT_00", "CREDIT_01", "CREDIT_02", "CREDIT_03", "CREDIT_04", - "CREDIT_05", "CREDIT_06", "CREDIT_07", "CREDIT_08", "CREDIT_09", - "CREDIT_10", "CREDIT_11", "CREDIT_12", "CREDIT_13", "CREDIT_14", - "CREDIT_15", "CREDIT_16", "CREDIT_17", "CREDIT_18", "CREDIT_19", + "CREDIT_00", + "CREDIT_01", + "CREDIT_02", + "CREDIT_03", + "CREDIT_04", + "CREDIT_05", + "CREDIT_06", + "CREDIT_07", + "CREDIT_08", + "CREDIT_09", + "CREDIT_10", + "CREDIT_11", + "CREDIT_12", + "CREDIT_13", + "CREDIT_14", + "CREDIT_15", + "CREDIT_16", + "CREDIT_17", + "CREDIT_18", + "CREDIT_19", ]; for _ in credit_ids { @@ -213,9 +249,18 @@ fn test_issue_credit_with_metadata() { let verification_hash = BytesN::from_array(&ctx.env, &[1u8; 32]); let mut metadata = Map::new(&ctx.env); - metadata.set(String::from_str(&ctx.env, "project_name"), String::from_str(&ctx.env, "Amazon Reforestation")); - metadata.set(String::from_str(&ctx.env, "sdg_goals"), String::from_str(&ctx.env, "13,15")); - metadata.set(String::from_str(&ctx.env, "co-benefits"), String::from_str(&ctx.env, "biodiversity")); + metadata.set( + String::from_str(&ctx.env, "project_name"), + String::from_str(&ctx.env, "Amazon Reforestation"), + ); + metadata.set( + String::from_str(&ctx.env, "sdg_goals"), + String::from_str(&ctx.env, "13,15"), + ); + metadata.set( + String::from_str(&ctx.env, "co-benefits"), + String::from_str(&ctx.env, "biodiversity"), + ); let credit_id = ctx.client.issue_credit( &issuer, diff --git a/soroban/contracts/carbon-credit-registry/src/tests/retirement.rs b/soroban/contracts/carbon-credit-registry/src/tests/retirement.rs index 1a2a02f..c3f1781 100644 --- a/soroban/contracts/carbon-credit-registry/src/tests/retirement.rs +++ b/soroban/contracts/carbon-credit-registry/src/tests/retirement.rs @@ -1,11 +1,8 @@ #![cfg(test)] -use soroban_sdk::{ - testutils::{Address as _}, - Address, -}; +use soroban_sdk::{testutils::Address as _, Address}; -use crate::{CreditStatus}; +use crate::CreditStatus; use super::utils::*; @@ -114,7 +111,10 @@ fn test_retirement_history() { // Get history (should have issuance + retirement) let history = ctx.client.get_credit_history(&credit_id); - assert!(history.len() >= 2, "Should have at least 2 events (issuance + retirement)"); + assert!( + history.len() >= 2, + "Should have at least 2 events (issuance + retirement)" + ); } // ============ EDGE CASE TESTS ============ @@ -185,9 +185,26 @@ fn test_retire_different_project_types() { let (ctx, issuer) = setup_with_issuer(); // Issue credits for different project types - let reforestation = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2024, 1000); - let solar = issue_credit_with_params(&ctx, &issuer, "Solar Energy", "India", "Gold Standard", 2024, 2000); - let wind = issue_credit_with_params(&ctx, &issuer, "Wind Energy", "Denmark", "Verra", 2024, 1500); + let reforestation = issue_credit_with_params( + &ctx, + &issuer, + "Reforestation", + "Brazil", + "Verra", + 2024, + 1000, + ); + let solar = issue_credit_with_params( + &ctx, + &issuer, + "Solar Energy", + "India", + "Gold Standard", + 2024, + 2000, + ); + let wind = + issue_credit_with_params(&ctx, &issuer, "Wind Energy", "Denmark", "Verra", 2024, 1500); // Retire all credits retire_test_credit(&ctx, reforestation.clone(), &issuer, 1000); @@ -209,9 +226,33 @@ fn test_retire_different_vintages() { let (ctx, issuer) = setup_with_issuer(); // Issue credits with different vintage years - let vintage_2020 = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2020, 1000); - let vintage_2022 = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2022, 1000); - let vintage_2024 = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2024, 1000); + let vintage_2020 = issue_credit_with_params( + &ctx, + &issuer, + "Reforestation", + "Brazil", + "Verra", + 2020, + 1000, + ); + let vintage_2022 = issue_credit_with_params( + &ctx, + &issuer, + "Reforestation", + "Brazil", + "Verra", + 2022, + 1000, + ); + let vintage_2024 = issue_credit_with_params( + &ctx, + &issuer, + "Reforestation", + "Brazil", + "Verra", + 2024, + 1000, + ); // Retire all vintages retire_test_credit(&ctx, vintage_2020, &issuer, 1000); diff --git a/soroban/contracts/carbon-credit-registry/src/tests/trading.rs b/soroban/contracts/carbon-credit-registry/src/tests/trading.rs index b030c9c..140f456 100644 --- a/soroban/contracts/carbon-credit-registry/src/tests/trading.rs +++ b/soroban/contracts/carbon-credit-registry/src/tests/trading.rs @@ -1,11 +1,8 @@ #![cfg(test)] -use soroban_sdk::{ - testutils::{Address as _}, - Address, -}; +use soroban_sdk::{testutils::Address as _, Address}; -use crate::{CreditStatus}; +use crate::CreditStatus; use super::utils::*; @@ -83,7 +80,10 @@ fn test_trading_history() { // Get credit history (should include issuance + 2 trades = 3 events) let history = ctx.client.get_credit_history(&credit_id); - assert!(history.len() >= 3, "Should have at least 3 events (issuance + 2 trades)"); + assert!( + history.len() >= 3, + "Should have at least 3 events (issuance + 2 trades)" + ); } // ============ EDGE CASE TESTS ============ @@ -177,8 +177,17 @@ fn test_trade_different_verification_standards() { let (ctx, issuer) = setup_with_issuer(); // Issue credits with different standards - let verra_credit = issue_credit_with_params(&ctx, &issuer, "Reforestation", "Brazil", "Verra", 2024, 1000); - let gold_credit = issue_credit_with_params(&ctx, &issuer, "Solar", "India", "Gold Standard", 2024, 1000); + let verra_credit = issue_credit_with_params( + &ctx, + &issuer, + "Reforestation", + "Brazil", + "Verra", + 2024, + 1000, + ); + let gold_credit = + issue_credit_with_params(&ctx, &issuer, "Solar", "India", "Gold Standard", 2024, 1000); let buyer = Address::generate(&ctx.env); diff --git a/soroban/contracts/carbon-credit-registry/src/tests/utils.rs b/soroban/contracts/carbon-credit-registry/src/tests/utils.rs index 41b1fe4..b2d4739 100644 --- a/soroban/contracts/carbon-credit-registry/src/tests/utils.rs +++ b/soroban/contracts/carbon-credit-registry/src/tests/utils.rs @@ -6,8 +6,7 @@ use soroban_sdk::{ }; use crate::{ - CarbonCreditRegistry, CarbonCreditRegistryClient, CreditStatus, - RetirementParams, TradingParams, + CarbonCreditRegistry, CarbonCreditRegistryClient, CreditStatus, RetirementParams, TradingParams, }; // ============ TEST CONTEXT ============ @@ -46,7 +45,8 @@ pub fn setup_with_issuer() -> (TestContext, Address) { standards.push_back(String::from_str(&ctx.env, "Verra")); standards.push_back(String::from_str(&ctx.env, "Gold Standard")); - ctx.client.register_issuer(&issuer, &issuer_name, &standards); + ctx.client + .register_issuer(&issuer, &issuer_name, &standards); (ctx, issuer) } @@ -56,11 +56,7 @@ pub fn setup_with_issuer() -> (TestContext, Address) { /// Issues a basic carbon credit for testing /// Note: Uses incrementing timestamp to ensure unique credit IDs #[allow(dead_code)] -pub fn issue_test_credit( - ctx: &TestContext, - issuer: &Address, - quantity: i128, -) -> BytesN<32> { +pub fn issue_test_credit(ctx: &TestContext, issuer: &Address, quantity: i128) -> BytesN<32> { let project_type = String::from_str(&ctx.env, "Reforestation"); let project_location = String::from_str(&ctx.env, "Brazil"); let verification_standard = String::from_str(&ctx.env, "Verra"); @@ -230,5 +226,8 @@ pub fn assert_contract_stats( assert_eq!(issuer_count, expected_issuer_count, "Issuer count mismatch"); assert_eq!(credit_count, expected_credit_count, "Credit count mismatch"); assert_eq!(total_issued, expected_total_issued, "Total issued mismatch"); - assert_eq!(total_retired, expected_total_retired, "Total retired mismatch"); + assert_eq!( + total_retired, expected_total_retired, + "Total retired mismatch" + ); } diff --git a/soroban/contracts/carbon-credit-registry/src/trading.rs b/soroban/contracts/carbon-credit-registry/src/trading.rs index b76ff80..b131edc 100644 --- a/soroban/contracts/carbon-credit-registry/src/trading.rs +++ b/soroban/contracts/carbon-credit-registry/src/trading.rs @@ -1,42 +1,43 @@ -use soroban_sdk::{Env, BytesN, String, Map, Vec, Bytes}; +use soroban_sdk::{Bytes, BytesN, Env, Map, String, Vec}; use crate::{ - CarbonCredit, CreditStatus, CreditEvent, EventType, DataKey, - TradingParams, RetirementParams, + CarbonCredit, CreditEvent, CreditStatus, DataKey, EventType, RetirementParams, TradingParams, }; /// Trade a carbon credit from one owner to another -pub fn trade_credit( - env: Env, - params: TradingParams, -) { +pub fn trade_credit(env: Env, params: TradingParams) { // Validate that the sender is authorized params.from.require_auth(); // Get the credit - let mut credit: CarbonCredit = env.storage().instance().get(&DataKey::Credit(params.credit_id.clone())) + let mut credit: CarbonCredit = env + .storage() + .instance() + .get(&DataKey::Credit(params.credit_id.clone())) .unwrap(); // Update credit ownership credit.current_owner = params.to.clone(); credit.status = CreditStatus::Traded; - env.storage().instance().set(&DataKey::Credit(params.credit_id.clone()), &credit); + env.storage() + .instance() + .set(&DataKey::Credit(params.credit_id.clone()), &credit); // Record trade event record_trade_event(&env, ¶ms); } /// Retire a carbon credit for carbon offset claims -pub fn retire_credit( - env: Env, - params: RetirementParams, -) { +pub fn retire_credit(env: Env, params: RetirementParams) { // Validate that the owner is authorized params.owner.require_auth(); // Get the credit - let mut credit: CarbonCredit = env.storage().instance().get(&DataKey::Credit(params.credit_id.clone())) + let mut credit: CarbonCredit = env + .storage() + .instance() + .get(&DataKey::Credit(params.credit_id.clone())) .unwrap(); // Update credit status @@ -45,7 +46,9 @@ pub fn retire_credit( credit.status = CreditStatus::Retired; } - env.storage().instance().set(&DataKey::Credit(params.credit_id.clone()), &credit); + env.storage() + .instance() + .set(&DataKey::Credit(params.credit_id.clone()), &credit); // Update contract statistics update_retirement_stats(&env, params.quantity); @@ -55,16 +58,20 @@ pub fn retire_credit( } /// Get trading history for a credit -pub fn get_trading_history( - env: Env, - credit_id: BytesN<32>, -) -> Vec { - let event_count: u32 = env.storage().instance().get(&DataKey::EventCount(credit_id.clone())) +pub fn get_trading_history(env: Env, credit_id: BytesN<32>) -> Vec { + let event_count: u32 = env + .storage() + .instance() + .get(&DataKey::EventCount(credit_id.clone())) .unwrap_or(0); let mut trading_events = Vec::new(&env); for i in 0..event_count { - if let Some(event) = env.storage().instance().get::(&DataKey::CreditEvent(credit_id.clone(), i)) { + if let Some(event) = env + .storage() + .instance() + .get::(&DataKey::CreditEvent(credit_id.clone(), i)) + { match event.event_type { EventType::Trade | EventType::Retirement => { trading_events.push_back(event); @@ -78,16 +85,19 @@ pub fn get_trading_history( } /// Record a trade event -fn record_trade_event( - env: &Env, - params: &TradingParams, -) { - let event_count: u32 = env.storage().instance().get(&DataKey::EventCount(params.credit_id.clone())) +fn record_trade_event(env: &Env, params: &TradingParams) { + let event_count: u32 = env + .storage() + .instance() + .get(&DataKey::EventCount(params.credit_id.clone())) .unwrap_or(0); let mut metadata = Map::new(env); metadata.set(String::from_str(env, "price"), String::from_str(env, "50")); // Simplified for now - metadata.set(String::from_str(env, "payment_token"), String::from_str(env, "token")); + metadata.set( + String::from_str(env, "payment_token"), + String::from_str(env, "token"), + ); let event = CreditEvent { event_type: EventType::Trade, @@ -100,21 +110,33 @@ fn record_trade_event( metadata, }; - env.storage().instance().set(&DataKey::CreditEvent(params.credit_id.clone(), event_count), &event); - env.storage().instance().set(&DataKey::EventCount(params.credit_id.clone()), &(event_count + 1)); + env.storage().instance().set( + &DataKey::CreditEvent(params.credit_id.clone(), event_count), + &event, + ); + env.storage().instance().set( + &DataKey::EventCount(params.credit_id.clone()), + &(event_count + 1), + ); } /// Record a retirement event -fn record_retirement_event( - env: &Env, - params: &RetirementParams, -) { - let event_count: u32 = env.storage().instance().get(&DataKey::EventCount(params.credit_id.clone())) +fn record_retirement_event(env: &Env, params: &RetirementParams) { + let event_count: u32 = env + .storage() + .instance() + .get(&DataKey::EventCount(params.credit_id.clone())) .unwrap_or(0); let mut metadata = Map::new(env); - metadata.set(String::from_str(env, "reason"), params.retirement_reason.clone()); - metadata.set(String::from_str(env, "certificate"), String::from_str(env, "cert")); + metadata.set( + String::from_str(env, "reason"), + params.retirement_reason.clone(), + ); + metadata.set( + String::from_str(env, "certificate"), + String::from_str(env, "cert"), + ); let event = CreditEvent { event_type: EventType::Retirement, @@ -127,18 +149,26 @@ fn record_retirement_event( metadata, }; - env.storage().instance().set(&DataKey::CreditEvent(params.credit_id.clone(), event_count), &event); - env.storage().instance().set(&DataKey::EventCount(params.credit_id.clone()), &(event_count + 1)); + env.storage().instance().set( + &DataKey::CreditEvent(params.credit_id.clone(), event_count), + &event, + ); + env.storage().instance().set( + &DataKey::EventCount(params.credit_id.clone()), + &(event_count + 1), + ); } /// Update retirement statistics -fn update_retirement_stats( - env: &Env, - retired_quantity: i128, -) { - let mut total_retired: i128 = env.storage().instance().get(&DataKey::TotalCreditsRetired) +fn update_retirement_stats(env: &Env, retired_quantity: i128) { + let mut total_retired: i128 = env + .storage() + .instance() + .get(&DataKey::TotalCreditsRetired) .unwrap_or(0); total_retired += retired_quantity; - env.storage().instance().set(&DataKey::TotalCreditsRetired, &total_retired); -} \ No newline at end of file + env.storage() + .instance() + .set(&DataKey::TotalCreditsRetired, &total_retired); +} diff --git a/soroban/contracts/carbon-credit-registry/src/utils.rs b/soroban/contracts/carbon-credit-registry/src/utils.rs index 13507a6..31f1fb7 100644 --- a/soroban/contracts/carbon-credit-registry/src/utils.rs +++ b/soroban/contracts/carbon-credit-registry/src/utils.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Env, Address, BytesN, Bytes}; +use soroban_sdk::{Address, Bytes, BytesN, Env}; /// Generate a unique credit ID based on issuer, verification hash, and timestamp pub fn generate_credit_id( @@ -9,12 +9,12 @@ pub fn generate_credit_id( // Simplified approach - use timestamp and verification hash let timestamp = env.ledger().timestamp(); let timestamp_bytes = timestamp.to_be_bytes(); - + // Create a simple hash from timestamp and verification hash let mut combined = [0u8; 40]; combined[0..8].copy_from_slice(×tamp_bytes); combined[8..40].copy_from_slice(&verification_hash.to_array()); - + let bytes_data = Bytes::from_slice(env, &combined); env.crypto().sha256(&bytes_data).into() -} \ No newline at end of file +}