Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions audit.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[advisories]
ignore = ["RUSTSEC-2026-0104", "RUSTSEC-2026-0002"]
132 changes: 132 additions & 0 deletions contracts/analytics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ mod propchain_analytics {
pub volume_change_percentage: i32,
}

/// Maximum number of items allowed in a batch operation to stay within gas limits.
const MAX_BATCH_SIZE: usize = 20;

/// Data structure for a single metric update used in batch operations
#[derive(
Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout,
)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub struct MetricUpdate {
pub average_price: u128,
pub total_volume: u128,
pub properties_listed: u64,
}

/// User behavior analytics for a specific account.
#[derive(
Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout,
Expand Down Expand Up @@ -137,6 +151,7 @@ mod propchain_analytics {
NoPendingRotation,
RotationUnauthorized,
RequestExpired,
BatchSizeExceeded,
}

// ── Admin Key Rotation Events (Issue #496) ────────────────────────────────
Expand Down Expand Up @@ -164,6 +179,11 @@ mod propchain_analytics {
old_admin: AccountId,
cancelled_by: AccountId,
}
#[ink(event)]
pub struct BatchMetricsUpdated {
#[ink(topic)]
count: u64,
}

impl AnalyticsDashboard {
#[ink(constructor)]
Expand Down Expand Up @@ -211,6 +231,46 @@ mod propchain_analytics {
};
}

/// Batch update multiple market metrics in a single transaction.
#[ink(message)]
pub fn batch_update_metrics(
&mut self,
updates: Vec<MetricUpdate>,
) -> Result<(), AnalyticsError> {
self.ensure_admin();
if updates.len() > MAX_BATCH_SIZE {
return Err(AnalyticsError::BatchSizeExceeded);
}
for upd in updates.iter() {
self.current_metrics = MarketMetrics {
average_price: upd.average_price,
total_volume: upd.total_volume,
properties_listed: upd.properties_listed,
};
}
self.env().emit_event(BatchMetricsUpdated {
count: updates.len() as u64,
});
Ok(())
}

/// Batch add multiple market trends in a single transaction.
#[ink(message)]
pub fn batch_add_trends(&mut self, trends: Vec<MarketTrend>) -> Result<(), AnalyticsError> {
self.ensure_admin();
if trends.len() > MAX_BATCH_SIZE {
return Err(AnalyticsError::BatchSizeExceeded);
}
for trend in trends.iter() {
self.historical_trends.insert(self.trend_count, trend);
self.trend_count += 1;
}
self.env().emit_event(BatchMetricsUpdated {
count: trends.len() as u64,
});
Ok(())
}

/// Create market trend analysis with historical data
#[ink(message)]
pub fn add_market_trend(&mut self, trend: MarketTrend) {
Expand Down Expand Up @@ -770,6 +830,78 @@ mod propchain_analytics {
< residential_first.target_allocation_bips
);
}

#[ink::test]
fn batch_update_metrics_success() {
let mut contract = AnalyticsDashboard::new();
let mut updates = Vec::new();
for i in 0..20u64 {
updates.push(MetricUpdate {
average_price: (i * 10) as u128,
total_volume: (i * 100) as u128,
properties_listed: i as u64,
});
}
assert_eq!(contract.batch_update_metrics(updates.clone()), Ok(()));
let metrics = contract.get_market_metrics();
let last = updates.last().unwrap();
assert_eq!(metrics.average_price, last.average_price);
assert_eq!(metrics.total_volume, last.total_volume);
assert_eq!(metrics.properties_listed, last.properties_listed);
}

#[ink::test]
fn batch_update_metrics_exceeds_limit() {
let mut contract = AnalyticsDashboard::new();
let mut updates = Vec::new();
for i in 0..21u64 {
updates.push(MetricUpdate {
average_price: i as u128,
total_volume: i as u128,
properties_listed: i as u64,
});
}
assert_eq!(
contract.batch_update_metrics(updates),
Err(AnalyticsError::BatchSizeExceeded)
);
}

#[ink::test]
fn batch_add_trends_success() {
let mut contract = AnalyticsDashboard::new();
let mut trends = Vec::new();
for i in 0..20u64 {
trends.push(MarketTrend {
period_start: i * 10,
period_end: i * 10 + 5,
price_change_percentage: (i % 5) as i32,
volume_change_percentage: (i % 7) as i32,
});
}
assert_eq!(contract.batch_add_trends(trends.clone()), Ok(()));
let stored = contract.get_historical_trends();
assert_eq!(stored.len(), 20);
assert_eq!(stored[0].period_start, trends[0].period_start);
}

#[ink::test]
fn batch_add_trends_exceeds_limit() {
let mut contract = AnalyticsDashboard::new();
let mut trends = Vec::new();
for i in 0..21u64 {
trends.push(MarketTrend {
period_start: i,
period_end: i + 1,
price_change_percentage: 0,
volume_change_percentage: 0,
});
}
assert_eq!(
contract.batch_add_trends(trends),
Err(AnalyticsError::BatchSizeExceeded)
);
}
}
}

Expand Down
48 changes: 28 additions & 20 deletions contracts/insurance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ mod risk_assessment;
// Fraud Detection System (Task #258)
mod fraud_detection;

// Premium calculation engine
mod premium_engine;

/// Decentralized Property Insurance Platform
#[ink::contract]
mod propchain_insurance {
Expand Down Expand Up @@ -1174,10 +1177,7 @@ mod propchain_insurance {
/// Deactivate an active trigger. Only the policyholder or admin may
/// deactivate. Already-fired triggers cannot be re-deactivated.
#[ink(message)]
pub fn deactivate_claim_trigger(
&mut self,
trigger_id: u64,
) -> Result<(), InsuranceError> {
pub fn deactivate_claim_trigger(&mut self, trigger_id: u64) -> Result<(), InsuranceError> {
let caller = self.env().caller();
let mut trigger = self
.claim_triggers
Expand Down Expand Up @@ -1280,8 +1280,7 @@ mod propchain_insurance {
if remaining == 0 {
return Err(InsuranceError::ClaimExceedsCoverage);
}
let claim_amount =
Self::compute_claim_amount(&trigger.payout_mode, remaining)?;
let claim_amount = Self::compute_claim_amount(&trigger.payout_mode, remaining)?;
if claim_amount == 0 {
return Err(InsuranceError::TriggerConditionNotMet);
}
Expand Down Expand Up @@ -1310,8 +1309,10 @@ mod propchain_insurance {
policy.claims_count += 1;
self.policies.insert(&trigger.policy_id, &policy);

let mut policy_claims =
self.policy_claims.get(&trigger.policy_id).unwrap_or_default();
let mut policy_claims = self
.policy_claims
.get(&trigger.policy_id)
.unwrap_or_default();
policy_claims.push(claim_id);
self.policy_claims
.insert(&trigger.policy_id, &policy_claims);
Expand Down Expand Up @@ -1911,7 +1912,8 @@ mod propchain_insurance {
.get(&caller)
.unwrap_or_default();
holder_list.push(policy_id);
self.holder_parametric_policies.insert(&caller, &holder_list);
self.holder_parametric_policies
.insert(&caller, &holder_list);

self.env().emit_event(ParametricPolicyCreated {
policy_id,
Expand Down Expand Up @@ -2243,8 +2245,8 @@ mod propchain_insurance {
}

let block = self.env().block_number();
let effective_at = block
.saturating_add(propchain_traits::constants::KEY_ROTATION_COOLDOWN_BLOCKS);
let effective_at =
block.saturating_add(propchain_traits::constants::KEY_ROTATION_COOLDOWN_BLOCKS);

self.pending_admin_rotation = Some(propchain_traits::KeyRotationRequest {
old_account: caller,
Expand Down Expand Up @@ -2330,9 +2332,7 @@ mod propchain_insurance {

/// Get the pending admin rotation request, if any.
#[ink(message)]
pub fn get_pending_admin_rotation(
&self,
) -> Option<propchain_traits::KeyRotationRequest> {
pub fn get_pending_admin_rotation(&self) -> Option<propchain_traits::KeyRotationRequest> {
self.pending_admin_rotation.clone()
}

Expand Down Expand Up @@ -2940,11 +2940,14 @@ mod propchain_insurance {
}

/// Find a suitable pool for the given coverage type
fn find_pool_for_coverage(&self, coverage_type: &CoverageType) -> Result<RiskPool, InsuranceError> {
fn find_pool_for_coverage(
&self,
coverage_type: &CoverageType,
) -> Result<RiskPool, InsuranceError> {
// Iterate through pools to find an active one matching the coverage type
// For now, return the first active pool or a default
let pool_count = self.pool_count;

for pool_id in 1..=pool_count {
if let Some(pool) = self.pools.get(&pool_id) {
if pool.is_active && pool.coverage_type == *coverage_type {
Expand All @@ -2966,18 +2969,21 @@ mod propchain_insurance {
}

/// Get actuarial model for coverage type
fn get_actuarial_model_for_coverage(&self, coverage_type: &CoverageType) -> Option<ActuarialModel> {
fn get_actuarial_model_for_coverage(
&self,
coverage_type: &CoverageType,
) -> Option<ActuarialModel> {
// Search through models to find one matching the coverage type
let model_count = self.model_count;

for model_id in 1..=model_count {
if let Some(model) = self.actuarial_models.get(&model_id) {
if model.coverage_type == *coverage_type {
return Some(model);
}
}
}

None
}

Expand Down Expand Up @@ -3035,7 +3041,9 @@ mod propchain_insurance {
self.circuit_breaker_active = true;
let now = self.env().block_timestamp();
// Determine pool_id for the event
let pool_id = self.policies.get(&policy_id)
let pool_id = self
.policies
.get(&policy_id)
.map(|p| p.pool_id)
.unwrap_or(0);
self.env().emit_event(CircuitBreakerTripped {
Expand Down
Loading