diff --git a/contracts/chainsetttle/src/lib.rs b/contracts/chainsetttle/src/lib.rs index f77c6a9..a14c07c 100644 --- a/contracts/chainsetttle/src/lib.rs +++ b/contracts/chainsetttle/src/lib.rs @@ -1,7 +1,7 @@ #![no_std] use soroban_sdk::{ - contract, contractclient, contractimpl, contracttype, token, Address, BytesN, Env, String, Vec, Symbol, + contract, contractimpl, contracterror, contracttype, token, Address, BytesN, Env, String, Vec, Symbol, }; // ============================================================ @@ -283,6 +283,52 @@ pub struct AdminAction { pub params: String, } +// ============================================================ +// STORAGE CONTEXT STRUCTS (batch reads) +// ============================================================ + +/// CreateShipmentCtx consolidates all persistent storage reads for create_shipment. +/// Keys accessed: +/// - DataKey::MaxShipmentValue (instance) +/// - DataKey::AllowedTokens (instance) +/// - DataKey::Blacklisted(Address) (instance) × (buyers + 3 others) +/// - DataKey::MinMilestonePercent (instance) +/// - DataKey::Shipment(shipment_id) (persistent) +/// - DataKey::TotalEscrowed(token) (persistent) +/// - DataKey::ContractStats (instance) +#[derive(Clone)] +pub struct CreateShipmentCtx { + pub max_value: i128, + pub allowed_tokens: Vec
, + pub min_pct: u32, + pub contract_stats: ContractStats, +} + +/// ConfirmMilestoneCtx consolidates all persistent storage reads for confirm_milestone. +/// Keys accessed: +/// - DataKey::Shipment(shipment_id) (persistent) +/// - DataKey::ContractStats (instance) +/// - DataKey::TotalEscrowed(token) (persistent) +#[derive(Clone)] +pub struct ConfirmMilestoneCtx { + pub shipment: Shipment, + pub contract_stats: ContractStats, +} + +/// ResolveDisputeCtx consolidates all persistent storage reads for resolve_dispute. +/// Keys accessed: +/// - DataKey::Shipment(shipment_id) (persistent) +/// - DataKey::DisputeContestedPercent(shipment_id, milestone_index) (persistent) +/// - DataKey::ContractStats (instance) +/// - DataKey::ActiveDisputes (persistent) +#[derive(Clone)] +pub struct ResolveDisputeCtx { + pub shipment: Shipment, + pub partial_contested_percent: Option, + pub contract_stats: ContractStats, + pub active_disputes: Vec, +} + // ============================================================ // STORAGE KEYS // ============================================================ @@ -932,26 +978,18 @@ impl ChainSettleContract { panic!("amount must be greater than zero"); } - // Check max shipment value cap. - let max_value: i128 = env - .storage() - .instance() - .get(&DataKey::MaxShipmentValue) - .unwrap_or(0); - if max_value > 0 && total_amount > max_value { + // Batch read all validation config and stats in a single context fetch. + let ctx = Self::fetch_create_shipment_ctx(&env); + + if ctx.max_value > 0 && total_amount > ctx.max_value { panic!("total amount exceeds maximum shipment value"); } // Enforce token whitelist when non-empty. - let allowed: Vec
= env - .storage() - .instance() - .get(&DataKey::AllowedTokens) - .unwrap_or_else(|| Vec::new(&env)); - if allowed.len() > 0 { + if ctx.allowed_tokens.len() > 0 { let mut found = false; - for i in 0..allowed.len() { - if allowed.get(i).unwrap() == token { + for i in 0..ctx.allowed_tokens.len() { + if ctx.allowed_tokens.get(i).unwrap() == token { found = true; break; } @@ -982,11 +1020,7 @@ impl ChainSettleContract { } } - let min_pct: u32 = env - .storage() - .instance() - .get(&DataKey::MinMilestonePercent) - .unwrap_or(5u32); + let min_pct = ctx.min_pct; let mut total_percent: u32 = 0; for i in 0..milestones.len() { let percent = milestones.get(i).unwrap().payment_percent; @@ -1167,16 +1201,7 @@ impl ChainSettleContract { ); // Update contract stats. - let mut stats: ContractStats = env - .storage() - .instance() - .get(&DataKey::ContractStats) - .unwrap_or(ContractStats { - total_shipments: 0, - total_volume: 0, - total_disputes: 0, - completed_shipments: 0, - }); + let mut stats = ctx.contract_stats; stats.total_shipments += 1; stats.total_volume += total_amount; env.storage() @@ -1627,7 +1652,9 @@ impl ChainSettleContract { env.storage().instance().extend_ttl(100_000, 6_300_000); Self::assert_not_paused(&env); - let mut shipment = Self::get_shipment_internal(&env, &shipment_id); + // Batch read shipment and contract stats in a single context fetch. + let ctx = Self::fetch_confirm_milestone_ctx(&env, &shipment_id); + let mut shipment = ctx.shipment; if shipment.status != ShipmentStatus::Active { panic!("shipment is not active"); @@ -1744,17 +1771,8 @@ impl ChainSettleContract { if Self::all_milestones_done(&shipment) { shipment.status = ShipmentStatus::Completed; - // Update completed shipments stat. - let mut stats: ContractStats = env - .storage() - .instance() - .get(&DataKey::ContractStats) - .unwrap_or(ContractStats { - total_shipments: 0, - total_volume: 0, - total_disputes: 0, - completed_shipments: 0, - }); + // Update completed shipments stat using pre-fetched context. + let mut stats = ctx.contract_stats; stats.completed_shipments += 1; env.storage() .instance() @@ -2402,7 +2420,9 @@ impl ChainSettleContract { ) { Self::assert_not_paused(&env); - let mut shipment = Self::get_shipment_internal(&env, &shipment_id); + // Batch read shipment, dispute status, stats and active disputes in a single context fetch. + let ctx = Self::fetch_resolve_dispute_ctx(&env, &shipment_id, milestone_index); + let mut shipment = ctx.shipment; if shipment.status != ShipmentStatus::Active { panic!("shipment is not active"); @@ -2414,19 +2434,15 @@ impl ChainSettleContract { panic!("milestone is not in disputed status"); } - // Detect whether this is a partial dispute. - let contested_key = - DataKey::DisputeContestedPercent(shipment_id.clone(), milestone_index); - let partial_contested_percent: Option = - env.storage().persistent().get(&contested_key); - let is_partial = partial_contested_percent.is_some(); + // Use pre-fetched partial contested percent from context. + let is_partial = ctx.partial_contested_percent.is_some(); let full_payment = (shipment.total_amount * milestone.payment_percent as i128) / 100; // The "payment" in scope is the portion subject to this resolution: // - full dispute → 100% of milestone value // - partial dispute → contested_percent% of milestone value - let payment = if let Some(cp) = partial_contested_percent { + let payment = if let Some(cp) = ctx.partial_contested_percent { (full_payment * cp as i128) / 100 } else { full_payment @@ -2531,6 +2547,8 @@ impl ChainSettleContract { } // Clean up the partial-dispute record. + let contested_key = + DataKey::DisputeContestedPercent(shipment_id.clone(), milestone_index); if is_partial { env.storage().persistent().remove(&contested_key); } @@ -2543,16 +2561,7 @@ impl ChainSettleContract { if Self::all_milestones_done(&shipment) { shipment.status = ShipmentStatus::Completed; - let mut stats: ContractStats = env - .storage() - .instance() - .get(&DataKey::ContractStats) - .unwrap_or(ContractStats { - total_shipments: 0, - total_volume: 0, - total_disputes: 0, - completed_shipments: 0, - }); + let mut stats = ctx.contract_stats; stats.completed_shipments += 1; env.storage() .instance() @@ -2570,15 +2579,10 @@ impl ChainSettleContract { .persistent() .set(&DataKey::Shipment(shipment_id.clone()), &shipment); - // Remove from active disputes list. - let disputes: Vec = env - .storage() - .persistent() - .get(&DataKey::ActiveDisputes) - .unwrap_or_else(|| Vec::new(&env)); + // Remove from active disputes list using pre-fetched disputes. let mut new_disputes: Vec = Vec::new(&env); - for i in 0..disputes.len() { - let d = disputes.get(i).unwrap(); + for i in 0..ctx.active_disputes.len() { + let d = ctx.active_disputes.get(i).unwrap(); if !(d.shipment_id == shipment_id && d.milestone_index == milestone_index) { new_disputes.push_back(d); } @@ -4038,6 +4042,103 @@ impl ChainSettleContract { Self::set_reputation_internal(env, supplier, &score); } + // ============================================================ + // STORAGE CONTEXT HELPERS (batch reads) + // ============================================================ + + /// Fetch CreateShipmentCtx: consolidates all validation storage reads for create_shipment. + /// Keys accessed: MaxShipmentValue, AllowedTokens, MinMilestonePercent, ContractStats. + fn fetch_create_shipment_ctx(env: &Env) -> CreateShipmentCtx { + let max_value: i128 = env + .storage() + .instance() + .get(&DataKey::MaxShipmentValue) + .unwrap_or(0); + let allowed_tokens: Vec
= env + .storage() + .instance() + .get(&DataKey::AllowedTokens) + .unwrap_or_else(|| Vec::new(env)); + let min_pct: u32 = env + .storage() + .instance() + .get(&DataKey::MinMilestonePercent) + .unwrap_or(5u32); + let contract_stats: ContractStats = env + .storage() + .instance() + .get(&DataKey::ContractStats) + .unwrap_or(ContractStats { + total_shipments: 0, + total_volume: 0, + total_disputes: 0, + completed_shipments: 0, + }); + + CreateShipmentCtx { + max_value, + allowed_tokens, + min_pct, + contract_stats, + } + } + + /// Fetch ConfirmMilestoneCtx: consolidates core storage reads for confirm_milestone. + /// Keys accessed: Shipment, ContractStats. + fn fetch_confirm_milestone_ctx(env: &Env, shipment_id: &String) -> ConfirmMilestoneCtx { + let shipment = Self::get_shipment_internal(env, shipment_id); + let contract_stats: ContractStats = env + .storage() + .instance() + .get(&DataKey::ContractStats) + .unwrap_or(ContractStats { + total_shipments: 0, + total_volume: 0, + total_disputes: 0, + completed_shipments: 0, + }); + + ConfirmMilestoneCtx { + shipment, + contract_stats, + } + } + + /// Fetch ResolveDisputeCtx: consolidates dispute resolution storage reads. + /// Keys accessed: Shipment, DisputeContestedPercent, ContractStats, ActiveDisputes. + fn fetch_resolve_dispute_ctx( + env: &Env, + shipment_id: &String, + milestone_index: u32, + ) -> ResolveDisputeCtx { + let shipment = Self::get_shipment_internal(env, shipment_id); + let contested_key = DataKey::DisputeContestedPercent(shipment_id.clone(), milestone_index); + let partial_contested_percent: Option = + env.storage().persistent().get(&contested_key); + let contract_stats: ContractStats = env + .storage() + .instance() + .get(&DataKey::ContractStats) + .unwrap_or(ContractStats { + total_shipments: 0, + total_volume: 0, + total_disputes: 0, + completed_shipments: 0, + }); + let active_disputes: Vec = env + .storage() + .persistent() + .get(&DataKey::ActiveDisputes) + .unwrap_or_else(|| Vec::new(env)); + + ResolveDisputeCtx { + shipment, + partial_contested_percent, + contract_stats, + active_disputes, + } + } + fn get_shipment_internal(env: &Env, shipment_id: &String) -> Shipment { env.storage() .persistent() @@ -4194,3 +4295,4 @@ mod test_upgrade; mod test_concurrent_disputes; mod test_boundaries; mod test_chaos; +mod test_regression; diff --git a/contracts/chainsetttle/src/storage.rs b/contracts/chainsetttle/src/storage.rs index 19ba957..7926da6 100644 --- a/contracts/chainsetttle/src/storage.rs +++ b/contracts/chainsetttle/src/storage.rs @@ -2,7 +2,7 @@ use soroban_sdk::{contracttype, Address, Env, String, Vec}; use crate::{ constants::{TTL_INITIAL_LEDGERS, TTL_MAX_LEDGERS}, - AuditEntry, CancelPolicy, ContractStats, DisputeEntry, FeeConfig, MultiAdminConfig, Shipment, + AuditEntry, CancelPolicy, ContractStats, DisputeEntry, FeeConfig, MultiAdminConfig, ReputationScore, Shipment, ShipmentStatus, };