diff --git a/contracts/teachlink/src/analytics.rs b/contracts/teachlink/src/analytics.rs
index 33c6335..21e2693 100644
--- a/contracts/teachlink/src/analytics.rs
+++ b/contracts/teachlink/src/analytics.rs
@@ -3,7 +3,7 @@
//! Bridge monitoring and analytics for bridge operations, validator performance, and chain metrics.
use crate::errors::BridgeError;
-use crate::storage::{BRIDGE_METRICS, CHAIN_METRICS, DAILY_VOLUMES, CHAIN_VOLUME_INDEX, CHAIN_METRICS_INDEX};
+
use crate::types::{BridgeMetrics, ChainMetrics};
use soroban_sdk::{Address, Bytes, Env, Map, Vec};
@@ -598,3 +598,17 @@ impl AnalyticsManager {
current_time - metrics.last_updated > METRICS_UPDATE_INTERVAL
}
}
+
+impl AnalyticsPort for AnalyticsManager {
+ fn bridge_metrics(env: &Env) -> BridgeMetrics {
+ Self::get_bridge_metrics(env)
+ }
+
+ fn health_score(env: &Env) -> u32 {
+ Self::calculate_health_score(env)
+ }
+
+ fn top_chains_by_volume(env: &Env, max: u32) -> Vec<(u32, i128)> {
+ Self::get_top_chains_by_volume_bounded(env, max)
+ }
+}
diff --git a/contracts/teachlink/src/arbitration.rs b/contracts/teachlink/src/arbitration.rs
index bf2ff0e..42f8838 100644
--- a/contracts/teachlink/src/arbitration.rs
+++ b/contracts/teachlink/src/arbitration.rs
@@ -1,4 +1,5 @@
use crate::errors::EscrowError;
+use crate::interfaces::ArbitrationPort;
use crate::storage::{ARBITRATORS, ESCROWS};
use crate::types::{ArbitratorProfile, Escrow, EscrowStatus};
use soroban_sdk::{Address, Env, Map, String, Vec};
@@ -197,3 +198,22 @@ impl ArbitrationManager {
Ok(())
}
}
+
+
+impl ArbitrationPort for ArbitrationManager {
+ fn pick_arbitrator(env: &Env) -> Result
{
+ Self::pick_arbitrator(env)
+ }
+
+ fn check_stalled(env: &Env, escrow: &Escrow) -> bool {
+ Self::check_stalled_escrow(env, escrow)
+ }
+
+ fn record_resolution(
+ env: &Env,
+ arbitrator: Address,
+ success: bool,
+ ) -> Result<(), EscrowError> {
+ Self::update_reputation(env, arbitrator, success)
+ }
+}
diff --git a/contracts/teachlink/src/audit.rs b/contracts/teachlink/src/audit.rs
index 383321b..57c1f1d 100644
--- a/contracts/teachlink/src/audit.rs
+++ b/contracts/teachlink/src/audit.rs
@@ -5,6 +5,7 @@
use crate::errors::BridgeError;
use crate::events::AuditRecordCreatedEvent;
+use crate::interfaces::AuditPort;
use crate::storage::{AUDIT_COUNTER, AUDIT_RECORDS, COMPLIANCE_REPORTS};
use crate::types::{AuditRecord, ComplianceReport, OperationType};
use soroban_sdk::{Address, Bytes, Env, Map, Vec};
@@ -480,3 +481,19 @@ impl AuditManager {
Ok(cleared_count)
}
}
+
+impl AuditPort for AuditManager {
+ fn create_record(
+ env: &Env,
+ op: OperationType,
+ operator: Address,
+ details: Bytes,
+ tx_hash: Bytes,
+ ) -> Result {
+ Self::create_audit_record(env, op, operator, details, tx_hash)
+ }
+
+ fn get_count(env: &Env) -> u64 {
+ Self::get_audit_count(env)
+ }
+}
diff --git a/contracts/teachlink/src/backup.rs b/contracts/teachlink/src/backup.rs
index 60e2676..782b92a 100644
--- a/contracts/teachlink/src/backup.rs
+++ b/contracts/teachlink/src/backup.rs
@@ -4,8 +4,8 @@
//! and audit trails for compliance. Off-chain systems use events to replicate
//! data; this module records manifests, verification, and RTO recovery metrics.
-use crate::audit::AuditManager;
use crate::errors::BridgeError;
+use crate::interfaces::AuditPort;
use crate::events::{BackupCreatedEvent, BackupVerifiedEvent, RecoveryExecutedEvent};
use crate::storage::{
BACKUP_COUNTER, BACKUP_MANIFESTS, BACKUP_SCHEDULES, BACKUP_SCHED_CNT, RECOVERY_CNT,
@@ -19,17 +19,7 @@ pub struct BackupManager;
impl BackupManager {
/// Create a backup manifest (authorized caller). Integrity hash is supplied by off-chain.
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // create_backup(...);
- /// ```
- pub fn create_backup(
+
env: &Env,
creator: Address,
integrity_hash: Bytes,
@@ -73,7 +63,7 @@ impl BackupManager {
.publish(env);
let details = Bytes::from_slice(env, &counter.to_be_bytes());
- AuditManager::create_audit_record(
+ Au::create_record(
env,
OperationType::BackupCreated,
creator,
@@ -109,17 +99,7 @@ impl BackupManager {
}
/// Verify backup integrity (compare expected hash to stored). Emit event and audit.
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // verify_backup(...);
- /// ```
- pub fn verify_backup(
+
env: &Env,
backup_id: u64,
verifier: Address,
@@ -140,7 +120,7 @@ impl BackupManager {
.publish(env);
let details = Bytes::from_slice(env, &[if valid { 1u8 } else { 0u8 }]);
- AuditManager::create_audit_record(
+ Au::create_record(
env,
OperationType::BackupVerified,
verifier,
@@ -232,17 +212,7 @@ impl BackupManager {
}
/// Record a recovery execution (RTO tracking and audit trail)
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // record_recovery(...);
- /// ```
- pub fn record_recovery(
+
env: &Env,
backup_id: u64,
executed_by: Address,
@@ -286,7 +256,7 @@ impl BackupManager {
.publish(env);
let details = Bytes::from_slice(env, &recovery_duration_secs.to_be_bytes());
- AuditManager::create_audit_record(
+ Au::create_record(
env,
OperationType::RecoveryExecuted,
executed_by,
diff --git a/contracts/teachlink/src/escrow.rs b/contracts/teachlink/src/escrow.rs
index bf795b5..de8d744 100644
--- a/contracts/teachlink/src/escrow.rs
+++ b/contracts/teachlink/src/escrow.rs
@@ -1,32 +1,18 @@
-use crate::arbitration::ArbitrationManager;
use crate::errors::EscrowError;
-use crate::escrow_analytics::EscrowAnalyticsManager;
use crate::events::{
EscrowApprovedEvent, EscrowCreatedEvent, EscrowDisputedEvent, EscrowRefundedEvent,
EscrowReleasedEvent, EscrowResolvedEvent,
};
-use crate::insurance::InsuranceManager;
+use crate::interfaces::{ArbitrationPort, EscrowObserver, InsurancePort};
use crate::storage::{ESCROWS, ESCROW_COUNT};
use crate::types::{DisputeOutcome, Escrow, EscrowApprovalKey, EscrowSigner, EscrowStatus};
use crate::validation::EscrowValidator;
-use soroban_sdk::{symbol_short, vec, Address, Bytes, Env, IntoVal, Map, Vec};
+use soroban_sdk::{symbol_short, vec, Address, Bytes, Env, IntoVal, Vec};
pub struct EscrowManager;
impl EscrowManager {
- /// Standard API for create_escrow
- ///
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // create_escrow(...);
- /// ```
- pub fn create_escrow(
+
env: &Env,
depositor: Address,
beneficiary: Address,
@@ -64,15 +50,15 @@ impl EscrowManager {
],
);
- // Process insurance premium
+ // Process insurance premium via injected InsurancePort
if env
.storage()
.instance()
.has(&crate::storage::INSURANCE_POOL)
{
- let premium = InsuranceManager::calculate_premium(env, amount);
+ let premium = I::calculate_premium(env, amount);
if premium > 0 {
- InsuranceManager::pay_premium_internal(env, depositor.clone(), premium)?;
+ I::pay_premium(env, depositor.clone(), premium)?;
}
}
@@ -98,11 +84,11 @@ impl EscrowManager {
dispute_reason: None,
};
- let mut escrows = Self::load_escrows(env);
- escrows.set(escrow_count, escrow.clone());
- env.storage().instance().set(&ESCROWS, &escrows);
+ env.storage()
+ .persistent()
+ .set(&(ESCROWS, escrow_count), &escrow);
- EscrowAnalyticsManager::update_creation(env, amount);
+ Obs::on_created(env, amount);
EscrowCreatedEvent { escrow }.publish(env);
@@ -283,19 +269,7 @@ impl EscrowManager {
Ok(())
}
- /// Standard API for dispute
- ///
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // dispute(...);
- /// ```
- pub fn dispute(
+
env: &Env,
escrow_id: u64,
disputer: Address,
@@ -312,14 +286,14 @@ impl EscrowManager {
// If arbitrator is default (zero address), pick a professional one
if Self::arbitrator_is_empty(env, &escrow.arbitrator) {
- escrow.arbitrator = ArbitrationManager::pick_arbitrator(env)?;
+ escrow.arbitrator = A::pick_arbitrator(env)?;
}
escrow.status = EscrowStatus::Disputed;
escrow.dispute_reason = Some(reason.clone());
Self::save_escrow(env, escrow_id, escrow);
- EscrowAnalyticsManager::update_dispute(env);
+ Obs::on_disputed(env);
EscrowDisputedEvent {
escrow_id,
@@ -331,19 +305,7 @@ impl EscrowManager {
Ok(())
}
- /// Standard API for resolve
- ///
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // resolve(...);
- /// ```
- pub fn resolve(
+
env: &Env,
escrow_id: u64,
arbitrator: Address,
@@ -379,12 +341,12 @@ impl EscrowManager {
escrow.status = new_status.clone();
- ArbitrationManager::update_reputation(env, arbitrator, true)?;
+ A::record_resolution(env, arbitrator, true)?;
let now = env.ledger().timestamp();
let created_at = escrow.created_at;
Self::save_escrow(env, escrow_id, escrow);
- EscrowAnalyticsManager::update_resolution(env, now - created_at);
+ Obs::on_resolved(env, now - created_at);
EscrowResolvedEvent {
escrow_id,
@@ -396,28 +358,12 @@ impl EscrowManager {
Ok(())
}
- /// Standard API for auto_check_dispute
- ///
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Returns
- ///
- /// * The return value of the function.
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // auto_check_dispute(...);
- /// ```
- pub fn auto_check_dispute(env: &Env, escrow_id: u64) -> Result<(), EscrowError> {
+
let mut escrow = Self::load_escrow(env, escrow_id)?;
- if ArbitrationManager::check_stalled_escrow(env, &escrow) {
+ if A::check_stalled(env, &escrow) {
escrow.status = EscrowStatus::Disputed;
escrow.dispute_reason = Some(Bytes::from_slice(env, b"Automated stall detection"));
- escrow.arbitrator = ArbitrationManager::pick_arbitrator(env)?;
+ escrow.arbitrator = A::pick_arbitrator(env)?;
Self::save_escrow(env, escrow_id, escrow);
EscrowDisputedEvent {
@@ -449,7 +395,7 @@ impl EscrowManager {
/// // get_escrow(...);
/// ```
pub fn get_escrow(env: &Env, escrow_id: u64) -> Option {
- Self::load_escrows(env).get(escrow_id)
+ env.storage().persistent().get(&(ESCROWS, escrow_id))
}
/// Standard API for get_escrow_count
@@ -520,22 +466,17 @@ impl EscrowManager {
*arbitrator == env.current_contract_address()
}
- fn load_escrows(env: &Env) -> Map {
- env.storage()
- .instance()
- .get(&ESCROWS)
- .unwrap_or_else(|| Map::new(env))
- }
-
fn load_escrow(env: &Env, escrow_id: u64) -> Result {
- let escrows = Self::load_escrows(env);
- escrows.get(escrow_id).ok_or(EscrowError::EscrowNotFound)
+ env.storage()
+ .persistent()
+ .get(&(ESCROWS, escrow_id))
+ .ok_or(EscrowError::EscrowNotFound)
}
fn save_escrow(env: &Env, escrow_id: u64, escrow: Escrow) {
- let mut escrows = Self::load_escrows(env);
- escrows.set(escrow_id, escrow);
- env.storage().instance().set(&ESCROWS, &escrows);
+ env.storage()
+ .persistent()
+ .set(&(ESCROWS, escrow_id), &escrow);
}
fn transfer_from_contract(env: &Env, token: &Address, to: &Address, amount: i128) {
diff --git a/contracts/teachlink/src/escrow_analytics.rs b/contracts/teachlink/src/escrow_analytics.rs
index 295f0ea..2e9e3c9 100644
--- a/contracts/teachlink/src/escrow_analytics.rs
+++ b/contracts/teachlink/src/escrow_analytics.rs
@@ -1,3 +1,4 @@
+use crate::interfaces::{EscrowMetricsPort, EscrowObserver};
use crate::storage::ESCROW_ANALYTICS;
use crate::types::EscrowMetrics;
use soroban_sdk::{Env, Map};
@@ -99,3 +100,23 @@ impl EscrowAnalyticsManager {
})
}
}
+
+impl EscrowObserver for EscrowAnalyticsManager {
+ fn on_created(env: &Env, amount: i128) {
+ Self::update_creation(env, amount);
+ }
+
+ fn on_disputed(env: &Env) {
+ Self::update_dispute(env);
+ }
+
+ fn on_resolved(env: &Env, duration: u64) {
+ Self::update_resolution(env, duration);
+ }
+}
+
+impl EscrowMetricsPort for EscrowAnalyticsManager {
+ fn get_metrics(env: &Env) -> EscrowMetrics {
+ Self::get_metrics(env)
+ }
+}
diff --git a/contracts/teachlink/src/insurance.rs b/contracts/teachlink/src/insurance.rs
index 4cb3367..67153dd 100644
--- a/contracts/teachlink/src/insurance.rs
+++ b/contracts/teachlink/src/insurance.rs
@@ -1,4 +1,5 @@
use crate::errors::EscrowError;
+use crate::interfaces::InsurancePort;
use crate::storage::INSURANCE_POOL;
use crate::types::InsurancePool;
#[cfg(test)]
@@ -236,3 +237,13 @@ impl InsuranceManager {
10
}
}
+
+impl InsurancePort for InsuranceManager {
+ fn calculate_premium(env: &Env, amount: i128) -> i128 {
+ Self::calculate_premium(env, amount)
+ }
+
+ fn pay_premium(env: &Env, payer: Address, amount: i128) -> Result<(), EscrowError> {
+ Self::pay_premium_internal(env, payer, amount)
+ }
+}
diff --git a/contracts/teachlink/src/interfaces.rs b/contracts/teachlink/src/interfaces.rs
new file mode 100644
index 0000000..901a5ec
--- /dev/null
+++ b/contracts/teachlink/src/interfaces.rs
@@ -0,0 +1,141 @@
+//! Dependency injection interfaces for inter-module communication.
+//!
+//! Each trait defines the surface that a calling module requires from a
+//! collaborating module. The concrete implementations live in their
+//! respective modules; test code can substitute lightweight mocks.
+//!
+//! # Coupling map (before this PR)
+//! ```text
+//! escrow → arbitration, insurance, escrow_analytics
+//! tokenization → provenance
+//! performance → analytics
+//! reporting → analytics, audit, escrow_analytics
+//! backup → audit
+//! ```
+//!
+//! After this PR every arrow is mediated by a trait defined here, so each
+//! module can be compiled and tested with a mock collaborator.
+
+use soroban_sdk::{Address, Bytes, Env, Vec};
+
+use crate::errors::{BridgeError, EscrowError};
+use crate::types::{
+ BridgeMetrics, Escrow, EscrowMetrics, OperationType, ProvenanceRecord, TransferType,
+};
+
+// ---------------------------------------------------------------------------
+// Arbitration port (used by escrow)
+// ---------------------------------------------------------------------------
+
+/// Abstraction over the arbitration module used by `EscrowManager`.
+pub trait ArbitrationPort {
+ /// Select an active arbitrator from the registry.
+ fn pick_arbitrator(env: &Env) -> Result;
+
+ /// Return `true` when an escrow has been pending without approvals past
+ /// the stall timeout.
+ fn check_stalled(env: &Env, escrow: &Escrow) -> bool;
+
+ /// Update the arbitrator's on-chain reputation score.
+ fn record_resolution(
+ env: &Env,
+ arbitrator: Address,
+ success: bool,
+ ) -> Result<(), EscrowError>;
+}
+
+// ---------------------------------------------------------------------------
+// Insurance port (used by escrow)
+// ---------------------------------------------------------------------------
+
+/// Abstraction over the insurance module used by `EscrowManager`.
+pub trait InsurancePort {
+ /// Compute the insurance premium for a given escrow amount.
+ fn calculate_premium(env: &Env, amount: i128) -> i128;
+
+ /// Transfer the premium from `payer` to the insurance pool.
+ fn pay_premium(env: &Env, payer: Address, amount: i128) -> Result<(), EscrowError>;
+}
+
+// ---------------------------------------------------------------------------
+// Escrow observer (used by escrow)
+// ---------------------------------------------------------------------------
+
+/// Callback interface for escrow lifecycle events consumed by analytics.
+pub trait EscrowObserver {
+ /// Called when a new escrow is created.
+ fn on_created(env: &Env, amount: i128);
+
+ /// Called when an escrow enters the Disputed state.
+ fn on_disputed(env: &Env);
+
+ /// Called when an escrow is resolved; `duration` is seconds since creation.
+ fn on_resolved(env: &Env, duration: u64);
+}
+
+// ---------------------------------------------------------------------------
+// Analytics port (used by performance and reporting)
+// ---------------------------------------------------------------------------
+
+/// Abstraction over the analytics module used by `PerformanceManager` and
+/// `ReportingManager`.
+pub trait AnalyticsPort {
+ /// Return the stored bridge metrics.
+ fn bridge_metrics(env: &Env) -> BridgeMetrics;
+
+ /// Compute a 0-10000 basis-point bridge health score.
+ fn health_score(env: &Env) -> u32;
+
+ /// Return up to `max` (chain_id, volume) pairs ordered by volume desc.
+ fn top_chains_by_volume(env: &Env, max: u32) -> Vec<(u32, i128)>;
+}
+
+// ---------------------------------------------------------------------------
+// Escrow metrics port (used by reporting)
+// ---------------------------------------------------------------------------
+
+/// Abstraction over the escrow-analytics module used by `ReportingManager`.
+pub trait EscrowMetricsPort {
+ /// Return the current aggregated escrow metrics.
+ fn get_metrics(env: &Env) -> EscrowMetrics;
+}
+
+// ---------------------------------------------------------------------------
+// Audit port (used by backup and reporting)
+// ---------------------------------------------------------------------------
+
+/// Abstraction over the audit module used by `BackupManager` and
+/// `ReportingManager`.
+pub trait AuditPort {
+ /// Append an audit record and return its id.
+ fn create_record(
+ env: &Env,
+ op: OperationType,
+ operator: Address,
+ details: Bytes,
+ tx_hash: Bytes,
+ ) -> Result;
+
+ /// Return the total number of audit records stored.
+ fn get_count(env: &Env) -> u64;
+}
+
+// ---------------------------------------------------------------------------
+// Provenance port (used by tokenization)
+// ---------------------------------------------------------------------------
+
+/// Abstraction over the provenance module used by `ContentTokenization`.
+pub trait ProvenancePort {
+ /// Append a transfer record to the provenance chain for `token_id`.
+ fn record_transfer(
+ env: &Env,
+ token_id: u64,
+ from: Option,
+ to: Address,
+ transfer_type: TransferType,
+ notes: Option,
+ );
+
+ /// Return all provenance records for `token_id`.
+ fn get_history(env: &Env, token_id: u64) -> Vec;
+}
diff --git a/contracts/teachlink/src/lib.rs b/contracts/teachlink/src/lib.rs
index a6c5302..44bb99b 100644
--- a/contracts/teachlink/src/lib.rs
+++ b/contracts/teachlink/src/lib.rs
@@ -1,22 +1,6 @@
//! TeachLink Soroban smart contract — entry point.
//!
-//! This file wires the public contract interface to focused sub-modules:
-//! - [`constants`] — compile-time configuration values
-//! - [`errors`] — error enum and panic helper
-//! - [`types`] — shared data types (`BridgeConfig`)
-//! - [`storage`] — storage keys and low-level helpers
-//! - [`validation`] — input validation guards
-//! - [`bridge`] — bridge-out and chain management
-//! - [`oracle`] — oracle price feed management
-
-#![cfg_attr(not(test), no_std)]
-
-pub mod bridge;
-pub mod constants;
-pub mod errors;
-pub mod oracle;
-pub mod storage;
-pub mod types;
+
pub mod validation;
use soroban_sdk::{contract, contractimpl, Address, Bytes, Env, Symbol, Vec};
@@ -1703,7 +1687,9 @@ impl TeachLinkBridge {
/// // get_cached_bridge_summary(...);
/// ```
pub fn get_cached_bridge_summary(env: Env) -> Result {
- performance::PerformanceManager::get_or_compute_summary(&env)
+ performance::PerformanceManager::get_or_compute_summary::(
+ &env,
+ )
}
/// Force recompute and cache bridge summary. Emits PerfMetricsComputedEvent.
@@ -1722,7 +1708,9 @@ impl TeachLinkBridge {
/// // compute_and_cache_bridge_summary(...);
/// ```
pub fn compute_and_cache_bridge_summary(env: Env) -> Result {
- performance::PerformanceManager::compute_and_cache_summary(&env)
+ performance::PerformanceManager::compute_and_cache_summary::(
+ &env,
+ )
}
/// Invalidate performance cache (admin only). Emits PerfCacheInvalidatedEvent.
@@ -1762,7 +1750,11 @@ impl TeachLinkBridge {
/// // get_dashboard_analytics(...);
/// ```
pub fn get_dashboard_analytics(env: Env) -> DashboardAnalytics {
- reporting::ReportingManager::get_dashboard_analytics(&env)
+ reporting::ReportingManager::get_dashboard_analytics::<
+ analytics::AnalyticsManager,
+ audit::AuditManager,
+ escrow_analytics::EscrowAnalyticsManager,
+ >(&env)
}
/// Create a report template
@@ -1879,13 +1871,11 @@ impl TeachLinkBridge {
period_start: u64,
period_end: u64,
) -> Result {
- reporting::ReportingManager::generate_report_snapshot(
- &env,
- generator,
- template_id,
- period_start,
- period_end,
- )
+ reporting::ReportingManager::generate_report_snapshot::<
+ analytics::AnalyticsManager,
+ audit::AuditManager,
+ escrow_analytics::EscrowAnalyticsManager,
+ >(&env, generator, template_id, period_start, period_end)
}
/// Get report snapshot by id
@@ -2040,7 +2030,10 @@ impl TeachLinkBridge {
/// // evaluate_alerts(...);
/// ```
pub fn evaluate_alerts(env: Env) -> Vec {
- reporting::ReportingManager::evaluate_alerts(&env)
+ reporting::ReportingManager::evaluate_alerts::<
+ analytics::AnalyticsManager,
+ escrow_analytics::EscrowAnalyticsManager,
+ >(&env)
}
/// Get recent report snapshots
@@ -2082,7 +2075,7 @@ impl TeachLinkBridge {
rto_tier: RtoTier,
encryption_ref: u64,
) -> Result {
- backup::BackupManager::create_backup(
+ backup::BackupManager::create_backup::(
&env,
creator,
integrity_hash,
@@ -2127,7 +2120,12 @@ impl TeachLinkBridge {
verifier: Address,
expected_hash: Bytes,
) -> Result {
- backup::BackupManager::verify_backup(&env, backup_id, verifier, expected_hash)
+ backup::BackupManager::verify_backup::(
+ &env,
+ backup_id,
+ verifier,
+ expected_hash,
+ )
}
/// Schedule automated backup
@@ -2188,7 +2186,7 @@ impl TeachLinkBridge {
recovery_duration_secs: u64,
success: bool,
) -> Result {
- backup::BackupManager::record_recovery(
+ backup::BackupManager::record_recovery::(
&env,
backup_id,
executed_by,
@@ -2638,7 +2636,11 @@ impl TeachLinkBridge {
/// // create_escrow(...);
/// ```
pub fn create_escrow(env: Env, params: EscrowParameters) -> Result {
- escrow::EscrowManager::create_escrow(
+ escrow::EscrowManager::create_escrow::<
+ arbitration::ArbitrationManager,
+ insurance::InsuranceManager,
+ escrow_analytics::EscrowAnalyticsManager,
+ >(
&env,
params.depositor,
params.beneficiary,
@@ -2745,7 +2747,10 @@ impl TeachLinkBridge {
disputer: Address,
reason: Bytes,
) -> Result<(), EscrowError> {
- escrow::EscrowManager::dispute(&env, escrow_id, disputer, reason)
+ escrow::EscrowManager::dispute::<
+ arbitration::ArbitrationManager,
+ escrow_analytics::EscrowAnalyticsManager,
+ >(&env, escrow_id, disputer, reason)
}
/// Automatically check if an escrow has stalled and trigger a dispute
@@ -2764,7 +2769,10 @@ impl TeachLinkBridge {
/// // auto_check_escrow_dispute(...);
/// ```
pub fn auto_check_escrow_dispute(env: Env, escrow_id: u64) -> Result<(), EscrowError> {
- escrow::EscrowManager::auto_check_dispute(&env, escrow_id)
+ escrow::EscrowManager::auto_check_dispute::(
+ &env,
+ escrow_id,
+ )
}
/// Resolve a dispute as the arbitrator
@@ -2784,7 +2792,10 @@ impl TeachLinkBridge {
arbitrator: Address,
outcome: DisputeOutcome,
) -> Result<(), EscrowError> {
- escrow::EscrowManager::resolve(&env, escrow_id, arbitrator, outcome)
+ escrow::EscrowManager::resolve::<
+ arbitration::ArbitrationManager,
+ escrow_analytics::EscrowAnalyticsManager,
+ >(&env, escrow_id, arbitrator, outcome)
}
// ========== Arbitration Management Functions ==========
@@ -3189,7 +3200,9 @@ impl TeachLinkBridge {
token_id: u64,
notes: Option,
) {
- tokenization::ContentTokenization::transfer(&env, from, to, token_id, notes);
+ tokenization::ContentTokenization::transfer::(
+ &env, from, to, token_id, notes,
+ );
}
/// Get a content token by ID
@@ -3439,7 +3452,9 @@ impl TeachLinkBridge {
/// ```
#[must_use]
pub fn get_content_all_owners(env: &Env, token_id: u64) -> Vec {
- tokenization::ContentTokenization::get_all_owners(env, token_id)
+ tokenization::ContentTokenization::get_all_owners::(
+ env, token_id,
+ )
}
// ========== Notification System Functions ==========
diff --git a/contracts/teachlink/src/performance.rs b/contracts/teachlink/src/performance.rs
index f96fa39..86a4b06 100644
--- a/contracts/teachlink/src/performance.rs
+++ b/contracts/teachlink/src/performance.rs
@@ -4,8 +4,8 @@
//! TTL-based freshness and admin-triggered invalidation to reduce gas for
//! repeated read-heavy calls.
-use crate::analytics;
use crate::errors::BridgeError;
+use crate::interfaces::AnalyticsPort;
use crate::events::PerfCacheInvalidatedEvent;
use crate::events::PerfMetricsComputedEvent;
use crate::storage::{PERF_CACHE, PERF_TS};
@@ -65,24 +65,7 @@ impl PerformanceManager {
}
/// Computes bridge summary (health score + top chains), writes cache, emits event.
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Returns
- ///
- /// * The return value of the function.
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // compute_and_cache_summary(...);
- /// ```
- pub fn compute_and_cache_summary(env: &Env) -> Result {
- let health_score = analytics::AnalyticsManager::calculate_health_score(env);
- let top_chains =
- analytics::AnalyticsManager::get_top_chains_by_volume_bounded(env, MAX_TOP_CHAINS);
+
let computed_at = env.ledger().timestamp();
let summary = CachedBridgeSummary {
health_score,
@@ -100,25 +83,11 @@ impl PerformanceManager {
}
/// Returns cached summary if fresh; otherwise computes, caches, and returns.
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Returns
- ///
- /// * The return value of the function.
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // get_or_compute_summary(...);
- /// ```
- pub fn get_or_compute_summary(env: &Env) -> Result {
+
if let Some(cached) = Self::get_cached_summary(env) {
return Ok(cached);
}
- Self::compute_and_cache_summary(env)
+ Self::compute_and_cache_summary::(env)
}
/// Invalidates performance cache (admin only). Emits PerfCacheInvalidatedEvent.
diff --git a/contracts/teachlink/src/provenance.rs b/contracts/teachlink/src/provenance.rs
index 223bfdf..0e9ae27 100644
--- a/contracts/teachlink/src/provenance.rs
+++ b/contracts/teachlink/src/provenance.rs
@@ -1,6 +1,7 @@
use soroban_sdk::{Address, Bytes, Env, Vec};
use crate::events::ProvenanceRecordedEvent;
+use crate::interfaces::ProvenancePort;
use crate::storage::PROVENANCE;
use crate::types::{ProvenanceRecord, TransferType};
@@ -233,3 +234,20 @@ impl ProvenanceTracker {
owners
}
}
+
+impl ProvenancePort for ProvenanceTracker {
+ fn record_transfer(
+ env: &Env,
+ token_id: u64,
+ from: Option,
+ to: Address,
+ transfer_type: TransferType,
+ notes: Option,
+ ) {
+ Self::record_transfer(env, token_id, from, to, transfer_type, notes);
+ }
+
+ fn get_history(env: &Env, token_id: u64) -> Vec {
+ Self::get_provenance(env, token_id)
+ }
+}
diff --git a/contracts/teachlink/src/reporting.rs b/contracts/teachlink/src/reporting.rs
index 5913258..2f509bc 100644
--- a/contracts/teachlink/src/reporting.rs
+++ b/contracts/teachlink/src/reporting.rs
@@ -4,10 +4,8 @@
//! collaboration comments, alert rules, and dashboard-ready aggregate analytics
//! for visualization.
-use crate::analytics::AnalyticsManager;
-use crate::audit::AuditManager;
use crate::errors::BridgeError;
-use crate::escrow_analytics::EscrowAnalyticsManager;
+use crate::interfaces::{AnalyticsPort, AuditPort, EscrowMetricsPort};
use crate::events::{
AlertTriggeredEvent, ReportCommentAddedEvent, ReportGeneratedEvent, ReportScheduledEvent,
};
@@ -195,17 +193,7 @@ impl ReportingManager {
}
/// Generate a report snapshot (stores result, emits event)
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // generate_report_snapshot(...);
- /// ```
- pub fn generate_report_snapshot(
+
env: &Env,
generator: Address,
template_id: u64,
@@ -217,7 +205,7 @@ impl ReportingManager {
let template =
Self::get_report_template(env, template_id).ok_or(BridgeError::InvalidInput)?;
- let analytics = Self::get_dashboard_analytics(env);
+ let analytics = Self::get_dashboard_analytics::(env);
// Summary stored as placeholder; full data available via get_dashboard_analytics
let _ = analytics;
let summary = Bytes::new(env);
@@ -529,30 +517,16 @@ impl ReportingManager {
}
/// Evaluate alert rules and emit AlertTriggeredEvent if any threshold is breached
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Returns
- ///
- /// * The return value of the function.
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // evaluate_alerts(...);
- /// ```
- pub fn evaluate_alerts(env: &Env) -> Vec {
+
let rules: Map = env
.storage()
.instance()
.get(&ALERT_RULES)
.unwrap_or_else(|| Map::new(env));
- let bridge_metrics = AnalyticsManager::get_bridge_metrics(env);
- let health = AnalyticsManager::calculate_health_score(env);
- let escrow_metrics = EscrowAnalyticsManager::get_metrics(env);
+ let bridge_metrics = An::bridge_metrics(env);
+ let health = An::health_score(env);
+ let escrow_metrics = Em::get_metrics(env);
let mut triggered = Vec::new(env);
for (rule_id, rule) in rules.iter() {
@@ -604,25 +578,7 @@ impl ReportingManager {
}
/// Get dashboard-ready aggregate analytics for visualizations
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Returns
- ///
- /// * The return value of the function.
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // get_dashboard_analytics(...);
- /// ```
- pub fn get_dashboard_analytics(env: &Env) -> DashboardAnalytics {
- let bridge_metrics = AnalyticsManager::get_bridge_metrics(env);
- let health = AnalyticsManager::calculate_health_score(env);
- let escrow_metrics = EscrowAnalyticsManager::get_metrics(env);
- let audit_count = AuditManager::get_audit_count(env);
+
let compliance_count: u32 = 0; // Could be extended to count ComplianceReports if stored by id range
diff --git a/contracts/teachlink/src/rewards.rs b/contracts/teachlink/src/rewards.rs
index f0493ce..79160f4 100644
--- a/contracts/teachlink/src/rewards.rs
+++ b/contracts/teachlink/src/rewards.rs
@@ -6,7 +6,7 @@ use crate::storage::{
use crate::types::{RewardRate, UserReward};
use crate::validation::RewardsValidator;
-use soroban_sdk::{symbol_short, vec, Address, Env, IntoVal, Map, String};
+use soroban_sdk::{symbol_short, vec, Address, Env, IntoVal, String};
pub struct Rewards;
@@ -36,12 +36,6 @@ impl Rewards {
env.storage().instance().set(&REWARD_POOL, &0i128);
env.storage().instance().set(&TOTAL_REWARDS_ISSUED, &0i128);
- let reward_rates: Map = Map::new(env);
- env.storage().instance().set(&REWARD_RATES, &reward_rates);
-
- let user_rewards: Map = Map::new(env);
- env.storage().instance().set(&USER_REWARDS, &user_rewards);
-
Ok(())
}
@@ -124,25 +118,24 @@ impl Rewards {
return Err(RewardsError::InsufficientRewardPoolBalance);
}
- let mut user_rewards: Map = env
+ let mut user_reward: UserReward = env
.storage()
- .instance()
- .get(&USER_REWARDS)
- .unwrap_or_else(|| Map::new(env));
-
- let mut user_reward = user_rewards.get(recipient.clone()).unwrap_or(UserReward {
- user: recipient.clone(),
- total_earned: 0,
- claimed: 0,
- pending: 0,
- last_claim_timestamp: 0,
- });
+ .persistent()
+ .get(&(USER_REWARDS, recipient.clone()))
+ .unwrap_or(UserReward {
+ user: recipient.clone(),
+ total_earned: 0,
+ claimed: 0,
+ pending: 0,
+ last_claim_timestamp: 0,
+ });
user_reward.total_earned += amount;
user_reward.pending += amount;
- user_rewards.set(recipient.clone(), user_reward);
- env.storage().instance().set(&USER_REWARDS, &user_rewards);
+ env.storage()
+ .persistent()
+ .set(&(USER_REWARDS, recipient.clone()), &user_reward);
let mut total_issued: i128 = env
.storage()
@@ -188,14 +181,10 @@ impl Rewards {
pub fn claim_rewards(env: &Env, user: Address) -> Result<(), RewardsError> {
user.require_auth();
- let mut user_rewards: Map = env
+ let mut user_reward: UserReward = env
.storage()
- .instance()
- .get(&USER_REWARDS)
- .unwrap_or_else(|| Map::new(env));
-
- let mut user_reward = user_rewards
- .get(user.clone())
+ .persistent()
+ .get(&(USER_REWARDS, user.clone()))
.ok_or(RewardsError::NoRewardsAvailable)?;
if user_reward.pending <= 0 {
@@ -226,8 +215,9 @@ impl Rewards {
user_reward.pending = 0;
user_reward.last_claim_timestamp = env.ledger().timestamp();
- user_rewards.set(user.clone(), user_reward);
- env.storage().instance().set(&USER_REWARDS, &user_rewards);
+ env.storage()
+ .persistent()
+ .set(&(USER_REWARDS, user.clone()), &user_reward);
let new_pool_balance = pool_balance - amount_to_claim;
env.storage()
@@ -272,23 +262,15 @@ impl Rewards {
return Err(RewardsError::RateCannotBeNegative);
}
- let mut reward_rates: Map = env
- .storage()
- .instance()
- .get(&REWARD_RATES)
- .unwrap_or_else(|| Map::new(env));
-
- reward_rates.set(
- reward_type.clone(),
- RewardRate {
+ env.storage().persistent().set(
+ &(REWARD_RATES, reward_type.clone()),
+ &RewardRate {
reward_type,
rate,
enabled,
},
);
- env.storage().instance().set(&REWARD_RATES, &reward_rates);
-
Ok(())
}
@@ -332,12 +314,7 @@ impl Rewards {
/// // get_user_rewards(...);
/// ```
pub fn get_user_rewards(env: &Env, user: Address) -> Option {
- let user_rewards: Map = env
- .storage()
- .instance()
- .get(&USER_REWARDS)
- .unwrap_or_else(|| Map::new(env));
- user_rewards.get(user)
+ env.storage().persistent().get(&(USER_REWARDS, user))
}
/// Standard API for get_reward_pool_balance
@@ -400,12 +377,9 @@ impl Rewards {
/// // get_reward_rate(...);
/// ```
pub fn get_reward_rate(env: &Env, reward_type: String) -> Option {
- let reward_rates: Map = env
- .storage()
- .instance()
- .get(&REWARD_RATES)
- .unwrap_or_else(|| Map::new(env));
- reward_rates.get(reward_type)
+ env.storage()
+ .persistent()
+ .get(&(REWARD_RATES, reward_type))
}
/// Standard API for get_rewards_admin
diff --git a/contracts/teachlink/src/tokenization.rs b/contracts/teachlink/src/tokenization.rs
index 51eea73..9339f1c 100644
--- a/contracts/teachlink/src/tokenization.rs
+++ b/contracts/teachlink/src/tokenization.rs
@@ -1,6 +1,7 @@
use soroban_sdk::{Address, Bytes, Env, Vec};
use crate::events::{ContentMintedEvent, MetadataUpdatedEvent, OwnershipTransferredEvent};
+use crate::interfaces::ProvenancePort;
use crate::storage::{CONTENT_TOKENS, OWNERSHIP, OWNER_TOKENS, TOKEN_COUNTER};
use crate::types::{ContentMetadata, ContentToken, ContentType, TransferType};
@@ -99,17 +100,7 @@ impl ContentTokenization {
}
/// Transfer ownership of a content token
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // transfer(...);
- /// ```
- pub fn transfer(env: &Env, from: Address, to: Address, token_id: u64, notes: Option) {
+
// Get the token
let token: ContentToken = env
.storage()
@@ -171,13 +162,13 @@ impl ContentTokenization {
}
.publish(env);
- // Record provenance (handled by provenance module)
- crate::provenance::ProvenanceTracker::record_transfer(
+ // Record provenance via injected ProvenancePort
+ P::record_transfer(
env,
token_id,
Some(from.clone()),
to.clone(),
- crate::types::TransferType::Transfer,
+ TransferType::Transfer,
notes,
);
}
@@ -240,29 +231,14 @@ impl ContentTokenization {
}
/// Get all owners of a token (current and historical)
- /// # Arguments
- ///
- /// * `env` - The environment (if applicable).
- ///
- /// # Returns
- ///
- /// * The return value of the function.
- ///
- /// # Examples
- ///
- /// ```rust
- /// // Example usage
- /// // get_all_owners(...);
- /// ```
- pub fn get_all_owners(env: &Env, token_id: u64) -> Vec {
+
let mut owners = Vec::new(env);
if let Some(current_owner) = Self::get_owner(env, token_id) {
owners.push_back(current_owner);
}
- // Add historical owners from provenance if needed
- let provenance_records =
- crate::provenance::ProvenanceTracker::get_provenance(env, token_id);
- for record in provenance_records {
+ // Add historical owners from provenance via injected ProvenancePort
+ let history = P::get_history(env, token_id);
+ for record in history {
if !owners.contains(&record.to) {
owners.push_back(record.to);
}
diff --git a/contracts/teachlink/tests/test_gas_optimization.rs b/contracts/teachlink/tests/test_gas_optimization.rs
new file mode 100644
index 0000000..7e1bc4c
--- /dev/null
+++ b/contracts/teachlink/tests/test_gas_optimization.rs
@@ -0,0 +1,321 @@
+#![cfg(test)]
+#![allow(clippy::needless_pass_by_value)]
+
+//! Tests verifying the gas-optimized storage patterns introduced in #161.
+//!
+//! The key optimizations verified here:
+//! - Escrows are stored per-key in persistent storage (not as a single Map blob).
+//! - User rewards and reward rates are stored per-key in persistent storage.
+//!
+//! Correct behaviour after the refactor is verified by:
+//! 1. Multiple escrows can be created and retrieved independently.
+//! 2. Updating one escrow does not affect another.
+//! 3. User rewards are isolated per address.
+//! 4. Reward rates are isolated per reward type.
+
+use soroban_sdk::{
+ contract, contractimpl, symbol_short, testutils::Address as _, Address, Bytes, Env, Map, Vec,
+};
+
+use teachlink_contract::{
+ EscrowParameters, EscrowRole, EscrowSigner, EscrowStatus, TeachLinkBridge,
+ TeachLinkBridgeClient,
+};
+
+// ---------------------------------------------------------------------------
+// Minimal test token contract
+// ---------------------------------------------------------------------------
+
+#[contract]
+pub struct GasTestToken;
+
+#[contractimpl]
+impl GasTestToken {
+ pub fn initialize(env: Env, admin: Address) {
+ env.storage()
+ .instance()
+ .set(&symbol_short!("admin"), &admin);
+ let balances: Map = Map::new(&env);
+ env.storage()
+ .instance()
+ .set(&symbol_short!("bals"), &balances);
+ }
+
+ pub fn balance(env: Env, address: Address) -> i128 {
+ let balances: Map = env
+ .storage()
+ .instance()
+ .get(&symbol_short!("bals"))
+ .unwrap_or_else(|| Map::new(&env));
+ balances.get(address).unwrap_or(0)
+ }
+
+ pub fn mint(env: Env, to: Address, amount: i128) {
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&symbol_short!("admin"))
+ .unwrap();
+ admin.require_auth();
+ let mut balances: Map = env
+ .storage()
+ .instance()
+ .get(&symbol_short!("bals"))
+ .unwrap_or_else(|| Map::new(&env));
+ let cur = balances.get(to.clone()).unwrap_or(0);
+ balances.set(to, cur + amount);
+ env.storage()
+ .instance()
+ .set(&symbol_short!("bals"), &balances);
+ }
+
+ pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
+ from.require_auth();
+ let mut balances: Map = env
+ .storage()
+ .instance()
+ .get(&symbol_short!("bals"))
+ .unwrap_or_else(|| Map::new(&env));
+ let from_bal = balances.get(from.clone()).unwrap_or(0);
+ assert!(from_bal >= amount, "Insufficient balance");
+ let to_bal = balances.get(to.clone()).unwrap_or(0);
+ balances.set(from, from_bal - amount);
+ balances.set(to, to_bal + amount);
+ env.storage()
+ .instance()
+ .set(&symbol_short!("bals"), &balances);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Helper
+// ---------------------------------------------------------------------------
+
+fn make_signer(env: &Env, addr: Address) -> EscrowSigner {
+ EscrowSigner {
+ address: addr,
+ role: EscrowRole::Primary,
+ weight: 1,
+ }
+}
+
+fn setup_env() -> (
+ Env,
+ TeachLinkBridgeClient<'static>,
+ GasTestTokenClient<'static>,
+ Address,
+) {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let contract_id = env.register(TeachLinkBridge, ());
+ let client = TeachLinkBridgeClient::new(&env, &contract_id);
+
+ let token_id = env.register(GasTestToken, ());
+ let token = GasTestTokenClient::new(&env, &token_id);
+
+ let admin = Address::generate(&env);
+ token.initialize(&admin);
+
+ // No insurance for these tests
+ client.initialize_insurance_pool(&token_id, &0);
+
+ (env, client, token, token_id)
+}
+
+// ---------------------------------------------------------------------------
+// Escrow per-key storage tests
+// ---------------------------------------------------------------------------
+
+#[test]
+fn test_two_escrows_stored_independently() {
+ let (env, client, token, token_id) = setup_env();
+
+ let depositor1 = Address::generate(&env);
+ let depositor2 = Address::generate(&env);
+ let beneficiary = Address::generate(&env);
+ let signer1 = Address::generate(&env);
+ let signer2 = Address::generate(&env);
+ let arb = Address::generate(&env);
+
+ token.mint(&depositor1, &1000);
+ token.mint(&depositor2, &1000);
+
+ let mut signers1 = Vec::new(&env);
+ signers1.push_back(make_signer(&env, signer1.clone()));
+
+ let mut signers2 = Vec::new(&env);
+ signers2.push_back(make_signer(&env, signer2.clone()));
+
+ let id1 = client.create_escrow(&EscrowParameters {
+ depositor: depositor1.clone(),
+ beneficiary: beneficiary.clone(),
+ token: token_id.clone(),
+ amount: 300,
+ signers: signers1,
+ threshold: 1,
+ release_time: None,
+ refund_time: None,
+ arbitrator: arb.clone(),
+ });
+
+ let id2 = client.create_escrow(&EscrowParameters {
+ depositor: depositor2.clone(),
+ beneficiary: beneficiary.clone(),
+ token: token_id.clone(),
+ amount: 500,
+ signers: signers2,
+ threshold: 1,
+ release_time: None,
+ refund_time: None,
+ arbitrator: arb.clone(),
+ });
+
+ // IDs are distinct
+ assert_ne!(id1, id2);
+
+ // Each escrow has its own amount stored correctly
+ let e1 = client.get_escrow(&id1).unwrap();
+ let e2 = client.get_escrow(&id2).unwrap();
+ assert_eq!(e1.amount, 300);
+ assert_eq!(e2.amount, 500);
+ assert_eq!(e1.depositor, depositor1);
+ assert_eq!(e2.depositor, depositor2);
+ assert_eq!(e1.status, EscrowStatus::Pending);
+ assert_eq!(e2.status, EscrowStatus::Pending);
+}
+
+#[test]
+fn test_releasing_one_escrow_does_not_affect_another() {
+ let (env, client, token, token_id) = setup_env();
+
+ let depositor = Address::generate(&env);
+ let beneficiary = Address::generate(&env);
+ let signer_a = Address::generate(&env);
+ let signer_b = Address::generate(&env);
+ let arb = Address::generate(&env);
+
+ token.mint(&depositor, &2000);
+
+ let mut signers_a = Vec::new(&env);
+ signers_a.push_back(make_signer(&env, signer_a.clone()));
+
+ let mut signers_b = Vec::new(&env);
+ signers_b.push_back(make_signer(&env, signer_b.clone()));
+
+ let id_a = client.create_escrow(&EscrowParameters {
+ depositor: depositor.clone(),
+ beneficiary: beneficiary.clone(),
+ token: token_id.clone(),
+ amount: 400,
+ signers: signers_a,
+ threshold: 1,
+ release_time: None,
+ refund_time: None,
+ arbitrator: arb.clone(),
+ });
+
+ let id_b = client.create_escrow(&EscrowParameters {
+ depositor: depositor.clone(),
+ beneficiary: beneficiary.clone(),
+ token: token_id.clone(),
+ amount: 600,
+ signers: signers_b,
+ threshold: 1,
+ release_time: None,
+ refund_time: None,
+ arbitrator: arb.clone(),
+ });
+
+ // Approve and release escrow A only
+ client.approve_escrow_release(&id_a, &signer_a);
+ client.release_escrow(&id_a, &signer_a);
+
+ let ea = client.get_escrow(&id_a).unwrap();
+ let eb = client.get_escrow(&id_b).unwrap();
+
+ assert_eq!(ea.status, EscrowStatus::Released);
+ // Escrow B must remain Pending — its storage slot is independent
+ assert_eq!(eb.status, EscrowStatus::Pending);
+}
+
+// ---------------------------------------------------------------------------
+// Rewards per-key storage tests
+// ---------------------------------------------------------------------------
+
+#[test]
+fn test_reward_rates_stored_per_type() {
+ let (env, client, token, token_id) = setup_env();
+
+ let rewards_admin = Address::generate(&env);
+ client.initialize_rewards(&token_id, &rewards_admin);
+
+ let type_a = soroban_sdk::String::from_str(&env, "course_complete");
+ let type_b = soroban_sdk::String::from_str(&env, "contribution");
+
+ client.set_reward_rate(&type_a, &100, &true);
+ client.set_reward_rate(&type_b, &250, &true);
+
+ let rate_a = client.get_reward_rate(&type_a).unwrap();
+ let rate_b = client.get_reward_rate(&type_b).unwrap();
+
+ assert_eq!(rate_a.rate, 100);
+ assert_eq!(rate_b.rate, 250);
+}
+
+#[test]
+fn test_user_rewards_isolated_per_address() {
+ let (env, client, token, token_id) = setup_env();
+
+ let rewards_admin = Address::generate(&env);
+ let user_a = Address::generate(&env);
+ let user_b = Address::generate(&env);
+ let funder = Address::generate(&env);
+
+ client.initialize_rewards(&token_id, &rewards_admin);
+
+ // Fund the pool
+ token.mint(&funder, &10_000);
+ client.fund_reward_pool(&funder, &10_000);
+
+ let rtype = soroban_sdk::String::from_str(&env, "test");
+
+ client.issue_reward(&user_a, &300, &rtype);
+ client.issue_reward(&user_b, &700, &rtype);
+
+ let ra = client.get_user_rewards(&user_a).unwrap();
+ let rb = client.get_user_rewards(&user_b).unwrap();
+
+ // Each user's reward record is stored independently
+ assert_eq!(ra.pending, 300);
+ assert_eq!(rb.pending, 700);
+ assert_eq!(ra.total_earned, 300);
+ assert_eq!(rb.total_earned, 700);
+}
+
+#[test]
+fn test_issuing_reward_does_not_affect_other_user() {
+ let (env, client, token, token_id) = setup_env();
+
+ let rewards_admin = Address::generate(&env);
+ let user_a = Address::generate(&env);
+ let user_b = Address::generate(&env);
+ let funder = Address::generate(&env);
+
+ client.initialize_rewards(&token_id, &rewards_admin);
+ token.mint(&funder, &10_000);
+ client.fund_reward_pool(&funder, &10_000);
+
+ let rtype = soroban_sdk::String::from_str(&env, "test");
+ client.issue_reward(&user_a, &500, &rtype);
+
+ // user_b has no rewards yet
+ let rb = client.get_user_rewards(&user_b);
+ assert!(rb.is_none());
+
+ // Issuing more to user_a does not corrupt user_b
+ client.issue_reward(&user_a, &200, &rtype);
+ let ra = client.get_user_rewards(&user_a).unwrap();
+ assert_eq!(ra.total_earned, 700);
+ assert_eq!(ra.pending, 700);
+}
diff --git a/contracts/teachlink/tests/test_module_isolation.rs b/contracts/teachlink/tests/test_module_isolation.rs
new file mode 100644
index 0000000..84d451e
--- /dev/null
+++ b/contracts/teachlink/tests/test_module_isolation.rs
@@ -0,0 +1,151 @@
+#![cfg(test)]
+#![allow(clippy::needless_pass_by_value)]
+
+//! Tests verifying module isolation via trait-based dependency injection (#152).
+//!
+//! Each test uses a lightweight mock implementation of a port trait instead of
+//! the real concrete type, confirming that calling modules compile and behave
+//! correctly when the collaborator is swapped out.
+
+use soroban_sdk::{testutils::Address as _, Address, Bytes, Env, Vec};
+
+use teachlink_contract::interfaces::{AnalyticsPort, AuditPort, EscrowMetricsPort, EscrowObserver};
+use teachlink_contract::{BridgeError, BridgeMetrics, EscrowMetrics, OperationType};
+
+pub struct MockAnalytics;
+
+impl AnalyticsPort for MockAnalytics {
+ fn bridge_metrics(_env: &Env) -> BridgeMetrics {
+ BridgeMetrics {
+ total_volume: 1_000,
+ total_transactions: 10,
+ active_validators: 3,
+ average_confirmation_time: 5,
+ success_rate: 9_500,
+ last_updated: 0,
+ }
+ }
+
+ fn health_score(_env: &Env) -> u32 {
+ 8_000
+ }
+
+ fn top_chains_by_volume(_env: &Env, _max: u32) -> Vec<(u32, i128)> {
+ Vec::new(_env)
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Mock: AuditPort (records no state — just returns a fixed id)
+// ---------------------------------------------------------------------------
+
+pub struct MockAudit;
+
+impl AuditPort for MockAudit {
+ fn create_record(
+ _env: &Env,
+ _op: OperationType,
+ _operator: Address,
+ _details: Bytes,
+ _tx_hash: Bytes,
+ ) -> Result {
+ Ok(42)
+ }
+
+ fn get_count(_env: &Env) -> u64 {
+ 0
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Mock: EscrowMetricsPort
+// ---------------------------------------------------------------------------
+
+pub struct MockEscrowMetrics;
+
+impl EscrowMetricsPort for MockEscrowMetrics {
+ fn get_metrics(_env: &Env) -> EscrowMetrics {
+ EscrowMetrics {
+ total_escrows: 5,
+ total_volume: 500,
+ total_disputes: 1,
+ total_resolved: 1,
+ average_resolution_time: 3_600,
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Mock: EscrowObserver (no-op)
+// ---------------------------------------------------------------------------
+
+pub struct MockObserver;
+
+impl EscrowObserver for MockObserver {
+ fn on_created(_env: &Env, _amount: i128) {}
+ fn on_disputed(_env: &Env) {}
+ fn on_resolved(_env: &Env, _duration: u64) {}
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+#[test]
+fn analytics_mock_returns_fixed_health_score() {
+ let env = Env::default();
+ let score = MockAnalytics::health_score(&env);
+ assert_eq!(score, 8_000);
+}
+
+#[test]
+fn analytics_mock_bridge_metrics_volume() {
+ let env = Env::default();
+ let metrics = MockAnalytics::bridge_metrics(&env);
+ assert_eq!(metrics.total_volume, 1_000);
+ assert_eq!(metrics.total_transactions, 10);
+}
+
+#[test]
+fn audit_mock_create_record_returns_fixed_id() {
+ let env = Env::default();
+ let addr = Address::generate(&env);
+ let result = MockAudit::create_record(
+ &env,
+ OperationType::BackupCreated,
+ addr,
+ Bytes::new(&env),
+ Bytes::new(&env),
+ );
+ assert_eq!(result.unwrap(), 42);
+}
+
+#[test]
+fn audit_mock_get_count_returns_zero() {
+ let env = Env::default();
+ assert_eq!(MockAudit::get_count(&env), 0);
+}
+
+#[test]
+fn escrow_metrics_mock_returns_expected_values() {
+ let env = Env::default();
+ let metrics = MockEscrowMetrics::get_metrics(&env);
+ assert_eq!(metrics.total_escrows, 5);
+ assert_eq!(metrics.total_disputes, 1);
+}
+
+#[test]
+fn observer_mock_on_created_is_no_op() {
+ let env = Env::default();
+ // Should not panic.
+ MockObserver::on_created(&env, 1_000);
+ MockObserver::on_disputed(&env);
+ MockObserver::on_resolved(&env, 7_200);
+}
+
+#[test]
+fn mock_analytics_top_chains_empty() {
+ let env = Env::default();
+ let chains = MockAnalytics::top_chains_by_volume(&env, 10);
+ assert_eq!(chains.len(), 0);
+}