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 b287343..bd73c25 100644
--- a/soroban/contracts/carbon-credit-registry/src/lib.rs
+++ b/soroban/contracts/carbon-credit-registry/src/lib.rs
@@ -1,13 +1,13 @@
#![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;
pub mod utils;
#[cfg(test)]
-mod test;
+mod tests;
// Core data structures for carbon credits
#[contracttype]
@@ -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/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
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..19c94bc
--- /dev/null
+++ b/soroban/contracts/carbon-credit-registry/src/tests/issuance.rs
@@ -0,0 +1,279 @@
+#![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/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;
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..c3f1781
--- /dev/null
+++ b/soroban/contracts/carbon-credit-registry/src/tests/retirement.rs
@@ -0,0 +1,298 @@
+#![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);
+}
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..140f456
--- /dev/null
+++ b/soroban/contracts/carbon-credit-registry/src/tests/trading.rs
@@ -0,0 +1,201 @@
+#![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);
+}
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..b2d4739
--- /dev/null
+++ b/soroban/contracts/carbon-credit-registry/src/tests/utils.rs
@@ -0,0 +1,233 @@
+#![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"
+ );
+}
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
+}
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;
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");
+}
+
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);
+}
+
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");
+}